@tanstack/db 0.4.20 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/collection/change-events.cjs +10 -12
- package/dist/cjs/collection/change-events.cjs.map +1 -1
- package/dist/cjs/collection/change-events.d.cts +1 -8
- package/dist/cjs/collection/index.cjs +18 -0
- package/dist/cjs/collection/index.cjs.map +1 -1
- package/dist/cjs/collection/index.d.cts +7 -5
- package/dist/cjs/index.cjs +21 -3
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +2 -0
- package/dist/cjs/indexes/auto-index.cjs +7 -3
- package/dist/cjs/indexes/auto-index.cjs.map +1 -1
- package/dist/cjs/local-storage.cjs.map +1 -1
- package/dist/cjs/local-storage.d.cts +2 -2
- package/dist/cjs/query/builder/functions.cjs +34 -0
- package/dist/cjs/query/builder/functions.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.d.cts +5 -0
- package/dist/cjs/query/builder/index.cjs +2 -2
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/query/builder/types.d.cts +3 -22
- package/dist/cjs/query/compiler/evaluators.cjs +57 -4
- package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
- package/dist/cjs/query/compiler/evaluators.d.cts +13 -0
- package/dist/cjs/query/compiler/expressions.cjs +4 -1
- package/dist/cjs/query/compiler/expressions.cjs.map +1 -1
- package/dist/cjs/query/compiler/group-by.cjs +3 -3
- package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.cjs +2 -2
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.cjs +18 -6
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.d.cts +7 -1
- package/dist/cjs/query/expression-helpers.cjs +217 -0
- package/dist/cjs/query/expression-helpers.cjs.map +1 -0
- package/dist/cjs/query/expression-helpers.d.cts +216 -0
- package/dist/cjs/query/index.d.cts +2 -0
- package/dist/cjs/query/live/collection-config-builder.cjs +13 -0
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.d.cts +1 -0
- package/dist/cjs/query/live/types.d.cts +6 -1
- package/dist/cjs/query/predicate-utils.cjs +816 -0
- package/dist/cjs/query/predicate-utils.cjs.map +1 -0
- package/dist/cjs/query/predicate-utils.d.cts +116 -0
- package/dist/cjs/query/subset-dedupe.cjs +111 -0
- package/dist/cjs/query/subset-dedupe.cjs.map +1 -0
- package/dist/cjs/query/subset-dedupe.d.cts +66 -0
- package/dist/cjs/types.d.cts +29 -0
- package/dist/cjs/utils/comparison.cjs +30 -0
- package/dist/cjs/utils/comparison.cjs.map +1 -1
- package/dist/cjs/utils/comparison.d.cts +7 -1
- package/dist/cjs/utils/index-optimization.cjs +26 -22
- package/dist/cjs/utils/index-optimization.cjs.map +1 -1
- package/dist/cjs/utils/index-optimization.d.cts +5 -4
- package/dist/esm/collection/change-events.d.ts +1 -8
- package/dist/esm/collection/change-events.js +7 -9
- package/dist/esm/collection/change-events.js.map +1 -1
- package/dist/esm/collection/index.d.ts +7 -5
- package/dist/esm/collection/index.js +18 -0
- package/dist/esm/collection/index.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +19 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/indexes/auto-index.js +7 -3
- package/dist/esm/indexes/auto-index.js.map +1 -1
- package/dist/esm/local-storage.d.ts +2 -2
- package/dist/esm/local-storage.js.map +1 -1
- package/dist/esm/query/builder/functions.d.ts +5 -0
- package/dist/esm/query/builder/functions.js +34 -0
- package/dist/esm/query/builder/functions.js.map +1 -1
- package/dist/esm/query/builder/index.js +2 -2
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/query/builder/types.d.ts +3 -22
- package/dist/esm/query/compiler/evaluators.d.ts +13 -0
- package/dist/esm/query/compiler/evaluators.js +59 -6
- package/dist/esm/query/compiler/evaluators.js.map +1 -1
- package/dist/esm/query/compiler/expressions.js +4 -1
- package/dist/esm/query/compiler/expressions.js.map +1 -1
- package/dist/esm/query/compiler/group-by.js +4 -4
- package/dist/esm/query/compiler/group-by.js.map +1 -1
- package/dist/esm/query/compiler/index.js +3 -3
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/order-by.d.ts +7 -1
- package/dist/esm/query/compiler/order-by.js +18 -6
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/expression-helpers.d.ts +216 -0
- package/dist/esm/query/expression-helpers.js +217 -0
- package/dist/esm/query/expression-helpers.js.map +1 -0
- package/dist/esm/query/index.d.ts +2 -0
- package/dist/esm/query/live/collection-config-builder.d.ts +1 -0
- package/dist/esm/query/live/collection-config-builder.js +13 -0
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/query/live/types.d.ts +6 -1
- package/dist/esm/query/predicate-utils.d.ts +116 -0
- package/dist/esm/query/predicate-utils.js +816 -0
- package/dist/esm/query/predicate-utils.js.map +1 -0
- package/dist/esm/query/subset-dedupe.d.ts +66 -0
- package/dist/esm/query/subset-dedupe.js +111 -0
- package/dist/esm/query/subset-dedupe.js.map +1 -0
- package/dist/esm/types.d.ts +29 -0
- package/dist/esm/utils/comparison.d.ts +7 -1
- package/dist/esm/utils/comparison.js +30 -0
- package/dist/esm/utils/comparison.js.map +1 -1
- package/dist/esm/utils/index-optimization.d.ts +5 -4
- package/dist/esm/utils/index-optimization.js +26 -22
- package/dist/esm/utils/index-optimization.js.map +1 -1
- package/package.json +2 -2
- package/src/collection/change-events.ts +14 -24
- package/src/collection/index.ts +32 -4
- package/src/index.ts +4 -0
- package/src/indexes/auto-index.ts +8 -4
- package/src/local-storage.ts +11 -3
- package/src/query/builder/functions.ts +39 -0
- package/src/query/builder/index.ts +2 -2
- package/src/query/builder/types.ts +3 -25
- package/src/query/compiler/evaluators.ts +103 -5
- package/src/query/compiler/expressions.ts +3 -0
- package/src/query/compiler/group-by.ts +4 -4
- package/src/query/compiler/index.ts +3 -3
- package/src/query/compiler/order-by.ts +33 -7
- package/src/query/expression-helpers.ts +522 -0
- package/src/query/index.ts +12 -0
- package/src/query/live/collection-config-builder.ts +27 -0
- package/src/query/live/types.ts +11 -1
- package/src/query/predicate-utils.ts +1415 -0
- package/src/query/subset-dedupe.ts +243 -0
- package/src/types.ts +39 -0
- package/src/utils/comparison.ts +70 -1
- package/src/utils/index-optimization.ts +77 -63
|
@@ -16,7 +16,7 @@ function processOrderBy(rawQuery, pipeline, orderByClause, selectClause, collect
|
|
|
16
16
|
);
|
|
17
17
|
return {
|
|
18
18
|
compiledExpression: evaluators.compileExpression(clauseWithoutAggregates),
|
|
19
|
-
compareOptions: clause
|
|
19
|
+
compareOptions: buildCompareOptions(clause, collection)
|
|
20
20
|
};
|
|
21
21
|
});
|
|
22
22
|
const valueExtractor = (row) => {
|
|
@@ -36,7 +36,7 @@ function processOrderBy(rawQuery, pipeline, orderByClause, selectClause, collect
|
|
|
36
36
|
const arrayA = a;
|
|
37
37
|
const arrayB = b;
|
|
38
38
|
for (let i = 0; i < orderByClause.length; i++) {
|
|
39
|
-
const clause =
|
|
39
|
+
const clause = compiledOrderBy[i];
|
|
40
40
|
const compareFn = comparison.makeComparator(clause.compareOptions);
|
|
41
41
|
const result = compareFn(arrayA[i], arrayB[i]);
|
|
42
42
|
if (result !== 0) {
|
|
@@ -46,7 +46,7 @@ function processOrderBy(rawQuery, pipeline, orderByClause, selectClause, collect
|
|
|
46
46
|
return arrayA.length - arrayB.length;
|
|
47
47
|
}
|
|
48
48
|
if (orderByClause.length === 1) {
|
|
49
|
-
const clause =
|
|
49
|
+
const clause = compiledOrderBy[0];
|
|
50
50
|
const compareFn = comparison.makeComparator(clause.compareOptions);
|
|
51
51
|
return compareFn(a, b);
|
|
52
52
|
}
|
|
@@ -65,12 +65,13 @@ function processOrderBy(rawQuery, pipeline, orderByClause, selectClause, collect
|
|
|
65
65
|
);
|
|
66
66
|
const followRefCollection = followRefResult.collection;
|
|
67
67
|
const fieldName = followRefResult.path[0];
|
|
68
|
+
const compareOpts = buildCompareOptions(clause, followRefCollection);
|
|
68
69
|
if (fieldName) {
|
|
69
70
|
autoIndex.ensureIndexForField(
|
|
70
71
|
fieldName,
|
|
71
72
|
followRefResult.path,
|
|
72
73
|
followRefCollection,
|
|
73
|
-
|
|
74
|
+
compareOpts,
|
|
74
75
|
compare
|
|
75
76
|
);
|
|
76
77
|
}
|
|
@@ -84,9 +85,9 @@ function processOrderBy(rawQuery, pipeline, orderByClause, selectClause, collect
|
|
|
84
85
|
return compare(extractedA, extractedB);
|
|
85
86
|
};
|
|
86
87
|
const index = indexOptimization.findIndexForField(
|
|
87
|
-
followRefCollection
|
|
88
|
+
followRefCollection,
|
|
88
89
|
followRefResult.path,
|
|
89
|
-
|
|
90
|
+
compareOpts
|
|
90
91
|
);
|
|
91
92
|
if (index && index.supports(`gt`)) {
|
|
92
93
|
const orderByAlias = orderByExpression.path.length > 1 ? String(orderByExpression.path[0]) : rawQuery.from.alias;
|
|
@@ -132,5 +133,16 @@ function processOrderBy(rawQuery, pipeline, orderByClause, selectClause, collect
|
|
|
132
133
|
// orderByWithFractionalIndex returns [key, [value, index]] - we keep this format
|
|
133
134
|
);
|
|
134
135
|
}
|
|
136
|
+
function buildCompareOptions(clause, collection) {
|
|
137
|
+
if (clause.compareOptions.stringSort !== void 0) {
|
|
138
|
+
return clause.compareOptions;
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
...collection.compareOptions,
|
|
142
|
+
direction: clause.compareOptions.direction,
|
|
143
|
+
nulls: clause.compareOptions.nulls
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
exports.buildCompareOptions = buildCompareOptions;
|
|
135
147
|
exports.processOrderBy = processOrderBy;
|
|
136
148
|
//# sourceMappingURL=order-by.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"order-by.cjs","sources":["../../../../src/query/compiler/order-by.ts"],"sourcesContent":["import { orderByWithFractionalIndex } from \"@tanstack/db-ivm\"\nimport { defaultComparator, makeComparator } from \"../../utils/comparison.js\"\nimport { PropRef, followRef } from \"../ir.js\"\nimport { ensureIndexForField } from \"../../indexes/auto-index.js\"\nimport { findIndexForField } from \"../../utils/index-optimization.js\"\nimport { compileExpression } from \"./evaluators.js\"\nimport { replaceAggregatesByRefs } from \"./group-by.js\"\nimport type { WindowOptions } from \"./types.js\"\nimport type { CompiledSingleRowExpression } from \"./evaluators.js\"\nimport type { OrderBy, OrderByClause, QueryIR, Select } from \"../ir.js\"\nimport type { NamespacedAndKeyedStream, NamespacedRow } from \"../../types.js\"\nimport type { IStreamBuilder, KeyValue } from \"@tanstack/db-ivm\"\nimport type { IndexInterface } from \"../../indexes/base-index.js\"\nimport type { Collection } from \"../../collection/index.js\"\n\nexport type OrderByOptimizationInfo = {\n alias: string\n orderBy: OrderBy\n offset: number\n limit: number\n comparator: (\n a: Record<string, unknown> | null | undefined,\n b: Record<string, unknown> | null | undefined\n ) => number\n valueExtractorForRawRow: (row: Record<string, unknown>) => any\n index: IndexInterface<string | number>\n dataNeeded?: () => number\n}\n\n/**\n * Processes the ORDER BY clause\n * Works with the new structure that has both namespaced row data and __select_results\n * Always uses fractional indexing and adds the index as __ordering_index to the result\n */\nexport function processOrderBy(\n rawQuery: QueryIR,\n pipeline: NamespacedAndKeyedStream,\n orderByClause: Array<OrderByClause>,\n selectClause: Select,\n collection: Collection,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n limit?: number,\n offset?: number\n): IStreamBuilder<KeyValue<unknown, [NamespacedRow, string]>> {\n // Pre-compile all order by expressions\n const compiledOrderBy = orderByClause.map((clause) => {\n const clauseWithoutAggregates = replaceAggregatesByRefs(\n clause.expression,\n selectClause,\n `__select_results`\n )\n return {\n compiledExpression: compileExpression(clauseWithoutAggregates),\n compareOptions: clause.compareOptions,\n }\n })\n\n // Create a value extractor function for the orderBy operator\n const valueExtractor = (row: NamespacedRow & { __select_results?: any }) => {\n // The namespaced row contains:\n // 1. Table aliases as top-level properties (e.g., row[\"tableName\"])\n // 2. SELECT results in __select_results (e.g., row.__select_results[\"aggregateAlias\"])\n // The replaceAggregatesByRefs function has already transformed any aggregate expressions\n // that match SELECT aggregates to use the __select_results namespace.\n const orderByContext = row\n\n if (orderByClause.length > 1) {\n // For multiple orderBy columns, create a composite key\n return compiledOrderBy.map((compiled) =>\n compiled.compiledExpression(orderByContext)\n )\n } else if (orderByClause.length === 1) {\n // For a single orderBy column, use the value directly\n const compiled = compiledOrderBy[0]!\n return compiled.compiledExpression(orderByContext)\n }\n\n // Default case - no ordering\n return null\n }\n\n // Create a multi-property comparator that respects the order and direction of each property\n const compare = (a: unknown, b: unknown) => {\n // If we're comparing arrays (multiple properties), compare each property in order\n if (orderByClause.length > 1) {\n const arrayA = a as Array<unknown>\n const arrayB = b as Array<unknown>\n for (let i = 0; i < orderByClause.length; i++) {\n const clause = orderByClause[i]!\n const compareFn = makeComparator(clause.compareOptions)\n const result = compareFn(arrayA[i], arrayB[i])\n if (result !== 0) {\n return result\n }\n }\n return arrayA.length - arrayB.length\n }\n\n // Single property comparison\n if (orderByClause.length === 1) {\n const clause = orderByClause[0]!\n const compareFn = makeComparator(clause.compareOptions)\n return compareFn(a, b)\n }\n\n return defaultComparator(a, b)\n }\n\n let setSizeCallback: ((getSize: () => number) => void) | undefined\n\n let orderByOptimizationInfo: OrderByOptimizationInfo | undefined\n\n // Optimize the orderBy operator to lazily load elements\n // by using the range index of the collection.\n // Only for orderBy clause on a single column for now (no composite ordering)\n if (limit && orderByClause.length === 1) {\n const clause = orderByClause[0]!\n const orderByExpression = clause.expression\n\n if (orderByExpression.type === `ref`) {\n const followRefResult = followRef(\n rawQuery,\n orderByExpression,\n collection\n )!\n\n const followRefCollection = followRefResult.collection\n const fieldName = followRefResult.path[0]\n if (fieldName) {\n ensureIndexForField(\n fieldName,\n followRefResult.path,\n followRefCollection,\n clause.compareOptions,\n compare\n )\n }\n\n const valueExtractorForRawRow = compileExpression(\n new PropRef(followRefResult.path),\n true\n ) as CompiledSingleRowExpression\n\n const comparator = (\n a: Record<string, unknown> | null | undefined,\n b: Record<string, unknown> | null | undefined\n ) => {\n const extractedA = a ? valueExtractorForRawRow(a) : a\n const extractedB = b ? valueExtractorForRawRow(b) : b\n return compare(extractedA, extractedB)\n }\n\n const index: IndexInterface<string | number> | undefined =\n findIndexForField(\n followRefCollection.indexes,\n followRefResult.path,\n clause.compareOptions\n )\n\n if (index && index.supports(`gt`)) {\n // We found an index that we can use to lazily load ordered data\n const orderByAlias =\n orderByExpression.path.length > 1\n ? String(orderByExpression.path[0])\n : rawQuery.from.alias\n\n orderByOptimizationInfo = {\n alias: orderByAlias,\n offset: offset ?? 0,\n limit,\n comparator,\n valueExtractorForRawRow,\n index,\n orderBy: orderByClause,\n }\n\n optimizableOrderByCollections[followRefCollection.id] =\n orderByOptimizationInfo\n\n setSizeCallback = (getSize: () => number) => {\n optimizableOrderByCollections[followRefCollection.id]![`dataNeeded`] =\n () => {\n const size = getSize()\n return Math.max(0, orderByOptimizationInfo!.limit - size)\n }\n }\n }\n }\n }\n\n // Use fractional indexing and return the tuple [value, index]\n return pipeline.pipe(\n orderByWithFractionalIndex(valueExtractor, {\n limit,\n offset,\n comparator: compare,\n setSizeCallback,\n setWindowFn: (\n windowFn: (options: { offset?: number; limit?: number }) => void\n ) => {\n setWindowFn(\n // We wrap the move function such that we update the orderByOptimizationInfo\n // because that is used by the `dataNeeded` callback to determine if we need to load more data\n (options) => {\n windowFn(options)\n if (orderByOptimizationInfo) {\n orderByOptimizationInfo.offset =\n options.offset ?? orderByOptimizationInfo.offset\n orderByOptimizationInfo.limit =\n options.limit ?? orderByOptimizationInfo.limit\n }\n }\n )\n },\n })\n // orderByWithFractionalIndex returns [key, [value, index]] - we keep this format\n )\n}\n"],"names":["replaceAggregatesByRefs","compileExpression","makeComparator","defaultComparator","followRef","ensureIndexForField","PropRef","findIndexForField","orderByWithFractionalIndex"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"order-by.cjs","sources":["../../../../src/query/compiler/order-by.ts"],"sourcesContent":["import { orderByWithFractionalIndex } from \"@tanstack/db-ivm\"\nimport { defaultComparator, makeComparator } from \"../../utils/comparison.js\"\nimport { PropRef, followRef } from \"../ir.js\"\nimport { ensureIndexForField } from \"../../indexes/auto-index.js\"\nimport { findIndexForField } from \"../../utils/index-optimization.js\"\nimport { compileExpression } from \"./evaluators.js\"\nimport { replaceAggregatesByRefs } from \"./group-by.js\"\nimport type { CompareOptions } from \"../builder/types.js\"\nimport type { WindowOptions } from \"./types.js\"\nimport type { CompiledSingleRowExpression } from \"./evaluators.js\"\nimport type { OrderBy, OrderByClause, QueryIR, Select } from \"../ir.js\"\nimport type {\n CollectionLike,\n NamespacedAndKeyedStream,\n NamespacedRow,\n} from \"../../types.js\"\nimport type { IStreamBuilder, KeyValue } from \"@tanstack/db-ivm\"\nimport type { IndexInterface } from \"../../indexes/base-index.js\"\nimport type { Collection } from \"../../collection/index.js\"\n\nexport type OrderByOptimizationInfo = {\n alias: string\n orderBy: OrderBy\n offset: number\n limit: number\n comparator: (\n a: Record<string, unknown> | null | undefined,\n b: Record<string, unknown> | null | undefined\n ) => number\n valueExtractorForRawRow: (row: Record<string, unknown>) => any\n index: IndexInterface<string | number>\n dataNeeded?: () => number\n}\n\n/**\n * Processes the ORDER BY clause\n * Works with the new structure that has both namespaced row data and __select_results\n * Always uses fractional indexing and adds the index as __ordering_index to the result\n */\nexport function processOrderBy(\n rawQuery: QueryIR,\n pipeline: NamespacedAndKeyedStream,\n orderByClause: Array<OrderByClause>,\n selectClause: Select,\n collection: Collection,\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,\n setWindowFn: (windowFn: (options: WindowOptions) => void) => void,\n limit?: number,\n offset?: number\n): IStreamBuilder<KeyValue<unknown, [NamespacedRow, string]>> {\n // Pre-compile all order by expressions\n const compiledOrderBy = orderByClause.map((clause) => {\n const clauseWithoutAggregates = replaceAggregatesByRefs(\n clause.expression,\n selectClause,\n `__select_results`\n )\n\n return {\n compiledExpression: compileExpression(clauseWithoutAggregates),\n compareOptions: buildCompareOptions(clause, collection),\n }\n })\n\n // Create a value extractor function for the orderBy operator\n const valueExtractor = (row: NamespacedRow & { __select_results?: any }) => {\n // The namespaced row contains:\n // 1. Table aliases as top-level properties (e.g., row[\"tableName\"])\n // 2. SELECT results in __select_results (e.g., row.__select_results[\"aggregateAlias\"])\n // The replaceAggregatesByRefs function has already transformed any aggregate expressions\n // that match SELECT aggregates to use the __select_results namespace.\n const orderByContext = row\n\n if (orderByClause.length > 1) {\n // For multiple orderBy columns, create a composite key\n return compiledOrderBy.map((compiled) =>\n compiled.compiledExpression(orderByContext)\n )\n } else if (orderByClause.length === 1) {\n // For a single orderBy column, use the value directly\n const compiled = compiledOrderBy[0]!\n return compiled.compiledExpression(orderByContext)\n }\n\n // Default case - no ordering\n return null\n }\n\n // Create a multi-property comparator that respects the order and direction of each property\n const compare = (a: unknown, b: unknown) => {\n // If we're comparing arrays (multiple properties), compare each property in order\n if (orderByClause.length > 1) {\n const arrayA = a as Array<unknown>\n const arrayB = b as Array<unknown>\n for (let i = 0; i < orderByClause.length; i++) {\n const clause = compiledOrderBy[i]!\n const compareFn = makeComparator(clause.compareOptions)\n const result = compareFn(arrayA[i], arrayB[i])\n if (result !== 0) {\n return result\n }\n }\n return arrayA.length - arrayB.length\n }\n\n // Single property comparison\n if (orderByClause.length === 1) {\n const clause = compiledOrderBy[0]!\n const compareFn = makeComparator(clause.compareOptions)\n return compareFn(a, b)\n }\n\n return defaultComparator(a, b)\n }\n\n let setSizeCallback: ((getSize: () => number) => void) | undefined\n\n let orderByOptimizationInfo: OrderByOptimizationInfo | undefined\n\n // Optimize the orderBy operator to lazily load elements\n // by using the range index of the collection.\n // Only for orderBy clause on a single column for now (no composite ordering)\n if (limit && orderByClause.length === 1) {\n const clause = orderByClause[0]!\n const orderByExpression = clause.expression\n\n if (orderByExpression.type === `ref`) {\n const followRefResult = followRef(\n rawQuery,\n orderByExpression,\n collection\n )!\n\n const followRefCollection = followRefResult.collection\n const fieldName = followRefResult.path[0]\n const compareOpts = buildCompareOptions(clause, followRefCollection)\n if (fieldName) {\n ensureIndexForField(\n fieldName,\n followRefResult.path,\n followRefCollection,\n compareOpts,\n compare\n )\n }\n\n const valueExtractorForRawRow = compileExpression(\n new PropRef(followRefResult.path),\n true\n ) as CompiledSingleRowExpression\n\n const comparator = (\n a: Record<string, unknown> | null | undefined,\n b: Record<string, unknown> | null | undefined\n ) => {\n const extractedA = a ? valueExtractorForRawRow(a) : a\n const extractedB = b ? valueExtractorForRawRow(b) : b\n return compare(extractedA, extractedB)\n }\n\n const index: IndexInterface<string | number> | undefined =\n findIndexForField(\n followRefCollection,\n followRefResult.path,\n compareOpts\n )\n\n if (index && index.supports(`gt`)) {\n // We found an index that we can use to lazily load ordered data\n const orderByAlias =\n orderByExpression.path.length > 1\n ? String(orderByExpression.path[0])\n : rawQuery.from.alias\n\n orderByOptimizationInfo = {\n alias: orderByAlias,\n offset: offset ?? 0,\n limit,\n comparator,\n valueExtractorForRawRow,\n index,\n orderBy: orderByClause,\n }\n\n optimizableOrderByCollections[followRefCollection.id] =\n orderByOptimizationInfo\n\n setSizeCallback = (getSize: () => number) => {\n optimizableOrderByCollections[followRefCollection.id]![`dataNeeded`] =\n () => {\n const size = getSize()\n return Math.max(0, orderByOptimizationInfo!.limit - size)\n }\n }\n }\n }\n }\n\n // Use fractional indexing and return the tuple [value, index]\n return pipeline.pipe(\n orderByWithFractionalIndex(valueExtractor, {\n limit,\n offset,\n comparator: compare,\n setSizeCallback,\n setWindowFn: (\n windowFn: (options: { offset?: number; limit?: number }) => void\n ) => {\n setWindowFn(\n // We wrap the move function such that we update the orderByOptimizationInfo\n // because that is used by the `dataNeeded` callback to determine if we need to load more data\n (options) => {\n windowFn(options)\n if (orderByOptimizationInfo) {\n orderByOptimizationInfo.offset =\n options.offset ?? orderByOptimizationInfo.offset\n orderByOptimizationInfo.limit =\n options.limit ?? orderByOptimizationInfo.limit\n }\n }\n )\n },\n })\n // orderByWithFractionalIndex returns [key, [value, index]] - we keep this format\n )\n}\n\n/**\n * Builds a comparison configuration object that uses the values provided in the orderBy clause.\n * If no string sort configuration is provided it defaults to the collection's string sort configuration.\n */\nexport function buildCompareOptions(\n clause: OrderByClause,\n collection: CollectionLike<any, any>\n): CompareOptions {\n if (clause.compareOptions.stringSort !== undefined) {\n return clause.compareOptions\n }\n\n return {\n ...collection.compareOptions,\n direction: clause.compareOptions.direction,\n nulls: clause.compareOptions.nulls,\n }\n}\n"],"names":["replaceAggregatesByRefs","compileExpression","makeComparator","defaultComparator","followRef","ensureIndexForField","PropRef","findIndexForField","orderByWithFractionalIndex"],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { CompareOptions } from '../builder/types.js';
|
|
1
2
|
import { WindowOptions } from './types.js';
|
|
2
3
|
import { OrderBy, OrderByClause, QueryIR, Select } from '../ir.js';
|
|
3
|
-
import { NamespacedAndKeyedStream, NamespacedRow } from '../../types.js';
|
|
4
|
+
import { CollectionLike, NamespacedAndKeyedStream, NamespacedRow } from '../../types.js';
|
|
4
5
|
import { IStreamBuilder, KeyValue } from '@tanstack/db-ivm';
|
|
5
6
|
import { IndexInterface } from '../../indexes/base-index.js';
|
|
6
7
|
import { Collection } from '../../collection/index.js';
|
|
@@ -20,3 +21,8 @@ export type OrderByOptimizationInfo = {
|
|
|
20
21
|
* Always uses fractional indexing and adds the index as __ordering_index to the result
|
|
21
22
|
*/
|
|
22
23
|
export declare function processOrderBy(rawQuery: QueryIR, pipeline: NamespacedAndKeyedStream, orderByClause: Array<OrderByClause>, selectClause: Select, collection: Collection, optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>, setWindowFn: (windowFn: (options: WindowOptions) => void) => void, limit?: number, offset?: number): IStreamBuilder<KeyValue<unknown, [NamespacedRow, string]>>;
|
|
24
|
+
/**
|
|
25
|
+
* Builds a comparison configuration object that uses the values provided in the orderBy clause.
|
|
26
|
+
* If no string sort configuration is provided it defaults to the collection's string sort configuration.
|
|
27
|
+
*/
|
|
28
|
+
export declare function buildCompareOptions(clause: OrderByClause, collection: CollectionLike<any, any>): CompareOptions;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
function extractFieldPath(expr) {
|
|
4
|
+
if (expr.type === `ref`) {
|
|
5
|
+
return expr.path;
|
|
6
|
+
}
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
function extractValue(expr) {
|
|
10
|
+
if (expr.type === `val`) {
|
|
11
|
+
return expr.value;
|
|
12
|
+
}
|
|
13
|
+
return void 0;
|
|
14
|
+
}
|
|
15
|
+
function walkExpression(expr, visitor) {
|
|
16
|
+
if (!expr) return;
|
|
17
|
+
visitor(expr);
|
|
18
|
+
if (expr.type === `func`) {
|
|
19
|
+
expr.args.forEach((arg) => walkExpression(arg, visitor));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function parseWhereExpression(expr, options) {
|
|
23
|
+
if (!expr) return null;
|
|
24
|
+
const { handlers, onUnknownOperator } = options;
|
|
25
|
+
if (expr.type === `val`) {
|
|
26
|
+
return expr.value;
|
|
27
|
+
}
|
|
28
|
+
if (expr.type === `ref`) {
|
|
29
|
+
return expr.path;
|
|
30
|
+
}
|
|
31
|
+
const { name, args } = expr;
|
|
32
|
+
const handler = handlers[name];
|
|
33
|
+
if (!handler) {
|
|
34
|
+
if (onUnknownOperator) {
|
|
35
|
+
return onUnknownOperator(name, args);
|
|
36
|
+
}
|
|
37
|
+
throw new Error(
|
|
38
|
+
`No handler provided for operator: ${name}. Available handlers: ${Object.keys(handlers).join(`, `)}`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
const parsedArgs = args.map((arg) => {
|
|
42
|
+
if (arg.type === `ref`) {
|
|
43
|
+
return arg.path;
|
|
44
|
+
}
|
|
45
|
+
if (arg.type === `val`) {
|
|
46
|
+
return arg.value;
|
|
47
|
+
}
|
|
48
|
+
return parseWhereExpression(arg, options);
|
|
49
|
+
});
|
|
50
|
+
return handler(...parsedArgs);
|
|
51
|
+
}
|
|
52
|
+
function parseOrderByExpression(orderBy) {
|
|
53
|
+
if (!orderBy || orderBy.length === 0) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
return orderBy.map((clause) => {
|
|
57
|
+
const field = extractFieldPath(clause.expression);
|
|
58
|
+
if (!field) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`ORDER BY expression must be a field reference, got: ${clause.expression.type}`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const { direction, nulls } = clause.compareOptions;
|
|
64
|
+
const result = {
|
|
65
|
+
field,
|
|
66
|
+
direction,
|
|
67
|
+
nulls
|
|
68
|
+
};
|
|
69
|
+
if (`stringSort` in clause.compareOptions) {
|
|
70
|
+
result.stringSort = clause.compareOptions.stringSort;
|
|
71
|
+
}
|
|
72
|
+
if (`locale` in clause.compareOptions) {
|
|
73
|
+
result.locale = clause.compareOptions.locale;
|
|
74
|
+
}
|
|
75
|
+
if (`localeOptions` in clause.compareOptions) {
|
|
76
|
+
result.localeOptions = clause.compareOptions.localeOptions;
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function extractSimpleComparisons(expr) {
|
|
82
|
+
if (!expr) return [];
|
|
83
|
+
const comparisons = [];
|
|
84
|
+
function extract(e) {
|
|
85
|
+
if (e.type === `func`) {
|
|
86
|
+
if (e.name === `and`) {
|
|
87
|
+
e.args.forEach((arg) => extract(arg));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (e.name === `not`) {
|
|
91
|
+
const [arg] = e.args;
|
|
92
|
+
if (!arg || arg.type !== `func`) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`extractSimpleComparisons requires a comparison or null check inside 'not' operator.`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
const nullCheckOps2 = [`isNull`, `isUndefined`];
|
|
98
|
+
if (nullCheckOps2.includes(arg.name)) {
|
|
99
|
+
const [fieldArg] = arg.args;
|
|
100
|
+
const field = fieldArg?.type === `ref` ? fieldArg.path : null;
|
|
101
|
+
if (field) {
|
|
102
|
+
comparisons.push({
|
|
103
|
+
field,
|
|
104
|
+
operator: `not_${arg.name}`
|
|
105
|
+
// No value for null/undefined checks
|
|
106
|
+
});
|
|
107
|
+
} else {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`extractSimpleComparisons requires a field reference for '${arg.name}' operator.`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const comparisonOps2 = [`eq`, `gt`, `gte`, `lt`, `lte`, `in`];
|
|
115
|
+
if (comparisonOps2.includes(arg.name)) {
|
|
116
|
+
const [leftArg, rightArg] = arg.args;
|
|
117
|
+
const field = leftArg?.type === `ref` ? leftArg.path : null;
|
|
118
|
+
const value = rightArg?.type === `val` ? rightArg.value : null;
|
|
119
|
+
if (field && value !== void 0) {
|
|
120
|
+
comparisons.push({
|
|
121
|
+
field,
|
|
122
|
+
operator: `not_${arg.name}`,
|
|
123
|
+
value
|
|
124
|
+
});
|
|
125
|
+
} else {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`extractSimpleComparisons requires simple field-value comparisons. Found complex expression for 'not(${arg.name})' operator.`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
throw new Error(
|
|
133
|
+
`extractSimpleComparisons does not support 'not(${arg.name})'. NOT can only wrap comparison operators (eq, gt, gte, lt, lte, in) or null checks (isNull, isUndefined).`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
const unsupportedOps = [
|
|
137
|
+
`or`,
|
|
138
|
+
`like`,
|
|
139
|
+
`ilike`,
|
|
140
|
+
`upper`,
|
|
141
|
+
`lower`,
|
|
142
|
+
`length`,
|
|
143
|
+
`concat`,
|
|
144
|
+
`add`,
|
|
145
|
+
`coalesce`,
|
|
146
|
+
`count`,
|
|
147
|
+
`avg`,
|
|
148
|
+
`sum`,
|
|
149
|
+
`min`,
|
|
150
|
+
`max`
|
|
151
|
+
];
|
|
152
|
+
if (unsupportedOps.includes(e.name)) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`extractSimpleComparisons does not support '${e.name}' operator. Use parseWhereExpression with custom handlers for complex expressions.`
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
const nullCheckOps = [`isNull`, `isUndefined`];
|
|
158
|
+
if (nullCheckOps.includes(e.name)) {
|
|
159
|
+
const [fieldArg] = e.args;
|
|
160
|
+
const field = fieldArg?.type === `ref` ? fieldArg.path : null;
|
|
161
|
+
if (field) {
|
|
162
|
+
comparisons.push({
|
|
163
|
+
field,
|
|
164
|
+
operator: e.name
|
|
165
|
+
// No value for null/undefined checks
|
|
166
|
+
});
|
|
167
|
+
} else {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`extractSimpleComparisons requires a field reference for '${e.name}' operator.`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `in`];
|
|
175
|
+
if (comparisonOps.includes(e.name)) {
|
|
176
|
+
const [leftArg, rightArg] = e.args;
|
|
177
|
+
const field = leftArg?.type === `ref` ? leftArg.path : null;
|
|
178
|
+
const value = rightArg?.type === `val` ? rightArg.value : null;
|
|
179
|
+
if (field && value !== void 0) {
|
|
180
|
+
comparisons.push({
|
|
181
|
+
field,
|
|
182
|
+
operator: e.name,
|
|
183
|
+
value
|
|
184
|
+
});
|
|
185
|
+
} else {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`extractSimpleComparisons requires simple field-value comparisons. Found complex expression for '${e.name}' operator.`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`extractSimpleComparisons encountered unknown operator: '${e.name}'`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
extract(expr);
|
|
198
|
+
return comparisons;
|
|
199
|
+
}
|
|
200
|
+
function parseLoadSubsetOptions(options) {
|
|
201
|
+
if (!options) {
|
|
202
|
+
return { filters: [], sorts: [] };
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
filters: extractSimpleComparisons(options.where),
|
|
206
|
+
sorts: parseOrderByExpression(options.orderBy),
|
|
207
|
+
limit: options.limit
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
exports.extractFieldPath = extractFieldPath;
|
|
211
|
+
exports.extractSimpleComparisons = extractSimpleComparisons;
|
|
212
|
+
exports.extractValue = extractValue;
|
|
213
|
+
exports.parseLoadSubsetOptions = parseLoadSubsetOptions;
|
|
214
|
+
exports.parseOrderByExpression = parseOrderByExpression;
|
|
215
|
+
exports.parseWhereExpression = parseWhereExpression;
|
|
216
|
+
exports.walkExpression = walkExpression;
|
|
217
|
+
//# sourceMappingURL=expression-helpers.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expression-helpers.cjs","sources":["../../../src/query/expression-helpers.ts"],"sourcesContent":["/**\n * Expression Helpers for TanStack DB\n *\n * These utilities help parse LoadSubsetOptions (where, orderBy, limit) from TanStack DB\n * into formats suitable for your API backend. They provide a generic way to traverse\n * expression trees without having to implement your own parser.\n *\n * @example\n * ```typescript\n * import { parseWhereExpression, parseOrderByExpression } from '@tanstack/db'\n *\n * queryFn: async (ctx) => {\n * const { limit, where, orderBy } = ctx.meta?.loadSubsetOptions ?? {}\n *\n * // Convert expression tree to filters\n * const filters = parseWhereExpression(where, {\n * eq: (field, value) => ({ [field]: value }),\n * lt: (field, value) => ({ [`${field}_lt`]: value }),\n * and: (filters) => Object.assign({}, ...filters)\n * })\n *\n * // Extract sort information\n * const sort = parseOrderByExpression(orderBy)\n *\n * return api.getProducts({ ...filters, sort, limit })\n * }\n * ```\n */\n\nimport type { IR, OperatorName } from \"../index.js\"\n\ntype BasicExpression<T = any> = IR.BasicExpression<T>\ntype OrderBy = IR.OrderBy\n\n/**\n * Represents a simple field path extracted from an expression.\n * Can include string keys for object properties and numbers for array indices.\n */\nexport type FieldPath = Array<string | number>\n\n/**\n * Represents a simple comparison operation\n */\nexport interface SimpleComparison {\n field: FieldPath\n operator: string\n value?: any // Optional for operators like isNull and isUndefined that don't have a value\n}\n\n/**\n * Options for customizing how WHERE expressions are parsed\n */\nexport interface ParseWhereOptions<T = any> {\n /**\n * Handler functions for different operators.\n * Each handler receives the parsed field path(s) and value(s) and returns your custom format.\n *\n * Supported operators from TanStack DB:\n * - Comparison: eq, gt, gte, lt, lte, in, like, ilike\n * - Logical: and, or, not\n * - Null checking: isNull, isUndefined\n * - String functions: upper, lower, length, concat\n * - Numeric: add\n * - Utility: coalesce\n * - Aggregates: count, avg, sum, min, max\n */\n handlers: {\n [K in OperatorName]?: (...args: Array<any>) => T\n } & {\n [key: string]: (...args: Array<any>) => T\n }\n /**\n * Optional handler for when an unknown operator is encountered.\n * If not provided, unknown operators throw an error.\n */\n onUnknownOperator?: (operator: string, args: Array<any>) => T\n}\n\n/**\n * Result of parsing an ORDER BY expression\n */\nexport interface ParsedOrderBy {\n field: FieldPath\n direction: `asc` | `desc`\n nulls: `first` | `last`\n /** String sorting method: 'lexical' (default) or 'locale' (locale-aware) */\n stringSort?: `lexical` | `locale`\n /** Locale for locale-aware string sorting (e.g., 'en-US') */\n locale?: string\n /** Additional options for locale-aware sorting */\n localeOptions?: object\n}\n\n/**\n * Extracts the field path from a PropRef expression.\n * Returns null for non-ref expressions.\n *\n * @param expr - The expression to extract from\n * @returns The field path array, or null\n *\n * @example\n * ```typescript\n * const field = extractFieldPath(someExpression)\n * // Returns: ['product', 'category']\n * ```\n */\nexport function extractFieldPath(expr: BasicExpression): FieldPath | null {\n if (expr.type === `ref`) {\n return expr.path\n }\n return null\n}\n\n/**\n * Extracts the value from a Value expression.\n * Returns undefined for non-value expressions.\n *\n * @param expr - The expression to extract from\n * @returns The extracted value\n *\n * @example\n * ```typescript\n * const val = extractValue(someExpression)\n * // Returns: 'electronics'\n * ```\n */\nexport function extractValue(expr: BasicExpression): any {\n if (expr.type === `val`) {\n return expr.value\n }\n return undefined\n}\n\n/**\n * Generic expression tree walker that visits each node in the expression.\n * Useful for implementing custom parsing logic.\n *\n * @param expr - The expression to walk\n * @param visitor - Visitor function called for each node\n *\n * @example\n * ```typescript\n * walkExpression(whereExpr, (node) => {\n * if (node.type === 'func' && node.name === 'eq') {\n * console.log('Found equality comparison')\n * }\n * })\n * ```\n */\nexport function walkExpression(\n expr: BasicExpression | undefined | null,\n visitor: (node: BasicExpression) => void\n): void {\n if (!expr) return\n\n visitor(expr)\n\n if (expr.type === `func`) {\n expr.args.forEach((arg: BasicExpression) => walkExpression(arg, visitor))\n }\n}\n\n/**\n * Parses a WHERE expression into a custom format using provided handlers.\n *\n * This is the main helper for converting TanStack DB where clauses into your API's filter format.\n * You provide handlers for each operator, and this function traverses the expression tree\n * and calls the appropriate handlers.\n *\n * @param expr - The WHERE expression to parse\n * @param options - Configuration with handler functions for each operator\n * @returns The parsed result in your custom format\n *\n * @example\n * ```typescript\n * // REST API with query parameters\n * const filters = parseWhereExpression(where, {\n * handlers: {\n * eq: (field, value) => ({ [field.join('.')]: value }),\n * lt: (field, value) => ({ [`${field.join('.')}_lt`]: value }),\n * gt: (field, value) => ({ [`${field.join('.')}_gt`]: value }),\n * and: (...filters) => Object.assign({}, ...filters),\n * or: (...filters) => ({ $or: filters })\n * }\n * })\n * // Returns: { category: 'electronics', price_lt: 100 }\n * ```\n *\n * @example\n * ```typescript\n * // GraphQL where clause\n * const where = parseWhereExpression(whereExpr, {\n * handlers: {\n * eq: (field, value) => ({ [field.join('_')]: { _eq: value } }),\n * lt: (field, value) => ({ [field.join('_')]: { _lt: value } }),\n * and: (...filters) => ({ _and: filters })\n * }\n * })\n * ```\n */\nexport function parseWhereExpression<T = any>(\n expr: BasicExpression<boolean> | undefined | null,\n options: ParseWhereOptions<T>\n): T | null {\n if (!expr) return null\n\n const { handlers, onUnknownOperator } = options\n\n // Handle value expressions\n if (expr.type === `val`) {\n return expr.value as unknown as T\n }\n\n // Handle property references\n if (expr.type === `ref`) {\n return expr.path as unknown as T\n }\n\n // Handle function expressions\n // After checking val and ref, expr must be func\n const { name, args } = expr\n const handler = handlers[name]\n\n if (!handler) {\n if (onUnknownOperator) {\n return onUnknownOperator(name, args)\n }\n throw new Error(\n `No handler provided for operator: ${name}. Available handlers: ${Object.keys(handlers).join(`, `)}`\n )\n }\n\n // Parse arguments recursively\n const parsedArgs = args.map((arg: BasicExpression) => {\n // For refs, extract the field path\n if (arg.type === `ref`) {\n return arg.path\n }\n // For values, extract the value\n if (arg.type === `val`) {\n return arg.value\n }\n // For nested functions, recurse (after checking ref and val, must be func)\n return parseWhereExpression(arg, options)\n })\n\n return handler(...parsedArgs)\n}\n\n/**\n * Parses an ORDER BY expression into a simple array of sort specifications.\n *\n * @param orderBy - The ORDER BY expression array\n * @returns Array of parsed order by specifications\n *\n * @example\n * ```typescript\n * const sorts = parseOrderByExpression(orderBy)\n * // Returns: [\n * // { field: ['category'], direction: 'asc', nulls: 'last' },\n * // { field: ['price'], direction: 'desc', nulls: 'last' }\n * // ]\n * ```\n */\nexport function parseOrderByExpression(\n orderBy: OrderBy | undefined | null\n): Array<ParsedOrderBy> {\n if (!orderBy || orderBy.length === 0) {\n return []\n }\n\n return orderBy.map((clause: IR.OrderByClause) => {\n const field = extractFieldPath(clause.expression)\n\n if (!field) {\n throw new Error(\n `ORDER BY expression must be a field reference, got: ${clause.expression.type}`\n )\n }\n\n const { direction, nulls } = clause.compareOptions\n const result: ParsedOrderBy = {\n field,\n direction,\n nulls,\n }\n\n // Add string collation options if present (discriminated union)\n if (`stringSort` in clause.compareOptions) {\n result.stringSort = clause.compareOptions.stringSort\n }\n if (`locale` in clause.compareOptions) {\n result.locale = clause.compareOptions.locale\n }\n if (`localeOptions` in clause.compareOptions) {\n result.localeOptions = clause.compareOptions.localeOptions\n }\n\n return result\n })\n}\n\n/**\n * Extracts all simple comparisons from a WHERE expression.\n * This is useful for simple APIs that only support basic filters.\n *\n * Note: This only works for simple AND-ed conditions and NOT-wrapped comparisons.\n * Throws an error if it encounters unsupported operations like OR or complex nested expressions.\n *\n * NOT operators are flattened by prefixing the operator name (e.g., `not(eq(...))` becomes `not_eq`).\n *\n * @param expr - The WHERE expression to parse\n * @returns Array of simple comparisons\n * @throws Error if expression contains OR or other unsupported operations\n *\n * @example\n * ```typescript\n * const comparisons = extractSimpleComparisons(where)\n * // Returns: [\n * // { field: ['category'], operator: 'eq', value: 'electronics' },\n * // { field: ['price'], operator: 'lt', value: 100 },\n * // { field: ['email'], operator: 'isNull' }, // No value for null checks\n * // { field: ['status'], operator: 'not_eq', value: 'archived' }\n * // ]\n * ```\n */\nexport function extractSimpleComparisons(\n expr: BasicExpression<boolean> | undefined | null\n): Array<SimpleComparison> {\n if (!expr) return []\n\n const comparisons: Array<SimpleComparison> = []\n\n function extract(e: BasicExpression): void {\n if (e.type === `func`) {\n // Handle AND - recurse into both sides\n if (e.name === `and`) {\n e.args.forEach((arg: BasicExpression) => extract(arg))\n return\n }\n\n // Handle NOT - recurse into argument and prefix operator with 'not_'\n if (e.name === `not`) {\n const [arg] = e.args\n if (!arg || arg.type !== `func`) {\n throw new Error(\n `extractSimpleComparisons requires a comparison or null check inside 'not' operator.`\n )\n }\n\n // Handle NOT with null/undefined checks\n const nullCheckOps = [`isNull`, `isUndefined`]\n if (nullCheckOps.includes(arg.name)) {\n const [fieldArg] = arg.args\n const field = fieldArg?.type === `ref` ? fieldArg.path : null\n\n if (field) {\n comparisons.push({\n field,\n operator: `not_${arg.name}`,\n // No value for null/undefined checks\n })\n } else {\n throw new Error(\n `extractSimpleComparisons requires a field reference for '${arg.name}' operator.`\n )\n }\n return\n }\n\n // Handle NOT with comparison operators\n const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `in`]\n if (comparisonOps.includes(arg.name)) {\n const [leftArg, rightArg] = arg.args\n const field = leftArg?.type === `ref` ? leftArg.path : null\n const value = rightArg?.type === `val` ? rightArg.value : null\n\n if (field && value !== undefined) {\n comparisons.push({\n field,\n operator: `not_${arg.name}`,\n value,\n })\n } else {\n throw new Error(\n `extractSimpleComparisons requires simple field-value comparisons. Found complex expression for 'not(${arg.name})' operator.`\n )\n }\n return\n }\n\n // NOT can only wrap simple comparisons or null checks\n throw new Error(\n `extractSimpleComparisons does not support 'not(${arg.name})'. NOT can only wrap comparison operators (eq, gt, gte, lt, lte, in) or null checks (isNull, isUndefined).`\n )\n }\n\n // Throw on unsupported operations\n const unsupportedOps = [\n `or`,\n `like`,\n `ilike`,\n `upper`,\n `lower`,\n `length`,\n `concat`,\n `add`,\n `coalesce`,\n `count`,\n `avg`,\n `sum`,\n `min`,\n `max`,\n ]\n if (unsupportedOps.includes(e.name)) {\n throw new Error(\n `extractSimpleComparisons does not support '${e.name}' operator. Use parseWhereExpression with custom handlers for complex expressions.`\n )\n }\n\n // Handle null/undefined check operators (single argument, no value)\n const nullCheckOps = [`isNull`, `isUndefined`]\n if (nullCheckOps.includes(e.name)) {\n const [fieldArg] = e.args\n\n // Extract field (must be a ref)\n const field = fieldArg?.type === `ref` ? fieldArg.path : null\n\n if (field) {\n comparisons.push({\n field,\n operator: e.name,\n // No value for null/undefined checks\n })\n } else {\n throw new Error(\n `extractSimpleComparisons requires a field reference for '${e.name}' operator.`\n )\n }\n return\n }\n\n // Handle comparison operators\n const comparisonOps = [`eq`, `gt`, `gte`, `lt`, `lte`, `in`]\n if (comparisonOps.includes(e.name)) {\n const [leftArg, rightArg] = e.args\n\n // Extract field and value\n const field = leftArg?.type === `ref` ? leftArg.path : null\n const value = rightArg?.type === `val` ? rightArg.value : null\n\n if (field && value !== undefined) {\n comparisons.push({\n field,\n operator: e.name,\n value,\n })\n } else {\n throw new Error(\n `extractSimpleComparisons requires simple field-value comparisons. Found complex expression for '${e.name}' operator.`\n )\n }\n } else {\n // Unknown operator\n throw new Error(\n `extractSimpleComparisons encountered unknown operator: '${e.name}'`\n )\n }\n }\n }\n\n extract(expr)\n return comparisons\n}\n\n/**\n * Convenience function to get all LoadSubsetOptions in a pre-parsed format.\n * Good starting point for simple use cases.\n *\n * @param options - The LoadSubsetOptions from ctx.meta\n * @returns Pre-parsed filters, sorts, and limit\n *\n * @example\n * ```typescript\n * queryFn: async (ctx) => {\n * const parsed = parseLoadSubsetOptions(ctx.meta?.loadSubsetOptions)\n *\n * // Convert to your API format\n * return api.getProducts({\n * ...Object.fromEntries(\n * parsed.filters.map(f => [`${f.field.join('.')}_${f.operator}`, f.value])\n * ),\n * sort: parsed.sorts.map(s => `${s.field.join('.')}:${s.direction}`).join(','),\n * limit: parsed.limit\n * })\n * }\n * ```\n */\nexport function parseLoadSubsetOptions(\n options:\n | {\n where?: BasicExpression<boolean>\n orderBy?: OrderBy\n limit?: number\n }\n | undefined\n | null\n): {\n filters: Array<SimpleComparison>\n sorts: Array<ParsedOrderBy>\n limit?: number\n} {\n if (!options) {\n return { filters: [], sorts: [] }\n }\n\n return {\n filters: extractSimpleComparisons(options.where),\n sorts: parseOrderByExpression(options.orderBy),\n limit: options.limit,\n }\n}\n"],"names":["nullCheckOps","comparisonOps"],"mappings":"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"}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { IR, OperatorName } from '../index.js';
|
|
2
|
+
type BasicExpression<T = any> = IR.BasicExpression<T>;
|
|
3
|
+
type OrderBy = IR.OrderBy;
|
|
4
|
+
/**
|
|
5
|
+
* Represents a simple field path extracted from an expression.
|
|
6
|
+
* Can include string keys for object properties and numbers for array indices.
|
|
7
|
+
*/
|
|
8
|
+
export type FieldPath = Array<string | number>;
|
|
9
|
+
/**
|
|
10
|
+
* Represents a simple comparison operation
|
|
11
|
+
*/
|
|
12
|
+
export interface SimpleComparison {
|
|
13
|
+
field: FieldPath;
|
|
14
|
+
operator: string;
|
|
15
|
+
value?: any;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Options for customizing how WHERE expressions are parsed
|
|
19
|
+
*/
|
|
20
|
+
export interface ParseWhereOptions<T = any> {
|
|
21
|
+
/**
|
|
22
|
+
* Handler functions for different operators.
|
|
23
|
+
* Each handler receives the parsed field path(s) and value(s) and returns your custom format.
|
|
24
|
+
*
|
|
25
|
+
* Supported operators from TanStack DB:
|
|
26
|
+
* - Comparison: eq, gt, gte, lt, lte, in, like, ilike
|
|
27
|
+
* - Logical: and, or, not
|
|
28
|
+
* - Null checking: isNull, isUndefined
|
|
29
|
+
* - String functions: upper, lower, length, concat
|
|
30
|
+
* - Numeric: add
|
|
31
|
+
* - Utility: coalesce
|
|
32
|
+
* - Aggregates: count, avg, sum, min, max
|
|
33
|
+
*/
|
|
34
|
+
handlers: {
|
|
35
|
+
[K in OperatorName]?: (...args: Array<any>) => T;
|
|
36
|
+
} & {
|
|
37
|
+
[key: string]: (...args: Array<any>) => T;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Optional handler for when an unknown operator is encountered.
|
|
41
|
+
* If not provided, unknown operators throw an error.
|
|
42
|
+
*/
|
|
43
|
+
onUnknownOperator?: (operator: string, args: Array<any>) => T;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Result of parsing an ORDER BY expression
|
|
47
|
+
*/
|
|
48
|
+
export interface ParsedOrderBy {
|
|
49
|
+
field: FieldPath;
|
|
50
|
+
direction: `asc` | `desc`;
|
|
51
|
+
nulls: `first` | `last`;
|
|
52
|
+
/** String sorting method: 'lexical' (default) or 'locale' (locale-aware) */
|
|
53
|
+
stringSort?: `lexical` | `locale`;
|
|
54
|
+
/** Locale for locale-aware string sorting (e.g., 'en-US') */
|
|
55
|
+
locale?: string;
|
|
56
|
+
/** Additional options for locale-aware sorting */
|
|
57
|
+
localeOptions?: object;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Extracts the field path from a PropRef expression.
|
|
61
|
+
* Returns null for non-ref expressions.
|
|
62
|
+
*
|
|
63
|
+
* @param expr - The expression to extract from
|
|
64
|
+
* @returns The field path array, or null
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* const field = extractFieldPath(someExpression)
|
|
69
|
+
* // Returns: ['product', 'category']
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export declare function extractFieldPath(expr: BasicExpression): FieldPath | null;
|
|
73
|
+
/**
|
|
74
|
+
* Extracts the value from a Value expression.
|
|
75
|
+
* Returns undefined for non-value expressions.
|
|
76
|
+
*
|
|
77
|
+
* @param expr - The expression to extract from
|
|
78
|
+
* @returns The extracted value
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```typescript
|
|
82
|
+
* const val = extractValue(someExpression)
|
|
83
|
+
* // Returns: 'electronics'
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export declare function extractValue(expr: BasicExpression): any;
|
|
87
|
+
/**
|
|
88
|
+
* Generic expression tree walker that visits each node in the expression.
|
|
89
|
+
* Useful for implementing custom parsing logic.
|
|
90
|
+
*
|
|
91
|
+
* @param expr - The expression to walk
|
|
92
|
+
* @param visitor - Visitor function called for each node
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```typescript
|
|
96
|
+
* walkExpression(whereExpr, (node) => {
|
|
97
|
+
* if (node.type === 'func' && node.name === 'eq') {
|
|
98
|
+
* console.log('Found equality comparison')
|
|
99
|
+
* }
|
|
100
|
+
* })
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export declare function walkExpression(expr: BasicExpression | undefined | null, visitor: (node: BasicExpression) => void): void;
|
|
104
|
+
/**
|
|
105
|
+
* Parses a WHERE expression into a custom format using provided handlers.
|
|
106
|
+
*
|
|
107
|
+
* This is the main helper for converting TanStack DB where clauses into your API's filter format.
|
|
108
|
+
* You provide handlers for each operator, and this function traverses the expression tree
|
|
109
|
+
* and calls the appropriate handlers.
|
|
110
|
+
*
|
|
111
|
+
* @param expr - The WHERE expression to parse
|
|
112
|
+
* @param options - Configuration with handler functions for each operator
|
|
113
|
+
* @returns The parsed result in your custom format
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```typescript
|
|
117
|
+
* // REST API with query parameters
|
|
118
|
+
* const filters = parseWhereExpression(where, {
|
|
119
|
+
* handlers: {
|
|
120
|
+
* eq: (field, value) => ({ [field.join('.')]: value }),
|
|
121
|
+
* lt: (field, value) => ({ [`${field.join('.')}_lt`]: value }),
|
|
122
|
+
* gt: (field, value) => ({ [`${field.join('.')}_gt`]: value }),
|
|
123
|
+
* and: (...filters) => Object.assign({}, ...filters),
|
|
124
|
+
* or: (...filters) => ({ $or: filters })
|
|
125
|
+
* }
|
|
126
|
+
* })
|
|
127
|
+
* // Returns: { category: 'electronics', price_lt: 100 }
|
|
128
|
+
* ```
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```typescript
|
|
132
|
+
* // GraphQL where clause
|
|
133
|
+
* const where = parseWhereExpression(whereExpr, {
|
|
134
|
+
* handlers: {
|
|
135
|
+
* eq: (field, value) => ({ [field.join('_')]: { _eq: value } }),
|
|
136
|
+
* lt: (field, value) => ({ [field.join('_')]: { _lt: value } }),
|
|
137
|
+
* and: (...filters) => ({ _and: filters })
|
|
138
|
+
* }
|
|
139
|
+
* })
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
export declare function parseWhereExpression<T = any>(expr: BasicExpression<boolean> | undefined | null, options: ParseWhereOptions<T>): T | null;
|
|
143
|
+
/**
|
|
144
|
+
* Parses an ORDER BY expression into a simple array of sort specifications.
|
|
145
|
+
*
|
|
146
|
+
* @param orderBy - The ORDER BY expression array
|
|
147
|
+
* @returns Array of parsed order by specifications
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```typescript
|
|
151
|
+
* const sorts = parseOrderByExpression(orderBy)
|
|
152
|
+
* // Returns: [
|
|
153
|
+
* // { field: ['category'], direction: 'asc', nulls: 'last' },
|
|
154
|
+
* // { field: ['price'], direction: 'desc', nulls: 'last' }
|
|
155
|
+
* // ]
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
export declare function parseOrderByExpression(orderBy: OrderBy | undefined | null): Array<ParsedOrderBy>;
|
|
159
|
+
/**
|
|
160
|
+
* Extracts all simple comparisons from a WHERE expression.
|
|
161
|
+
* This is useful for simple APIs that only support basic filters.
|
|
162
|
+
*
|
|
163
|
+
* Note: This only works for simple AND-ed conditions and NOT-wrapped comparisons.
|
|
164
|
+
* Throws an error if it encounters unsupported operations like OR or complex nested expressions.
|
|
165
|
+
*
|
|
166
|
+
* NOT operators are flattened by prefixing the operator name (e.g., `not(eq(...))` becomes `not_eq`).
|
|
167
|
+
*
|
|
168
|
+
* @param expr - The WHERE expression to parse
|
|
169
|
+
* @returns Array of simple comparisons
|
|
170
|
+
* @throws Error if expression contains OR or other unsupported operations
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```typescript
|
|
174
|
+
* const comparisons = extractSimpleComparisons(where)
|
|
175
|
+
* // Returns: [
|
|
176
|
+
* // { field: ['category'], operator: 'eq', value: 'electronics' },
|
|
177
|
+
* // { field: ['price'], operator: 'lt', value: 100 },
|
|
178
|
+
* // { field: ['email'], operator: 'isNull' }, // No value for null checks
|
|
179
|
+
* // { field: ['status'], operator: 'not_eq', value: 'archived' }
|
|
180
|
+
* // ]
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
export declare function extractSimpleComparisons(expr: BasicExpression<boolean> | undefined | null): Array<SimpleComparison>;
|
|
184
|
+
/**
|
|
185
|
+
* Convenience function to get all LoadSubsetOptions in a pre-parsed format.
|
|
186
|
+
* Good starting point for simple use cases.
|
|
187
|
+
*
|
|
188
|
+
* @param options - The LoadSubsetOptions from ctx.meta
|
|
189
|
+
* @returns Pre-parsed filters, sorts, and limit
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```typescript
|
|
193
|
+
* queryFn: async (ctx) => {
|
|
194
|
+
* const parsed = parseLoadSubsetOptions(ctx.meta?.loadSubsetOptions)
|
|
195
|
+
*
|
|
196
|
+
* // Convert to your API format
|
|
197
|
+
* return api.getProducts({
|
|
198
|
+
* ...Object.fromEntries(
|
|
199
|
+
* parsed.filters.map(f => [`${f.field.join('.')}_${f.operator}`, f.value])
|
|
200
|
+
* ),
|
|
201
|
+
* sort: parsed.sorts.map(s => `${s.field.join('.')}:${s.direction}`).join(','),
|
|
202
|
+
* limit: parsed.limit
|
|
203
|
+
* })
|
|
204
|
+
* }
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
export declare function parseLoadSubsetOptions(options: {
|
|
208
|
+
where?: BasicExpression<boolean>;
|
|
209
|
+
orderBy?: OrderBy;
|
|
210
|
+
limit?: number;
|
|
211
|
+
} | undefined | null): {
|
|
212
|
+
filters: Array<SimpleComparison>;
|
|
213
|
+
sorts: Array<ParsedOrderBy>;
|
|
214
|
+
limit?: number;
|
|
215
|
+
};
|
|
216
|
+
export {};
|
|
@@ -5,3 +5,5 @@ export { compileQuery } from './compiler/index.js';
|
|
|
5
5
|
export { createLiveQueryCollection, liveQueryCollectionOptions, } from './live-query-collection.js';
|
|
6
6
|
export { type LiveQueryCollectionConfig } from './live/types.js';
|
|
7
7
|
export { type LiveQueryCollectionUtils } from './live/collection-config-builder.js';
|
|
8
|
+
export { isWhereSubset, unionWherePredicates, minusWherePredicates, isOrderBySubset, isLimitSubset, isPredicateSubset, } from './predicate-utils.js';
|
|
9
|
+
export { DeduplicatedLoadSubset } from './subset-dedupe.js';
|
|
@@ -41,6 +41,7 @@ class CollectionConfigBuilder {
|
|
|
41
41
|
if (this.query.orderBy && this.query.orderBy.length > 0) {
|
|
42
42
|
this.compare = createOrderByComparator(this.orderByIndices);
|
|
43
43
|
}
|
|
44
|
+
this.compareOptions = this.config.defaultStringCollation ?? extractCollectionFromSource(this.query).compareOptions;
|
|
44
45
|
this.compileBasePipeline();
|
|
45
46
|
}
|
|
46
47
|
/**
|
|
@@ -63,6 +64,7 @@ class CollectionConfigBuilder {
|
|
|
63
64
|
getKey: this.config.getKey || ((item) => this.resultKeys.get(item)),
|
|
64
65
|
sync: this.getSyncConfig(),
|
|
65
66
|
compare: this.compare,
|
|
67
|
+
defaultStringCollation: this.compareOptions,
|
|
66
68
|
gcTime: this.config.gcTime || 5e3,
|
|
67
69
|
// 5 seconds by default for live queries
|
|
68
70
|
schema: this.config.schema,
|
|
@@ -577,6 +579,17 @@ function extractCollectionsFromQuery(query) {
|
|
|
577
579
|
extractFromQuery(query);
|
|
578
580
|
return collections;
|
|
579
581
|
}
|
|
582
|
+
function extractCollectionFromSource(query) {
|
|
583
|
+
const from = query.from;
|
|
584
|
+
if (from.type === `collectionRef`) {
|
|
585
|
+
return from.collection;
|
|
586
|
+
} else if (from.type === `queryRef`) {
|
|
587
|
+
return extractCollectionFromSource(from.query);
|
|
588
|
+
}
|
|
589
|
+
throw new Error(
|
|
590
|
+
`Failed to extract collection. Invalid FROM clause: ${JSON.stringify(query)}`
|
|
591
|
+
);
|
|
592
|
+
}
|
|
580
593
|
function extractCollectionAliases(query) {
|
|
581
594
|
const aliasesById = /* @__PURE__ */ new Map();
|
|
582
595
|
function recordAlias(source) {
|