All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.azure.cosmos.implementation.query.OrderByDocumentQueryExecutionContext Maven / Gradle / Ivy

Go to download

This Package contains Microsoft Azure Cosmos SDK (with Reactive Extension Reactor support) for Azure Cosmos DB SQL API

There is a newer version: 4.60.0
Show newest version
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.cosmos.implementation.query;

import com.azure.cosmos.BridgeInternal;
import com.azure.cosmos.CosmosException;
import com.azure.cosmos.implementation.BadRequestException;
import com.azure.cosmos.implementation.ClientSideRequestStatistics;
import com.azure.cosmos.implementation.DiagnosticsClientContext;
import com.azure.cosmos.implementation.DocumentClientRetryPolicy;
import com.azure.cosmos.implementation.HttpConstants;
import com.azure.cosmos.implementation.PartitionKeyRange;
import com.azure.cosmos.implementation.QueryMetrics;
import com.azure.cosmos.implementation.RequestChargeTracker;
import com.azure.cosmos.implementation.Resource;
import com.azure.cosmos.implementation.ResourceId;
import com.azure.cosmos.implementation.ResourceType;
import com.azure.cosmos.implementation.RxDocumentServiceRequest;
import com.azure.cosmos.implementation.Utils;
import com.azure.cosmos.implementation.Utils.ValueHolder;
import com.azure.cosmos.implementation.apachecommons.lang.NotImplementedException;
import com.azure.cosmos.implementation.apachecommons.lang.tuple.ImmutablePair;
import com.azure.cosmos.implementation.apachecommons.lang.tuple.Pair;
import com.azure.cosmos.implementation.feedranges.FeedRangeEpkImpl;
import com.azure.cosmos.implementation.query.orderbyquery.OrderByRowResult;
import com.azure.cosmos.implementation.query.orderbyquery.OrderbyRowComparer;
import com.azure.cosmos.implementation.routing.Range;
import com.azure.cosmos.models.CosmosQueryRequestOptions;
import com.azure.cosmos.models.FeedResponse;
import com.azure.cosmos.models.ModelBridgeInternal;
import com.azure.cosmos.models.SqlQuerySpec;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;

/**
 * While this class is public, but it is not part of our published public APIs.
 * This is meant to be internally used only by our sdk.
 */
