@dxos/echo-pipeline 0.8.3 → 0.8.4-main.05e74ebcff
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/LICENSE +102 -5
- package/README.md +1 -1
- package/dist/lib/neutral/chunk-46DCRDA7.mjs +271 -0
- package/dist/lib/neutral/chunk-46DCRDA7.mjs.map +7 -0
- package/dist/lib/{browser/chunk-35I6ERLG.mjs → neutral/chunk-LR7UFVQX.mjs} +313 -709
- package/dist/lib/neutral/chunk-LR7UFVQX.mjs.map +7 -0
- package/dist/lib/{browser → neutral}/filter/index.mjs +3 -2
- package/dist/lib/neutral/index.mjs +7332 -0
- package/dist/lib/neutral/index.mjs.map +7 -0
- package/dist/lib/neutral/meta.json +1 -0
- package/dist/lib/{browser → neutral}/testing/index.mjs +90 -311
- package/dist/lib/neutral/testing/index.mjs.map +7 -0
- package/dist/types/src/automerge/automerge-host-subduction.test.d.ts +2 -0
- package/dist/types/src/automerge/automerge-host-subduction.test.d.ts.map +1 -0
- package/dist/types/src/automerge/automerge-host.d.ts +119 -54
- package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
- package/dist/types/src/automerge/automerge-repo-subduction.test.d.ts +2 -0
- package/dist/types/src/automerge/automerge-repo-subduction.test.d.ts.map +1 -0
- package/dist/types/src/automerge/automerge-repo.test.d.ts +1 -1
- package/dist/types/src/automerge/automerge-repo.test.d.ts.map +1 -1
- package/dist/types/src/automerge/automerge-subduction.test.d.ts +2 -0
- package/dist/types/src/automerge/automerge-subduction.test.d.ts.map +1 -0
- package/dist/types/src/automerge/collection-synchronizer.d.ts +25 -4
- package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-data-monitor.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-network-adapter.d.ts +23 -10
- package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-replicator.d.ts +46 -15
- package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
- package/dist/types/src/automerge/errors.d.ts +35 -0
- package/dist/types/src/automerge/errors.d.ts.map +1 -0
- package/dist/types/src/automerge/handle-state.d.ts +52 -0
- package/dist/types/src/automerge/handle-state.d.ts.map +1 -0
- package/dist/types/src/automerge/heads-store.d.ts +9 -2
- package/dist/types/src/automerge/heads-store.d.ts.map +1 -1
- package/dist/types/src/automerge/index.d.ts +3 -1
- package/dist/types/src/automerge/index.d.ts.map +1 -1
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +11 -5
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +9 -8
- package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
- package/dist/types/src/automerge/mesh-echo-replicator.d.ts +6 -5
- package/dist/types/src/automerge/mesh-echo-replicator.d.ts.map +1 -1
- package/dist/types/src/automerge/network-protocol.d.ts.map +1 -1
- package/dist/types/src/automerge/space-collection.d.ts.map +1 -1
- package/dist/types/src/common/codec.d.ts +1 -1
- package/dist/types/src/common/codec.d.ts.map +1 -1
- package/dist/types/src/common/feeds.d.ts.map +1 -1
- package/dist/types/src/common/space-id.d.ts.map +1 -1
- package/dist/types/src/db-host/automerge-data-source.d.ts +35 -0
- package/dist/types/src/db-host/automerge-data-source.d.ts.map +1 -0
- package/dist/types/src/db-host/automerge-data-source.test.d.ts +2 -0
- package/dist/types/src/db-host/automerge-data-source.test.d.ts.map +1 -0
- package/dist/types/src/db-host/automerge-metrics.d.ts.map +1 -1
- package/dist/types/src/db-host/data-service.d.ts +13 -7
- package/dist/types/src/db-host/data-service.d.ts.map +1 -1
- package/dist/types/src/db-host/database-root.d.ts +9 -3
- package/dist/types/src/db-host/database-root.d.ts.map +1 -1
- package/dist/types/src/db-host/documents-synchronizer.d.ts +30 -8
- package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
- package/dist/types/src/db-host/echo-host.d.ts +74 -28
- package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
- package/dist/types/src/db-host/index.d.ts +2 -0
- package/dist/types/src/db-host/index.d.ts.map +1 -1
- package/dist/types/src/db-host/invalidation-hint.d.ts +19 -0
- package/dist/types/src/db-host/invalidation-hint.d.ts.map +1 -0
- package/dist/types/src/db-host/local-queue-service.d.ts +19 -0
- package/dist/types/src/db-host/local-queue-service.d.ts.map +1 -0
- package/dist/types/src/db-host/query-invalidation.test.d.ts +2 -0
- package/dist/types/src/db-host/query-invalidation.test.d.ts.map +1 -0
- package/dist/types/src/db-host/query-service.d.ts +19 -11
- package/dist/types/src/db-host/query-service.d.ts.map +1 -1
- package/dist/types/src/db-host/queue-data-source.d.ts +38 -0
- package/dist/types/src/db-host/queue-data-source.d.ts.map +1 -0
- package/dist/types/src/db-host/queue-service.test.d.ts +2 -0
- package/dist/types/src/db-host/queue-service.test.d.ts.map +1 -0
- package/dist/types/src/db-host/space-state-manager.d.ts +4 -3
- package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
- package/dist/types/src/db-host/stub.d.ts +11 -0
- package/dist/types/src/db-host/stub.d.ts.map +1 -0
- package/dist/types/src/edge/echo-edge-replicator.d.ts +10 -7
- package/dist/types/src/edge/echo-edge-replicator.d.ts.map +1 -1
- package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts +39 -0
- package/dist/types/src/edge/echo-edge-subduction-replicator.d.ts.map +1 -0
- package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts +2 -0
- package/dist/types/src/edge/echo-edge-subduction-replicator.test.d.ts.map +1 -0
- package/dist/types/src/edge/index.d.ts +1 -0
- package/dist/types/src/edge/index.d.ts.map +1 -1
- package/dist/types/src/edge/inflight-request-limiter.d.ts.map +1 -1
- package/dist/types/src/filter/filter-match.d.ts +4 -1
- package/dist/types/src/filter/filter-match.d.ts.map +1 -1
- package/dist/types/src/metadata/metadata-store.d.ts +1 -1
- package/dist/types/src/metadata/metadata-store.d.ts.map +1 -1
- package/dist/types/src/pipeline/message-selector.d.ts.map +1 -1
- package/dist/types/src/pipeline/pipeline.d.ts +3 -3
- package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
- package/dist/types/src/pipeline/timeframe-clock.d.ts.map +1 -1
- package/dist/types/src/query/errors.d.ts +22 -10
- package/dist/types/src/query/errors.d.ts.map +1 -1
- package/dist/types/src/query/plan.d.ts +66 -8
- package/dist/types/src/query/plan.d.ts.map +1 -1
- package/dist/types/src/query/query-executor.d.ts +66 -11
- package/dist/types/src/query/query-executor.d.ts.map +1 -1
- package/dist/types/src/query/query-planner.d.ts +12 -1
- package/dist/types/src/query/query-planner.d.ts.map +1 -1
- package/dist/types/src/space/admission-discovery-extension.d.ts.map +1 -1
- package/dist/types/src/space/auth.d.ts +3 -3
- package/dist/types/src/space/auth.d.ts.map +1 -1
- package/dist/types/src/space/control-pipeline.d.ts +5 -4
- package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
- package/dist/types/src/space/space-manager.d.ts +8 -8
- package/dist/types/src/space/space-manager.d.ts.map +1 -1
- package/dist/types/src/space/space-protocol.d.ts +10 -9
- package/dist/types/src/space/space-protocol.d.ts.map +1 -1
- package/dist/types/src/space/space.d.ts +6 -6
- package/dist/types/src/space/space.d.ts.map +1 -1
- package/dist/types/src/testing/change-metadata.d.ts.map +1 -1
- package/dist/types/src/testing/index.d.ts +0 -2
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/test-agent-builder.d.ts +2 -2
- package/dist/types/src/testing/test-agent-builder.d.ts.map +1 -1
- package/dist/types/src/testing/test-feed-builder.d.ts.map +1 -1
- package/dist/types/src/testing/test-network-adapter.d.ts +1 -1
- package/dist/types/src/testing/test-network-adapter.d.ts.map +1 -1
- package/dist/types/src/testing/test-replicator.d.ts +14 -13
- package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
- package/dist/types/src/util.d.ts +1 -1
- package/dist/types/src/util.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +59 -55
- package/src/automerge/automerge-host-subduction.test.ts +263 -0
- package/src/automerge/automerge-host.test.ts +162 -39
- package/src/automerge/automerge-host.ts +679 -201
- package/src/automerge/automerge-repo-subduction.test.ts +1054 -0
- package/src/automerge/automerge-repo.test.ts +349 -91
- package/src/automerge/automerge-subduction.test.ts +330 -0
- package/src/automerge/collection-synchronizer.test.ts +98 -17
- package/src/automerge/collection-synchronizer.ts +141 -37
- package/src/automerge/echo-data-monitor.ts +1 -1
- package/src/automerge/echo-network-adapter.test.ts +5 -4
- package/src/automerge/echo-network-adapter.ts +76 -31
- package/src/automerge/echo-replicator.ts +50 -15
- package/src/automerge/errors.ts +10 -0
- package/src/automerge/handle-state.ts +62 -0
- package/src/automerge/heads-store.ts +42 -3
- package/src/automerge/index.ts +3 -1
- package/src/automerge/leveldb-storage-adapter.ts +77 -11
- package/src/automerge/mesh-echo-replicator-connection.ts +12 -8
- package/src/automerge/mesh-echo-replicator.ts +15 -11
- package/src/automerge/storage-adapter.test.ts +1 -1
- package/src/common/space-id.ts +1 -1
- package/src/db-host/automerge-data-source.test.ts +299 -0
- package/src/db-host/automerge-data-source.ts +187 -0
- package/src/db-host/data-service.ts +35 -26
- package/src/db-host/database-root.ts +20 -8
- package/src/db-host/documents-synchronizer.test.ts +145 -14
- package/src/db-host/documents-synchronizer.ts +163 -51
- package/src/db-host/echo-host.ts +384 -99
- package/src/db-host/index.ts +2 -0
- package/src/db-host/invalidation-hint.ts +50 -0
- package/src/db-host/local-queue-service.ts +127 -0
- package/src/db-host/query-invalidation.test.ts +270 -0
- package/src/db-host/query-service.ts +111 -106
- package/src/db-host/queue-data-source.ts +160 -0
- package/src/db-host/queue-service.test.ts +145 -0
- package/src/db-host/space-state-manager.ts +12 -10
- package/src/db-host/stub.ts +33 -0
- package/src/edge/echo-edge-replicator.test.ts +12 -10
- package/src/edge/echo-edge-replicator.ts +144 -58
- package/src/edge/echo-edge-subduction-replicator.test.ts +158 -0
- package/src/edge/echo-edge-subduction-replicator.ts +377 -0
- package/src/edge/index.ts +1 -0
- package/src/filter/filter-match.test.ts +68 -32
- package/src/filter/filter-match.ts +230 -21
- package/src/metadata/metadata-store.ts +11 -8
- package/src/pipeline/pipeline-stress.test.ts +1 -1
- package/src/pipeline/pipeline.test.ts +2 -2
- package/src/pipeline/pipeline.ts +9 -7
- package/src/query/errors.ts +3 -1
- package/src/query/plan.ts +84 -9
- package/src/query/query-executor.ts +1197 -165
- package/src/query/query-planner.test.ts +993 -81
- package/src/query/query-planner.ts +503 -27
- package/src/space/admission-discovery-extension.ts +3 -3
- package/src/space/auth.ts +6 -6
- package/src/space/control-pipeline.test.ts +5 -4
- package/src/space/control-pipeline.ts +16 -13
- package/src/space/space-manager.browser.test.ts +1 -1
- package/src/space/space-manager.ts +16 -20
- package/src/space/space-protocol.browser.test.ts +15 -14
- package/src/space/space-protocol.test.ts +15 -14
- package/src/space/space-protocol.ts +22 -18
- package/src/space/space.test.ts +3 -3
- package/src/space/space.ts +11 -11
- package/src/testing/index.ts +0 -2
- package/src/testing/test-agent-builder.ts +3 -3
- package/src/testing/test-network-adapter.ts +24 -3
- package/src/testing/test-replicator.ts +19 -20
- package/src/util.ts +7 -3
- package/dist/lib/browser/chunk-35I6ERLG.mjs.map +0 -7
- package/dist/lib/browser/chunk-CGS2ULMK.mjs +0 -11
- package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +0 -7
- package/dist/lib/browser/chunk-TQJTKNMS.mjs +0 -126
- package/dist/lib/browser/chunk-TQJTKNMS.mjs.map +0 -7
- package/dist/lib/browser/index.mjs +0 -4743
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/testing/index.mjs.map +0 -7
- package/dist/lib/node/chunk-HOPOFWAL.cjs +0 -147
- package/dist/lib/node/chunk-HOPOFWAL.cjs.map +0 -7
- package/dist/lib/node/chunk-JXX6LF5U.cjs +0 -2084
- package/dist/lib/node/chunk-JXX6LF5U.cjs.map +0 -7
- package/dist/lib/node/chunk-Q7SFCCGT.cjs +0 -33
- package/dist/lib/node/chunk-Q7SFCCGT.cjs.map +0 -7
- package/dist/lib/node/filter/index.cjs +0 -32
- package/dist/lib/node/filter/index.cjs.map +0 -7
- package/dist/lib/node/index.cjs +0 -4699
- package/dist/lib/node/index.cjs.map +0 -7
- package/dist/lib/node/meta.json +0 -1
- package/dist/lib/node/testing/index.cjs +0 -753
- package/dist/lib/node/testing/index.cjs.map +0 -7
- package/dist/lib/node-esm/chunk-5BHLPT24.mjs +0 -2067
- package/dist/lib/node-esm/chunk-5BHLPT24.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-RVK35BS7.mjs +0 -126
- package/dist/lib/node-esm/chunk-RVK35BS7.mjs.map +0 -7
- package/dist/lib/node-esm/filter/index.mjs +0 -11
- package/dist/lib/node-esm/filter/index.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -4743
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/testing/index.mjs +0 -742
- package/dist/lib/node-esm/testing/index.mjs.map +0 -7
- package/dist/types/src/db-host/documents-iterator.d.ts +0 -7
- package/dist/types/src/db-host/documents-iterator.d.ts.map +0 -1
- package/dist/types/src/testing/test-data.d.ts +0 -18
- package/dist/types/src/testing/test-data.d.ts.map +0 -1
- package/dist/types/src/testing/test-schema.d.ts +0 -37
- package/dist/types/src/testing/test-schema.d.ts.map +0 -1
- package/src/db-host/documents-iterator.ts +0 -74
- package/src/testing/test-data.ts +0 -127
- package/src/testing/test-schema.ts +0 -53
- /package/dist/lib/{browser → neutral}/filter/index.mjs.map +0 -0
|
@@ -2,13 +2,31 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { Order, Query } from '@dxos/echo';
|
|
6
|
+
import { QueryAST } from '@dxos/echo-protocol';
|
|
6
7
|
import { invariant } from '@dxos/invariant';
|
|
7
|
-
import type { DXN
|
|
8
|
+
import type { DXN } from '@dxos/keys';
|
|
8
9
|
|
|
9
10
|
import { QueryError } from './errors';
|
|
10
11
|
import { QueryPlan } from './plan';
|
|
11
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Creates a QueryError with "Query too complex" message and includes the prettified query in the context.
|
|
15
|
+
*/
|
|
16
|
+
const queryTooComplexError = (query: QueryAST.Query | null): QueryError => {
|
|
17
|
+
if (query === null) {
|
|
18
|
+
return new QueryError({
|
|
19
|
+
message: 'Query too complex',
|
|
20
|
+
context: { query: null },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const prettyQuery = Query.pretty(Query.fromAst(query));
|
|
24
|
+
return new QueryError({
|
|
25
|
+
message: `Query too complex: ${prettyQuery}`,
|
|
26
|
+
context: { query, prettyQuery },
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
12
30
|
export type QueryPlannerOptions = {
|
|
13
31
|
defaultTextSearchKind: QueryPlan.TextSearchKind;
|
|
14
32
|
};
|
|
@@ -32,9 +50,12 @@ export class QueryPlanner {
|
|
|
32
50
|
}
|
|
33
51
|
|
|
34
52
|
createPlan(query: QueryAST.Query): QueryPlan.Plan {
|
|
53
|
+
this._validateQueryScoped(query);
|
|
35
54
|
let plan = this._generate(query, { ...DEFAULT_CONTEXT, originalQuery: query });
|
|
36
55
|
plan = this._optimizeEmptyFilters(plan);
|
|
37
56
|
plan = this._optimizeSoloUnions(plan);
|
|
57
|
+
plan = this._ensureOrderStep(plan);
|
|
58
|
+
plan = this._optimizeLimits(plan);
|
|
38
59
|
return plan;
|
|
39
60
|
}
|
|
40
61
|
|
|
@@ -54,12 +75,21 @@ export class QueryPlanner {
|
|
|
54
75
|
return this._generateRelationTraversalClause(query, context);
|
|
55
76
|
case 'reference-traversal':
|
|
56
77
|
return this._generateReferenceTraversalClause(query, context);
|
|
78
|
+
case 'hierarchy-traversal':
|
|
79
|
+
return this._generateHierarchyTraversalClause(query, context);
|
|
57
80
|
case 'union':
|
|
58
81
|
return this._generateUnionClause(query, context);
|
|
59
82
|
case 'set-difference':
|
|
60
83
|
return this._generateSetDifferenceClause(query, context);
|
|
84
|
+
case 'order':
|
|
85
|
+
return this._generateOrderClause(query, context);
|
|
86
|
+
case 'limit':
|
|
87
|
+
return this._generateLimitClause(query, context);
|
|
88
|
+
case 'from':
|
|
89
|
+
return this._generateFromClause(query, context);
|
|
61
90
|
default:
|
|
62
|
-
throw new QueryError(
|
|
91
|
+
throw new QueryError({
|
|
92
|
+
message: `Unsupported query type: ${(query as any).type}`,
|
|
63
93
|
context: { query: context.originalQuery },
|
|
64
94
|
});
|
|
65
95
|
}
|
|
@@ -69,15 +99,25 @@ export class QueryPlanner {
|
|
|
69
99
|
const newContext = {
|
|
70
100
|
...context,
|
|
71
101
|
};
|
|
72
|
-
if (query.options.spaceIds) {
|
|
73
|
-
newContext.selectionSpaces = query.options.spaceIds as readonly SpaceId[];
|
|
74
|
-
}
|
|
75
102
|
if (query.options.deleted) {
|
|
76
103
|
newContext.deletedHandling = query.options.deleted;
|
|
77
104
|
}
|
|
78
105
|
return this._generate(query.query, newContext);
|
|
79
106
|
}
|
|
80
107
|
|
|
108
|
+
private _generateFromClause(query: QueryAST.QueryFromClause, context: GenerationContext): QueryPlan.Plan {
|
|
109
|
+
if (query.from._tag === 'scope') {
|
|
110
|
+
return this._generate(query.query, { ...context, scope: query.from.scope });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Subquery from: flatten by rewriting the outer query's leaf select into a filter on the subquery.
|
|
114
|
+
const subquery = query.from.query;
|
|
115
|
+
const rewritten = QueryAST.map(query.query, (node) =>
|
|
116
|
+
node.type === 'select' ? { type: 'filter', selection: subquery, filter: node.filter } : node,
|
|
117
|
+
);
|
|
118
|
+
return this._generate(rewritten, context);
|
|
119
|
+
}
|
|
120
|
+
|
|
81
121
|
private _generateSelectClause(query: QueryAST.QuerySelectClause, context: GenerationContext): QueryPlan.Plan {
|
|
82
122
|
return this._generateSelectionFromFilter(query.filter, context);
|
|
83
123
|
}
|
|
@@ -86,6 +126,7 @@ export class QueryPlanner {
|
|
|
86
126
|
// TODO(dmaretskyi): If the tip of the query ast is a [select, ...filter] shape we can reorder the filters so the query is most efficient.
|
|
87
127
|
private _generateSelectionFromFilter(filter: QueryAST.Filter, context: GenerationContext): QueryPlan.Plan {
|
|
88
128
|
switch (filter.type) {
|
|
129
|
+
// Props
|
|
89
130
|
case 'object': {
|
|
90
131
|
if (
|
|
91
132
|
context.selectionInverted &&
|
|
@@ -102,7 +143,7 @@ export class QueryPlanner {
|
|
|
102
143
|
]);
|
|
103
144
|
}
|
|
104
145
|
if (context.selectionInverted) {
|
|
105
|
-
throw
|
|
146
|
+
throw queryTooComplexError(context.originalQuery);
|
|
106
147
|
}
|
|
107
148
|
|
|
108
149
|
// Try to utilize indexes during selection, prioritizing selecting by id, then by typename.
|
|
@@ -111,7 +152,7 @@ export class QueryPlanner {
|
|
|
111
152
|
return QueryPlan.Plan.make([
|
|
112
153
|
{
|
|
113
154
|
_tag: 'SelectStep',
|
|
114
|
-
|
|
155
|
+
scope: context.scope,
|
|
115
156
|
selector: {
|
|
116
157
|
_tag: 'IdSelector',
|
|
117
158
|
objectIds: filter.id,
|
|
@@ -127,7 +168,7 @@ export class QueryPlanner {
|
|
|
127
168
|
return QueryPlan.Plan.make([
|
|
128
169
|
{
|
|
129
170
|
_tag: 'SelectStep',
|
|
130
|
-
|
|
171
|
+
scope: context.scope,
|
|
131
172
|
selector: {
|
|
132
173
|
_tag: 'TypeSelector',
|
|
133
174
|
typename: [filter.typename as DXN.String],
|
|
@@ -135,16 +176,17 @@ export class QueryPlanner {
|
|
|
135
176
|
},
|
|
136
177
|
},
|
|
137
178
|
...this._generateDeletedHandlingSteps(context),
|
|
179
|
+
// TODO(dmaretskyi): Normally we could skip filtering by typename here, but since the index does not separate schema versions, we need to do additional filter to only select the correct version.
|
|
138
180
|
{
|
|
139
181
|
_tag: 'FilterStep',
|
|
140
|
-
filter: { ...filter
|
|
182
|
+
filter: { ...filter },
|
|
141
183
|
},
|
|
142
184
|
]);
|
|
143
185
|
} else {
|
|
144
186
|
return QueryPlan.Plan.make([
|
|
145
187
|
{
|
|
146
188
|
_tag: 'SelectStep',
|
|
147
|
-
|
|
189
|
+
scope: context.scope,
|
|
148
190
|
selector: {
|
|
149
191
|
_tag: 'WildcardSelector',
|
|
150
192
|
},
|
|
@@ -157,44 +199,223 @@ export class QueryPlanner {
|
|
|
157
199
|
]);
|
|
158
200
|
}
|
|
159
201
|
}
|
|
202
|
+
|
|
203
|
+
// Tag
|
|
204
|
+
case 'tag': {
|
|
205
|
+
return QueryPlan.Plan.make([
|
|
206
|
+
{
|
|
207
|
+
_tag: 'SelectStep',
|
|
208
|
+
scope: context.scope,
|
|
209
|
+
selector: {
|
|
210
|
+
_tag: 'WildcardSelector',
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
...this._generateDeletedHandlingSteps(context),
|
|
214
|
+
{
|
|
215
|
+
_tag: 'FilterStep',
|
|
216
|
+
filter: { ...filter },
|
|
217
|
+
},
|
|
218
|
+
]);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Text
|
|
160
222
|
case 'text-search': {
|
|
161
223
|
return QueryPlan.Plan.make([
|
|
162
224
|
{
|
|
163
225
|
_tag: 'SelectStep',
|
|
164
|
-
|
|
226
|
+
scope: context.scope,
|
|
165
227
|
selector: {
|
|
166
228
|
_tag: 'TextSelector',
|
|
167
229
|
text: filter.text,
|
|
168
230
|
searchKind: filter.searchKind ?? this._options.defaultTextSearchKind,
|
|
231
|
+
typename: null,
|
|
169
232
|
},
|
|
170
233
|
},
|
|
171
234
|
...this._generateDeletedHandlingSteps(context),
|
|
172
235
|
]);
|
|
173
236
|
}
|
|
237
|
+
|
|
238
|
+
// Timestamp
|
|
239
|
+
case 'timestamp': {
|
|
240
|
+
if (context.selectionInverted) {
|
|
241
|
+
throw new QueryError({
|
|
242
|
+
message:
|
|
243
|
+
'Negated timestamp filters are not supported. Use Filter.updated/Filter.created with the inverted range instead.',
|
|
244
|
+
context: { query: context.originalQuery },
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return QueryPlan.Plan.make([
|
|
248
|
+
{
|
|
249
|
+
_tag: 'SelectStep',
|
|
250
|
+
scope: context.scope,
|
|
251
|
+
selector: _timestampFilterToSelector(filter),
|
|
252
|
+
},
|
|
253
|
+
...this._generateDeletedHandlingSteps(context),
|
|
254
|
+
]);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ChildOf
|
|
258
|
+
case 'child-of': {
|
|
259
|
+
return QueryPlan.Plan.make([
|
|
260
|
+
{
|
|
261
|
+
_tag: 'SelectStep',
|
|
262
|
+
scope: context.scope,
|
|
263
|
+
selector: {
|
|
264
|
+
_tag: 'WildcardSelector',
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
...this._generateDeletedHandlingSteps(context),
|
|
268
|
+
{
|
|
269
|
+
_tag: 'FilterStep',
|
|
270
|
+
filter: { ...filter },
|
|
271
|
+
},
|
|
272
|
+
]);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Compare
|
|
174
276
|
case 'compare':
|
|
175
|
-
throw
|
|
277
|
+
throw queryTooComplexError(context.originalQuery);
|
|
176
278
|
case 'in':
|
|
177
|
-
throw
|
|
279
|
+
throw queryTooComplexError(context.originalQuery);
|
|
178
280
|
case 'range':
|
|
179
|
-
throw
|
|
281
|
+
throw queryTooComplexError(context.originalQuery);
|
|
282
|
+
|
|
283
|
+
// Boolean
|
|
180
284
|
case 'not':
|
|
181
285
|
return this._generateSelectionFromFilter(filter.filter, {
|
|
182
286
|
...context,
|
|
183
287
|
selectionInverted: !context.selectionInverted,
|
|
184
288
|
});
|
|
185
|
-
case 'and':
|
|
186
|
-
|
|
289
|
+
case 'and': {
|
|
290
|
+
const flatFilters = _flattenAnd(filter.filters);
|
|
291
|
+
const timestampFilters = flatFilters.filter((f): f is QueryAST.FilterTimestamp => f.type === 'timestamp');
|
|
292
|
+
const childOfFilters = flatFilters.filter((f): f is QueryAST.FilterChildOf => f.type === 'child-of');
|
|
293
|
+
const otherFilters = flatFilters.filter((f) => f.type !== 'timestamp' && f.type !== 'child-of');
|
|
294
|
+
|
|
295
|
+
if (timestampFilters.length > 0 && context.selectionInverted) {
|
|
296
|
+
throw new QueryError({
|
|
297
|
+
message:
|
|
298
|
+
'Negated timestamp filters are not supported. Use Filter.updated/Filter.created with the inverted range instead.',
|
|
299
|
+
context: { query: context.originalQuery },
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (timestampFilters.length > 0 && otherFilters.length <= 1 && childOfFilters.length === 0) {
|
|
304
|
+
const innerFilter = otherFilters[0];
|
|
305
|
+
const innerPlan = innerFilter
|
|
306
|
+
? this._generateSelectionFromFilter(innerFilter, context)
|
|
307
|
+
: QueryPlan.Plan.make([
|
|
308
|
+
{
|
|
309
|
+
_tag: 'SelectStep',
|
|
310
|
+
scope: context.scope,
|
|
311
|
+
selector: { _tag: 'WildcardSelector' },
|
|
312
|
+
},
|
|
313
|
+
...this._generateDeletedHandlingSteps(context),
|
|
314
|
+
]);
|
|
315
|
+
|
|
316
|
+
const selectIdx = innerPlan.steps.findIndex((s) => s._tag === 'SelectStep');
|
|
317
|
+
if (selectIdx !== -1) {
|
|
318
|
+
const newSteps = [...innerPlan.steps];
|
|
319
|
+
newSteps.splice(selectIdx + 1, 0, {
|
|
320
|
+
_tag: 'FilterStep',
|
|
321
|
+
filter: { type: 'and', filters: timestampFilters },
|
|
322
|
+
});
|
|
323
|
+
return QueryPlan.Plan.make(newSteps);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const selector = _mergeTimestampSelectors(timestampFilters.map(_timestampFilterToSelector));
|
|
327
|
+
return QueryPlan.Plan.make([
|
|
328
|
+
{
|
|
329
|
+
_tag: 'SelectStep',
|
|
330
|
+
scope: context.scope,
|
|
331
|
+
selector,
|
|
332
|
+
},
|
|
333
|
+
...this._generateDeletedHandlingSteps(context),
|
|
334
|
+
]);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (timestampFilters.length > 0 && childOfFilters.length === 0) {
|
|
338
|
+
throw new QueryError({
|
|
339
|
+
message:
|
|
340
|
+
'Timestamp filters can only be combined with a single type or property filter via AND. Split complex filters into a subquery.',
|
|
341
|
+
context: { query: context.originalQuery },
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (childOfFilters.length > 0) {
|
|
346
|
+
const remainingFilters = flatFilters.filter((f) => f.type !== 'child-of');
|
|
347
|
+
const innerPlan =
|
|
348
|
+
remainingFilters.length === 1
|
|
349
|
+
? this._generateSelectionFromFilter(remainingFilters[0], context)
|
|
350
|
+
: remainingFilters.length > 1
|
|
351
|
+
? this._generateSelectionFromFilter({ type: 'and', filters: remainingFilters }, context)
|
|
352
|
+
: QueryPlan.Plan.make([
|
|
353
|
+
{
|
|
354
|
+
_tag: 'SelectStep',
|
|
355
|
+
scope: context.scope,
|
|
356
|
+
selector: { _tag: 'WildcardSelector' },
|
|
357
|
+
},
|
|
358
|
+
...this._generateDeletedHandlingSteps(context),
|
|
359
|
+
]);
|
|
360
|
+
|
|
361
|
+
const childOfSteps: QueryPlan.Step[] = childOfFilters.map((f) => ({
|
|
362
|
+
_tag: 'FilterStep' as const,
|
|
363
|
+
filter: f,
|
|
364
|
+
}));
|
|
365
|
+
|
|
366
|
+
return QueryPlan.Plan.make([...innerPlan.steps, ...childOfSteps]);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Simple AND: all filters are root-executable against in-memory objects after a wildcard select.
|
|
370
|
+
// Supports combining true root selectors (`object`, `tag`) plus negations / unions of those.
|
|
371
|
+
// Property-level filters (`compare`, `in`, `range`, `contains`) are intentionally excluded because
|
|
372
|
+
// `filterMatchObject` only handles them as nested predicates inside an `object` filter, not at the root.
|
|
373
|
+
const isRootExecutable = (filter: QueryAST.Filter): boolean => {
|
|
374
|
+
switch (filter.type) {
|
|
375
|
+
case 'object':
|
|
376
|
+
case 'tag':
|
|
377
|
+
return true;
|
|
378
|
+
case 'not':
|
|
379
|
+
return isRootExecutable(filter.filter);
|
|
380
|
+
case 'or':
|
|
381
|
+
return filter.filters.every(isRootExecutable);
|
|
382
|
+
default:
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
if (flatFilters.every(isRootExecutable)) {
|
|
387
|
+
const innerFilter: QueryAST.Filter = { type: 'and', filters: flatFilters };
|
|
388
|
+
const plannedFilter: QueryAST.Filter = context.selectionInverted
|
|
389
|
+
? { type: 'not', filter: innerFilter }
|
|
390
|
+
: innerFilter;
|
|
391
|
+
return QueryPlan.Plan.make([
|
|
392
|
+
{
|
|
393
|
+
_tag: 'SelectStep',
|
|
394
|
+
scope: context.scope,
|
|
395
|
+
selector: { _tag: 'WildcardSelector' },
|
|
396
|
+
},
|
|
397
|
+
...this._generateDeletedHandlingSteps(context),
|
|
398
|
+
{
|
|
399
|
+
_tag: 'FilterStep',
|
|
400
|
+
filter: plannedFilter,
|
|
401
|
+
},
|
|
402
|
+
]);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
throw queryTooComplexError(context.originalQuery);
|
|
406
|
+
}
|
|
187
407
|
case 'or':
|
|
188
408
|
// Optimized case
|
|
189
409
|
if (filter.filters.every(isTrivialTypenameFilter)) {
|
|
190
|
-
const typenames = filter.filters.map((
|
|
191
|
-
invariant(
|
|
192
|
-
return
|
|
410
|
+
const typenames = filter.filters.map((filter) => {
|
|
411
|
+
invariant(filter.type === 'object' && filter.typename !== null);
|
|
412
|
+
return filter.typename;
|
|
193
413
|
});
|
|
414
|
+
|
|
194
415
|
return QueryPlan.Plan.make([
|
|
195
416
|
{
|
|
196
417
|
_tag: 'SelectStep',
|
|
197
|
-
|
|
418
|
+
scope: context.scope,
|
|
198
419
|
selector: {
|
|
199
420
|
_tag: 'TypeSelector',
|
|
200
421
|
typename: typenames as DXN.String[],
|
|
@@ -202,13 +423,18 @@ export class QueryPlanner {
|
|
|
202
423
|
},
|
|
203
424
|
},
|
|
204
425
|
...this._generateDeletedHandlingSteps(context),
|
|
426
|
+
{
|
|
427
|
+
_tag: 'FilterStep',
|
|
428
|
+
filter: { ...filter },
|
|
429
|
+
},
|
|
205
430
|
]);
|
|
206
431
|
} else {
|
|
207
|
-
throw
|
|
432
|
+
throw queryTooComplexError(context.originalQuery);
|
|
208
433
|
}
|
|
209
434
|
|
|
210
435
|
default:
|
|
211
|
-
throw new QueryError(
|
|
436
|
+
throw new QueryError({
|
|
437
|
+
message: `Unsupported filter type: ${(filter as any).type}`,
|
|
212
438
|
context: { query: context.originalQuery },
|
|
213
439
|
});
|
|
214
440
|
}
|
|
@@ -286,7 +512,7 @@ export class QueryPlanner {
|
|
|
286
512
|
traversal: {
|
|
287
513
|
_tag: 'ReferenceTraversal',
|
|
288
514
|
direction: 'incoming',
|
|
289
|
-
property: query.property,
|
|
515
|
+
property: query.property ?? null,
|
|
290
516
|
},
|
|
291
517
|
},
|
|
292
518
|
...this._generateDeletedHandlingSteps(context),
|
|
@@ -392,6 +618,38 @@ export class QueryPlanner {
|
|
|
392
618
|
]);
|
|
393
619
|
}
|
|
394
620
|
|
|
621
|
+
private _generateHierarchyTraversalClause(
|
|
622
|
+
query: QueryAST.QueryHierarchyTraversalClause,
|
|
623
|
+
context: GenerationContext,
|
|
624
|
+
): QueryPlan.Plan {
|
|
625
|
+
return QueryPlan.Plan.make([
|
|
626
|
+
...this._generate(query.anchor, context).steps,
|
|
627
|
+
{
|
|
628
|
+
_tag: 'TraverseStep',
|
|
629
|
+
traversal: {
|
|
630
|
+
_tag: 'HierarchyTraversal',
|
|
631
|
+
direction: query.direction,
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
...this._generateDeletedHandlingSteps(context),
|
|
635
|
+
]);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
private _validateQueryScoped(query: QueryAST.Query): void {
|
|
639
|
+
let hasScope = false;
|
|
640
|
+
QueryAST.visit(query, (node) => {
|
|
641
|
+
if (node.type === 'from') {
|
|
642
|
+
hasScope = true;
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
if (!hasScope) {
|
|
646
|
+
throw new QueryError({
|
|
647
|
+
message: 'Query must be scoped with a from() clause',
|
|
648
|
+
context: { query },
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
395
653
|
/**
|
|
396
654
|
* Removes filter steps that have no predicates.
|
|
397
655
|
*/
|
|
@@ -425,6 +683,176 @@ export class QueryPlanner {
|
|
|
425
683
|
// TODO(dmaretskyi): Implement this.
|
|
426
684
|
return plan;
|
|
427
685
|
}
|
|
686
|
+
|
|
687
|
+
private _generateOrderClause(query: QueryAST.QueryOrderClause, context: GenerationContext): QueryPlan.Plan {
|
|
688
|
+
return QueryPlan.Plan.make([
|
|
689
|
+
...this._generate(query.query, context).steps,
|
|
690
|
+
{
|
|
691
|
+
_tag: 'OrderStep',
|
|
692
|
+
order: query.order,
|
|
693
|
+
},
|
|
694
|
+
]);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
private _generateLimitClause(query: QueryAST.QueryLimitClause, context: GenerationContext): QueryPlan.Plan {
|
|
698
|
+
return QueryPlan.Plan.make([
|
|
699
|
+
...this._generate(query.query, context).steps,
|
|
700
|
+
{
|
|
701
|
+
_tag: 'LimitStep',
|
|
702
|
+
limit: query.limit,
|
|
703
|
+
},
|
|
704
|
+
]);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// After complete plan is built, inspect it from the end:
|
|
708
|
+
// - Walk backwards until hitting an object set changer.
|
|
709
|
+
// - If an order step is found, skip.
|
|
710
|
+
// - Otherwise insert natural order before any limit steps (since limit should be applied after ordering).
|
|
711
|
+
// This method is recursive and also processes sub-plans in unions and set differences.
|
|
712
|
+
private _ensureOrderStep(plan: QueryPlan.Plan): QueryPlan.Plan {
|
|
713
|
+
// First, recursively process any sub-plans.
|
|
714
|
+
const processedSteps = plan.steps.map((step): QueryPlan.Step => {
|
|
715
|
+
if (step._tag === 'UnionStep') {
|
|
716
|
+
return {
|
|
717
|
+
_tag: 'UnionStep',
|
|
718
|
+
plans: step.plans.map((subPlan) => this._ensureOrderStep(subPlan)),
|
|
719
|
+
};
|
|
720
|
+
} else if (step._tag === 'SetDifferenceStep') {
|
|
721
|
+
return {
|
|
722
|
+
_tag: 'SetDifferenceStep',
|
|
723
|
+
source: this._ensureOrderStep(step.source),
|
|
724
|
+
exclude: this._ensureOrderStep(step.exclude),
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
return step;
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
const processedPlan = QueryPlan.Plan.make(processedSteps);
|
|
731
|
+
|
|
732
|
+
const OBJECT_SET_CHANGERS = new Set(['SelectStep', 'TraverseStep', 'UnionStep', 'SetDifferenceStep']);
|
|
733
|
+
for (let i = processedPlan.steps.length - 1; i >= 0; i--) {
|
|
734
|
+
const step = processedPlan.steps[i];
|
|
735
|
+
if (step._tag === 'OrderStep') {
|
|
736
|
+
return processedPlan;
|
|
737
|
+
}
|
|
738
|
+
if (OBJECT_SET_CHANGERS.has(step._tag)) {
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Find the position to insert the order step (before any limit steps).
|
|
744
|
+
let insertIndex = processedPlan.steps.length;
|
|
745
|
+
for (let i = processedPlan.steps.length - 1; i >= 0; i--) {
|
|
746
|
+
if (processedPlan.steps[i]._tag === 'LimitStep') {
|
|
747
|
+
insertIndex = i;
|
|
748
|
+
} else {
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const newSteps = [...processedPlan.steps];
|
|
754
|
+
newSteps.splice(insertIndex, 0, {
|
|
755
|
+
_tag: 'OrderStep',
|
|
756
|
+
order: [Order.natural.ast],
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
return QueryPlan.Plan.make(newSteps);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Propagates limits from LimitStep to SelectStep and OrderStep when possible.
|
|
764
|
+
* Limits can only be propagated if there are no unions or traversals between the LimitStep and SelectStep/OrderStep.
|
|
765
|
+
*/
|
|
766
|
+
private _optimizeLimits(plan: QueryPlan.Plan): QueryPlan.Plan {
|
|
767
|
+
// First, recursively process any sub-plans.
|
|
768
|
+
const processedSteps = plan.steps.map((step): QueryPlan.Step => {
|
|
769
|
+
if (step._tag === 'UnionStep') {
|
|
770
|
+
return {
|
|
771
|
+
_tag: 'UnionStep',
|
|
772
|
+
plans: step.plans.map((subPlan) => this._optimizeLimits(subPlan)),
|
|
773
|
+
};
|
|
774
|
+
} else if (step._tag === 'SetDifferenceStep') {
|
|
775
|
+
return {
|
|
776
|
+
_tag: 'SetDifferenceStep',
|
|
777
|
+
source: this._optimizeLimits(step.source),
|
|
778
|
+
exclude: this._optimizeLimits(step.exclude),
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
return step;
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
// Find if there's a LimitStep at the end (possibly after OrderStep).
|
|
785
|
+
let limitStepIndex = -1;
|
|
786
|
+
let limitValue: number | undefined;
|
|
787
|
+
|
|
788
|
+
for (let i = processedSteps.length - 1; i >= 0; i--) {
|
|
789
|
+
const step = processedSteps[i];
|
|
790
|
+
if (step._tag === 'LimitStep') {
|
|
791
|
+
limitStepIndex = i;
|
|
792
|
+
limitValue = step.limit;
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
// Only allow OrderStep after LimitStep when looking backwards.
|
|
796
|
+
if (step._tag !== 'OrderStep') {
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
if (limitStepIndex === -1 || limitValue === undefined) {
|
|
802
|
+
return QueryPlan.Plan.make(processedSteps);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Check if we can propagate the limit to a SelectStep and/or OrderStep.
|
|
806
|
+
// We can only do this if there are no unions, traversals, or set differences between them.
|
|
807
|
+
const BLOCKERS = new Set(['UnionStep', 'TraverseStep', 'SetDifferenceStep']);
|
|
808
|
+
|
|
809
|
+
let selectStepIndex = -1;
|
|
810
|
+
let orderStepIndex = -1;
|
|
811
|
+
for (let i = 0; i < limitStepIndex; i++) {
|
|
812
|
+
const step = processedSteps[i];
|
|
813
|
+
if (step._tag === 'SelectStep') {
|
|
814
|
+
selectStepIndex = i;
|
|
815
|
+
}
|
|
816
|
+
if (step._tag === 'OrderStep') {
|
|
817
|
+
orderStepIndex = i;
|
|
818
|
+
}
|
|
819
|
+
if (BLOCKERS.has(step._tag)) {
|
|
820
|
+
// Found a blocker after the select/order step - can't propagate.
|
|
821
|
+
selectStepIndex = -1;
|
|
822
|
+
orderStepIndex = -1;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (selectStepIndex === -1 && orderStepIndex === -1) {
|
|
827
|
+
return QueryPlan.Plan.make(processedSteps);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Create a mutable copy of steps to modify.
|
|
831
|
+
const newSteps = [...processedSteps];
|
|
832
|
+
|
|
833
|
+
// Propagate the limit to the SelectStep if found.
|
|
834
|
+
if (selectStepIndex !== -1) {
|
|
835
|
+
const selectStep = newSteps[selectStepIndex] as QueryPlan.SelectStep;
|
|
836
|
+
newSteps[selectStepIndex] = {
|
|
837
|
+
...selectStep,
|
|
838
|
+
limit: limitValue,
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Propagate the limit to the OrderStep if found.
|
|
843
|
+
if (orderStepIndex !== -1) {
|
|
844
|
+
const orderStep = newSteps[orderStepIndex] as QueryPlan.OrderStep;
|
|
845
|
+
newSteps[orderStepIndex] = {
|
|
846
|
+
...orderStep,
|
|
847
|
+
limit: limitValue,
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Remove the LimitStep since limit is now propagated.
|
|
852
|
+
newSteps.splice(limitStepIndex, 1);
|
|
853
|
+
|
|
854
|
+
return QueryPlan.Plan.make(newSteps);
|
|
855
|
+
}
|
|
428
856
|
}
|
|
429
857
|
|
|
430
858
|
/**
|
|
@@ -437,9 +865,9 @@ type GenerationContext = {
|
|
|
437
865
|
originalQuery: QueryAST.Query | null;
|
|
438
866
|
|
|
439
867
|
/**
|
|
440
|
-
*
|
|
868
|
+
* The scope to select from (space IDs, queues, etc.).
|
|
441
869
|
*/
|
|
442
|
-
|
|
870
|
+
scope: QueryAST.Scope;
|
|
443
871
|
|
|
444
872
|
/**
|
|
445
873
|
* How to handle deleted objects.
|
|
@@ -454,7 +882,7 @@ type GenerationContext = {
|
|
|
454
882
|
|
|
455
883
|
const DEFAULT_CONTEXT: GenerationContext = {
|
|
456
884
|
originalQuery: null,
|
|
457
|
-
|
|
885
|
+
scope: {},
|
|
458
886
|
deletedHandling: 'exclude',
|
|
459
887
|
selectionInverted: false,
|
|
460
888
|
};
|
|
@@ -474,6 +902,54 @@ const createRelationTraversalStep = (direction: QueryPlan.RelationTraversal['dir
|
|
|
474
902
|
},
|
|
475
903
|
});
|
|
476
904
|
|
|
905
|
+
/**
|
|
906
|
+
* Recursively flattens nested `and` filters into a single list.
|
|
907
|
+
*/
|
|
908
|
+
const _flattenAnd = (filters: readonly QueryAST.Filter[]): QueryAST.Filter[] => {
|
|
909
|
+
const result: QueryAST.Filter[] = [];
|
|
910
|
+
for (const f of filters) {
|
|
911
|
+
if (f.type === 'and') {
|
|
912
|
+
result.push(..._flattenAnd(f.filters));
|
|
913
|
+
} else {
|
|
914
|
+
result.push(f);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return result;
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
const _timestampFilterToSelector = (filter: QueryAST.FilterTimestamp): QueryPlan.TimestampSelector => {
|
|
921
|
+
const selector: QueryPlan.TimestampSelector = { _tag: 'TimestampSelector' };
|
|
922
|
+
const key =
|
|
923
|
+
filter.field === 'updatedAt'
|
|
924
|
+
? filter.operator === 'gte' || filter.operator === 'gt'
|
|
925
|
+
? 'updatedAfter'
|
|
926
|
+
: 'updatedBefore'
|
|
927
|
+
: filter.operator === 'gte' || filter.operator === 'gt'
|
|
928
|
+
? 'createdAfter'
|
|
929
|
+
: 'createdBefore';
|
|
930
|
+
selector[key] = filter.value;
|
|
931
|
+
return selector;
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
const _mergeTimestampSelectors = (selectors: QueryPlan.TimestampSelector[]): QueryPlan.TimestampSelector => {
|
|
935
|
+
const merged: QueryPlan.TimestampSelector = { _tag: 'TimestampSelector' };
|
|
936
|
+
for (const s of selectors) {
|
|
937
|
+
if (s.updatedAfter != null) {
|
|
938
|
+
merged.updatedAfter = Math.max(merged.updatedAfter ?? 0, s.updatedAfter);
|
|
939
|
+
}
|
|
940
|
+
if (s.updatedBefore != null) {
|
|
941
|
+
merged.updatedBefore = Math.min(merged.updatedBefore ?? Infinity, s.updatedBefore);
|
|
942
|
+
}
|
|
943
|
+
if (s.createdAfter != null) {
|
|
944
|
+
merged.createdAfter = Math.max(merged.createdAfter ?? 0, s.createdAfter);
|
|
945
|
+
}
|
|
946
|
+
if (s.createdBefore != null) {
|
|
947
|
+
merged.createdBefore = Math.min(merged.createdBefore ?? Infinity, s.createdBefore);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return merged;
|
|
951
|
+
};
|
|
952
|
+
|
|
477
953
|
const isTrivialTypenameFilter = (filter: QueryAST.Filter): boolean => {
|
|
478
954
|
return (
|
|
479
955
|
filter.type === 'object' &&
|