@azure/cosmos
Version:
Microsoft Azure Cosmos DB Service Node.js SDK for NOSQL API
277 lines (276 loc) • 11.6 kB
JavaScript
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var OrderByQueryRangeStrategy_exports = {};
__export(OrderByQueryRangeStrategy_exports, {
OrderByQueryRangeStrategy: () => OrderByQueryRangeStrategy
});
module.exports = __toCommonJS(OrderByQueryRangeStrategy_exports);
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;
const targetRangeMapping = continuationRanges[continuationRanges.length - 1].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;
const leftFilter = this.createRangeFilterCondition(orderByItems, queryInfo, "left");
const rightRanges = targetRanges.filter(
(mapping) => this.isRangeAfterAnother(mapping.minInclusive, targetRangeMapping.maxExclusive)
);
const rightFilter = this.createRangeFilterCondition(orderByItems, queryInfo, "right");
if (leftRanges.length > 0) {
leftRanges.forEach((range) => {
result.rangeTokenPairs.push({
range,
continuationToken: void 0,
filteringCondition: leftFilter
});
});
}
result.rangeTokenPairs.push({
range: targetRange,
continuationToken: targetContinuationToken,
filteringCondition: rightFilter
});
if (rightRanges.length > 0) {
rightRanges.forEach((range) => {
result.rangeTokenPairs.push({
range,
continuationToken: void 0,
filteringCondition: rightFilter
});
});
}
}
if (!resumeRangeFound) {
filteredRanges = [...targetRanges];
filteredRanges.forEach((range) => {
result.rangeTokenPairs.push({
range,
continuationToken: void 0,
filteringCondition: void 0
});
});
}
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) {
let sortOrders;
try {
sortOrders = this.extractSortOrders(queryInfo);
} catch (error) {
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}`
);
}
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 = [];
for (let i = 0; i < orderByItems.length && i < sortOrders.length; i++) {
const orderByItem = orderByItems[i];
const sortOrder = sortOrders[i];
if (!orderByItem || orderByItem.item === void 0) {
continue;
}
try {
const fieldPath = this.extractFieldPath(queryInfo, i);
const condition = this.createComparisonCondition(
fieldPath,
orderByItem.item,
sortOrder,
rangePosition
);
if (condition) {
filterConditions.push(condition);
}
} catch (error) {
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}`
);
}
}
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");
}
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;
}
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) {
let orderByExpressions;
if (queryInfo) {
if (queryInfo.orderByExpressions && Array.isArray(queryInfo.orderByExpressions)) {
orderByExpressions = 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];
if (typeof expression === "string") {
return expression;
}
if (expression && typeof expression === "object") {
if (expression.expression) {
return expression.expression;
}
if (expression.path) {
return expression.path.replace(/^\//, "");
}
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";
let operator;
if (rangePosition === "left") {
operator = isDescending ? "<" : ">";
} else {
operator = isDescending ? "<=" : ">=";
}
const formattedValue = this.formatValueForSQL(value);
const condition = `${fieldPath} ${operator} ${formattedValue}`;
return condition;
}
/**
* Formats a value for use in SQL condition
*/
formatValueForSQL(value) {
if (value === null || value === void 0) {
return "null";
}
const valueType = typeof value;
switch (valueType) {
case "string":
return `'${value.toString().replace(/'/g, "''")}'`;
case "number":
case "bigint":
return value.toString();
case "boolean":
return value ? "true" : "false";
default:
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) {
if (boundary1 === "" && boundary2 === "") return 0;
if (boundary1 === "") return -1;
if (boundary2 === "") return 1;
return boundary1.localeCompare(boundary2);
}
isRangeBeforeAnother(range1MaxExclusive, range2MinInclusive) {
return this.comparePartitionKeyBoundaries(range1MaxExclusive, range2MinInclusive) <= 0;
}
isRangeAfterAnother(range1MinInclusive, range2MaxExclusive) {
return this.comparePartitionKeyBoundaries(range1MinInclusive, range2MaxExclusive) >= 0;
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
OrderByQueryRangeStrategy
});