/// <summary> /// Gets whether two OrderByQueryResult instances are equal. /// </summary> /// <param name="x">The first.</param> /// <param name="y">The second.</param> /// <returns>Whether two OrderByQueryResult instances are equal.</returns> public bool Equals(CosmosElement x, CosmosElement y) { OrderByQueryResult orderByQueryResultX = new OrderByQueryResult(x); OrderByQueryResult orderByQueryResultY = new OrderByQueryResult(y); return(this.orderByConsumeComparer.CompareOrderByItems( orderByQueryResultX.OrderByItems, orderByQueryResultY.OrderByItems) == 0); }
/// <summary> /// When resuming an order by query we need to filter the document producers. /// </summary> /// <param name="producer">The producer to filter down.</param> /// <param name="sortOrders">The sort orders.</param> /// <param name="continuationToken">The continuation token.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A task to await on.</returns> private async Task FilterAsync( DocumentProducerTree producer, SortOrder[] sortOrders, OrderByContinuationToken continuationToken, CancellationToken cancellationToken) { // When we resume a query on a partition there is a possibility that we only read a partial page from the backend // meaning that will we repeat some documents if we didn't do anything about it. // The solution is to filter all the documents that come before in the sort order, since we have already emitted them to the client. // The key is to seek until we get an order by value that matches the order by value we left off on. // Once we do that we need to seek to the correct _rid within the term, // since there might be many documents with the same order by value we left off on. foreach (DocumentProducerTree tree in producer) { if (!ResourceId.TryParse(continuationToken.Rid, out ResourceId continuationRid)) { this.TraceWarning(string.Format( CultureInfo.InvariantCulture, "Invalid Rid in the continuation token {0} for OrderBy~Context.", continuationToken.CompositeContinuationToken.Token)); throw new BadRequestException(RMResources.InvalidContinuationToken); } Dictionary <string, ResourceId> resourceIds = new Dictionary <string, ResourceId>(); int itemToSkip = continuationToken.SkipCount; bool continuationRidVerified = false; while (true) { OrderByQueryResult orderByResult = new OrderByQueryResult(tree.Current); // Throw away documents until it matches the item from the continuation token. int cmp = 0; for (int i = 0; i < sortOrders.Length; ++i) { cmp = ItemComparer.Instance.Compare( continuationToken.OrderByItems[i].Item, orderByResult.OrderByItems[i].Item); if (cmp != 0) { cmp = sortOrders[i] != SortOrder.Descending ? cmp : -cmp; break; } } if (cmp < 0) { // We might have passed the item due to deletions and filters. break; } if (cmp == 0) { ResourceId rid; if (!resourceIds.TryGetValue(orderByResult.Rid, out rid)) { if (!ResourceId.TryParse(orderByResult.Rid, out rid)) { this.TraceWarning(string.Format( CultureInfo.InvariantCulture, "Invalid Rid in the continuation token {0} for OrderBy~Context.", continuationToken.CompositeContinuationToken.Token)); throw new BadRequestException(RMResources.InvalidContinuationToken); } resourceIds.Add(orderByResult.Rid, rid); } if (!continuationRidVerified) { if (continuationRid.Database != rid.Database || continuationRid.DocumentCollection != rid.DocumentCollection) { this.TraceWarning(string.Format( CultureInfo.InvariantCulture, "Invalid Rid in the continuation token {0} for OrderBy~Context.", continuationToken.CompositeContinuationToken.Token)); throw new BadRequestException(RMResources.InvalidContinuationToken); } continuationRidVerified = true; } // Once the item matches the order by items from the continuation tokens // We still need to remove all the documents that have a lower rid in the rid sort order. // If there is a tie in the sort order the documents should be in _rid order in the same direction as the first order by field. // So if it's ORDER BY c.age ASC, c.name DESC the _rids are ASC // If ti's ORDER BY c.age DESC, c.name DESC the _rids are DESC cmp = continuationRid.Document.CompareTo(rid.Document); if (sortOrders[0] == SortOrder.Descending) { cmp = -cmp; } // We might have passed the item due to deletions and filters. // We also have a skip count for JOINs if (cmp < 0 || (cmp == 0 && itemToSkip-- <= 0)) { break; } } if (!await tree.MoveNextAsync(cancellationToken)) { break; } } } }
/// <summary> /// Drains a page of documents from this context. /// </summary> /// <param name="maxElements">The maximum number of elements.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A task that when awaited on return a page of documents.</returns> public override async Task <FeedResponse <CosmosElement> > DrainAsync(int maxElements, CancellationToken cancellationToken) { //// In order to maintain the continuation toke for the user we must drain with a few constraints //// 1) We always drain from the partition, which has the highest priority item first //// 2) If multiple partitions have the same priority item then we drain from the left most first //// otherwise we would need to keep track of how many of each item we drained from each partition //// (just like parallel queries). //// Visually that look the following case where we have three partitions that are numbered and store letters. //// For teaching purposes I have made each item a tuple of the following form: //// <item stored in partition, partition number> //// So that duplicates across partitions are distinct, but duplicates within partitions are indistinguishable. //// |-------| |-------| |-------| //// | <a,1> | | <a,2> | | <a,3> | //// | <a,1> | | <b,2> | | <c,3> | //// | <a,1> | | <b,2> | | <c,3> | //// | <d,1> | | <c,2> | | <c,3> | //// | <d,1> | | <e,2> | | <f,3> | //// | <e,1> | | <h,2> | | <j,3> | //// | <f,1> | | <i,2> | | <k,3> | //// |-------| |-------| |-------| //// Now the correct drain order in this case is: //// <a,1>,<a,1>,<a,1>,<a,2>,<a,3>,<b,2>,<b,2>,<c,2>,<c,3>,<c,3>,<c,3>, //// <d,1>,<d,1>,<e,1>,<e,2>,<f,1>,<f,3>,<h,2>,<i,2>,<j,3>,<k,3> //// In more mathematical terms //// 1) <x, y> always comes before <z, y> where x < z //// 2) <i, j> always come before <i, k> where j < k List <CosmosElement> results = new List <CosmosElement>(); while (!this.IsDone && results.Count < maxElements) { // Only drain from the highest priority document producer // We need to pop and push back the document producer tree, since the priority changes according to the sort order. DocumentProducerTree currentDocumentProducerTree = this.PopCurrentDocumentProducerTree(); OrderByQueryResult orderByQueryResult = new OrderByQueryResult(currentDocumentProducerTree.Current); // Only add the payload, since other stuff is garbage from the caller's perspective. results.Add(orderByQueryResult.Payload); // If we are at the begining of the page and seeing an rid from the previous page we should increment the skip count // due to the fact that JOINs can make a document appear multiple times and across continuations, so we don't want to // surface this more than needed. More information can be found in the continuation token docs. if (this.ShouldIncrementSkipCount(currentDocumentProducerTree.CurrentDocumentProducerTree.Root)) { ++this.skipCount; } else { this.skipCount = 0; } this.previousRid = orderByQueryResult.Rid; await currentDocumentProducerTree.MoveNextAsync(cancellationToken); this.PushCurrentDocumentProducerTree(currentDocumentProducerTree); } return(new FeedResponse <CosmosElement>( results, results.Count, this.GetResponseHeaders(), false, this.GetQueryMetrics(), null, null, this.GetAndResetResponseLengthBytes())); }