UNPKG

@azure/cosmos

Version:
310 lines • 15.5 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. /** * Strategy for filtering partition ranges in ORDER BY query execution context * Supports resuming from continuation tokens with proper range-token pair management * @hidden */ export class OrderByQueryRangeStrategy { getStrategyType() { return "OrderByQuery"; } filterPartitionRanges(targetRanges, continuationRanges, queryInfo) { if (!targetRanges || targetRanges.length === 0 || !continuationRanges || continuationRanges.length === 0) { return { rangeTokenPairs: [], }; } if (!queryInfo?.orderByItems || !Array.isArray(queryInfo.orderByItems) || queryInfo.orderByItems.length === 0) { throw new Error("Unable to resume ORDER BY query from continuation token. orderByItems is required for ORDER BY queries."); } const result = { rangeTokenPairs: [], }; let filteredRanges = []; let resumeRangeFound = false; if (continuationRanges && continuationRanges.length > 0) { resumeRangeFound = true; // Find the range to resume from based on the composite token const targetRangeMapping = continuationRanges[continuationRanges.length - 1].range; // It is assumed that range mapping array is going to contain only range const targetRange = targetRangeMapping; const targetContinuationToken = continuationRanges[continuationRanges.length - 1].continuationToken; const leftRanges = targetRanges.filter((mapping) => this.isRangeBeforeAnother(mapping.maxExclusive, targetRangeMapping.minInclusive)); const orderByItems = queryInfo.orderByItems; // Create filtering condition for left ranges based on ORDER BY items and sort orders const leftFilter = this.createRangeFilterCondition(orderByItems, queryInfo, "left"); const rightRanges = targetRanges.filter((mapping) => this.isRangeAfterAnother(mapping.minInclusive, targetRangeMapping.maxExclusive)); // Create filtering condition for right ranges based on ORDER BY items and sort orders const rightFilter = this.createRangeFilterCondition(orderByItems, queryInfo, "right"); // Apply filtering logic for left ranges if (leftRanges.length > 0) { leftRanges.forEach((range) => { result.rangeTokenPairs.push({ range: range, continuationToken: undefined, filteringCondition: leftFilter, }); }); } result.rangeTokenPairs.push({ range: targetRange, continuationToken: targetContinuationToken, filteringCondition: rightFilter, }); // Apply filtering logic for right ranges if (rightRanges.length > 0) { rightRanges.forEach((range) => { result.rangeTokenPairs.push({ range: range, continuationToken: undefined, filteringCondition: rightFilter, }); }); } } // If we couldn't find a specific resume point, include all ranges // This can happen with certain types of ORDER BY continuation tokens if (!resumeRangeFound) { filteredRanges = [...targetRanges]; filteredRanges.forEach((range) => { result.rangeTokenPairs.push({ range: range, continuationToken: undefined, filteringCondition: undefined, }); }); } return result; } /** * Creates a filter condition for ranges based on ORDER BY items and sort orders * This filter ensures that ranges only return documents based on their position relative to the continuation point * @param orderByItems - Array of order by items from the continuation token * @param queryInfo - Query information containing sort orders and other metadata * @param rangePosition - Whether this is for "left" or "right" ranges relative to continuation point * @returns SQL filter condition string for the specified range position */ createRangeFilterCondition(orderByItems, queryInfo, rangePosition) { // Extract sort orders from query info let sortOrders; try { sortOrders = this.extractSortOrders(queryInfo); } catch (error) { // If we can't extract sort orders, we cannot create reliable filter conditions throw new Error(`Unable to resume ORDER BY query from continuation token. The ORDER BY sort direction configuration ` + `in the query plan is invalid or missing. This may indicate a client version mismatch or corrupted continuation token. ` + `Please retry the query without a continuation token. Original error: ${error}`); } // Extract orderByExpressions from nested structure let orderByExpressions; if (queryInfo && queryInfo.queryInfo && typeof queryInfo.queryInfo === "object" && queryInfo.queryInfo.queryInfo && queryInfo.queryInfo.queryInfo.orderByExpressions && Array.isArray(queryInfo.queryInfo.queryInfo.orderByExpressions)) { orderByExpressions = queryInfo.queryInfo.queryInfo.orderByExpressions; } if (!orderByExpressions || !Array.isArray(orderByExpressions)) { throw new Error("Unable to resume ORDER BY query from continuation token. The ORDER BY field configuration " + "in the query plan is invalid or missing. This may indicate a client version mismatch or corrupted continuation token. " + "Please retry the query without a continuation token."); } const filterConditions = []; // Process each order by item to create filter conditions for (let i = 0; i < orderByItems.length && i < sortOrders.length; i++) { const orderByItem = orderByItems[i]; const sortOrder = sortOrders[i]; if (!orderByItem || orderByItem.item === undefined) { continue; } try { // Determine the field path from ORDER BY expressions in query plan const fieldPath = this.extractFieldPath(queryInfo, i); // Create the comparison condition based on sort order and range position const condition = this.createComparisonCondition(fieldPath, orderByItem.item, sortOrder, rangePosition); if (condition) { filterConditions.push(condition); } } catch (error) { // If we can't extract field path for ORDER BY expressions, we cannot safely resume from continuation token // This would lead to incorrect query results, so we must fail the entire request throw new Error(`Unable to resume ORDER BY query from continuation token. The ORDER BY field configuration ` + `in the query plan is invalid or incompatible with the continuation token format. ` + `This may indicate a client version mismatch or corrupted continuation token. ` + `Please retry the query without a continuation token. Original error: ${error}`); } } // Combine multiple conditions with AND for multi-field ORDER BY const combinedFilter = filterConditions.length > 0 ? `(${filterConditions.join(" AND ")})` : ""; return combinedFilter; } /** * Extracts sort orders from query info * @throws Error if sort order information is missing or invalid */ extractSortOrders(queryInfo) { if (!queryInfo) { throw new Error("Query information is required to determine ORDER BY sort directions"); } // Extract orderBy from the nested structure: queryInfo.queryInfo.queryInfo.orderBy let orderBy; if (queryInfo.queryInfo && typeof queryInfo.queryInfo === "object" && queryInfo.queryInfo.queryInfo && typeof queryInfo.queryInfo.queryInfo === "object" && queryInfo.queryInfo.queryInfo.orderBy && Array.isArray(queryInfo.queryInfo.queryInfo.orderBy)) { orderBy = queryInfo.queryInfo.queryInfo.orderBy; } if (!orderBy) { throw new Error("ORDER BY sort direction information is missing from query plan"); } return orderBy.map((order, index) => { if (typeof order === "string") { return order; } // Handle object format if needed if (order && typeof order === "object") { const sortOrder = order.direction || order.order || order.sortOrder; if (sortOrder) { return sortOrder; } } throw new Error(`ORDER BY sort direction at position ${index + 1} has an invalid format in the query plan`); }); } /** * Extracts field path from ORDER BY expressions in query plan * @throws Error if orderByExpressions are not found or index is out of bounds or expression format is invalid */ extractFieldPath(queryInfo, index) { // Try multiple paths to find orderByExpressions due to nested structure let orderByExpressions; if (queryInfo) { // Direct path if (queryInfo.orderByExpressions && Array.isArray(queryInfo.orderByExpressions)) { orderByExpressions = queryInfo.orderByExpressions; } // Nested path: queryInfo.queryInfo.queryInfo.orderByExpressions else if (queryInfo.queryInfo && typeof queryInfo.queryInfo === "object" && queryInfo.queryInfo.queryInfo && queryInfo.queryInfo.queryInfo.orderByExpressions && Array.isArray(queryInfo.queryInfo.queryInfo.orderByExpressions)) { orderByExpressions = queryInfo.queryInfo.queryInfo.orderByExpressions; } } if (!orderByExpressions) { throw new Error("ORDER BY field information is missing from query plan"); } if (index >= orderByExpressions.length) { throw new Error(`ORDER BY field configuration mismatch: expected at least ${index + 1} fields but found ${orderByExpressions.length}`); } const expression = orderByExpressions[index]; // Handle different formats of ORDER BY expressions if (typeof expression === "string") { // Simple string expression like "c.id" or "_FullTextScore(...)" return expression; } if (expression && typeof expression === "object") { // Object format like { expression: "c.id", type: "PropertyRef" } if (expression.expression) { return expression.expression; } if (expression.path) { return expression.path.replace(/^\//, ""); // Remove leading slash } if (expression.field) { return expression.field; } } throw new Error(`ORDER BY field at position ${index + 1} has an unrecognized format in the query plan`); } /** * Creates a comparison condition based on the field, value, sort order, and range position */ createComparisonCondition(fieldPath, value, sortOrder, rangePosition) { const isDescending = sortOrder.toLowerCase() === "descending" || sortOrder.toLowerCase() === "desc"; // For left ranges (ranges that come before the target): // - In ascending order: field > value (left ranges should seek for larger values) // - In descending order: field < value (left ranges should seek for smaller values) // For right ranges (ranges that come after the target): // - In ascending order: field >= value (right ranges have larger values) // - In descending order: field <= value (right ranges have smaller values in desc order) let operator; if (rangePosition === "left") { operator = isDescending ? "<" : ">"; } else { // right operator = isDescending ? "<=" : ">="; } // Format the value based on its type const formattedValue = this.formatValueForSQL(value); // Create the condition with proper field reference const condition = `${fieldPath} ${operator} ${formattedValue}`; return condition; } /** * Formats a value for use in SQL condition */ formatValueForSQL(value) { if (value === null || value === undefined) { return "null"; } const valueType = typeof value; switch (valueType) { case "string": // Escape single quotes and wrap in quotes return `'${value.toString().replace(/'/g, "''")}'`; case "number": case "bigint": return value.toString(); case "boolean": return value ? "true" : "false"; default: // For objects and arrays, convert to JSON string if (typeof value === "object") { return `'${JSON.stringify(value).replace(/'/g, "''")}'`; } return `'${value.toString().replace(/'/g, "''")}'`; } } /** * Compares partition key range boundaries with proper handling for inclusive/exclusive semantics * @param boundary1 - First boundary to compare * @param boundary2 - Second boundary to compare * @returns negative if boundary1 is less than boundary2, positive if boundary1 is greater than boundary2, 0 if equal */ comparePartitionKeyBoundaries(boundary1, boundary2) { // Handle empty string cases (empty string represents the minimum boundary) if (boundary1 === "" && boundary2 === "") return 0; if (boundary1 === "") return -1; // "" < "AA" if (boundary2 === "") return 1; // "AA" > "" // Use standard lexicographic comparison for non-empty boundaries return boundary1.localeCompare(boundary2); } isRangeBeforeAnother(range1MaxExclusive, range2MinInclusive) { // Since range1.maxExclusive is NOT part of range1, and range2.minInclusive IS part of range2, // range1 comes before range2 if range1.maxExclusive <= range2.minInclusive return this.comparePartitionKeyBoundaries(range1MaxExclusive, range2MinInclusive) <= 0; } isRangeAfterAnother(range1MinInclusive, range2MaxExclusive) { // Since range2.maxExclusive is NOT part of range2, and range1.minInclusive IS part of range1, // range1 comes after range2 if range1.minInclusive >= range2.maxExclusive return this.comparePartitionKeyBoundaries(range1MinInclusive, range2MaxExclusive) >= 0; } } //# sourceMappingURL=OrderByQueryRangeStrategy.js.map