@tanstack/db 0.5.33 → 0.6.1
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.map +1 -1
- package/dist/cjs/collection/change-events.d.cts +3 -2
- package/dist/cjs/collection/changes.cjs +13 -4
- package/dist/cjs/collection/changes.cjs.map +1 -1
- package/dist/cjs/collection/changes.d.cts +10 -1
- package/dist/cjs/collection/cleanup-queue.cjs +89 -0
- package/dist/cjs/collection/cleanup-queue.cjs.map +1 -0
- package/dist/cjs/collection/cleanup-queue.d.cts +30 -0
- package/dist/cjs/collection/events.cjs +14 -0
- package/dist/cjs/collection/events.cjs.map +1 -1
- package/dist/cjs/collection/events.d.cts +39 -1
- package/dist/cjs/collection/index.cjs +66 -28
- package/dist/cjs/collection/index.cjs.map +1 -1
- package/dist/cjs/collection/index.d.cts +49 -36
- package/dist/cjs/collection/indexes.cjs +211 -62
- package/dist/cjs/collection/indexes.cjs.map +1 -1
- package/dist/cjs/collection/indexes.d.cts +27 -17
- package/dist/cjs/collection/lifecycle.cjs +5 -22
- package/dist/cjs/collection/lifecycle.cjs.map +1 -1
- package/dist/cjs/collection/lifecycle.d.cts +0 -1
- package/dist/cjs/collection/mutations.cjs +18 -0
- package/dist/cjs/collection/mutations.cjs.map +1 -1
- package/dist/cjs/collection/mutations.d.cts +1 -0
- package/dist/cjs/collection/state.cjs +381 -53
- package/dist/cjs/collection/state.cjs.map +1 -1
- package/dist/cjs/collection/state.d.cts +65 -1
- package/dist/cjs/collection/subscription.cjs +6 -0
- package/dist/cjs/collection/subscription.cjs.map +1 -1
- package/dist/cjs/collection/subscription.d.cts +4 -0
- package/dist/cjs/collection/sync.cjs +108 -1
- package/dist/cjs/collection/sync.cjs.map +1 -1
- package/dist/cjs/collection/sync.d.cts +2 -0
- package/dist/cjs/collection/transaction-metadata.cjs +5 -0
- package/dist/cjs/collection/transaction-metadata.cjs.map +1 -0
- package/dist/cjs/collection/transaction-metadata.d.cts +1 -0
- package/dist/cjs/errors.cjs +8 -0
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +3 -0
- package/dist/cjs/index.cjs +22 -4
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +11 -3
- package/dist/cjs/indexes/auto-index.cjs +13 -6
- package/dist/cjs/indexes/auto-index.cjs.map +1 -1
- package/dist/cjs/indexes/base-index.cjs +0 -3
- package/dist/cjs/indexes/base-index.cjs.map +1 -1
- package/dist/cjs/indexes/base-index.d.cts +2 -6
- package/dist/cjs/indexes/basic-index.cjs +361 -0
- package/dist/cjs/indexes/basic-index.cjs.map +1 -0
- package/dist/cjs/indexes/basic-index.d.cts +102 -0
- package/dist/cjs/indexes/btree-index.cjs.map +1 -1
- package/dist/cjs/indexes/btree-index.d.cts +1 -1
- package/dist/cjs/indexes/index-options.d.cts +8 -9
- package/dist/cjs/indexes/index-registry.cjs +89 -0
- package/dist/cjs/indexes/index-registry.cjs.map +1 -0
- package/dist/cjs/indexes/index-registry.d.cts +61 -0
- package/dist/cjs/local-only.cjs +5 -0
- package/dist/cjs/local-only.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.cjs +27 -11
- package/dist/cjs/query/builder/functions.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.d.cts +25 -3
- package/dist/cjs/query/builder/index.cjs +200 -39
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/query/builder/index.d.cts +4 -3
- package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
- package/dist/cjs/query/builder/ref-proxy.d.cts +14 -3
- package/dist/cjs/query/builder/types.d.cts +84 -19
- package/dist/cjs/query/compiler/evaluators.cjs +51 -0
- package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
- package/dist/cjs/query/compiler/group-by.cjs +100 -28
- package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/group-by.d.cts +4 -2
- package/dist/cjs/query/compiler/index.cjs +283 -11
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.d.cts +30 -2
- package/dist/cjs/query/compiler/order-by.cjs +29 -10
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.d.cts +1 -1
- package/dist/cjs/query/compiler/select.cjs +8 -0
- package/dist/cjs/query/compiler/select.cjs.map +1 -1
- package/dist/cjs/query/index.d.cts +2 -1
- package/dist/cjs/query/ir.cjs +18 -1
- package/dist/cjs/query/ir.cjs.map +1 -1
- package/dist/cjs/query/ir.d.cts +21 -1
- package/dist/cjs/query/live/collection-config-builder.cjs +501 -5
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.d.cts +7 -0
- package/dist/cjs/query/live/types.d.cts +3 -3
- package/dist/cjs/query/live/utils.cjs +43 -3
- package/dist/cjs/query/live/utils.cjs.map +1 -1
- package/dist/cjs/query/live/utils.d.cts +1 -0
- package/dist/cjs/query/live-query-collection.cjs.map +1 -1
- package/dist/cjs/query/live-query-collection.d.cts +9 -6
- package/dist/cjs/query/query-once.cjs.map +1 -1
- package/dist/cjs/query/query-once.d.cts +7 -5
- package/dist/cjs/query/subset-dedupe.cjs +9 -3
- package/dist/cjs/query/subset-dedupe.cjs.map +1 -1
- package/dist/cjs/types.d.cts +42 -8
- package/dist/cjs/utils/array-utils.cjs +27 -0
- package/dist/cjs/utils/array-utils.cjs.map +1 -0
- package/dist/cjs/utils/array-utils.d.cts +16 -0
- package/dist/cjs/utils/comparison.cjs +11 -0
- package/dist/cjs/utils/comparison.cjs.map +1 -1
- package/dist/cjs/utils/index-optimization.cjs +4 -0
- package/dist/cjs/utils/index-optimization.cjs.map +1 -1
- package/dist/cjs/utils.cjs +7 -9
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +6 -1
- package/dist/cjs/virtual-props.cjs +33 -0
- package/dist/cjs/virtual-props.cjs.map +1 -0
- package/dist/cjs/virtual-props.d.cts +196 -0
- package/dist/esm/collection/change-events.d.ts +3 -2
- package/dist/esm/collection/change-events.js.map +1 -1
- package/dist/esm/collection/changes.d.ts +10 -1
- package/dist/esm/collection/changes.js +13 -4
- package/dist/esm/collection/changes.js.map +1 -1
- package/dist/esm/collection/cleanup-queue.d.ts +30 -0
- package/dist/esm/collection/cleanup-queue.js +89 -0
- package/dist/esm/collection/cleanup-queue.js.map +1 -0
- package/dist/esm/collection/events.d.ts +39 -1
- package/dist/esm/collection/events.js +14 -0
- package/dist/esm/collection/events.js.map +1 -1
- package/dist/esm/collection/index.d.ts +49 -36
- package/dist/esm/collection/index.js +67 -29
- package/dist/esm/collection/index.js.map +1 -1
- package/dist/esm/collection/indexes.d.ts +27 -17
- package/dist/esm/collection/indexes.js +211 -62
- package/dist/esm/collection/indexes.js.map +1 -1
- package/dist/esm/collection/lifecycle.d.ts +0 -1
- package/dist/esm/collection/lifecycle.js +5 -22
- package/dist/esm/collection/lifecycle.js.map +1 -1
- package/dist/esm/collection/mutations.d.ts +1 -0
- package/dist/esm/collection/mutations.js +18 -0
- package/dist/esm/collection/mutations.js.map +1 -1
- package/dist/esm/collection/state.d.ts +65 -1
- package/dist/esm/collection/state.js +381 -53
- package/dist/esm/collection/state.js.map +1 -1
- package/dist/esm/collection/subscription.d.ts +4 -0
- package/dist/esm/collection/subscription.js +6 -0
- package/dist/esm/collection/subscription.js.map +1 -1
- package/dist/esm/collection/sync.d.ts +2 -0
- package/dist/esm/collection/sync.js +108 -1
- package/dist/esm/collection/sync.js.map +1 -1
- package/dist/esm/collection/transaction-metadata.d.ts +1 -0
- package/dist/esm/collection/transaction-metadata.js +5 -0
- package/dist/esm/collection/transaction-metadata.js.map +1 -0
- package/dist/esm/errors.d.ts +3 -0
- package/dist/esm/errors.js +8 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +11 -3
- package/dist/esm/index.js +25 -7
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/indexes/auto-index.js +13 -6
- package/dist/esm/indexes/auto-index.js.map +1 -1
- package/dist/esm/indexes/base-index.d.ts +2 -6
- package/dist/esm/indexes/base-index.js +1 -4
- package/dist/esm/indexes/base-index.js.map +1 -1
- package/dist/esm/indexes/basic-index.d.ts +102 -0
- package/dist/esm/indexes/basic-index.js +361 -0
- package/dist/esm/indexes/basic-index.js.map +1 -0
- package/dist/esm/indexes/btree-index.d.ts +1 -1
- package/dist/esm/indexes/btree-index.js.map +1 -1
- package/dist/esm/indexes/index-options.d.ts +8 -9
- package/dist/esm/indexes/index-registry.d.ts +61 -0
- package/dist/esm/indexes/index-registry.js +89 -0
- package/dist/esm/indexes/index-registry.js.map +1 -0
- package/dist/esm/local-only.js +5 -0
- package/dist/esm/local-only.js.map +1 -1
- package/dist/esm/query/builder/functions.d.ts +25 -3
- package/dist/esm/query/builder/functions.js +27 -11
- package/dist/esm/query/builder/functions.js.map +1 -1
- package/dist/esm/query/builder/index.d.ts +4 -3
- package/dist/esm/query/builder/index.js +201 -40
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/query/builder/ref-proxy.d.ts +14 -3
- package/dist/esm/query/builder/ref-proxy.js.map +1 -1
- package/dist/esm/query/builder/types.d.ts +84 -19
- package/dist/esm/query/compiler/evaluators.js +51 -0
- package/dist/esm/query/compiler/evaluators.js.map +1 -1
- package/dist/esm/query/compiler/group-by.d.ts +4 -2
- package/dist/esm/query/compiler/group-by.js +101 -29
- package/dist/esm/query/compiler/group-by.js.map +1 -1
- package/dist/esm/query/compiler/index.d.ts +30 -2
- package/dist/esm/query/compiler/index.js +285 -13
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/order-by.d.ts +1 -1
- package/dist/esm/query/compiler/order-by.js +30 -11
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/compiler/select.js +8 -0
- package/dist/esm/query/compiler/select.js.map +1 -1
- package/dist/esm/query/index.d.ts +2 -1
- package/dist/esm/query/ir.d.ts +21 -1
- package/dist/esm/query/ir.js +18 -1
- package/dist/esm/query/ir.js.map +1 -1
- package/dist/esm/query/live/collection-config-builder.d.ts +7 -0
- package/dist/esm/query/live/collection-config-builder.js +503 -7
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/query/live/types.d.ts +3 -3
- package/dist/esm/query/live/utils.d.ts +1 -0
- package/dist/esm/query/live/utils.js +43 -3
- package/dist/esm/query/live/utils.js.map +1 -1
- package/dist/esm/query/live-query-collection.d.ts +9 -6
- package/dist/esm/query/live-query-collection.js.map +1 -1
- package/dist/esm/query/query-once.d.ts +7 -5
- package/dist/esm/query/query-once.js.map +1 -1
- package/dist/esm/query/subset-dedupe.js +9 -3
- package/dist/esm/query/subset-dedupe.js.map +1 -1
- package/dist/esm/types.d.ts +42 -8
- package/dist/esm/utils/array-utils.d.ts +16 -0
- package/dist/esm/utils/array-utils.js +27 -0
- package/dist/esm/utils/array-utils.js.map +1 -0
- package/dist/esm/utils/comparison.js +11 -0
- package/dist/esm/utils/comparison.js.map +1 -1
- package/dist/esm/utils/index-optimization.js +4 -0
- package/dist/esm/utils/index-optimization.js.map +1 -1
- package/dist/esm/utils.d.ts +6 -1
- package/dist/esm/utils.js +7 -9
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/virtual-props.d.ts +196 -0
- package/dist/esm/virtual-props.js +33 -0
- package/dist/esm/virtual-props.js.map +1 -0
- package/package.json +4 -3
- package/skills/db-core/SKILL.md +4 -2
- package/skills/db-core/collection-setup/SKILL.md +30 -11
- package/skills/db-core/collection-setup/references/electric-adapter.md +1 -1
- package/skills/db-core/collection-setup/references/powersync-adapter.md +4 -0
- package/skills/db-core/collection-setup/references/query-adapter.md +32 -0
- package/skills/db-core/custom-adapter/SKILL.md +58 -9
- package/skills/db-core/live-queries/SKILL.md +162 -2
- package/skills/db-core/mutations-optimistic/SKILL.md +1 -1
- package/skills/db-core/persistence/SKILL.md +241 -0
- package/skills/meta-framework/SKILL.md +1 -1
- package/src/collection/change-events.ts +13 -9
- package/src/collection/changes.ts +30 -7
- package/src/collection/cleanup-queue.ts +105 -0
- package/src/collection/events.ts +65 -0
- package/src/collection/index.ts +110 -45
- package/src/collection/indexes.ts +283 -76
- package/src/collection/lifecycle.ts +5 -26
- package/src/collection/mutations.ts +21 -0
- package/src/collection/state.ts +545 -71
- package/src/collection/subscription.ts +7 -0
- package/src/collection/sync.ts +137 -0
- package/src/collection/transaction-metadata.ts +1 -0
- package/src/errors.ts +9 -0
- package/src/index.ts +46 -3
- package/src/indexes/auto-index.ts +18 -8
- package/src/indexes/base-index.ts +2 -10
- package/src/indexes/basic-index.ts +507 -0
- package/src/indexes/btree-index.ts +1 -1
- package/src/indexes/index-options.ts +17 -37
- package/src/indexes/index-registry.ts +174 -0
- package/src/local-only.ts +7 -0
- package/src/query/builder/functions.ts +84 -7
- package/src/query/builder/index.ts +329 -9
- package/src/query/builder/ref-proxy.ts +22 -4
- package/src/query/builder/types.ts +257 -62
- package/src/query/compiler/evaluators.ts +57 -0
- package/src/query/compiler/group-by.ts +156 -35
- package/src/query/compiler/index.ts +445 -15
- package/src/query/compiler/order-by.ts +51 -12
- package/src/query/compiler/select.ts +9 -0
- package/src/query/index.ts +7 -0
- package/src/query/ir.ts +23 -2
- package/src/query/live/collection-config-builder.ts +809 -9
- package/src/query/live/types.ts +10 -4
- package/src/query/live/utils.ts +64 -3
- package/src/query/live-query-collection.ts +43 -18
- package/src/query/query-once.ts +31 -12
- package/src/query/subset-dedupe.ts +11 -7
- package/src/types.ts +49 -9
- package/src/utils/array-utils.ts +49 -0
- package/src/utils/comparison.ts +14 -0
- package/src/utils/index-optimization.ts +4 -0
- package/src/utils.ts +12 -9
- package/src/virtual-props.ts +282 -0
- package/dist/cjs/indexes/lazy-index.cjs +0 -190
- package/dist/cjs/indexes/lazy-index.cjs.map +0 -1
- package/dist/cjs/indexes/lazy-index.d.cts +0 -96
- package/dist/esm/indexes/lazy-index.d.ts +0 -96
- package/dist/esm/indexes/lazy-index.js +0 -190
- package/dist/esm/indexes/lazy-index.js.map +0 -1
- package/src/indexes/lazy-index.ts +0 -251
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
distinct,
|
|
3
|
+
filter,
|
|
4
|
+
join as joinOperator,
|
|
5
|
+
map,
|
|
6
|
+
reduce,
|
|
7
|
+
} from '@tanstack/db-ivm'
|
|
2
8
|
import { optimizeQuery } from '../optimizer.js'
|
|
3
9
|
import {
|
|
4
10
|
CollectionInputNotFoundError,
|
|
@@ -9,7 +15,13 @@ import {
|
|
|
9
15
|
LimitOffsetRequireOrderByError,
|
|
10
16
|
UnsupportedFromTypeError,
|
|
11
17
|
} from '../../errors.js'
|
|
12
|
-
import {
|
|
18
|
+
import { VIRTUAL_PROP_NAMES } from '../../virtual-props.js'
|
|
19
|
+
import {
|
|
20
|
+
IncludesSubquery,
|
|
21
|
+
PropRef,
|
|
22
|
+
Value as ValClass,
|
|
23
|
+
getWhereExpression,
|
|
24
|
+
} from '../ir.js'
|
|
13
25
|
import { compileExpression, toBooleanPredicate } from './evaluators.js'
|
|
14
26
|
import { processJoins } from './joins.js'
|
|
15
27
|
import { containsAggregate, processGroupBy } from './group-by.js'
|
|
@@ -20,6 +32,7 @@ import type { OrderByOptimizationInfo } from './order-by.js'
|
|
|
20
32
|
import type {
|
|
21
33
|
BasicExpression,
|
|
22
34
|
CollectionRef,
|
|
35
|
+
IncludesMaterialization,
|
|
23
36
|
QueryIR,
|
|
24
37
|
QueryRef,
|
|
25
38
|
} from '../ir.js'
|
|
@@ -34,6 +47,34 @@ import type { QueryCache, QueryMapping, WindowOptions } from './types.js'
|
|
|
34
47
|
|
|
35
48
|
export type { WindowOptions } from './types.js'
|
|
36
49
|
|
|
50
|
+
/** Symbol used to tag parent $selected with routing metadata for includes */
|
|
51
|
+
export const INCLUDES_ROUTING = Symbol(`includesRouting`)
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Result of compiling an includes subquery, including the child pipeline
|
|
55
|
+
* and metadata needed to route child results to parent-scoped Collections.
|
|
56
|
+
*/
|
|
57
|
+
export interface IncludesCompilationResult {
|
|
58
|
+
/** Filtered child pipeline (post inner-join with parent keys) */
|
|
59
|
+
pipeline: ResultStream
|
|
60
|
+
/** Result field name on parent (e.g., "issues") */
|
|
61
|
+
fieldName: string
|
|
62
|
+
/** Parent-side correlation ref (e.g., project.id) */
|
|
63
|
+
correlationField: PropRef
|
|
64
|
+
/** Child-side correlation ref (e.g., issue.projectId) */
|
|
65
|
+
childCorrelationField: PropRef
|
|
66
|
+
/** Whether the child query has an ORDER BY clause */
|
|
67
|
+
hasOrderBy: boolean
|
|
68
|
+
/** Full compilation result for the child query (for nested includes + alias tracking) */
|
|
69
|
+
childCompilationResult: CompilationResult
|
|
70
|
+
/** Parent-side projection refs for parent-referencing filters */
|
|
71
|
+
parentProjection?: Array<PropRef>
|
|
72
|
+
/** How the output layer materializes the child result on the parent row */
|
|
73
|
+
materialization: IncludesMaterialization
|
|
74
|
+
/** Internal field used to unwrap scalar child selects */
|
|
75
|
+
scalarField?: string
|
|
76
|
+
}
|
|
77
|
+
|
|
37
78
|
/**
|
|
38
79
|
* Result of query compilation including both the pipeline and source-specific WHERE clauses
|
|
39
80
|
*/
|
|
@@ -68,6 +109,9 @@ export interface CompilationResult {
|
|
|
68
109
|
* the inner aliases where collection subscriptions were created.
|
|
69
110
|
*/
|
|
70
111
|
aliasRemapping: Record<string, string>
|
|
112
|
+
|
|
113
|
+
/** Child pipelines for includes subqueries */
|
|
114
|
+
includes?: Array<IncludesCompilationResult>
|
|
71
115
|
}
|
|
72
116
|
|
|
73
117
|
/**
|
|
@@ -94,6 +138,9 @@ export function compileQuery(
|
|
|
94
138
|
setWindowFn: (windowFn: (options: WindowOptions) => void) => void,
|
|
95
139
|
cache: QueryCache = new WeakMap(),
|
|
96
140
|
queryMapping: QueryMapping = new WeakMap(),
|
|
141
|
+
// For includes: parent key stream to inner-join with this query's FROM
|
|
142
|
+
parentKeyStream?: KeyedStream,
|
|
143
|
+
childCorrelationField?: PropRef,
|
|
97
144
|
): CompilationResult {
|
|
98
145
|
// Check if the original raw query has already been compiled
|
|
99
146
|
const cachedResult = cache.get(rawQuery)
|
|
@@ -107,7 +154,9 @@ export function compileQuery(
|
|
|
107
154
|
validateQueryStructure(rawQuery)
|
|
108
155
|
|
|
109
156
|
// Optimize the query before compilation
|
|
110
|
-
const { optimizedQuery
|
|
157
|
+
const { optimizedQuery, sourceWhereClauses } = optimizeQuery(rawQuery)
|
|
158
|
+
// Use a mutable binding so we can shallow-clone select before includes mutation
|
|
159
|
+
let query = optimizedQuery
|
|
111
160
|
|
|
112
161
|
// Create mapping from optimized query to original for caching
|
|
113
162
|
queryMapping.set(query, rawQuery)
|
|
@@ -153,14 +202,62 @@ export function compileQuery(
|
|
|
153
202
|
)
|
|
154
203
|
sources[mainSource] = mainInput
|
|
155
204
|
|
|
205
|
+
// If this is an includes child query, inner-join the raw input with parent keys.
|
|
206
|
+
// This filters the child collection to only rows matching parents in the result set.
|
|
207
|
+
// The inner join happens BEFORE namespace wrapping / WHERE / SELECT / ORDER BY,
|
|
208
|
+
// so the child pipeline only processes rows that match parents.
|
|
209
|
+
let filteredMainInput = mainInput
|
|
210
|
+
if (parentKeyStream && childCorrelationField) {
|
|
211
|
+
// Re-key child input by correlation field: [correlationValue, [childKey, childRow]]
|
|
212
|
+
const childFieldPath = childCorrelationField.path.slice(1) // remove alias prefix
|
|
213
|
+
const childRekeyed = mainInput.pipe(
|
|
214
|
+
map(([key, row]: [unknown, any]) => {
|
|
215
|
+
const correlationValue = getNestedValue(row, childFieldPath)
|
|
216
|
+
return [correlationValue, [key, row]] as [unknown, [unknown, any]]
|
|
217
|
+
}),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
// Inner join: only children whose correlation key exists in parent keys pass through
|
|
221
|
+
const joined = childRekeyed.pipe(joinOperator(parentKeyStream, `inner`))
|
|
222
|
+
|
|
223
|
+
// Extract: [correlationValue, [[childKey, childRow], parentContext]] → [childKey, childRow]
|
|
224
|
+
// Tag the row with __correlationKey for output routing
|
|
225
|
+
// If parentSide is non-null (parent context projected), attach as __parentContext
|
|
226
|
+
filteredMainInput = joined.pipe(
|
|
227
|
+
filter(([_correlationValue, [childSide]]: any) => {
|
|
228
|
+
return childSide != null
|
|
229
|
+
}),
|
|
230
|
+
map(([correlationValue, [childSide, parentSide]]: any) => {
|
|
231
|
+
const [childKey, childRow] = childSide
|
|
232
|
+
const tagged: any = { ...childRow, __correlationKey: correlationValue }
|
|
233
|
+
if (parentSide != null) {
|
|
234
|
+
tagged.__parentContext = parentSide
|
|
235
|
+
}
|
|
236
|
+
const effectiveKey =
|
|
237
|
+
parentSide != null
|
|
238
|
+
? `${String(childKey)}::${JSON.stringify(parentSide)}`
|
|
239
|
+
: childKey
|
|
240
|
+
return [effectiveKey, tagged]
|
|
241
|
+
}),
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
// Update sources so the rest of the pipeline uses the filtered input
|
|
245
|
+
sources[mainSource] = filteredMainInput
|
|
246
|
+
}
|
|
247
|
+
|
|
156
248
|
// Prepare the initial pipeline with the main source wrapped in its alias
|
|
157
|
-
let pipeline: NamespacedAndKeyedStream =
|
|
249
|
+
let pipeline: NamespacedAndKeyedStream = filteredMainInput.pipe(
|
|
158
250
|
map(([key, row]) => {
|
|
159
251
|
// Initialize the record with a nested structure
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
]
|
|
252
|
+
// If __parentContext exists (from parent-referencing includes), merge parent
|
|
253
|
+
// aliases into the namespaced row so WHERE can resolve parent refs
|
|
254
|
+
const { __parentContext, ...cleanRow } = row as any
|
|
255
|
+
const nsRow: Record<string, any> = { [mainSource]: cleanRow }
|
|
256
|
+
if (__parentContext) {
|
|
257
|
+
Object.assign(nsRow, __parentContext)
|
|
258
|
+
;(nsRow as any).__parentContext = __parentContext
|
|
259
|
+
}
|
|
260
|
+
const ret = [key, nsRow] as [string, Record<string, typeof row>]
|
|
164
261
|
return ret
|
|
165
262
|
}),
|
|
166
263
|
)
|
|
@@ -215,6 +312,163 @@ export function compileQuery(
|
|
|
215
312
|
}
|
|
216
313
|
}
|
|
217
314
|
|
|
315
|
+
// Extract includes from SELECT, compile child pipelines, and replace with placeholders.
|
|
316
|
+
// This must happen AFTER WHERE (so parent pipeline is filtered) but BEFORE processSelect
|
|
317
|
+
// (so IncludesSubquery nodes are stripped before select compilation).
|
|
318
|
+
const includesResults: Array<IncludesCompilationResult> = []
|
|
319
|
+
const includesRoutingFns: Array<{
|
|
320
|
+
fieldName: string
|
|
321
|
+
getRouting: (nsRow: any) => {
|
|
322
|
+
correlationKey: unknown
|
|
323
|
+
parentContext: Record<string, any> | null
|
|
324
|
+
}
|
|
325
|
+
}> = []
|
|
326
|
+
if (query.select) {
|
|
327
|
+
const includesEntries = extractIncludesFromSelect(query.select)
|
|
328
|
+
// Shallow-clone select before mutating so we don't modify the shared IR
|
|
329
|
+
// (the optimizer copies select by reference, so rawQuery.select === query.select)
|
|
330
|
+
if (includesEntries.length > 0) {
|
|
331
|
+
query = { ...query, select: { ...query.select } }
|
|
332
|
+
}
|
|
333
|
+
for (const { key, subquery } of includesEntries) {
|
|
334
|
+
// Branch parent pipeline: map to [correlationValue, parentContext]
|
|
335
|
+
// When parentProjection exists, project referenced parent fields; otherwise null (zero overhead)
|
|
336
|
+
const compiledCorrelation = compileExpression(subquery.correlationField)
|
|
337
|
+
let parentKeys: any
|
|
338
|
+
if (subquery.parentProjection && subquery.parentProjection.length > 0) {
|
|
339
|
+
const compiledProjections = subquery.parentProjection.map((ref) => ({
|
|
340
|
+
alias: ref.path[0]!,
|
|
341
|
+
field: ref.path.slice(1),
|
|
342
|
+
compiled: compileExpression(ref),
|
|
343
|
+
}))
|
|
344
|
+
parentKeys = pipeline.pipe(
|
|
345
|
+
map(([_key, nsRow]: any) => {
|
|
346
|
+
const parentContext: Record<string, Record<string, any>> = {}
|
|
347
|
+
for (const proj of compiledProjections) {
|
|
348
|
+
if (!parentContext[proj.alias]) {
|
|
349
|
+
parentContext[proj.alias] = {}
|
|
350
|
+
}
|
|
351
|
+
const value = proj.compiled(nsRow)
|
|
352
|
+
// Set nested field in the alias namespace
|
|
353
|
+
let target = parentContext[proj.alias]!
|
|
354
|
+
for (let i = 0; i < proj.field.length - 1; i++) {
|
|
355
|
+
if (!target[proj.field[i]!]) {
|
|
356
|
+
target[proj.field[i]!] = {}
|
|
357
|
+
}
|
|
358
|
+
target = target[proj.field[i]!]
|
|
359
|
+
}
|
|
360
|
+
target[proj.field[proj.field.length - 1]!] = value
|
|
361
|
+
}
|
|
362
|
+
return [compiledCorrelation(nsRow), parentContext] as any
|
|
363
|
+
}),
|
|
364
|
+
)
|
|
365
|
+
} else {
|
|
366
|
+
parentKeys = pipeline.pipe(
|
|
367
|
+
map(
|
|
368
|
+
([_key, nsRow]: any) => [compiledCorrelation(nsRow), null] as any,
|
|
369
|
+
),
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Deduplicate: when multiple parents share the same correlation key (and
|
|
374
|
+
// parentContext), clamp multiplicity to 1 so the inner join doesn't
|
|
375
|
+
// produce duplicate child entries that cause incorrect deletions.
|
|
376
|
+
parentKeys = parentKeys.pipe(
|
|
377
|
+
reduce((values: Array<[any, number]>) =>
|
|
378
|
+
values.map(([v, mult]) => [v, mult > 0 ? 1 : 0] as [any, number]),
|
|
379
|
+
),
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
// If parent filters exist, append them to the child query's WHERE
|
|
383
|
+
const childQuery =
|
|
384
|
+
subquery.parentFilters && subquery.parentFilters.length > 0
|
|
385
|
+
? {
|
|
386
|
+
...subquery.query,
|
|
387
|
+
where: [
|
|
388
|
+
...(subquery.query.where || []),
|
|
389
|
+
...subquery.parentFilters,
|
|
390
|
+
],
|
|
391
|
+
}
|
|
392
|
+
: subquery.query
|
|
393
|
+
|
|
394
|
+
// Recursively compile child query WITH the parent key stream
|
|
395
|
+
const childResult = compileQuery(
|
|
396
|
+
childQuery,
|
|
397
|
+
allInputs,
|
|
398
|
+
collections,
|
|
399
|
+
subscriptions,
|
|
400
|
+
callbacks,
|
|
401
|
+
lazySources,
|
|
402
|
+
optimizableOrderByCollections,
|
|
403
|
+
setWindowFn,
|
|
404
|
+
cache,
|
|
405
|
+
queryMapping,
|
|
406
|
+
parentKeys,
|
|
407
|
+
subquery.childCorrelationField,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
// Merge child's alias metadata into parent's
|
|
411
|
+
Object.assign(aliasToCollectionId, childResult.aliasToCollectionId)
|
|
412
|
+
Object.assign(aliasRemapping, childResult.aliasRemapping)
|
|
413
|
+
|
|
414
|
+
includesResults.push({
|
|
415
|
+
pipeline: childResult.pipeline,
|
|
416
|
+
fieldName: subquery.fieldName,
|
|
417
|
+
correlationField: subquery.correlationField,
|
|
418
|
+
childCorrelationField: subquery.childCorrelationField,
|
|
419
|
+
hasOrderBy: !!(
|
|
420
|
+
subquery.query.orderBy && subquery.query.orderBy.length > 0
|
|
421
|
+
),
|
|
422
|
+
childCompilationResult: childResult,
|
|
423
|
+
parentProjection: subquery.parentProjection,
|
|
424
|
+
materialization: subquery.materialization,
|
|
425
|
+
scalarField: subquery.scalarField,
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
// Capture routing function for INCLUDES_ROUTING tagging
|
|
429
|
+
if (subquery.parentProjection && subquery.parentProjection.length > 0) {
|
|
430
|
+
const compiledProjs = subquery.parentProjection.map((ref) => ({
|
|
431
|
+
alias: ref.path[0]!,
|
|
432
|
+
field: ref.path.slice(1),
|
|
433
|
+
compiled: compileExpression(ref),
|
|
434
|
+
}))
|
|
435
|
+
const compiledCorr = compiledCorrelation
|
|
436
|
+
includesRoutingFns.push({
|
|
437
|
+
fieldName: subquery.fieldName,
|
|
438
|
+
getRouting: (nsRow: any) => {
|
|
439
|
+
const parentContext: Record<string, Record<string, any>> = {}
|
|
440
|
+
for (const proj of compiledProjs) {
|
|
441
|
+
if (!parentContext[proj.alias]) {
|
|
442
|
+
parentContext[proj.alias] = {}
|
|
443
|
+
}
|
|
444
|
+
const value = proj.compiled(nsRow)
|
|
445
|
+
let target = parentContext[proj.alias]!
|
|
446
|
+
for (let i = 0; i < proj.field.length - 1; i++) {
|
|
447
|
+
if (!target[proj.field[i]!]) {
|
|
448
|
+
target[proj.field[i]!] = {}
|
|
449
|
+
}
|
|
450
|
+
target = target[proj.field[i]!]
|
|
451
|
+
}
|
|
452
|
+
target[proj.field[proj.field.length - 1]!] = value
|
|
453
|
+
}
|
|
454
|
+
return { correlationKey: compiledCorr(nsRow), parentContext }
|
|
455
|
+
},
|
|
456
|
+
})
|
|
457
|
+
} else {
|
|
458
|
+
includesRoutingFns.push({
|
|
459
|
+
fieldName: subquery.fieldName,
|
|
460
|
+
getRouting: (nsRow: any) => ({
|
|
461
|
+
correlationKey: compiledCorrelation(nsRow),
|
|
462
|
+
parentContext: null,
|
|
463
|
+
}),
|
|
464
|
+
})
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Replace includes entry in select with a null placeholder
|
|
468
|
+
replaceIncludesInSelect(query.select!, key)
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
218
472
|
if (query.distinct && !query.fnSelect && !query.select) {
|
|
219
473
|
throw new DistinctRequiresSelectError()
|
|
220
474
|
}
|
|
@@ -261,7 +515,29 @@ export function compileQuery(
|
|
|
261
515
|
)
|
|
262
516
|
}
|
|
263
517
|
|
|
264
|
-
//
|
|
518
|
+
// Tag $selected with routing metadata for includes.
|
|
519
|
+
// This lets collection-config-builder extract routing info (correlationKey + parentContext)
|
|
520
|
+
// from parent results without depending on the user's select.
|
|
521
|
+
if (includesRoutingFns.length > 0) {
|
|
522
|
+
pipeline = pipeline.pipe(
|
|
523
|
+
map(([key, namespacedRow]: any) => {
|
|
524
|
+
const routing: Record<
|
|
525
|
+
string,
|
|
526
|
+
{ correlationKey: unknown; parentContext: Record<string, any> | null }
|
|
527
|
+
> = {}
|
|
528
|
+
for (const { fieldName, getRouting } of includesRoutingFns) {
|
|
529
|
+
routing[fieldName] = getRouting(namespacedRow)
|
|
530
|
+
}
|
|
531
|
+
namespacedRow.$selected[INCLUDES_ROUTING] = routing
|
|
532
|
+
return [key, namespacedRow]
|
|
533
|
+
}),
|
|
534
|
+
)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Process the GROUP BY clause if it exists.
|
|
538
|
+
// When in includes mode (parentKeyStream), pass mainSource so that groupBy
|
|
539
|
+
// preserves __correlationKey for per-parent aggregation.
|
|
540
|
+
const groupByMainSource = parentKeyStream ? mainSource : undefined
|
|
265
541
|
if (query.groupBy && query.groupBy.length > 0) {
|
|
266
542
|
pipeline = processGroupBy(
|
|
267
543
|
pipeline,
|
|
@@ -269,6 +545,8 @@ export function compileQuery(
|
|
|
269
545
|
query.having,
|
|
270
546
|
query.select,
|
|
271
547
|
query.fnHaving,
|
|
548
|
+
mainCollectionId,
|
|
549
|
+
groupByMainSource,
|
|
272
550
|
)
|
|
273
551
|
} else if (query.select) {
|
|
274
552
|
// Check if SELECT contains aggregates but no GROUP BY (implicit single-group aggregation)
|
|
@@ -283,6 +561,8 @@ export function compileQuery(
|
|
|
283
561
|
query.having,
|
|
284
562
|
query.select,
|
|
285
563
|
query.fnHaving,
|
|
564
|
+
mainCollectionId,
|
|
565
|
+
groupByMainSource,
|
|
286
566
|
)
|
|
287
567
|
}
|
|
288
568
|
}
|
|
@@ -322,6 +602,21 @@ export function compileQuery(
|
|
|
322
602
|
|
|
323
603
|
// Process orderBy parameter if it exists
|
|
324
604
|
if (query.orderBy && query.orderBy.length > 0) {
|
|
605
|
+
// When in includes mode with limit/offset, use grouped ordering so that
|
|
606
|
+
// the limit is applied per parent (per correlation key), not globally.
|
|
607
|
+
const includesGroupKeyFn =
|
|
608
|
+
parentKeyStream &&
|
|
609
|
+
(query.limit !== undefined || query.offset !== undefined)
|
|
610
|
+
? (_key: unknown, row: unknown) => {
|
|
611
|
+
const correlationKey = (row as any)?.[mainSource]?.__correlationKey
|
|
612
|
+
const parentContext = (row as any)?.__parentContext
|
|
613
|
+
if (parentContext != null) {
|
|
614
|
+
return JSON.stringify([correlationKey, parentContext])
|
|
615
|
+
}
|
|
616
|
+
return correlationKey
|
|
617
|
+
}
|
|
618
|
+
: undefined
|
|
619
|
+
|
|
325
620
|
const orderedPipeline = processOrderBy(
|
|
326
621
|
rawQuery,
|
|
327
622
|
pipeline,
|
|
@@ -332,26 +627,43 @@ export function compileQuery(
|
|
|
332
627
|
setWindowFn,
|
|
333
628
|
query.limit,
|
|
334
629
|
query.offset,
|
|
630
|
+
includesGroupKeyFn,
|
|
335
631
|
)
|
|
336
632
|
|
|
337
633
|
// Final step: extract the $selected and include orderBy index
|
|
338
|
-
const resultPipeline = orderedPipeline.pipe(
|
|
634
|
+
const resultPipeline: ResultStream = orderedPipeline.pipe(
|
|
339
635
|
map(([key, [row, orderByIndex]]) => {
|
|
340
636
|
// Extract the final results from $selected and include orderBy index
|
|
341
637
|
const raw = (row as any).$selected
|
|
342
|
-
const finalResults =
|
|
638
|
+
const finalResults = attachVirtualPropsToSelected(
|
|
639
|
+
unwrapValue(raw),
|
|
640
|
+
row as Record<string, any>,
|
|
641
|
+
)
|
|
642
|
+
// When in includes mode, embed the correlation key and parentContext
|
|
643
|
+
if (parentKeyStream) {
|
|
644
|
+
const correlationKey = (row as any)[mainSource]?.__correlationKey
|
|
645
|
+
const parentContext = (row as any).__parentContext ?? null
|
|
646
|
+
// Strip internal routing properties that may leak via spread selects
|
|
647
|
+
delete finalResults.__correlationKey
|
|
648
|
+
delete finalResults.__parentContext
|
|
649
|
+
return [
|
|
650
|
+
key,
|
|
651
|
+
[finalResults, orderByIndex, correlationKey, parentContext],
|
|
652
|
+
] as any
|
|
653
|
+
}
|
|
343
654
|
return [key, [finalResults, orderByIndex]] as [unknown, [any, string]]
|
|
344
655
|
}),
|
|
345
|
-
)
|
|
656
|
+
) as ResultStream
|
|
346
657
|
|
|
347
658
|
const result = resultPipeline
|
|
348
659
|
// Cache the result before returning (use original query as key)
|
|
349
|
-
const compilationResult = {
|
|
660
|
+
const compilationResult: CompilationResult = {
|
|
350
661
|
collectionId: mainCollectionId,
|
|
351
662
|
pipeline: result,
|
|
352
663
|
sourceWhereClauses,
|
|
353
664
|
aliasToCollectionId,
|
|
354
665
|
aliasRemapping,
|
|
666
|
+
includes: includesResults.length > 0 ? includesResults : undefined,
|
|
355
667
|
}
|
|
356
668
|
cache.set(rawQuery, compilationResult)
|
|
357
669
|
|
|
@@ -366,7 +678,22 @@ export function compileQuery(
|
|
|
366
678
|
map(([key, row]) => {
|
|
367
679
|
// Extract the final results from $selected and return [key, [results, undefined]]
|
|
368
680
|
const raw = (row as any).$selected
|
|
369
|
-
const finalResults =
|
|
681
|
+
const finalResults = attachVirtualPropsToSelected(
|
|
682
|
+
unwrapValue(raw),
|
|
683
|
+
row as Record<string, any>,
|
|
684
|
+
)
|
|
685
|
+
// When in includes mode, embed the correlation key and parentContext
|
|
686
|
+
if (parentKeyStream) {
|
|
687
|
+
const correlationKey = (row as any)[mainSource]?.__correlationKey
|
|
688
|
+
const parentContext = (row as any).__parentContext ?? null
|
|
689
|
+
// Strip internal routing properties that may leak via spread selects
|
|
690
|
+
delete finalResults.__correlationKey
|
|
691
|
+
delete finalResults.__parentContext
|
|
692
|
+
return [
|
|
693
|
+
key,
|
|
694
|
+
[finalResults, undefined, correlationKey, parentContext],
|
|
695
|
+
] as any
|
|
696
|
+
}
|
|
370
697
|
return [key, [finalResults, undefined]] as [
|
|
371
698
|
unknown,
|
|
372
699
|
[any, string | undefined],
|
|
@@ -376,12 +703,13 @@ export function compileQuery(
|
|
|
376
703
|
|
|
377
704
|
const result = resultPipeline
|
|
378
705
|
// Cache the result before returning (use original query as key)
|
|
379
|
-
const compilationResult = {
|
|
706
|
+
const compilationResult: CompilationResult = {
|
|
380
707
|
collectionId: mainCollectionId,
|
|
381
708
|
pipeline: result,
|
|
382
709
|
sourceWhereClauses,
|
|
383
710
|
aliasToCollectionId,
|
|
384
711
|
aliasRemapping,
|
|
712
|
+
includes: includesResults.length > 0 ? includesResults : undefined,
|
|
385
713
|
}
|
|
386
714
|
cache.set(rawQuery, compilationResult)
|
|
387
715
|
|
|
@@ -593,6 +921,35 @@ function unwrapValue(value: any): any {
|
|
|
593
921
|
return isValue(value) ? value.value : value
|
|
594
922
|
}
|
|
595
923
|
|
|
924
|
+
function attachVirtualPropsToSelected(
|
|
925
|
+
selected: any,
|
|
926
|
+
row: Record<string, any>,
|
|
927
|
+
): any {
|
|
928
|
+
if (!selected || typeof selected !== `object`) {
|
|
929
|
+
return selected
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
let needsMerge = false
|
|
933
|
+
for (const prop of VIRTUAL_PROP_NAMES) {
|
|
934
|
+
if (selected[prop] == null && prop in row) {
|
|
935
|
+
needsMerge = true
|
|
936
|
+
break
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
if (!needsMerge) {
|
|
941
|
+
return selected
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
for (const prop of VIRTUAL_PROP_NAMES) {
|
|
945
|
+
if (selected[prop] == null && prop in row) {
|
|
946
|
+
selected[prop] = row[prop]
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
return selected
|
|
951
|
+
}
|
|
952
|
+
|
|
596
953
|
/**
|
|
597
954
|
* Recursively maps optimized subqueries to their original queries for proper caching.
|
|
598
955
|
* This ensures that when we encounter the same QueryRef object in different contexts,
|
|
@@ -709,4 +1066,77 @@ export function followRef(
|
|
|
709
1066
|
}
|
|
710
1067
|
}
|
|
711
1068
|
|
|
1069
|
+
/**
|
|
1070
|
+
* Walks a Select object to find IncludesSubquery entries at the top level.
|
|
1071
|
+
* Throws if an IncludesSubquery is found nested inside a sub-object, since
|
|
1072
|
+
* the compiler only supports includes at the top level of a select.
|
|
1073
|
+
*/
|
|
1074
|
+
function extractIncludesFromSelect(
|
|
1075
|
+
select: Record<string, any>,
|
|
1076
|
+
): Array<{ key: string; subquery: IncludesSubquery }> {
|
|
1077
|
+
const results: Array<{ key: string; subquery: IncludesSubquery }> = []
|
|
1078
|
+
for (const [key, value] of Object.entries(select)) {
|
|
1079
|
+
if (key.startsWith(`__SPREAD_SENTINEL__`)) continue
|
|
1080
|
+
if (value instanceof IncludesSubquery) {
|
|
1081
|
+
results.push({ key, subquery: value })
|
|
1082
|
+
} else if (isNestedSelectObject(value)) {
|
|
1083
|
+
// Check nested objects for IncludesSubquery — not supported yet
|
|
1084
|
+
assertNoNestedIncludes(value, key)
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
return results
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/** Check if a value is a nested plain object in a select (not an IR expression node) */
|
|
1091
|
+
function isNestedSelectObject(value: any): value is Record<string, any> {
|
|
1092
|
+
return (
|
|
1093
|
+
value != null &&
|
|
1094
|
+
typeof value === `object` &&
|
|
1095
|
+
!Array.isArray(value) &&
|
|
1096
|
+
typeof value.type !== `string`
|
|
1097
|
+
)
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function assertNoNestedIncludes(
|
|
1101
|
+
obj: Record<string, any>,
|
|
1102
|
+
parentPath: string,
|
|
1103
|
+
): void {
|
|
1104
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1105
|
+
if (key.startsWith(`__SPREAD_SENTINEL__`)) continue
|
|
1106
|
+
if (value instanceof IncludesSubquery) {
|
|
1107
|
+
throw new Error(
|
|
1108
|
+
`Includes subqueries must be at the top level of select(). ` +
|
|
1109
|
+
`Found nested includes at "${parentPath}.${key}".`,
|
|
1110
|
+
)
|
|
1111
|
+
}
|
|
1112
|
+
if (isNestedSelectObject(value)) {
|
|
1113
|
+
assertNoNestedIncludes(value, `${parentPath}.${key}`)
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* Replaces an IncludesSubquery entry in the select object with a null Value placeholder.
|
|
1120
|
+
* This ensures processSelect() doesn't encounter it.
|
|
1121
|
+
*/
|
|
1122
|
+
function replaceIncludesInSelect(
|
|
1123
|
+
select: Record<string, any>,
|
|
1124
|
+
key: string,
|
|
1125
|
+
): void {
|
|
1126
|
+
select[key] = new ValClass(null)
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* Gets a nested value from an object by path segments.
|
|
1131
|
+
* For v1 with single-level correlation fields (e.g., `projectId`), it's just `obj[path[0]]`.
|
|
1132
|
+
*/
|
|
1133
|
+
function getNestedValue(obj: any, path: Array<string>): any {
|
|
1134
|
+
let value = obj
|
|
1135
|
+
for (const segment of path) {
|
|
1136
|
+
if (value == null) return value
|
|
1137
|
+
value = value[segment]
|
|
1138
|
+
}
|
|
1139
|
+
return value
|
|
1140
|
+
}
|
|
1141
|
+
|
|
712
1142
|
export type CompileQueryFn = typeof compileQuery
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
groupedOrderByWithFractionalIndex,
|
|
3
|
+
orderByWithFractionalIndex,
|
|
4
|
+
} from '@tanstack/db-ivm'
|
|
2
5
|
import { defaultComparator, makeComparator } from '../../utils/comparison.js'
|
|
3
6
|
import { PropRef, followRef } from '../ir.js'
|
|
4
7
|
import { ensureIndexForField } from '../../indexes/auto-index.js'
|
|
@@ -51,6 +54,7 @@ export function processOrderBy(
|
|
|
51
54
|
setWindowFn: (windowFn: (options: WindowOptions) => void) => void,
|
|
52
55
|
limit?: number,
|
|
53
56
|
offset?: number,
|
|
57
|
+
groupKeyFn?: (key: unknown, value: unknown) => unknown,
|
|
54
58
|
): IStreamBuilder<KeyValue<unknown, [NamespacedRow, string]>> {
|
|
55
59
|
// Pre-compile all order by expressions
|
|
56
60
|
const compiledOrderBy = orderByClause.map((clause) => {
|
|
@@ -126,7 +130,9 @@ export function processOrderBy(
|
|
|
126
130
|
// to loadSubset so the sync layer can optimize the query.
|
|
127
131
|
// We try to use an index on the FIRST orderBy column for lazy loading,
|
|
128
132
|
// even for multi-column orderBy (using wider bounds on first column).
|
|
129
|
-
|
|
133
|
+
// Skip this optimization when using grouped ordering (includes with limit),
|
|
134
|
+
// because the limit is per-group, not global — the child collection needs all data loaded.
|
|
135
|
+
if (limit && !groupKeyFn) {
|
|
130
136
|
let index: IndexInterface<string | number> | undefined
|
|
131
137
|
let followRefCollection: Collection | undefined
|
|
132
138
|
let firstColumnValueExtractor: CompiledSingleRowExpression | undefined
|
|
@@ -152,12 +158,19 @@ export function processOrderBy(
|
|
|
152
158
|
)
|
|
153
159
|
|
|
154
160
|
if (fieldName) {
|
|
161
|
+
// Use a single-column comparator for the index, not the
|
|
162
|
+
// multi-column `compare` function. The multi-column comparator
|
|
163
|
+
// expects array values [col1, col2, ...] but the index stores
|
|
164
|
+
// individual field values. Passing `compare` here causes the
|
|
165
|
+
// BTree to treat all single values as equal (since number[0]
|
|
166
|
+
// === undefined for both sides of the comparison).
|
|
167
|
+
const firstColumnCompareFn = makeComparator(compareOpts)
|
|
155
168
|
ensureIndexForField(
|
|
156
169
|
fieldName,
|
|
157
170
|
followRefResult.path,
|
|
158
171
|
followRefCollection,
|
|
159
172
|
compareOpts,
|
|
160
|
-
|
|
173
|
+
firstColumnCompareFn,
|
|
161
174
|
)
|
|
162
175
|
}
|
|
163
176
|
|
|
@@ -277,19 +290,45 @@ export function processOrderBy(
|
|
|
277
290
|
optimizableOrderByCollections[targetCollectionId] =
|
|
278
291
|
orderByOptimizationInfo
|
|
279
292
|
|
|
280
|
-
// Set up lazy loading callback
|
|
281
|
-
if
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
293
|
+
// Set up lazy loading callback to track how much more data is needed
|
|
294
|
+
// This is used by loadMoreIfNeeded to determine if more data should be loaded
|
|
295
|
+
setSizeCallback = (getSize: () => number) => {
|
|
296
|
+
optimizableOrderByCollections[targetCollectionId]![`dataNeeded`] =
|
|
297
|
+
() => {
|
|
298
|
+
const size = getSize()
|
|
299
|
+
return Math.max(0, orderByOptimizationInfo!.limit - size)
|
|
300
|
+
}
|
|
289
301
|
}
|
|
290
302
|
}
|
|
291
303
|
}
|
|
292
304
|
|
|
305
|
+
// Use grouped ordering when a groupKeyFn is provided (includes with limit/offset),
|
|
306
|
+
// otherwise use the standard global ordering operator.
|
|
307
|
+
if (groupKeyFn) {
|
|
308
|
+
return pipeline.pipe(
|
|
309
|
+
groupedOrderByWithFractionalIndex(valueExtractor, {
|
|
310
|
+
limit,
|
|
311
|
+
offset,
|
|
312
|
+
comparator: compare,
|
|
313
|
+
setSizeCallback,
|
|
314
|
+
groupKeyFn,
|
|
315
|
+
setWindowFn: (
|
|
316
|
+
windowFn: (options: { offset?: number; limit?: number }) => void,
|
|
317
|
+
) => {
|
|
318
|
+
setWindowFn((options) => {
|
|
319
|
+
windowFn(options)
|
|
320
|
+
if (orderByOptimizationInfo) {
|
|
321
|
+
orderByOptimizationInfo.offset =
|
|
322
|
+
options.offset ?? orderByOptimizationInfo.offset
|
|
323
|
+
orderByOptimizationInfo.limit =
|
|
324
|
+
options.limit ?? orderByOptimizationInfo.limit
|
|
325
|
+
}
|
|
326
|
+
})
|
|
327
|
+
},
|
|
328
|
+
}),
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
|
|
293
332
|
// Use fractional indexing and return the tuple [value, index]
|
|
294
333
|
return pipeline.pipe(
|
|
295
334
|
orderByWithFractionalIndex(valueExtractor, {
|