/// <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( ItemProducerTree 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 (ItemProducerTree tree in producer) { if (!ResourceId.TryParse(continuationToken.Rid, out ResourceId continuationRid)) { throw new CosmosException( statusCode: HttpStatusCode.BadRequest, message: $"Invalid Rid in the continuation token {continuationToken.CompositeContinuationToken.Token} for OrderBy~Context."); } 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)) { throw new CosmosException( statusCode: HttpStatusCode.BadRequest, message: $"Invalid Rid in the continuation token {continuationToken.CompositeContinuationToken.Token} for OrderBy~Context~TryParse."); } resourceIds.Add(orderByResult.Rid, rid); } if (!continuationRidVerified) { if (continuationRid.Database != rid.Database || continuationRid.DocumentCollection != rid.DocumentCollection) { throw new CosmosException( statusCode: HttpStatusCode.BadRequest, message: $"Invalid Rid in the continuation token {continuationToken.CompositeContinuationToken.Token} for OrderBy~Context."); } 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; } } (bool successfullyMovedNext, QueryResponseCore? failureResponse)moveNextResponse = await tree.MoveNextAsync(cancellationToken); if (!moveNextResponse.successfullyMovedNext) { if (moveNextResponse.failureResponse != null) { this.FailureResponse = moveNextResponse.failureResponse; } break; } } } }
public void TestRoundTripAsCosmosElement() { CompositeContinuationToken compositeContinuationToken = new CompositeContinuationToken() { Token = "someToken", Range = new Documents.Routing.Range <string>("asdf", "asdf", false, false), }; List <OrderByItem> orderByItems = new List <OrderByItem>() { new OrderByItem(CosmosObject.Create(new Dictionary <string, CosmosElement>() { { "item", CosmosString.Create("asdf") } })), new OrderByItem(CosmosObject.Create(new Dictionary <string, CosmosElement>() { { "item", CosmosInt64.Create(1337) } })), new OrderByItem(CosmosObject.Create(new Dictionary <string, CosmosElement>() { { "item", CosmosBinary.Create(new byte[] { 1, 2, 3 }) } })), }; string rid = "someRid"; int skipCount = 42; string filter = "someFilter"; OrderByContinuationToken orderByContinuationToken = new OrderByContinuationToken( compositeContinuationToken, orderByItems, rid, skipCount, filter); CosmosElement cosmosElementToken = OrderByContinuationToken.ToCosmosElement(orderByContinuationToken); Assert.AreEqual( @"{""compositeToken"":{""token"":""someToken"",""range"":{""min"":""asdf"",""max"":""asdf""}},""orderByItems"":[{""item"":""asdf""},{""item"":LL1337},{""item"":BAQID}],""rid"":""someRid"",""skipCount"":42,""filter"":""someFilter""}", cosmosElementToken.ToString()); TryCatch <OrderByContinuationToken> tryOrderByContinuationTokenFromCosmosElement = OrderByContinuationToken.TryCreateFromCosmosElement(cosmosElementToken); Assert.IsTrue(tryOrderByContinuationTokenFromCosmosElement.Succeeded); OrderByContinuationToken orderByContinuationTokenFromCosmosElement = tryOrderByContinuationTokenFromCosmosElement.Result; Assert.IsNotNull(orderByContinuationTokenFromCosmosElement); Assert.AreEqual(cosmosElementToken.ToString(), OrderByContinuationToken.ToCosmosElement(orderByContinuationTokenFromCosmosElement).ToString()); }