public class OrderByDocumentQueryExecutionContext
        extends ParallelDocumentQueryExecutionContextBase {
    private final static String FormatPlaceHolder = "{documentdb-formattableorderbyquery-filter}";
    private final static String True = "true";
    private final String collectionRid;
    private final OrderbyRowComparer consumeComparer;
    private final RequestChargeTracker tracker;
    private final ConcurrentMap queryMetricMap;
    List clientSideRequestStatisticsList;
    private Flux> orderByObservable;
    private final Map targetRangeToOrderByContinuationTokenMap;

    private OrderByDocumentQueryExecutionContext(
            DiagnosticsClientContext diagnosticsClientContext,
            IDocumentQueryClient client,
            ResourceType resourceTypeEnum,
            Class klass,
            SqlQuerySpec query,
            CosmosQueryRequestOptions cosmosQueryRequestOptions,
            String resourceLink,
            String rewrittenQuery,
            boolean isContinuationExpected,
            boolean getLazyFeedResponse,
            OrderbyRowComparer consumeComparer,
            String collectionRid,
            UUID correlatedActivityId) {
        super(diagnosticsClientContext, client, resourceTypeEnum, klass, query, cosmosQueryRequestOptions, resourceLink, rewrittenQuery,
                isContinuationExpected, getLazyFeedResponse, correlatedActivityId);
        this.collectionRid = collectionRid;
        this.consumeComparer = consumeComparer;
        this.tracker = new RequestChargeTracker();
        this.queryMetricMap = new ConcurrentHashMap<>();
        this.clientSideRequestStatisticsList = new ArrayList<>();
        targetRangeToOrderByContinuationTokenMap = new HashMap<>();
    }

    public static  Flux> createAsync(
            DiagnosticsClientContext diagnosticsClientContext,
            IDocumentQueryClient client,
            PipelinedDocumentQueryParams initParams) {

        OrderByDocumentQueryExecutionContext context = new OrderByDocumentQueryExecutionContext(diagnosticsClientContext,
                client,
                initParams.getResourceTypeEnum(),
                initParams.getResourceType(),
                initParams.getQuery(),
                initParams.getCosmosQueryRequestOptions(),
                initParams.getResourceLink(),
                initParams.getQueryInfo().getRewrittenQuery(),
                initParams.isContinuationExpected(),
                initParams.isGetLazyResponseFeed(),
                new OrderbyRowComparer(initParams.getQueryInfo().getOrderBy()),
                initParams.getCollectionRid(),
                initParams.getCorrelatedActivityId());

        context.setTop(initParams.getTop());

        try {
            context.initialize(
                    initParams.getFeedRanges(),
                    initParams.getQueryInfo().getOrderBy(),
                    initParams.getQueryInfo().getOrderByExpressions(),
                    initParams.getInitialPageSize(),
                    ModelBridgeInternal.getRequestContinuationFromQueryRequestOptions(initParams.getCosmosQueryRequestOptions()));

            return Flux.just(context);
        } catch (CosmosException dce) {
            return Flux.error(dce);
        }
    }

    private void initialize(
        List feedRanges, List sortOrders,
        Collection orderByExpressions,
        int initialPageSize,
        String continuationToken) throws CosmosException {
        if (continuationToken == null) {
            // First iteration so use null continuation tokens and "true" filters
            Map partitionKeyRangeToContinuationToken = new HashMap<>();
            for (FeedRangeEpkImpl feedRangeEpk : feedRanges) {
                partitionKeyRangeToContinuationToken.put(feedRangeEpk,
                        null);
            }
            super.initialize(collectionRid,
                    partitionKeyRangeToContinuationToken,
                    initialPageSize,
                    new SqlQuerySpec(querySpec.getQueryText().replace(FormatPlaceHolder, True),
                            querySpec.getParameters()));
        } else {
            // Check to see if order by continuation token is a valid JSON.
            OrderByContinuationToken orderByContinuationToken;
            ValueHolder outOrderByContinuationToken = new ValueHolder();
            if (!OrderByContinuationToken.tryParse(continuationToken,
                    outOrderByContinuationToken)) {
                String message = String.format("INVALID JSON in continuation token %s for OrderBy~Context",
                        continuationToken);
                throw BridgeInternal.createCosmosException(HttpConstants.StatusCodes.BADREQUEST,
                        message);
            }

            orderByContinuationToken = outOrderByContinuationToken.v;

            CompositeContinuationToken compositeContinuationToken = orderByContinuationToken
                    .getCompositeContinuationToken();
            // Check to see if the ranges inside are valid
            if (compositeContinuationToken.getRange().isEmpty()) {
                String message = String.format("INVALID RANGE in the continuation token %s for OrderBy~Context.",
                        continuationToken);
                throw BridgeInternal.createCosmosException(HttpConstants.StatusCodes.BADREQUEST,
                        message);
            }

            // Checking early if the token has a valid resource id
            Pair booleanResourceIdPair = ResourceId.tryParse(orderByContinuationToken.getRid());
            if (!booleanResourceIdPair.getLeft()) {
                throw new BadRequestException(String.format("INVALID Rid in the continuation token %s for " +
                                                             "OrderBy~Context.",
                                                                        orderByContinuationToken.getCompositeContinuationToken().getToken()));
            }

            // At this point the token is valid.
            FormattedFilterInfo formattedFilterInfo = this.getFormattedFilters(orderByExpressions,
                                                                               orderByContinuationToken
                                                                                   .getOrderByItems(),
                                                                               sortOrders,
                                                                               orderByContinuationToken.getInclusive());

            PartitionMapper.PartitionMapping partitionMapping =
                PartitionMapper.getPartitionMapping(feedRanges, Collections.singletonList(orderByContinuationToken));

            initializeWithTokenAndFilter(partitionMapping.getMappingLeftOfTarget(), initialPageSize,
                                         formattedFilterInfo.filterForRangesLeftOfTheTargetRange);
            initializeWithTokenAndFilter(partitionMapping.getTargetMapping(), initialPageSize,
                                         formattedFilterInfo.filterForTargetRange);
            initializeWithTokenAndFilter(partitionMapping.getMappingRightOfTarget(), initialPageSize,
                                         formattedFilterInfo.filterForRangesRightOfTheTargetRange);
        }

        orderByObservable = OrderByUtils.orderedMerge(resourceType,
                consumeComparer,
                tracker,
                documentProducers,
                queryMetricMap,
                targetRangeToOrderByContinuationTokenMap,
                clientSideRequestStatisticsList);
    }

    private void initializeWithTokenAndFilter(Map rangeToTokenMapping,
                                              int initialPageSize,
                                              String filter) {
        for (Map.Entry entry :
            rangeToTokenMapping.entrySet()) {
            targetRangeToOrderByContinuationTokenMap.put(entry.getKey(), entry.getValue());
            Map partitionKeyRangeToContinuationToken = new HashMap();
            partitionKeyRangeToContinuationToken.put(entry.getKey(), null);
            super.initialize(collectionRid,
                             partitionKeyRangeToContinuationToken,
                             initialPageSize,
                             new SqlQuerySpec(querySpec.getQueryText().replace(FormatPlaceHolder,
                                                                               filter),
                                              querySpec.getParameters()));

        }
    }

    private OrderByDocumentQueryExecutionContext.FormattedFilterInfo getFormattedFilters(
            Collection orderByExpressionCollection,
            QueryItem[] orderByItems,
            Collection sortOrderCollection,
            boolean inclusive) {
        // Convert to arrays
        SortOrder[] sortOrders = new SortOrder[sortOrderCollection.size()];
        sortOrderCollection.toArray(sortOrders);

        String[] expressions = new String[orderByExpressionCollection.size()];
        orderByExpressionCollection.toArray(expressions);

        // Validate the inputs
        if (expressions.length != sortOrders.length) {
            throw new IllegalArgumentException("expressions.size() != sortOrders.size()");
        }

        if (expressions.length != orderByItems.length) {
            throw new IllegalArgumentException("expressions.size() != orderByItems.length");
        }

        // 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 partitions.
        // 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;
        boolean isSingleOrderBy = numOrderByItems == 1;
        StringBuilder left = new StringBuilder();
        StringBuilder target = new StringBuilder();
        StringBuilder right = new StringBuilder();

        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[0];
            SortOrder sortOrder = sortOrders[0];
            QueryItem orderByItem = orderByItems[0];
            Object rawItem = orderByItem.getItem();
            String orderByItemToString;
            if (rawItem instanceof String) {
                orderByItemToString = "\"" + rawItem.toString().replaceAll("\"",
                        "\\\"") + "\"";
            } else {
                orderByItemToString = rawItem.toString();
            }

            left.append(String.format("%s %s %s",
                    expression,
                    (sortOrder == SortOrder.Descending ? "<" : ">"),
                    orderByItemToString));

            if (inclusive) {
                target.append(String.format("%s %s %s",
                        expression,
                        (sortOrder == SortOrder.Descending ? "<=" : ">="),
                        orderByItemToString));
            } else {
                target.append(String.format("%s %s %s",
                        expression,
                        (sortOrder == SortOrder.Descending ? "<" : ">"),
                        orderByItemToString));
            }

            right.append(String.format("%s %s %s",
                    expression,
                    (sortOrder == SortOrder.Descending ? "<=" : ">="),
                    orderByItemToString));
        } else {
            // This code path needs to be implemented, but it's error prone and needs
            // testing.
            // You can port the implementation from the .net SDK and it should work if
            // ported right.
            throw new NotImplementedException(
                    "Resuming a multi order by query from a continuation token is not supported yet.");
        }

        return new FormattedFilterInfo(left.toString(),
                target.toString(),
                right.toString());
    }

    @Override
    protected OrderByDocumentProducer createDocumentProducer(
            String collectionRid,
            PartitionKeyRange targetRange,
            String continuationToken,
            int initialPageSize,
            CosmosQueryRequestOptions cosmosQueryRequestOptions,
            SqlQuerySpec querySpecForInit,
            Map commonRequestHeaders,
            TriFunction createRequestFunc,
            Function>> executeFunc,
            Callable createRetryPolicyFunc, FeedRangeEpkImpl feedRange) {
        return new OrderByDocumentProducer(consumeComparer,
                client,
                collectionRid,
                cosmosQueryRequestOptions,
                createRequestFunc,
                executeFunc,
                targetRange,
                feedRange,
                collectionRid,
                createRetryPolicyFunc,
                resourceType,
                correlatedActivityId,
                initialPageSize,
                continuationToken,
                top,
                this.targetRangeToOrderByContinuationTokenMap);
    }

    private static class ItemToPageTransformer
            implements Function>, Flux>> {
        private final static int DEFAULT_PAGE_SIZE = 100;
        private final RequestChargeTracker tracker;
        private final int maxPageSize;
        private final ConcurrentMap queryMetricMap;
        private final Function, String> orderByContinuationTokenCallback;
        private final List clientSideRequestStatisticsList;
        private volatile FeedResponse> previousPage;

        public ItemToPageTransformer(
            RequestChargeTracker tracker,
            int maxPageSize,
            ConcurrentMap queryMetricsMap,
            Function, String> orderByContinuationTokenCallback,
            List clientSideRequestStatisticsList) {
            this.tracker = tracker;
            this.maxPageSize = maxPageSize > 0 ? maxPageSize : DEFAULT_PAGE_SIZE;
            this.queryMetricMap = queryMetricsMap;
            this.orderByContinuationTokenCallback = orderByContinuationTokenCallback;
            this.previousPage = null;
            this.clientSideRequestStatisticsList = clientSideRequestStatisticsList;
        }

        private static Map headerResponse(
                double requestCharge) {
            return Utils.immutableMapOf(HttpConstants.HttpHeaders.REQUEST_CHARGE,
                    String.valueOf(requestCharge));
        }

        private FeedResponse> addOrderByContinuationToken(
                FeedResponse> page,
                String orderByContinuationToken) {
            Map headers = new HashMap<>(page.getResponseHeaders());
            headers.put(HttpConstants.HttpHeaders.CONTINUATION,
                    orderByContinuationToken);
            return BridgeInternal.createFeedResponseWithQueryMetrics(page.getResults(),
                headers,
                BridgeInternal.queryMetricsFromFeedResponse(page),
                ModelBridgeInternal.getQueryPlanDiagnosticsContext(page),
                false,
                false,
                page.getCosmosDiagnostics());
        }

        @Override
        public Flux> apply(Flux> source) {
            return source
                    // .windows: creates an observable of observable where inner observable
                    // emits max maxPageSize elements
                    .window(maxPageSize).map(Flux::collectList)
                    // flattens the observable>>> to
                    // Observable>>
                    .flatMap(resultListObs -> resultListObs,
                            1)
                    // translates Observable>> to
                    // Observable>>>
                    .map(orderByRowResults -> {
                        // construct a page from result with request charge
                        FeedResponse> feedResponse = BridgeInternal.createFeedResponse(
                                orderByRowResults,
                                headerResponse(tracker.getAndResetCharge()));
                        if (!queryMetricMap.isEmpty()) {
                            for (Map.Entry entry : queryMetricMap.entrySet()) {
                                BridgeInternal.putQueryMetricsIntoMap(feedResponse,
                                    entry.getKey(),
                                    entry.getValue());
                            }
                        }
                        return feedResponse;
                    })
                    // Emit an empty page so the downstream observables know when there are no more
                    // results.
                    .concatWith(Flux.defer(() -> {
                        return Flux.just(BridgeInternal.createFeedResponse(Utils.immutableListOf(),
                                null));
                    }))
                    // CREATE pairs from the stream to allow the observables downstream to "peek"
                    // 1, 2, 3, null -> (null, 1), (1, 2), (2, 3), (3, null)
                    .map(orderByRowResults -> {
                        ImmutablePair>, FeedResponse>> previousCurrent = new ImmutablePair>, FeedResponse>>(
                                this.previousPage,
                                orderByRowResults);
                        this.previousPage = orderByRowResults;
                        return previousCurrent;
                    })
                    // remove the (null, 1)
                    .skip(1)
                    // Add the continuation token based on the current and next page.
                    .map(currentNext -> {
                        FeedResponse> current = currentNext.left;
                        FeedResponse> next = currentNext.right;

                        FeedResponse> page;
                        if (next.getResults().size() == 0) {
                            // No more pages no send current page with null continuation token
                            page = current;
                            page = this.addOrderByContinuationToken(page,
                                    null);
                        } else {
                            // Give the first page but use the first value in the next page to generate the
                            // continuation token
                            page = current;
                            List> results = next.getResults();
                            OrderByRowResult firstElementInNextPage = results.get(0);
                            String orderByContinuationToken = this.orderByContinuationTokenCallback
                                    .apply(firstElementInNextPage);
                            page = this.addOrderByContinuationToken(page,
                                    orderByContinuationToken);
                        }

                        return page;
                    }).map(feedOfOrderByRowResults -> {
                        // FeedResponse> to FeedResponse
                        List unwrappedResults = new ArrayList();
                        for (OrderByRowResult orderByRowResult : feedOfOrderByRowResults.getResults()) {
                            unwrappedResults.add(orderByRowResult.getPayload());
                        }

                    FeedResponse feedResponse = BridgeInternal.createFeedResponseWithQueryMetrics(unwrappedResults,
                        feedOfOrderByRowResults.getResponseHeaders(),
                        BridgeInternal.queryMetricsFromFeedResponse(feedOfOrderByRowResults),
                        ModelBridgeInternal.getQueryPlanDiagnosticsContext(feedOfOrderByRowResults),
                        false,
                        false, feedOfOrderByRowResults.getCosmosDiagnostics());
                    BridgeInternal.addClientSideDiagnosticsToFeed(feedResponse.getCosmosDiagnostics(),
                                                                  clientSideRequestStatisticsList);
                    return feedResponse;
                }).switchIfEmpty(Flux.defer(() -> {
                        // create an empty page if there is no result
                    FeedResponse frp =  BridgeInternal.createFeedResponseWithQueryMetrics(Utils.immutableListOf(),
                                headerResponse(
                                    tracker.getAndResetCharge()),
                            queryMetricMap,
                            null,
                            false,
                            false,
                            null);
                    BridgeInternal.addClientSideDiagnosticsToFeed(frp.getCosmosDiagnostics(),
                                                                  clientSideRequestStatisticsList);
                    return Flux.just(frp);
                    }));
        }
    }

    @Override
    public Flux> drainAsync(
            int maxPageSize) {
        //// In order to maintain the continuation token 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:
        //// 
        //// So that duplicates across partitions are distinct, but duplicates within
        //// partitions are indistinguishable.
        //// |-------| |-------| |-------|
        //// |  | |  | |  |
        //// |  | |  | |  |
        //// |  | |  | |  |
        //// |  | |  | |  |
        //// |  | |  | |  |
        //// |  | |  | |  |
        //// |  | |  | |  |
        //// |-------| |-------| |-------|
        //// Now the correct drain order in this case is:
        //// ,,,,,,,,,,,
        //// ,,,,,,,,,
        //// In more mathematical terms
        //// 1)  always comes before  where x < z
        //// 2)  always come before  where j < k
        return this.orderByObservable.transformDeferred(new ItemToPageTransformer(tracker,
                maxPageSize,
                this.queryMetricMap,
                this::getContinuationToken,
                this.clientSideRequestStatisticsList));
    }

    @Override
    public Flux> executeAsync() {
        return drainAsync(ModelBridgeInternal.getMaxItemCountFromQueryRequestOptions(cosmosQueryRequestOptions));
    }

    private String getContinuationToken(
            OrderByRowResult orderByRowResult) {
        // rid
        String rid = orderByRowResult.getResourceId();

        // CompositeContinuationToken
        String backendContinuationToken = orderByRowResult.getSourceBackendContinuationToken();
        Range range = orderByRowResult.getSourceRange().getRange();

        boolean inclusive = true;
        CompositeContinuationToken compositeContinuationToken = new CompositeContinuationToken(backendContinuationToken,
                range);

        // OrderByItems
        QueryItem[] orderByItems = new QueryItem[orderByRowResult.getOrderByItems().size()];
        orderByRowResult.getOrderByItems().toArray(orderByItems);

        return new OrderByContinuationToken(compositeContinuationToken,
                orderByItems,
                rid,
                inclusive).toJson();
    }

    private final class FormattedFilterInfo {
        private final String filterForRangesLeftOfTheTargetRange;
        private final String filterForTargetRange;
        private final String filterForRangesRightOfTheTargetRange;

        public FormattedFilterInfo(
                String filterForRangesLeftOfTheTargetRange,
                String filterForTargetRange,
                String filterForRangesRightOfTheTargetRange) {
            if (filterForRangesLeftOfTheTargetRange == null) {
                throw new IllegalArgumentException("filterForRangesLeftOfTheTargetRange must not be null.");
            }

            if (filterForTargetRange == null) {
                throw new IllegalArgumentException("filterForTargetRange must not be null.");
            }

            if (filterForRangesRightOfTheTargetRange == null) {
                throw new IllegalArgumentException("filterForRangesRightOfTheTargetRange must not be null.");
            }

            this.filterForRangesLeftOfTheTargetRange = filterForRangesLeftOfTheTargetRange;
            this.filterForTargetRange = filterForTargetRange;
            this.filterForRangesRightOfTheTargetRange = filterForRangesRightOfTheTargetRange;
        }

        /**
         * @return the filterForRangesLeftOfTheTargetRange
         */
        public String getFilterForRangesLeftOfTheTargetRange() {
            return filterForRangesLeftOfTheTargetRange;
        }

        /**
         * @return the filterForTargetRange
         */
        public String getFilterForTargetRange() {
            return filterForTargetRange;
        }

        /**
         * @return the filterForRangesRightOfTheTargetRange
         */
        public String getFilterForRangesRightOfTheTargetRange() {
            return filterForRangesRightOfTheTargetRange;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy