private static Tuple <string, string, string> GetFormattedFilters( string[] expressions, CosmosElement[] orderByItems, SortOrder[] sortOrders) { // When we run cross partition queries, // we only serialize the continuation token for the partition that we left off on. // The only problem is that when we resume the order by query, // we don't have continuation tokens for all other partition. // The saving grace is that the data has a composite sort order(query sort order, partition key range id) // so we can generate range filters which in turn the backend will turn into rid based continuation tokens, // which is enough to get the streams of data flowing from all partitions. // The details of how this is done is described below: int numOrderByItems = expressions.Length; bool isSingleOrderBy = numOrderByItems == 1; StringBuilder left = new StringBuilder(); StringBuilder target = new StringBuilder(); StringBuilder right = new StringBuilder(); Tuple <StringBuilder, StringBuilder, StringBuilder> builders = new Tuple <StringBuilder, StringBuilder, StringBuilder>(left, right, target); if (isSingleOrderBy) { //For a single order by query we resume the continuations in this manner // Suppose the query is SELECT* FROM c ORDER BY c.string ASC // And we left off on partition N with the value "B" // Then // All the partitions to the left will have finished reading "B" // Partition N is still reading "B" // All the partitions to the right have let to read a "B // Therefore the filters should be // > "B" , >= "B", and >= "B" respectively // Repeat the same logic for DESC and you will get // < "B", <= "B", and <= "B" respectively // The general rule becomes // For ASC // > for partitions to the left // >= for the partition we left off on // >= for the partitions to the right // For DESC // < for partitions to the left // <= for the partition we left off on // <= for the partitions to the right string expression = expressions.First(); SortOrder sortOrder = sortOrders.First(); CosmosElement orderByItem = orderByItems.First(); string orderByItemToString = JsonConvert.SerializeObject(orderByItem, DefaultJsonSerializationSettings.Value); left.Append($"{expression} {(sortOrder == SortOrder.Descending ? "<" : ">")} {orderByItemToString}"); target.Append($"{expression} {(sortOrder == SortOrder.Descending ? "<=" : ">=")} {orderByItemToString}"); right.Append($"{expression} {(sortOrder == SortOrder.Descending ? "<=" : ">=")} {orderByItemToString}"); } else { //For a multi order by query // Suppose the query is SELECT* FROM c ORDER BY c.string ASC, c.number ASC // And we left off on partition N with the value("A", 1) // Then // All the partitions to the left will have finished reading("A", 1) // Partition N is still reading("A", 1) // All the partitions to the right have let to read a "(A", 1) // The filters are harder to derive since their are multiple columns // But the problem reduces to "How do you know one document comes after another in a multi order by query" // The answer is to just look at it one column at a time. // For this particular scenario: // If a first column is greater ex. ("B", blah), then the document comes later in the sort order // Therefore we want all documents where the first column is greater than "A" which means > "A" // Or if the first column is a tie, then you look at the second column ex. ("A", blah). // Therefore we also want all documents where the first column was a tie but the second column is greater which means = "A" AND > 1 // Therefore the filters should be // (> "A") OR (= "A" AND > 1), (> "A") OR (= "A" AND >= 1), (> "A") OR (= "A" AND >= 1) // Notice that if we repeated the same logic we for single order by we would have gotten // > "A" AND > 1, >= "A" AND >= 1, >= "A" AND >= 1 // which is wrong since we missed some documents // Repeat the same logic for ASC, DESC // (> "A") OR (= "A" AND < 1), (> "A") OR (= "A" AND <= 1), (> "A") OR (= "A" AND <= 1) // Again for DESC, ASC // (< "A") OR (= "A" AND > 1), (< "A") OR (= "A" AND >= 1), (< "A") OR (= "A" AND >= 1) // And again for DESC DESC // (< "A") OR (= "A" AND < 1), (< "A") OR (= "A" AND <= 1), (< "A") OR (= "A" AND <= 1) // The general we look at all prefixes of the order by columns to look for tie breakers. // Except for the full prefix whose last column follows the rules for single item order by // And then you just OR all the possibilities together for (int prefixLength = 1; prefixLength <= numOrderByItems; prefixLength++) { ArraySegment <string> expressionPrefix = new ArraySegment <string>(expressions, 0, prefixLength); ArraySegment <SortOrder> sortOrderPrefix = new ArraySegment <SortOrder>(sortOrders, 0, prefixLength); ArraySegment <CosmosElement> orderByItemsPrefix = new ArraySegment <CosmosElement>(orderByItems, 0, prefixLength); bool lastPrefix = prefixLength == numOrderByItems; bool firstPrefix = prefixLength == 1; CosmosOrderByItemQueryExecutionContext.AppendToBuilders(builders, "("); for (int index = 0; index < prefixLength; index++) { string expression = expressionPrefix.ElementAt(index); SortOrder sortOrder = sortOrderPrefix.ElementAt(index); CosmosElement orderByItem = orderByItemsPrefix.ElementAt(index); bool lastItem = index == prefixLength - 1; // Append Expression CosmosOrderByItemQueryExecutionContext.AppendToBuilders(builders, expression); CosmosOrderByItemQueryExecutionContext.AppendToBuilders(builders, " "); // Append binary operator if (lastItem) { string inequality = sortOrder == SortOrder.Descending ? "<" : ">"; CosmosOrderByItemQueryExecutionContext.AppendToBuilders(builders, inequality); if (lastPrefix) { CosmosOrderByItemQueryExecutionContext.AppendToBuilders(builders, string.Empty, "=", "="); } } else { CosmosOrderByItemQueryExecutionContext.AppendToBuilders(builders, "="); } // Append SortOrder string orderByItemToString = JsonConvert.SerializeObject(orderByItem, DefaultJsonSerializationSettings.Value); CosmosOrderByItemQueryExecutionContext.AppendToBuilders(builders, " "); CosmosOrderByItemQueryExecutionContext.AppendToBuilders(builders, orderByItemToString); CosmosOrderByItemQueryExecutionContext.AppendToBuilders(builders, " "); if (!lastItem) { CosmosOrderByItemQueryExecutionContext.AppendToBuilders(builders, "AND "); } } CosmosOrderByItemQueryExecutionContext.AppendToBuilders(builders, ")"); if (!lastPrefix) { CosmosOrderByItemQueryExecutionContext.AppendToBuilders(builders, " OR "); } } } return(new Tuple <string, string, string>(left.ToString(), target.ToString(), right.ToString())); }
private static void AppendToBuilders(Tuple <StringBuilder, StringBuilder, StringBuilder> builders, object str) { CosmosOrderByItemQueryExecutionContext.AppendToBuilders(builders, str, str, str); }