@dxos/echo-pipeline 0.8.4-main.ae835ea → 0.8.4-main.bc2380dfbc
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/{browser/chunk-WJJXJTNS.mjs → neutral/chunk-46DCRDA7.mjs} +44 -13
- package/dist/lib/neutral/chunk-46DCRDA7.mjs.map +7 -0
- package/dist/lib/{browser/chunk-FR6GJ4S4.mjs → neutral/chunk-LR7UFVQX.mjs} +145 -618
- package/dist/lib/neutral/chunk-LR7UFVQX.mjs.map +7 -0
- package/dist/lib/{browser → neutral}/filter/index.mjs +1 -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 +49 -292
- 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 +105 -27
- 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 +22 -2
- 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 +17 -11
- package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-replicator.d.ts +28 -16
- 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 +2 -0
- package/dist/types/src/automerge/index.d.ts.map +1 -1
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +10 -4
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts +8 -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/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 +12 -6
- 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 +18 -10
- 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 +3 -2
- 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 +7 -6
- 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 +1 -1
- package/dist/types/src/filter/filter-match.d.ts.map +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 +2 -2
- 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 +16 -20
- package/dist/types/src/query/errors.d.ts.map +1 -1
- package/dist/types/src/query/plan.d.ts +59 -8
- package/dist/types/src/query/plan.d.ts.map +1 -1
- package/dist/types/src/query/query-executor.d.ts +55 -12
- package/dist/types/src/query/query-executor.d.ts.map +1 -1
- package/dist/types/src/query/query-planner.d.ts +10 -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 +4 -3
- package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
- package/dist/types/src/space/space-manager.d.ts +7 -7
- package/dist/types/src/space/space-manager.d.ts.map +1 -1
- package/dist/types/src/space/space-protocol.d.ts +9 -8
- package/dist/types/src/space/space-protocol.d.ts.map +1 -1
- package/dist/types/src/space/space.d.ts +5 -5
- 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.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 +13 -13
- package/dist/types/src/testing/test-replicator.d.ts.map +1 -1
- package/dist/types/src/util.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +56 -54
- package/src/automerge/automerge-host-subduction.test.ts +263 -0
- package/src/automerge/automerge-host.test.ts +145 -32
- package/src/automerge/automerge-host.ts +442 -163
- package/src/automerge/automerge-repo-subduction.test.ts +1054 -0
- package/src/automerge/automerge-repo.test.ts +284 -77
- package/src/automerge/automerge-subduction.test.ts +330 -0
- package/src/automerge/collection-synchronizer.test.ts +96 -16
- package/src/automerge/collection-synchronizer.ts +125 -26
- package/src/automerge/echo-network-adapter.test.ts +2 -1
- package/src/automerge/echo-network-adapter.ts +44 -32
- package/src/automerge/echo-replicator.ts +30 -16
- 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 +2 -0
- package/src/automerge/leveldb-storage-adapter.ts +70 -4
- package/src/automerge/mesh-echo-replicator-connection.ts +8 -8
- package/src/automerge/mesh-echo-replicator.ts +14 -11
- 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 +27 -10
- package/src/db-host/database-root.ts +19 -7
- package/src/db-host/documents-synchronizer.test.ts +145 -14
- package/src/db-host/documents-synchronizer.ts +158 -59
- package/src/db-host/echo-host.ts +376 -90
- 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 +108 -111
- 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 +11 -9
- package/src/db-host/stub.ts +33 -0
- package/src/edge/echo-edge-replicator.test.ts +9 -9
- package/src/edge/echo-edge-replicator.ts +50 -42
- 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 +41 -25
- package/src/filter/filter-match.ts +75 -20
- package/src/metadata/metadata-store.ts +8 -5
- package/src/pipeline/pipeline-stress.test.ts +1 -3
- package/src/pipeline/pipeline.test.ts +0 -1
- package/src/pipeline/pipeline.ts +2 -3
- package/src/query/errors.ts +2 -2
- package/src/query/plan.ts +73 -9
- package/src/query/query-executor.ts +1064 -172
- package/src/query/query-planner.test.ts +854 -86
- package/src/query/query-planner.ts +444 -38
- package/src/space/admission-discovery-extension.ts +1 -1
- package/src/space/auth.ts +6 -6
- package/src/space/control-pipeline.test.ts +2 -2
- package/src/space/control-pipeline.ts +7 -7
- package/src/space/space-manager.ts +12 -17
- package/src/space/space-protocol.browser.test.ts +13 -12
- package/src/space/space-protocol.test.ts +13 -13
- package/src/space/space-protocol.ts +16 -15
- package/src/space/space.test.ts +2 -2
- package/src/space/space.ts +9 -10
- package/src/testing/index.ts +0 -2
- package/src/testing/test-agent-builder.ts +0 -1
- package/src/testing/test-network-adapter.ts +24 -3
- package/src/testing/test-replicator.ts +15 -20
- package/src/util.ts +7 -3
- package/dist/lib/browser/chunk-CGS2ULMK.mjs +0 -11
- package/dist/lib/browser/chunk-CGS2ULMK.mjs.map +0 -7
- package/dist/lib/browser/chunk-FR6GJ4S4.mjs.map +0 -7
- package/dist/lib/browser/chunk-WJJXJTNS.mjs.map +0 -7
- package/dist/lib/browser/index.mjs +0 -5492
- 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-esm/chunk-HSLMI22Q.mjs +0 -11
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-JQCCVDLT.mjs +0 -2144
- package/dist/lib/node-esm/chunk-JQCCVDLT.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-LEQ77KAT.mjs +0 -240
- package/dist/lib/node-esm/chunk-LEQ77KAT.mjs.map +0 -7
- package/dist/lib/node-esm/filter/index.mjs +0 -13
- package/dist/lib/node-esm/filter/index.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -5492
- 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 -764
- 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
|
@@ -3,31 +3,39 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import type { AutomergeUrl, DocumentId } from '@automerge/automerge-repo';
|
|
6
|
-
import * as
|
|
7
|
-
import * as
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
6
|
+
import type * as SqlClient from '@effect/sql/SqlClient';
|
|
7
|
+
import type * as Effect from 'effect/Effect';
|
|
8
|
+
import * as Runtime from 'effect/Runtime';
|
|
9
|
+
|
|
10
|
+
import { ContextDisposedError, LifecycleState, Resource } from '@dxos/context';
|
|
11
|
+
import { type Obj, Query } from '@dxos/echo';
|
|
12
|
+
import {
|
|
13
|
+
DatabaseDirectory,
|
|
14
|
+
EncodedReference,
|
|
15
|
+
type ObjectPropPath,
|
|
16
|
+
ObjectStructure,
|
|
17
|
+
type QueryAST,
|
|
18
|
+
isEncodedReference,
|
|
19
|
+
} from '@dxos/echo-protocol';
|
|
20
|
+
import { ATTR_PARENT, ATTR_RELATION_SOURCE, ATTR_RELATION_TARGET } from '@dxos/echo/internal';
|
|
21
|
+
import { type RuntimeProvider, runAndForwardErrors, unwrapExit } from '@dxos/effect';
|
|
22
|
+
import { EscapedPropPath, type IndexEngine, type ObjectMeta, type ReverseRef } from '@dxos/index-core';
|
|
12
23
|
import { invariant } from '@dxos/invariant';
|
|
13
|
-
import { DXN, type ObjectId,
|
|
24
|
+
import { DXN, type ObjectId, type SpaceId } from '@dxos/keys';
|
|
14
25
|
import { log } from '@dxos/log';
|
|
15
|
-
import { objectPointerCodec } from '@dxos/protocols';
|
|
16
26
|
import { type QueryReactivity, type QueryResult } from '@dxos/protocols/proto/dxos/echo/query';
|
|
17
|
-
import { getDeep, isNonNullable } from '@dxos/util';
|
|
27
|
+
import { compositeKey, getDeep, isNonNullable } from '@dxos/util';
|
|
18
28
|
|
|
19
29
|
import type { AutomergeHost } from '../automerge';
|
|
20
|
-
import {
|
|
21
|
-
import
|
|
22
|
-
import {
|
|
23
|
-
|
|
30
|
+
import type { InvalidationHint, SpaceStateManager } from '../db-host';
|
|
31
|
+
import { filterMatchObject, filterMatchObjectJSON } from '../filter';
|
|
32
|
+
import { QueryError } from './errors';
|
|
24
33
|
import type { QueryPlan } from './plan';
|
|
25
34
|
import { QueryPlanner } from './query-planner';
|
|
26
35
|
|
|
27
|
-
const isNullable: Predicate.Refinement<unknown, null | undefined> = Predicate.isNullable;
|
|
28
|
-
|
|
29
36
|
type QueryExecutorOptions = {
|
|
30
|
-
|
|
37
|
+
indexEngine: IndexEngine;
|
|
38
|
+
runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient>;
|
|
31
39
|
automergeHost: AutomergeHost;
|
|
32
40
|
spaceStateManager: SpaceStateManager;
|
|
33
41
|
|
|
@@ -48,11 +56,84 @@ type QueryExecutionResult = {
|
|
|
48
56
|
*/
|
|
49
57
|
type QueryItem = {
|
|
50
58
|
objectId: ObjectId;
|
|
51
|
-
|
|
59
|
+
|
|
52
60
|
spaceId: SpaceId;
|
|
53
|
-
|
|
61
|
+
queueId: ObjectId | null;
|
|
62
|
+
queueNamespace: string | null;
|
|
63
|
+
|
|
64
|
+
// For objects from automerge documents.
|
|
65
|
+
documentId: DocumentId | null;
|
|
66
|
+
|
|
67
|
+
// For objects from automerge documents.
|
|
68
|
+
doc: ObjectStructure | null;
|
|
69
|
+
|
|
70
|
+
// For objects from queues.
|
|
71
|
+
data: Obj.JSON | null;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Relevance rank for this item.
|
|
75
|
+
* Higher values indicate better matches for FTS/vector searches.
|
|
76
|
+
* Defaults to 1 for non-ranked queries (predicate matches).
|
|
77
|
+
*/
|
|
78
|
+
rank: number;
|
|
54
79
|
};
|
|
55
80
|
|
|
81
|
+
const QueryItem = Object.freeze({
|
|
82
|
+
/**
|
|
83
|
+
* Checks if the item is deleted.
|
|
84
|
+
* Only applies to this item, not its parents.
|
|
85
|
+
*/
|
|
86
|
+
isDeleted: (item: QueryItem) => {
|
|
87
|
+
if (item.doc) {
|
|
88
|
+
return ObjectStructure.isDeleted(item.doc);
|
|
89
|
+
} else if (item.data) {
|
|
90
|
+
return item.data['@deleted'] === true;
|
|
91
|
+
} else {
|
|
92
|
+
throw new Error('Invalid query item');
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
getProperty: (item: QueryItem, property: ObjectPropPath) => {
|
|
97
|
+
if (item.doc) {
|
|
98
|
+
return getDeep(item.doc.data, property);
|
|
99
|
+
} else if (item.data) {
|
|
100
|
+
return getDeep(item.data, property);
|
|
101
|
+
} else {
|
|
102
|
+
throw new Error('Invalid query item');
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
getParent: (item: QueryItem): DXN.String | undefined => {
|
|
107
|
+
if (item.doc) {
|
|
108
|
+
return ObjectStructure.getParent(item.doc)?.['/'] as DXN.String | undefined;
|
|
109
|
+
} else if (item.data) {
|
|
110
|
+
return item.data[ATTR_PARENT] as DXN.String;
|
|
111
|
+
} else {
|
|
112
|
+
throw new Error('Invalid query item');
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
getRelationSource: (item: QueryItem): DXN.String | undefined => {
|
|
117
|
+
if (item.doc) {
|
|
118
|
+
return ObjectStructure.getRelationSource(item.doc)?.['/'] as DXN.String | undefined;
|
|
119
|
+
} else if (item.data) {
|
|
120
|
+
return item.data[ATTR_RELATION_SOURCE] as DXN.String;
|
|
121
|
+
} else {
|
|
122
|
+
throw new Error('Invalid query item');
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
getRelationTarget: (item: QueryItem): DXN.String | undefined => {
|
|
127
|
+
if (item.doc) {
|
|
128
|
+
return ObjectStructure.getRelationTarget(item.doc)?.['/'] as DXN.String | undefined;
|
|
129
|
+
} else if (item.data) {
|
|
130
|
+
return item.data[ATTR_RELATION_TARGET] as DXN.String;
|
|
131
|
+
} else {
|
|
132
|
+
throw new Error('Invalid query item');
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
56
137
|
/**
|
|
57
138
|
* Recursive data structure that represents the execution trace of a query.
|
|
58
139
|
*/
|
|
@@ -135,10 +216,167 @@ type StepExecutionResult = {
|
|
|
135
216
|
trace: ExecutionTrace;
|
|
136
217
|
};
|
|
137
218
|
|
|
138
|
-
|
|
219
|
+
declare global {
|
|
220
|
+
interface ImportMeta {
|
|
221
|
+
env: ImportMetaEnv;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
interface ImportMetaEnv {
|
|
225
|
+
DX_TRACE_QUERY_EXECUTION: string;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const TRACE_QUERY_EXECUTION = !!import.meta.env?.DX_TRACE_QUERY_EXECUTION;
|
|
230
|
+
|
|
231
|
+
const MAX_DEPTH_FOR_DELETION_TRACING = 10;
|
|
232
|
+
const MAX_DEPTH_FOR_CHILD_OF_TRACING = 10;
|
|
139
233
|
|
|
140
234
|
/**
|
|
141
|
-
*
|
|
235
|
+
* Cached scope constraints extracted from a query plan.
|
|
236
|
+
* Used to quickly determine whether an invalidation hint can affect this query.
|
|
237
|
+
*
|
|
238
|
+
* A null dimension means the query is unconstrained on that dimension (matches any value).
|
|
239
|
+
*/
|
|
240
|
+
type QueryScopes = {
|
|
241
|
+
/**
|
|
242
|
+
* False for text-search, traversal, union, set-difference, or child-of queries.
|
|
243
|
+
* When false, matchesHint always returns true (conservative: always re-execute).
|
|
244
|
+
*/
|
|
245
|
+
isSimple: boolean;
|
|
246
|
+
spaceIds: Set<SpaceId> | null;
|
|
247
|
+
queueIds: Set<ObjectId> | null;
|
|
248
|
+
typenames: Set<string> | null;
|
|
249
|
+
objectIds: Set<ObjectId> | null;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const extractScopes = (plan: QueryPlan.Plan): QueryScopes => {
|
|
253
|
+
const scopes: QueryScopes = {
|
|
254
|
+
isSimple: true,
|
|
255
|
+
spaceIds: null,
|
|
256
|
+
queueIds: null,
|
|
257
|
+
typenames: null,
|
|
258
|
+
objectIds: null,
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
for (const step of plan.steps) {
|
|
262
|
+
switch (step._tag) {
|
|
263
|
+
case 'SelectStep': {
|
|
264
|
+
// Extract spaceIds from scope.
|
|
265
|
+
if (step.scope.spaceIds && step.scope.spaceIds.length > 0) {
|
|
266
|
+
if (!scopes.spaceIds) {
|
|
267
|
+
scopes.spaceIds = new Set();
|
|
268
|
+
}
|
|
269
|
+
for (const spaceId of step.scope.spaceIds) {
|
|
270
|
+
scopes.spaceIds.add(spaceId as SpaceId);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Extract queueIds from explicit feed DXNs (queue-kinded) and derive spaceIds from them.
|
|
275
|
+
if (step.scope.feeds && step.scope.feeds.length > 0 && !step.scope.allFeedsFromSpaces) {
|
|
276
|
+
let parseFailed = false;
|
|
277
|
+
const derivedQueueIds = new Set<ObjectId>();
|
|
278
|
+
const derivedSpaceIds = new Set<SpaceId>();
|
|
279
|
+
for (const queueDxnStr of step.scope.feeds as DXN.String[]) {
|
|
280
|
+
try {
|
|
281
|
+
const queueDXN = DXN.parse(queueDxnStr).asQueueDXN();
|
|
282
|
+
if (queueDXN) {
|
|
283
|
+
derivedQueueIds.add(queueDXN.queueId as ObjectId);
|
|
284
|
+
derivedSpaceIds.add(queueDXN.spaceId as SpaceId);
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
parseFailed = true;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (!parseFailed) {
|
|
292
|
+
if (derivedQueueIds.size > 0) {
|
|
293
|
+
scopes.queueIds ??= new Set<ObjectId>();
|
|
294
|
+
for (const id of derivedQueueIds) {
|
|
295
|
+
scopes.queueIds.add(id);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (derivedSpaceIds.size > 0) {
|
|
299
|
+
scopes.spaceIds ??= new Set<SpaceId>();
|
|
300
|
+
for (const id of derivedSpaceIds) {
|
|
301
|
+
// Derive spaceId from the queue DXN so space-scoped hints can skip this query.
|
|
302
|
+
scopes.spaceIds.add(id);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// On any parse error, leave queue-derived dimensions unconstrained.
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Extract typename / objectId constraints from selector.
|
|
310
|
+
switch (step.selector._tag) {
|
|
311
|
+
case 'TypeSelector': {
|
|
312
|
+
if (step.selector.inverted) {
|
|
313
|
+
// Inverted type selectors come from Filter.not — mark as non-simple.
|
|
314
|
+
scopes.isSimple = false;
|
|
315
|
+
} else {
|
|
316
|
+
if (!scopes.typenames) {
|
|
317
|
+
scopes.typenames = new Set();
|
|
318
|
+
}
|
|
319
|
+
for (const typename of step.selector.typename) {
|
|
320
|
+
scopes.typenames.add(typename as string);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
case 'IdSelector': {
|
|
326
|
+
if (!scopes.objectIds) {
|
|
327
|
+
scopes.objectIds = new Set();
|
|
328
|
+
}
|
|
329
|
+
for (const id of step.selector.objectIds) {
|
|
330
|
+
scopes.objectIds.add(id);
|
|
331
|
+
}
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
case 'TextSelector': {
|
|
335
|
+
scopes.isSimple = false;
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
default:
|
|
339
|
+
// WildcardSelector, TimestampSelector — no type/id constraint.
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
case 'FilterStep': {
|
|
345
|
+
// child-of filters require transitive parent traversal which can't be hinted.
|
|
346
|
+
if (step.filter.type === 'child-of') {
|
|
347
|
+
scopes.isSimple = false;
|
|
348
|
+
}
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
case 'TraverseStep':
|
|
352
|
+
case 'UnionStep':
|
|
353
|
+
case 'SetDifferenceStep':
|
|
354
|
+
scopes.isSimple = false;
|
|
355
|
+
break;
|
|
356
|
+
default:
|
|
357
|
+
// ClearWorkingSetStep, FilterDeletedStep, OrderStep, LimitStep are fine.
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return scopes;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const setsOverlap = <T>(a: ReadonlySet<T>, b: ReadonlySet<T>): boolean => {
|
|
366
|
+
const [smaller, larger]: [ReadonlySet<T>, ReadonlySet<T>] = a.size <= b.size ? [a, b] : [b, a];
|
|
367
|
+
for (const item of smaller) {
|
|
368
|
+
if (larger.has(item)) {
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return false;
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const overlapsOrUnconstrained = <T>(hintSet: ReadonlySet<T> | undefined, scopeSet: Set<T> | null): boolean =>
|
|
376
|
+
hintSet === undefined || scopeSet === null || setsOverlap(hintSet, scopeSet);
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Executes query plans against the IndexEngine and AutomergeHost.
|
|
142
380
|
*
|
|
143
381
|
* The QueryExecutor is responsible for:
|
|
144
382
|
* - Executing query plans step by step
|
|
@@ -148,7 +386,8 @@ const TRACE_QUERY_EXECUTION = false;
|
|
|
148
386
|
* - Handling different types of query operations (select, filter, traverse, etc.)
|
|
149
387
|
*/
|
|
150
388
|
export class QueryExecutor extends Resource {
|
|
151
|
-
private readonly
|
|
389
|
+
private readonly _indexEngine: IndexEngine;
|
|
390
|
+
private readonly _runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient>;
|
|
152
391
|
private readonly _automergeHost: AutomergeHost;
|
|
153
392
|
private readonly _spaceStateManager: SpaceStateManager;
|
|
154
393
|
/**
|
|
@@ -160,13 +399,15 @@ export class QueryExecutor extends Resource {
|
|
|
160
399
|
private readonly _reactivity: QueryReactivity;
|
|
161
400
|
|
|
162
401
|
private _plan: QueryPlan.Plan;
|
|
402
|
+
#scopes: QueryScopes;
|
|
163
403
|
private _trace: ExecutionTrace = ExecutionTrace.makeEmpty();
|
|
164
404
|
private _lastResultSet: QueryItem[] = [];
|
|
165
405
|
|
|
166
406
|
constructor(options: QueryExecutorOptions) {
|
|
167
407
|
super();
|
|
168
408
|
|
|
169
|
-
this.
|
|
409
|
+
this._indexEngine = options.indexEngine;
|
|
410
|
+
this._runtime = options.runtime;
|
|
170
411
|
this._automergeHost = options.automergeHost;
|
|
171
412
|
this._spaceStateManager = options.spaceStateManager;
|
|
172
413
|
|
|
@@ -176,6 +417,11 @@ export class QueryExecutor extends Resource {
|
|
|
176
417
|
|
|
177
418
|
const queryPlanner = new QueryPlanner();
|
|
178
419
|
this._plan = queryPlanner.createPlan(this._query);
|
|
420
|
+
this.#scopes = extractScopes(this._plan);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
get queryId(): string {
|
|
424
|
+
return this._id;
|
|
179
425
|
}
|
|
180
426
|
|
|
181
427
|
get query(): QueryAST.Query {
|
|
@@ -190,31 +436,54 @@ export class QueryExecutor extends Resource {
|
|
|
190
436
|
return this._trace;
|
|
191
437
|
}
|
|
192
438
|
|
|
193
|
-
protected override async _open(ctx: Context): Promise<void> {}
|
|
194
|
-
|
|
195
|
-
protected override async _close(ctx: Context): Promise<void> {}
|
|
196
|
-
|
|
197
439
|
getResults(): QueryResult[] {
|
|
198
440
|
return this._lastResultSet.map(
|
|
199
441
|
(item): QueryResult => ({
|
|
200
442
|
id: item.objectId,
|
|
201
|
-
documentId: item.documentId,
|
|
443
|
+
documentId: item.documentId ?? undefined,
|
|
444
|
+
queueId: item.queueId ?? undefined,
|
|
445
|
+
queueNamespace: item.queueNamespace ?? undefined,
|
|
202
446
|
spaceId: item.spaceId,
|
|
203
447
|
|
|
204
|
-
|
|
205
|
-
|
|
448
|
+
rank: item.rank,
|
|
449
|
+
|
|
450
|
+
documentJson: item.doc ? JSON.stringify(item.doc) : item.data ? JSON.stringify(item.data) : undefined,
|
|
206
451
|
}),
|
|
207
452
|
);
|
|
208
453
|
}
|
|
209
454
|
|
|
455
|
+
/**
|
|
456
|
+
* Returns true if the given invalidation hint could affect this query's result set.
|
|
457
|
+
* When false, the query can safely be skipped for this invalidation cycle.
|
|
458
|
+
*
|
|
459
|
+
* Conservative: returns true for complex queries (traversals, text-search, unions) and
|
|
460
|
+
* for any dimension where either the hint or the query is unconstrained.
|
|
461
|
+
*/
|
|
462
|
+
matchesHint(hint: InvalidationHint): boolean {
|
|
463
|
+
if (!this.#scopes.isSimple) {
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
return (
|
|
467
|
+
overlapsOrUnconstrained(hint.spaceIds, this.#scopes.spaceIds) &&
|
|
468
|
+
overlapsOrUnconstrained(hint.queueIds, this.#scopes.queueIds) &&
|
|
469
|
+
overlapsOrUnconstrained(hint.typenames, this.#scopes.typenames) &&
|
|
470
|
+
overlapsOrUnconstrained(hint.objectIds, this.#scopes.objectIds)
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
210
474
|
async execQuery(): Promise<QueryExecutionResult> {
|
|
211
475
|
invariant(this._lifecycleState === LifecycleState.OPEN);
|
|
212
476
|
|
|
477
|
+
log('exec query', {
|
|
478
|
+
queryId: this._id,
|
|
479
|
+
query: Query.pretty(Query.fromAst(this._query)),
|
|
480
|
+
});
|
|
481
|
+
|
|
213
482
|
const prevResultSet = this._lastResultSet;
|
|
214
483
|
const { workingSet, trace } = await this._execPlan(this._plan, []);
|
|
215
484
|
this._lastResultSet = workingSet;
|
|
216
485
|
trace.name = 'Root';
|
|
217
|
-
trace.details = JSON.stringify({ id: this._id });
|
|
486
|
+
trace.details = JSON.stringify({ id: this._id, query: Query.pretty(Query.fromAst(this._query)) });
|
|
218
487
|
this._trace = trace;
|
|
219
488
|
|
|
220
489
|
const changed =
|
|
@@ -223,13 +492,16 @@ export class QueryExecutor extends Resource {
|
|
|
223
492
|
(item, index) =>
|
|
224
493
|
workingSet[index].objectId !== item.objectId ||
|
|
225
494
|
workingSet[index].spaceId !== item.spaceId ||
|
|
226
|
-
workingSet[index].documentId !== item.documentId
|
|
495
|
+
workingSet[index].documentId !== item.documentId ||
|
|
496
|
+
workingSet[index].queueId !== item.queueId ||
|
|
497
|
+
workingSet[index].queueNamespace !== item.queueNamespace,
|
|
227
498
|
);
|
|
228
499
|
|
|
229
500
|
// Disabled because concurrent queries don't print hierarchies correctly.
|
|
230
501
|
// ExecutionTrace.putOnPerformanceTimeline(trace);
|
|
231
502
|
|
|
232
503
|
if (TRACE_QUERY_EXECUTION) {
|
|
504
|
+
// eslint-disable-next-line no-console
|
|
233
505
|
console.log(ExecutionTrace.format(trace));
|
|
234
506
|
}
|
|
235
507
|
|
|
@@ -240,7 +512,6 @@ export class QueryExecutor extends Resource {
|
|
|
240
512
|
|
|
241
513
|
private async _execPlan(plan: QueryPlan.Plan, workingSet: QueryItem[]): Promise<StepExecutionResult> {
|
|
242
514
|
const trace = ExecutionTrace.makeEmpty();
|
|
243
|
-
const begin = performance.now();
|
|
244
515
|
for (const step of plan.steps) {
|
|
245
516
|
if (this._ctx.disposed) {
|
|
246
517
|
throw new ContextDisposedError();
|
|
@@ -288,6 +559,9 @@ export class QueryExecutor extends Resource {
|
|
|
288
559
|
case 'OrderStep':
|
|
289
560
|
({ workingSet: newWorkingSet, trace } = await this._execOrderStep(step, workingSet));
|
|
290
561
|
break;
|
|
562
|
+
case 'LimitStep':
|
|
563
|
+
({ workingSet: newWorkingSet, trace } = await this._execLimitStep(step, workingSet));
|
|
564
|
+
break;
|
|
291
565
|
default:
|
|
292
566
|
throw new Error(`Unknown step type: ${(step as any)._tag}`);
|
|
293
567
|
}
|
|
@@ -300,6 +574,10 @@ export class QueryExecutor extends Resource {
|
|
|
300
574
|
private async _execSelectStep(step: QueryPlan.SelectStep, workingSet: QueryItem[]): Promise<StepExecutionResult> {
|
|
301
575
|
workingSet = [...workingSet];
|
|
302
576
|
|
|
577
|
+
const spaces = (step.scope.spaceIds ?? []) as SpaceId[];
|
|
578
|
+
const queues = (step.scope.feeds ?? []) as DXN.String[];
|
|
579
|
+
const allQueuesFromSpaces = step.scope.allFeedsFromSpaces ?? false;
|
|
580
|
+
|
|
303
581
|
const trace: ExecutionTrace = {
|
|
304
582
|
...ExecutionTrace.makeEmpty(),
|
|
305
583
|
name: 'Select',
|
|
@@ -310,11 +588,9 @@ export class QueryExecutor extends Resource {
|
|
|
310
588
|
switch (step.selector._tag) {
|
|
311
589
|
case 'WildcardSelector': {
|
|
312
590
|
const beginIndexQuery = performance.now();
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
});
|
|
317
|
-
trace.indexHits = +indexHits.length;
|
|
591
|
+
const queueIds = extractQueueIds(queues);
|
|
592
|
+
const metas = await this._queryAllFromSqlIndex(spaces, allQueuesFromSpaces, queueIds);
|
|
593
|
+
trace.indexHits = metas.length;
|
|
318
594
|
trace.indexQueryTime += performance.now() - beginIndexQuery;
|
|
319
595
|
|
|
320
596
|
if (this._ctx.disposed) {
|
|
@@ -322,11 +598,11 @@ export class QueryExecutor extends Resource {
|
|
|
322
598
|
}
|
|
323
599
|
|
|
324
600
|
const documentLoadStart = performance.now();
|
|
325
|
-
const results = await this.
|
|
601
|
+
const results = await this._loadDocumentsAfterSqlQuery(metas);
|
|
326
602
|
trace.documentsLoaded += results.length;
|
|
327
603
|
trace.documentLoadTime += performance.now() - documentLoadStart;
|
|
328
604
|
|
|
329
|
-
workingSet.push(...results.filter(isNonNullable)
|
|
605
|
+
workingSet.push(...results.filter(isNonNullable));
|
|
330
606
|
trace.objectCount = workingSet.length;
|
|
331
607
|
|
|
332
608
|
break;
|
|
@@ -334,10 +610,18 @@ export class QueryExecutor extends Resource {
|
|
|
334
610
|
|
|
335
611
|
case 'IdSelector': {
|
|
336
612
|
const beginLoad = performance.now();
|
|
613
|
+
|
|
337
614
|
const items = await Promise.all(
|
|
338
|
-
step.selector.objectIds.map((id) =>
|
|
339
|
-
|
|
340
|
-
|
|
615
|
+
step.selector.objectIds.map((id) => {
|
|
616
|
+
if (queues.length > 0) {
|
|
617
|
+
const { spaceId } = DXN.parse(queues[0]).asQueueDXN()!;
|
|
618
|
+
return this._loadFromDXN(DXN.parse(queues[0]).extend([id]), { sourceSpaceId: spaceId });
|
|
619
|
+
} else if (spaces.length > 0) {
|
|
620
|
+
return this._loadFromDXN(DXN.fromLocalObjectId(id), { sourceSpaceId: spaces[0] });
|
|
621
|
+
} else {
|
|
622
|
+
return null; // Unknown scope.
|
|
623
|
+
}
|
|
624
|
+
}),
|
|
341
625
|
);
|
|
342
626
|
trace.documentLoadTime += performance.now() - beginLoad;
|
|
343
627
|
|
|
@@ -348,11 +632,15 @@ export class QueryExecutor extends Resource {
|
|
|
348
632
|
|
|
349
633
|
case 'TypeSelector': {
|
|
350
634
|
const beginIndexQuery = performance.now();
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
635
|
+
const queueIds = extractQueueIds(queues);
|
|
636
|
+
const metas = await this._queryTypesFromSqlIndex(
|
|
637
|
+
spaces,
|
|
638
|
+
step.selector.typename,
|
|
639
|
+
step.selector.inverted,
|
|
640
|
+
allQueuesFromSpaces,
|
|
641
|
+
queueIds,
|
|
642
|
+
);
|
|
643
|
+
trace.indexHits = metas.length;
|
|
356
644
|
trace.indexQueryTime += performance.now() - beginIndexQuery;
|
|
357
645
|
|
|
358
646
|
if (this._ctx.disposed) {
|
|
@@ -360,43 +648,144 @@ export class QueryExecutor extends Resource {
|
|
|
360
648
|
}
|
|
361
649
|
|
|
362
650
|
const documentLoadStart = performance.now();
|
|
363
|
-
const results = await this.
|
|
651
|
+
const results = await this._loadDocumentsAfterSqlQuery(metas);
|
|
364
652
|
trace.documentsLoaded += results.length;
|
|
365
653
|
trace.documentLoadTime += performance.now() - documentLoadStart;
|
|
366
654
|
|
|
367
|
-
workingSet.push(...results.filter(isNonNullable)
|
|
655
|
+
workingSet.push(...results.filter(isNonNullable));
|
|
656
|
+
trace.objectCount = workingSet.length;
|
|
657
|
+
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
case 'TimestampSelector': {
|
|
662
|
+
const beginIndexQuery = performance.now();
|
|
663
|
+
const queueIds = extractQueueIds(queues);
|
|
664
|
+
const metas = await this._runInRuntime(
|
|
665
|
+
this._indexEngine.queryByTimeRange({
|
|
666
|
+
spaceIds: spaces,
|
|
667
|
+
updatedAfter: step.selector.updatedAfter,
|
|
668
|
+
updatedBefore: step.selector.updatedBefore,
|
|
669
|
+
createdAfter: step.selector.createdAfter,
|
|
670
|
+
createdBefore: step.selector.createdBefore,
|
|
671
|
+
includeAllQueues: allQueuesFromSpaces,
|
|
672
|
+
queueIds,
|
|
673
|
+
}),
|
|
674
|
+
);
|
|
675
|
+
trace.indexHits = metas.length;
|
|
676
|
+
trace.indexQueryTime += performance.now() - beginIndexQuery;
|
|
677
|
+
|
|
678
|
+
if (this._ctx.disposed) {
|
|
679
|
+
return { workingSet, trace };
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const documentLoadStart = performance.now();
|
|
683
|
+
const results = await this._loadDocumentsAfterSqlQuery(metas);
|
|
684
|
+
trace.documentsLoaded += results.length;
|
|
685
|
+
trace.documentLoadTime += performance.now() - documentLoadStart;
|
|
686
|
+
|
|
687
|
+
workingSet.push(...results.filter(isNonNullable));
|
|
368
688
|
trace.objectCount = workingSet.length;
|
|
369
689
|
|
|
370
690
|
break;
|
|
371
691
|
}
|
|
372
692
|
|
|
373
693
|
case 'TextSelector': {
|
|
694
|
+
// TODO(dmaretskyi): type + FTS queries would be very common so we should support those, maybe chunk the fts index.
|
|
695
|
+
// TODO(dmaretskyi): nice to have matched text snippets/highlighting.
|
|
696
|
+
if (step.selector.searchKind === 'vector') {
|
|
697
|
+
// Vector search is not currently supported.
|
|
698
|
+
log.warn('Vector search is not supported');
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Full-text search using SQLite FTS5.
|
|
374
703
|
const beginIndexQuery = performance.now();
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
704
|
+
invariant(spaces.length <= 1, 'Multiple spaces are not supported for full-text search');
|
|
705
|
+
const queueIds = extractQueueIds(queues);
|
|
706
|
+
const textResults = await this._runInRuntime(
|
|
707
|
+
this._indexEngine.queryText({
|
|
378
708
|
query: step.selector.text,
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
},
|
|
386
|
-
});
|
|
387
|
-
trace.indexHits = +indexHits.length;
|
|
709
|
+
spaceId: spaces,
|
|
710
|
+
includeAllQueues: allQueuesFromSpaces,
|
|
711
|
+
queueIds,
|
|
712
|
+
}),
|
|
713
|
+
);
|
|
714
|
+
trace.indexHits = textResults.length;
|
|
388
715
|
trace.indexQueryTime += performance.now() - beginIndexQuery;
|
|
389
716
|
|
|
390
717
|
if (this._ctx.disposed) {
|
|
391
718
|
return { workingSet, trace };
|
|
392
719
|
}
|
|
393
720
|
|
|
721
|
+
// Load documents from the results.
|
|
394
722
|
const documentLoadStart = performance.now();
|
|
395
|
-
|
|
396
|
-
|
|
723
|
+
|
|
724
|
+
// Separate queue items from space items.
|
|
725
|
+
const queueResults = textResults.filter((r) => r.queueId);
|
|
726
|
+
const spaceResults = textResults.filter((r) => !r.queueId);
|
|
727
|
+
|
|
728
|
+
// Build a map from recordId to rank for all FTS results.
|
|
729
|
+
const rankMap = new Map(textResults.map((r) => [r.recordId, r.rank]));
|
|
730
|
+
|
|
731
|
+
// Load queue items from indexed snapshots.
|
|
732
|
+
let queueItems: QueryItem[] = [];
|
|
733
|
+
if (queueResults.length > 0) {
|
|
734
|
+
const snapshots = await this._runInRuntime(
|
|
735
|
+
this._indexEngine.querySnapshotsJSON(queueResults.map((r) => r.recordId)),
|
|
736
|
+
);
|
|
737
|
+
const snapshotMap = new Map(snapshots.map((s) => [s.recordId, s.snapshot]));
|
|
738
|
+
queueItems = queueResults
|
|
739
|
+
.map((result): QueryItem | null => {
|
|
740
|
+
const snapshot = snapshotMap.get(result.recordId);
|
|
741
|
+
if (!snapshot || typeof snapshot !== 'object') {
|
|
742
|
+
return null;
|
|
743
|
+
}
|
|
744
|
+
return {
|
|
745
|
+
objectId: result.objectId as ObjectId,
|
|
746
|
+
spaceId: result.spaceId as SpaceId,
|
|
747
|
+
queueId: result.queueId as ObjectId,
|
|
748
|
+
queueNamespace: result.queueNamespace || null,
|
|
749
|
+
documentId: null,
|
|
750
|
+
doc: null,
|
|
751
|
+
data: snapshot as Obj.JSON,
|
|
752
|
+
rank: rankMap.get(result.recordId) ?? 1,
|
|
753
|
+
};
|
|
754
|
+
})
|
|
755
|
+
.filter(isNonNullable);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Load space items from documents.
|
|
759
|
+
const spaceItems = await Promise.all(
|
|
760
|
+
spaceResults.map(async (result): Promise<QueryItem | null> => {
|
|
761
|
+
const dxn = DXN.fromLocalObjectId(result.objectId);
|
|
762
|
+
const item = await this._loadFromDXN(dxn, { sourceSpaceId: result.spaceId as SpaceId });
|
|
763
|
+
if (item) {
|
|
764
|
+
// Override the default rank with the FTS rank.
|
|
765
|
+
item.rank = rankMap.get(result.recordId) ?? 1;
|
|
766
|
+
}
|
|
767
|
+
return item;
|
|
768
|
+
}),
|
|
769
|
+
);
|
|
770
|
+
|
|
771
|
+
const items = [...queueItems, ...spaceItems.filter(isNonNullable)];
|
|
772
|
+
trace.documentsLoaded += items.length;
|
|
397
773
|
trace.documentLoadTime += performance.now() - documentLoadStart;
|
|
398
774
|
|
|
399
|
-
workingSet.push(
|
|
775
|
+
workingSet.push(
|
|
776
|
+
...items.filter((item) => {
|
|
777
|
+
if (spaces.includes(item.spaceId)) {
|
|
778
|
+
return true;
|
|
779
|
+
}
|
|
780
|
+
if (item.queueId) {
|
|
781
|
+
return queues.some((dxn) => {
|
|
782
|
+
const { queueId, spaceId } = DXN.parse(dxn).asQueueDXN()!;
|
|
783
|
+
return queueId === item.queueId && spaceId === item.spaceId;
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
return false;
|
|
787
|
+
}),
|
|
788
|
+
);
|
|
400
789
|
trace.objectCount = workingSet.length;
|
|
401
790
|
break;
|
|
402
791
|
}
|
|
@@ -405,17 +794,46 @@ export class QueryExecutor extends Resource {
|
|
|
405
794
|
throw new Error(`Unknown selector type: ${(step.selector as any)._tag}`);
|
|
406
795
|
}
|
|
407
796
|
|
|
797
|
+
// Apply limit if specified on the select step.
|
|
798
|
+
if (step.limit !== undefined && workingSet.length > step.limit) {
|
|
799
|
+
workingSet = workingSet.slice(0, step.limit);
|
|
800
|
+
trace.objectCount = workingSet.length;
|
|
801
|
+
}
|
|
802
|
+
|
|
408
803
|
return { workingSet, trace };
|
|
409
804
|
}
|
|
410
805
|
|
|
411
806
|
private async _execFilterStep(step: QueryPlan.FilterStep, workingSet: QueryItem[]): Promise<StepExecutionResult> {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
807
|
+
if (step.filter.type === 'child-of') {
|
|
808
|
+
return this._execChildOfFilterStep(step.filter, workingSet);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const timestampParams = extractTimestampParams(step.filter);
|
|
812
|
+
if (timestampParams !== null) {
|
|
813
|
+
return this._execTimestampFilterStep(step, workingSet, timestampParams);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (filterContainsTimestamp(step.filter)) {
|
|
817
|
+
throw new QueryError({
|
|
818
|
+
message:
|
|
819
|
+
'Timestamp filter in unsupported composition (not, or). Use Filter.updated/Filter.created with explicit range instead.',
|
|
820
|
+
context: {},
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const result = workingSet.filter((item) => {
|
|
825
|
+
if (item.doc) {
|
|
826
|
+
return filterMatchObject(step.filter, {
|
|
827
|
+
id: item.objectId,
|
|
828
|
+
spaceId: item.spaceId,
|
|
829
|
+
doc: item.doc,
|
|
830
|
+
});
|
|
831
|
+
} else if (item.data) {
|
|
832
|
+
return filterMatchObjectJSON(step.filter, item.data);
|
|
833
|
+
} else {
|
|
834
|
+
return false;
|
|
835
|
+
}
|
|
836
|
+
});
|
|
419
837
|
|
|
420
838
|
return {
|
|
421
839
|
workingSet: result,
|
|
@@ -428,12 +846,52 @@ export class QueryExecutor extends Resource {
|
|
|
428
846
|
};
|
|
429
847
|
}
|
|
430
848
|
|
|
849
|
+
private async _execTimestampFilterStep(
|
|
850
|
+
step: QueryPlan.FilterStep,
|
|
851
|
+
workingSet: QueryItem[],
|
|
852
|
+
params: { updatedAfter?: number; updatedBefore?: number; createdAfter?: number; createdBefore?: number },
|
|
853
|
+
): Promise<StepExecutionResult> {
|
|
854
|
+
const spaces = [...new Set(workingSet.map((item) => item.spaceId).filter(isNonNullable))];
|
|
855
|
+
const metas = await this._runInRuntime(
|
|
856
|
+
this._indexEngine.queryByTimeRange({
|
|
857
|
+
spaceIds: spaces,
|
|
858
|
+
...params,
|
|
859
|
+
includeAllQueues: false,
|
|
860
|
+
queueIds: [],
|
|
861
|
+
}),
|
|
862
|
+
);
|
|
863
|
+
const matchingIds = new Set(metas.map((m) => m.objectId));
|
|
864
|
+
const result = workingSet.filter((item) => matchingIds.has(item.objectId));
|
|
865
|
+
return {
|
|
866
|
+
workingSet: result,
|
|
867
|
+
trace: {
|
|
868
|
+
...ExecutionTrace.makeEmpty(),
|
|
869
|
+
name: 'Filter(timestamp)',
|
|
870
|
+
details: JSON.stringify(params),
|
|
871
|
+
objectCount: result.length,
|
|
872
|
+
},
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
|
|
431
876
|
private async _execFilterDeletedStep(
|
|
432
877
|
step: QueryPlan.FilterDeletedStep,
|
|
433
878
|
workingSet: QueryItem[],
|
|
434
879
|
): Promise<StepExecutionResult> {
|
|
435
880
|
const expected = step.mode === 'only-deleted';
|
|
436
|
-
|
|
881
|
+
|
|
882
|
+
const deletedState = workingSet.map((item) => QueryItem.isDeleted(item));
|
|
883
|
+
await Promise.all(
|
|
884
|
+
workingSet.map(async (item, index) => {
|
|
885
|
+
if (deletedState[index]) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
deletedState[index] ||= await this._getTransitiveDeletionState(item, MAX_DEPTH_FOR_DELETION_TRACING);
|
|
889
|
+
}),
|
|
890
|
+
);
|
|
891
|
+
|
|
892
|
+
const result = workingSet.filter((item, index) => deletedState[index] === expected);
|
|
893
|
+
|
|
894
|
+
// TODO(dmaretskyi): How do we handle items with parents and cascade deletions? -- perhaps we forbid queue items from having parents -- i.e. queue is their parent.
|
|
437
895
|
return {
|
|
438
896
|
workingSet: result,
|
|
439
897
|
trace: {
|
|
@@ -445,6 +903,78 @@ export class QueryExecutor extends Resource {
|
|
|
445
903
|
};
|
|
446
904
|
}
|
|
447
905
|
|
|
906
|
+
private async _execChildOfFilterStep(
|
|
907
|
+
filter: QueryAST.FilterChildOf,
|
|
908
|
+
workingSet: QueryItem[],
|
|
909
|
+
): Promise<StepExecutionResult> {
|
|
910
|
+
const parentObjectIds = new Set<string>();
|
|
911
|
+
for (const parentDxnStr of filter.parents) {
|
|
912
|
+
const dxn = DXN.parse(parentDxnStr);
|
|
913
|
+
const echoDXN = dxn.asEchoDXN();
|
|
914
|
+
if (echoDXN) {
|
|
915
|
+
parentObjectIds.add(echoDXN.echoId);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
const maxDepth = filter.transitive ? MAX_DEPTH_FOR_CHILD_OF_TRACING : 1;
|
|
919
|
+
|
|
920
|
+
const matches = await Promise.all(
|
|
921
|
+
workingSet.map(async (item) => this._isChildOfAny(item, parentObjectIds, maxDepth)),
|
|
922
|
+
);
|
|
923
|
+
const result = workingSet.filter((_item, index) => matches[index]);
|
|
924
|
+
|
|
925
|
+
return {
|
|
926
|
+
workingSet: result,
|
|
927
|
+
trace: {
|
|
928
|
+
...ExecutionTrace.makeEmpty(),
|
|
929
|
+
name: 'Filter(child-of)',
|
|
930
|
+
details: JSON.stringify({ parents: filter.parents, transitive: filter.transitive }),
|
|
931
|
+
objectCount: result.length,
|
|
932
|
+
},
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Checks if an item is a child of any of the given parent object IDs.
|
|
938
|
+
* Walks up the parent chain (and feed ownership for queue items) until a match is found or depth is exhausted.
|
|
939
|
+
*/
|
|
940
|
+
private async _isChildOfAny(item: QueryItem, parentObjectIds: Set<string>, remainingDepth: number): Promise<boolean> {
|
|
941
|
+
if (remainingDepth <= 0) {
|
|
942
|
+
return false;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const parentRefs: { dxnStr: string; objectId: string }[] = [];
|
|
946
|
+
|
|
947
|
+
const directParent = QueryItem.getParent(item);
|
|
948
|
+
if (directParent) {
|
|
949
|
+
const echoDXN = DXN.parse(directParent).asEchoDXN();
|
|
950
|
+
if (echoDXN) {
|
|
951
|
+
parentRefs.push({ dxnStr: directParent, objectId: echoDXN.echoId });
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (item.queueId && !directParent) {
|
|
956
|
+
parentRefs.push({
|
|
957
|
+
dxnStr: DXN.fromSpaceAndObjectId(item.spaceId, item.queueId).toString(),
|
|
958
|
+
objectId: item.queueId,
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
for (const ref of parentRefs) {
|
|
963
|
+
if (parentObjectIds.has(ref.objectId)) {
|
|
964
|
+
return true;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
for (const ref of parentRefs) {
|
|
969
|
+
const parentItem = await this._loadFromDXN(DXN.parse(ref.dxnStr), { sourceSpaceId: item.spaceId });
|
|
970
|
+
if (parentItem && (await this._isChildOfAny(parentItem, parentObjectIds, remainingDepth - 1))) {
|
|
971
|
+
return true;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
977
|
+
|
|
448
978
|
// TODO(dmaretskyi): This needs to be completed.
|
|
449
979
|
private async _execTraverseStep(step: QueryPlan.TraverseStep, workingSet: QueryItem[]): Promise<StepExecutionResult> {
|
|
450
980
|
const trace: ExecutionTrace = {
|
|
@@ -459,11 +989,12 @@ export class QueryExecutor extends Resource {
|
|
|
459
989
|
case 'ReferenceTraversal': {
|
|
460
990
|
switch (step.traversal.direction) {
|
|
461
991
|
case 'outgoing': {
|
|
992
|
+
invariant(step.traversal.property !== null, 'Outgoing reference traversal requires a property');
|
|
462
993
|
const property = EscapedPropPath.unescape(step.traversal.property);
|
|
463
994
|
|
|
464
995
|
const refs = workingSet
|
|
465
996
|
.flatMap((item) => {
|
|
466
|
-
const ref =
|
|
997
|
+
const ref = QueryItem.getProperty(item, property);
|
|
467
998
|
const refs = Array.isArray(ref) ? ref : [ref];
|
|
468
999
|
return refs.map((ref) => {
|
|
469
1000
|
try {
|
|
@@ -493,19 +1024,13 @@ export class QueryExecutor extends Resource {
|
|
|
493
1024
|
break;
|
|
494
1025
|
}
|
|
495
1026
|
case 'incoming': {
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
kind: 'inbound-reference',
|
|
501
|
-
property: step.traversal.property,
|
|
502
|
-
anchors: workingSet.map((item) => item.objectId),
|
|
503
|
-
},
|
|
504
|
-
});
|
|
505
|
-
trace.indexHits += indexHits.length;
|
|
1027
|
+
const beginIndexQuery = performance.now();
|
|
1028
|
+
const metas = await this._queryIncomingReferencesFromSqlIndex(workingSet, step.traversal.property);
|
|
1029
|
+
trace.indexHits += metas.length;
|
|
1030
|
+
trace.indexQueryTime += performance.now() - beginIndexQuery;
|
|
506
1031
|
|
|
507
1032
|
const documentLoadStart = performance.now();
|
|
508
|
-
const results = await this.
|
|
1033
|
+
const results = await this._loadDocumentsAfterSqlQuery(metas);
|
|
509
1034
|
trace.documentsLoaded += results.length;
|
|
510
1035
|
trace.documentLoadTime += performance.now() - documentLoadStart;
|
|
511
1036
|
|
|
@@ -523,21 +1048,20 @@ export class QueryExecutor extends Resource {
|
|
|
523
1048
|
case 'relation-to-target': {
|
|
524
1049
|
const refs = workingSet
|
|
525
1050
|
.map((item) => {
|
|
526
|
-
const
|
|
1051
|
+
const dxn =
|
|
527
1052
|
step.traversal.direction === 'relation-to-source'
|
|
528
|
-
?
|
|
529
|
-
:
|
|
530
|
-
|
|
531
|
-
if (!isEncodedReference(ref)) {
|
|
1053
|
+
? QueryItem.getRelationSource(item)
|
|
1054
|
+
: QueryItem.getRelationTarget(item);
|
|
1055
|
+
if (!dxn) {
|
|
532
1056
|
return null;
|
|
533
1057
|
}
|
|
534
1058
|
try {
|
|
535
1059
|
return {
|
|
536
|
-
ref: DXN.parse(
|
|
1060
|
+
ref: DXN.parse(dxn),
|
|
537
1061
|
spaceId: item.spaceId,
|
|
538
1062
|
};
|
|
539
1063
|
} catch {
|
|
540
|
-
log.warn('invalid reference', { ref:
|
|
1064
|
+
log.warn('invalid reference', { ref: dxn });
|
|
541
1065
|
return null;
|
|
542
1066
|
}
|
|
543
1067
|
})
|
|
@@ -557,20 +1081,16 @@ export class QueryExecutor extends Resource {
|
|
|
557
1081
|
|
|
558
1082
|
case 'source-to-relation':
|
|
559
1083
|
case 'target-to-relation': {
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
},
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
trace.indexHits += indexHits.length;
|
|
1084
|
+
const beginIndexQuery = performance.now();
|
|
1085
|
+
const metas = await this._queryRelationsFromSqlIndex(
|
|
1086
|
+
workingSet,
|
|
1087
|
+
step.traversal.direction === 'source-to-relation' ? 'source' : 'target',
|
|
1088
|
+
);
|
|
1089
|
+
trace.indexHits += metas.length;
|
|
1090
|
+
trace.indexQueryTime += performance.now() - beginIndexQuery;
|
|
571
1091
|
|
|
572
1092
|
const documentLoadStart = performance.now();
|
|
573
|
-
const results = await this.
|
|
1093
|
+
const results = await this._loadDocumentsAfterSqlQuery(metas);
|
|
574
1094
|
trace.documentsLoaded += results.length;
|
|
575
1095
|
trace.documentLoadTime += performance.now() - documentLoadStart;
|
|
576
1096
|
|
|
@@ -582,6 +1102,80 @@ export class QueryExecutor extends Resource {
|
|
|
582
1102
|
}
|
|
583
1103
|
break;
|
|
584
1104
|
}
|
|
1105
|
+
case 'HierarchyTraversal': {
|
|
1106
|
+
switch (step.traversal.direction) {
|
|
1107
|
+
case 'to-parent': {
|
|
1108
|
+
// Traverse from child to parent using the parent reference in the document.
|
|
1109
|
+
const refs = workingSet
|
|
1110
|
+
.map((item) => {
|
|
1111
|
+
if (!item.doc) {
|
|
1112
|
+
return null; // TODO(dmaretskyi): Queue items not supported here.
|
|
1113
|
+
}
|
|
1114
|
+
const ref = ObjectStructure.getParent(item.doc);
|
|
1115
|
+
if (!EncodedReference.isEncodedReference(ref)) {
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
try {
|
|
1119
|
+
return {
|
|
1120
|
+
ref: DXN.parse(ref['/']),
|
|
1121
|
+
spaceId: item.spaceId,
|
|
1122
|
+
};
|
|
1123
|
+
} catch {
|
|
1124
|
+
log.warn('invalid parent reference', { ref: ref['/'] });
|
|
1125
|
+
return null;
|
|
1126
|
+
}
|
|
1127
|
+
})
|
|
1128
|
+
.filter(isNonNullable);
|
|
1129
|
+
|
|
1130
|
+
const beginLoad = performance.now();
|
|
1131
|
+
const items = await Promise.all(
|
|
1132
|
+
refs.map(({ ref, spaceId }) => this._loadFromDXN(ref, { sourceSpaceId: spaceId })),
|
|
1133
|
+
);
|
|
1134
|
+
trace.documentLoadTime += performance.now() - beginLoad;
|
|
1135
|
+
|
|
1136
|
+
newWorkingSet.push(...items.filter(isNonNullable));
|
|
1137
|
+
trace.objectCount = newWorkingSet.length;
|
|
1138
|
+
break;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
case 'to-children': {
|
|
1142
|
+
// Traverse from parent to children using the SQL index.
|
|
1143
|
+
// Covers both standard parent/child hierarchy (via the `parent` field) and
|
|
1144
|
+
// feed -> queue items (via the `queueId` field — a feed's queue id matches the feed's object id).
|
|
1145
|
+
// Group working set by spaceId.
|
|
1146
|
+
const bySpace = new Map<SpaceId, ObjectId[]>();
|
|
1147
|
+
for (const item of workingSet) {
|
|
1148
|
+
const existing = bySpace.get(item.spaceId);
|
|
1149
|
+
if (existing) {
|
|
1150
|
+
existing.push(item.objectId);
|
|
1151
|
+
} else {
|
|
1152
|
+
bySpace.set(item.spaceId, [item.objectId]);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const beginIndexQuery = performance.now();
|
|
1157
|
+
const allMetas: ObjectMeta[] = [];
|
|
1158
|
+
for (const [spaceId, parentIds] of bySpace) {
|
|
1159
|
+
const children = await this._runInRuntime(
|
|
1160
|
+
this._indexEngine.queryChildren({ spaceId: [spaceId], parentIds }),
|
|
1161
|
+
);
|
|
1162
|
+
allMetas.push(...children);
|
|
1163
|
+
}
|
|
1164
|
+
trace.indexHits += allMetas.length;
|
|
1165
|
+
trace.indexQueryTime += performance.now() - beginIndexQuery;
|
|
1166
|
+
|
|
1167
|
+
const documentLoadStart = performance.now();
|
|
1168
|
+
const results = await this._loadDocumentsAfterSqlQuery(allMetas);
|
|
1169
|
+
trace.documentsLoaded += results.filter(isNonNullable).length;
|
|
1170
|
+
trace.documentLoadTime += performance.now() - documentLoadStart;
|
|
1171
|
+
|
|
1172
|
+
newWorkingSet.push(...results.filter(isNonNullable));
|
|
1173
|
+
trace.objectCount = newWorkingSet.length;
|
|
1174
|
+
break;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
break;
|
|
1178
|
+
}
|
|
585
1179
|
default:
|
|
586
1180
|
throw new Error(`Unknown traversal type: ${(step.traversal as any)._tag}`);
|
|
587
1181
|
}
|
|
@@ -603,7 +1197,7 @@ export class QueryExecutor extends Resource {
|
|
|
603
1197
|
for (const resultSet of resultSets) {
|
|
604
1198
|
for (const item of resultSet.workingSet) {
|
|
605
1199
|
// Could be duplicate object ids in different spaces or in different epochs of the same space.
|
|
606
|
-
results.set(
|
|
1200
|
+
results.set(compositeKey(item.spaceId, String(item.documentId), item.objectId), item);
|
|
607
1201
|
}
|
|
608
1202
|
trace.children.push(resultSet.trace);
|
|
609
1203
|
}
|
|
@@ -637,19 +1231,38 @@ export class QueryExecutor extends Resource {
|
|
|
637
1231
|
}
|
|
638
1232
|
|
|
639
1233
|
private async _execOrderStep(step: QueryPlan.OrderStep, workingSet: QueryItem[]): Promise<StepExecutionResult> {
|
|
640
|
-
|
|
1234
|
+
let sortedWorkingSet = [...workingSet].sort((a, b) => this._compareMultiOrder(a, b, step.order));
|
|
1235
|
+
|
|
1236
|
+
// Apply limit if specified on the order step.
|
|
1237
|
+
if (step.limit !== undefined && sortedWorkingSet.length > step.limit) {
|
|
1238
|
+
sortedWorkingSet = sortedWorkingSet.slice(0, step.limit);
|
|
1239
|
+
}
|
|
641
1240
|
|
|
642
1241
|
return {
|
|
643
1242
|
workingSet: sortedWorkingSet,
|
|
644
1243
|
trace: {
|
|
645
1244
|
...ExecutionTrace.makeEmpty(),
|
|
646
1245
|
name: 'Order',
|
|
647
|
-
details: JSON.stringify(step.order),
|
|
1246
|
+
details: JSON.stringify({ order: step.order, limit: step.limit }),
|
|
648
1247
|
objectCount: sortedWorkingSet.length,
|
|
649
1248
|
},
|
|
650
1249
|
};
|
|
651
1250
|
}
|
|
652
1251
|
|
|
1252
|
+
private async _execLimitStep(step: QueryPlan.LimitStep, workingSet: QueryItem[]): Promise<StepExecutionResult> {
|
|
1253
|
+
const limitedWorkingSet = workingSet.slice(0, step.limit);
|
|
1254
|
+
|
|
1255
|
+
return {
|
|
1256
|
+
workingSet: limitedWorkingSet,
|
|
1257
|
+
trace: {
|
|
1258
|
+
...ExecutionTrace.makeEmpty(),
|
|
1259
|
+
name: 'Limit',
|
|
1260
|
+
details: JSON.stringify({ limit: step.limit }),
|
|
1261
|
+
objectCount: limitedWorkingSet.length,
|
|
1262
|
+
},
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
|
|
653
1266
|
private _compareMultiOrder(a: QueryItem, b: QueryItem, orders: readonly QueryAST.Order[]): number {
|
|
654
1267
|
// Short circuit for common cases.
|
|
655
1268
|
if (orders.length === 0) {
|
|
@@ -675,15 +1288,20 @@ export class QueryExecutor extends Resource {
|
|
|
675
1288
|
const comparison = this._compareByProperty(a, b, order.property);
|
|
676
1289
|
return order.direction === 'desc' ? -comparison : comparison;
|
|
677
1290
|
}
|
|
1291
|
+
case 'rank': {
|
|
1292
|
+
// Higher rank = better match. By default, descending order (best first).
|
|
1293
|
+
const comparison = a.rank - b.rank;
|
|
1294
|
+
return order.direction === 'desc' ? -comparison : comparison;
|
|
1295
|
+
}
|
|
678
1296
|
default:
|
|
679
|
-
// Should never reach here with proper TypeScript types
|
|
1297
|
+
// Should never reach here with proper TypeScript types.
|
|
680
1298
|
return 0;
|
|
681
1299
|
}
|
|
682
1300
|
}
|
|
683
1301
|
|
|
684
1302
|
private _compareByProperty(a: QueryItem, b: QueryItem, property: string): number {
|
|
685
|
-
const aValue = a
|
|
686
|
-
const bValue = b
|
|
1303
|
+
const aValue = QueryItem.getProperty(a, [property]);
|
|
1304
|
+
const bValue = QueryItem.getProperty(b, [property]);
|
|
687
1305
|
|
|
688
1306
|
// Both null or undefined
|
|
689
1307
|
if (aValue == null && bValue == null) {
|
|
@@ -719,103 +1337,377 @@ export class QueryExecutor extends Resource {
|
|
|
719
1337
|
return String(aValue).localeCompare(String(bValue));
|
|
720
1338
|
}
|
|
721
1339
|
|
|
722
|
-
private async
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
1340
|
+
private async _runInRuntime<T>(effect: Effect.Effect<T, unknown, SqlClient.SqlClient>): Promise<T> {
|
|
1341
|
+
const runtimeProvider = this._runtime;
|
|
1342
|
+
invariant(runtimeProvider, 'SQL runtime is required.');
|
|
1343
|
+
const runtime = await runAndForwardErrors(runtimeProvider);
|
|
1344
|
+
return await unwrapExit(await effect.pipe(Runtime.runPromiseExit(runtime)));
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
private async _queryAllFromSqlIndex(
|
|
1348
|
+
spaceIds: readonly SpaceId[],
|
|
1349
|
+
includeAllQueues: boolean,
|
|
1350
|
+
queueIds: readonly ObjectId[] | null,
|
|
1351
|
+
): Promise<readonly ObjectMeta[]> {
|
|
1352
|
+
return await this._runInRuntime(this._indexEngine.queryAll({ spaceIds, includeAllQueues, queueIds }));
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
private async _queryTypesFromSqlIndex(
|
|
1356
|
+
spaceIds: readonly SpaceId[],
|
|
1357
|
+
typeDxns: readonly string[],
|
|
1358
|
+
inverted: boolean,
|
|
1359
|
+
includeAllQueues: boolean,
|
|
1360
|
+
queueIds: readonly ObjectId[] | null,
|
|
1361
|
+
): Promise<readonly ObjectMeta[]> {
|
|
1362
|
+
return await this._runInRuntime(
|
|
1363
|
+
this._indexEngine.queryTypes({ spaceIds, typeDxns, inverted, includeAllQueues, queueIds }),
|
|
727
1364
|
);
|
|
728
1365
|
}
|
|
729
1366
|
|
|
1367
|
+
private async _queryIncomingReferencesFromSqlIndex(
|
|
1368
|
+
workingSet: QueryItem[],
|
|
1369
|
+
property: EscapedPropPath | null,
|
|
1370
|
+
): Promise<readonly ObjectMeta[]> {
|
|
1371
|
+
const anchorDxns = workingSet.map((item) => DXN.fromLocalObjectId(item.objectId).toString());
|
|
1372
|
+
const rows: readonly ReverseRef[] = (
|
|
1373
|
+
await Promise.all(
|
|
1374
|
+
anchorDxns.map((targetDXN) => this._runInRuntime(this._indexEngine.queryReverseRef({ targetDXN }))),
|
|
1375
|
+
)
|
|
1376
|
+
).flat();
|
|
1377
|
+
|
|
1378
|
+
const recordIds = rows
|
|
1379
|
+
.filter((row) => {
|
|
1380
|
+
if (property === null) {
|
|
1381
|
+
return true;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
const queryPath = EscapedPropPath.unescape(property);
|
|
1385
|
+
const rowPath = EscapedPropPath.unescape(row.propPath);
|
|
1386
|
+
return QueryExecutor._matchesReferencePropertyPath(rowPath, queryPath);
|
|
1387
|
+
})
|
|
1388
|
+
.map((row) => row.recordId);
|
|
1389
|
+
|
|
1390
|
+
const uniqueRecordIds = Array.from(new Set<number>(recordIds));
|
|
1391
|
+
return await this._runInRuntime(this._indexEngine.lookupByRecordIds(uniqueRecordIds));
|
|
1392
|
+
}
|
|
1393
|
+
|
|
730
1394
|
/**
|
|
731
|
-
*
|
|
1395
|
+
* Matches a reverse-reference row path against a query property path.
|
|
1396
|
+
* Allows numeric segments in the row path (array indices) that are not present in the query.
|
|
1397
|
+
*
|
|
1398
|
+
* Examples:
|
|
1399
|
+
* - query: ['assignee'] matches row: ['assignee'] and ['assignee', '0'].
|
|
1400
|
+
* - query: ['items', 'assignee'] matches row: ['items', '0', 'assignee'].
|
|
1401
|
+
* - query: ['a', 'b'] does NOT match row: ['a'].
|
|
1402
|
+
* - query: ['a'] does NOT match row: ['a', 'b'].
|
|
732
1403
|
*/
|
|
733
|
-
private readonly
|
|
1404
|
+
private static _matchesReferencePropertyPath(rowPath: readonly string[], queryPath: readonly string[]): boolean {
|
|
1405
|
+
const isNumericSegment = (segment: string) => /^[0-9]+$/.test(segment);
|
|
734
1406
|
|
|
735
|
-
|
|
736
|
-
|
|
1407
|
+
let i = 0; // queryPath index.
|
|
1408
|
+
let j = 0; // rowPath index.
|
|
737
1409
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
1410
|
+
while (i < queryPath.length && j < rowPath.length) {
|
|
1411
|
+
if (rowPath[j] === queryPath[i]) {
|
|
1412
|
+
i++;
|
|
1413
|
+
j++;
|
|
1414
|
+
continue;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// Row may contain array indices that aren't present in the query path.
|
|
1418
|
+
if (isNumericSegment(rowPath[j])) {
|
|
1419
|
+
j++;
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
return false;
|
|
742
1424
|
}
|
|
743
1425
|
|
|
744
|
-
|
|
745
|
-
if (
|
|
746
|
-
return
|
|
1426
|
+
// Must consume full query path.
|
|
1427
|
+
if (i !== queryPath.length) {
|
|
1428
|
+
return false;
|
|
747
1429
|
}
|
|
748
1430
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
1431
|
+
// Any remaining row segments must be numeric (array indices).
|
|
1432
|
+
for (; j < rowPath.length; j++) {
|
|
1433
|
+
if (!isNumericSegment(rowPath[j])) {
|
|
1434
|
+
return false;
|
|
1435
|
+
}
|
|
753
1436
|
}
|
|
754
1437
|
|
|
755
|
-
|
|
756
|
-
|
|
1438
|
+
return true;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
private async _queryRelationsFromSqlIndex(
|
|
1442
|
+
workingSet: QueryItem[],
|
|
1443
|
+
endpoint: 'source' | 'target',
|
|
1444
|
+
): Promise<readonly ObjectMeta[]> {
|
|
1445
|
+
const anchorDxns = workingSet.map((item) => DXN.fromLocalObjectId(item.objectId).toString());
|
|
1446
|
+
return await this._runInRuntime(this._indexEngine.queryRelations({ endpoint, anchorDxns }));
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
private async _loadDocumentsAfterSqlQuery(metas: readonly ObjectMeta[]): Promise<(QueryItem | null)[]> {
|
|
1450
|
+
const snapshotMap = await this._loadQueueSnapshotMap(metas);
|
|
1451
|
+
return await Promise.all(
|
|
1452
|
+
metas.map(async (meta) => {
|
|
1453
|
+
// Branch 1: Document-backed object.
|
|
1454
|
+
if (meta.documentId) {
|
|
1455
|
+
return this._loadFromAutomerge(meta);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// Branch 2: Queue-backed object.
|
|
1459
|
+
if (meta.queueId) {
|
|
1460
|
+
return this._loadFromQueue(meta, snapshotMap);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
return null;
|
|
1464
|
+
}),
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
private async _loadQueueSnapshotMap(metas: readonly ObjectMeta[]): Promise<Map<number, unknown>> {
|
|
1469
|
+
const queueMetas = metas.filter((meta) => !meta.documentId && !!meta.queueId);
|
|
1470
|
+
if (queueMetas.length === 0) {
|
|
1471
|
+
return new Map();
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
const snapshots = await this._runInRuntime(
|
|
1475
|
+
this._indexEngine.querySnapshotsJSON(queueMetas.map((meta) => meta.recordId)),
|
|
1476
|
+
);
|
|
1477
|
+
return new Map(snapshots.map((s) => [s.recordId, s.snapshot]));
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
/**
|
|
1481
|
+
* Loads a queue-backed object from an indexed snapshot (by `recordId`).
|
|
1482
|
+
* Returns `null` when the snapshot is missing or is not a JSON object.
|
|
1483
|
+
*/
|
|
1484
|
+
private _loadFromQueue(meta: ObjectMeta, snapshotMap: Map<number, unknown>): QueryItem | null {
|
|
1485
|
+
const snapshot = snapshotMap.get(meta.recordId);
|
|
1486
|
+
if (!snapshot || typeof snapshot !== 'object') {
|
|
757
1487
|
return null;
|
|
758
1488
|
}
|
|
759
1489
|
|
|
760
1490
|
return {
|
|
761
|
-
objectId,
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
1491
|
+
objectId: meta.objectId as ObjectId,
|
|
1492
|
+
spaceId: meta.spaceId as SpaceId,
|
|
1493
|
+
queueId: meta.queueId as ObjectId,
|
|
1494
|
+
queueNamespace: meta.queueNamespace || null,
|
|
1495
|
+
documentId: null,
|
|
1496
|
+
doc: null,
|
|
1497
|
+
data: snapshot as Obj.JSON,
|
|
1498
|
+
rank: 1,
|
|
765
1499
|
};
|
|
766
1500
|
}
|
|
767
1501
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
1502
|
+
/**
|
|
1503
|
+
* Loads a document-backed object from an Automerge `DatabaseDirectory`.
|
|
1504
|
+
* Returns `null` if the document can't be loaded, the inline object isn't present, or if the meta does not have a
|
|
1505
|
+
* document id (e.g. queue-backed objects).
|
|
1506
|
+
*/
|
|
1507
|
+
private async _loadFromAutomerge(meta: ObjectMeta): Promise<QueryItem | null> {
|
|
1508
|
+
if (!meta.documentId) {
|
|
772
1509
|
return null;
|
|
773
1510
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
if (!spaceRoot) {
|
|
779
|
-
log.warn('no space state found for', { spaceId });
|
|
1511
|
+
const handle = await this._automergeHost.loadDoc<DatabaseDirectory>(this._ctx, meta.documentId as DocumentId, {
|
|
1512
|
+
fetchFromNetwork: false,
|
|
1513
|
+
});
|
|
1514
|
+
if (!handle) {
|
|
780
1515
|
return null;
|
|
781
1516
|
}
|
|
782
|
-
const
|
|
783
|
-
if (!
|
|
784
|
-
log.warn('no space state found for', { spaceId });
|
|
1517
|
+
const object = DatabaseDirectory.getInlineObject(handle.doc(), meta.objectId);
|
|
1518
|
+
if (!object) {
|
|
785
1519
|
return null;
|
|
786
1520
|
}
|
|
1521
|
+
return {
|
|
1522
|
+
objectId: meta.objectId,
|
|
1523
|
+
documentId: meta.documentId as DocumentId,
|
|
1524
|
+
spaceId: meta.spaceId as SpaceId,
|
|
1525
|
+
queueId: null,
|
|
1526
|
+
queueNamespace: null,
|
|
1527
|
+
doc: object,
|
|
1528
|
+
data: null,
|
|
1529
|
+
rank: 1,
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
private async _loadFromDXN(dxn: DXN, { sourceSpaceId }: { sourceSpaceId: SpaceId }): Promise<QueryItem | null> {
|
|
1534
|
+
switch (dxn.kind) {
|
|
1535
|
+
case DXN.kind.ECHO: {
|
|
1536
|
+
const echoDXN = dxn.asEchoDXN();
|
|
1537
|
+
if (!echoDXN) {
|
|
1538
|
+
log.warn('unable to resolve DXN', { dxn });
|
|
1539
|
+
return null;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
const spaceId = echoDXN.spaceId ?? sourceSpaceId;
|
|
787
1543
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1544
|
+
const spaceRoot = this._spaceStateManager.getRootBySpaceId(spaceId);
|
|
1545
|
+
if (!spaceRoot) {
|
|
1546
|
+
log.warn('no space state found for', { spaceId });
|
|
1547
|
+
return null;
|
|
1548
|
+
}
|
|
1549
|
+
const dbDirectory = spaceRoot.doc();
|
|
1550
|
+
if (!dbDirectory) {
|
|
1551
|
+
log.warn('no space state found for', { spaceId });
|
|
1552
|
+
return null;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
const inlineObject = DatabaseDirectory.getInlineObject(dbDirectory, echoDXN.echoId);
|
|
1556
|
+
if (inlineObject) {
|
|
1557
|
+
return {
|
|
1558
|
+
objectId: echoDXN.echoId,
|
|
1559
|
+
documentId: spaceRoot.documentId,
|
|
1560
|
+
spaceId,
|
|
1561
|
+
queueId: null,
|
|
1562
|
+
queueNamespace: null,
|
|
1563
|
+
data: null,
|
|
1564
|
+
doc: inlineObject,
|
|
1565
|
+
rank: 1,
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
const link = DatabaseDirectory.getLink(dbDirectory, echoDXN.echoId);
|
|
1570
|
+
if (!link) {
|
|
1571
|
+
return null;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
const handle = await this._automergeHost.loadDoc<DatabaseDirectory>(this._ctx, link as AutomergeUrl, {
|
|
1575
|
+
fetchFromNetwork: false,
|
|
1576
|
+
});
|
|
1577
|
+
if (!handle) {
|
|
1578
|
+
return null;
|
|
1579
|
+
}
|
|
1580
|
+
const object = DatabaseDirectory.getInlineObject(handle.doc(), echoDXN.echoId);
|
|
1581
|
+
if (!object) {
|
|
1582
|
+
return null;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
return {
|
|
1586
|
+
objectId: echoDXN.echoId,
|
|
1587
|
+
documentId: handle.documentId,
|
|
1588
|
+
spaceId,
|
|
1589
|
+
queueId: null,
|
|
1590
|
+
queueNamespace: null,
|
|
1591
|
+
data: null,
|
|
1592
|
+
doc: object,
|
|
1593
|
+
rank: 1,
|
|
1594
|
+
};
|
|
1595
|
+
break;
|
|
1596
|
+
}
|
|
1597
|
+
case DXN.kind.QUEUE: {
|
|
1598
|
+
const queueDXN = dxn.asQueueDXN();
|
|
1599
|
+
if (!queueDXN || !queueDXN.objectId) {
|
|
1600
|
+
log.warn('unable to resolve queue DXN', { dxn });
|
|
1601
|
+
return null;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
const { spaceId, queueId, objectId } = queueDXN;
|
|
1605
|
+
const meta = await this._runInRuntime(
|
|
1606
|
+
this._indexEngine.lookupByObjectId({
|
|
1607
|
+
objectId,
|
|
1608
|
+
spaceId,
|
|
1609
|
+
queueId,
|
|
1610
|
+
}),
|
|
1611
|
+
);
|
|
1612
|
+
if (!meta) {
|
|
1613
|
+
return null;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
const snapshotMap = await this._loadQueueSnapshotMap([meta]);
|
|
1617
|
+
return this._loadFromQueue(meta, snapshotMap);
|
|
1618
|
+
}
|
|
1619
|
+
default: {
|
|
1620
|
+
log.warn('unable to resolve DXN', { dxn });
|
|
1621
|
+
return null;
|
|
1622
|
+
}
|
|
796
1623
|
}
|
|
1624
|
+
}
|
|
797
1625
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1626
|
+
private async _getTransitiveDeletionState(item: QueryItem, remainingDepth: number): Promise<boolean> {
|
|
1627
|
+
const strongDeps = [
|
|
1628
|
+
QueryItem.getParent(item),
|
|
1629
|
+
QueryItem.getRelationSource(item),
|
|
1630
|
+
QueryItem.getRelationTarget(item),
|
|
1631
|
+
].filter((x) => x !== undefined);
|
|
1632
|
+
|
|
1633
|
+
if (strongDeps.length === 0) {
|
|
1634
|
+
return false;
|
|
801
1635
|
}
|
|
802
1636
|
|
|
803
|
-
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
|
|
1637
|
+
// TODO(dmaretskyi): This could be optimized to bail early if any of the dependencies are deleted.
|
|
1638
|
+
const strongDepStates = await Promise.all(
|
|
1639
|
+
strongDeps.map(async (dxn) => {
|
|
1640
|
+
const dep = await this._loadFromDXN(DXN.parse(dxn), { sourceSpaceId: item.spaceId });
|
|
1641
|
+
if (!dep) {
|
|
1642
|
+
return false;
|
|
1643
|
+
}
|
|
1644
|
+
if (QueryItem.isDeleted(dep)) {
|
|
1645
|
+
return true;
|
|
1646
|
+
}
|
|
1647
|
+
if (remainingDepth > 0) {
|
|
1648
|
+
return this._getTransitiveDeletionState(dep, remainingDepth - 1);
|
|
1649
|
+
}
|
|
1650
|
+
return false;
|
|
1651
|
+
}),
|
|
1652
|
+
);
|
|
1653
|
+
|
|
1654
|
+
return strongDepStates.some((x) => x);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
const extractQueueIds = (queues: readonly DXN.String[]): ObjectId[] | null => {
|
|
1659
|
+
if (queues.length === 0) {
|
|
1660
|
+
return null;
|
|
1661
|
+
}
|
|
1662
|
+
return queues.map((dxnStr) => DXN.parse(dxnStr).asQueueDXN()?.queueId).filter(Boolean) as ObjectId[];
|
|
1663
|
+
};
|
|
1664
|
+
|
|
1665
|
+
/**
|
|
1666
|
+
* Extract timestamp parameters from a filter AST node.
|
|
1667
|
+
* Returns null if the filter doesn't contain timestamp nodes.
|
|
1668
|
+
*/
|
|
1669
|
+
const extractTimestampParams = (
|
|
1670
|
+
filter: QueryAST.Filter,
|
|
1671
|
+
): { updatedAfter?: number; updatedBefore?: number; createdAfter?: number; createdBefore?: number } | null => {
|
|
1672
|
+
const collect = (f: QueryAST.Filter): QueryAST.FilterTimestamp[] => {
|
|
1673
|
+
if (f.type === 'timestamp') {
|
|
1674
|
+
return [f];
|
|
1675
|
+
}
|
|
1676
|
+
if (f.type === 'and') {
|
|
1677
|
+
return f.filters.flatMap(collect);
|
|
807
1678
|
}
|
|
1679
|
+
return [];
|
|
1680
|
+
};
|
|
808
1681
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
1682
|
+
const timestamps = collect(filter);
|
|
1683
|
+
if (timestamps.length === 0) {
|
|
1684
|
+
return null;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
const params: { updatedAfter?: number; updatedBefore?: number; createdAfter?: number; createdBefore?: number } = {};
|
|
1688
|
+
for (const ts of timestamps) {
|
|
1689
|
+
if (ts.field === 'updatedAt' && (ts.operator === 'gt' || ts.operator === 'gte')) {
|
|
1690
|
+
params.updatedAfter = ts.value;
|
|
1691
|
+
} else if (ts.field === 'updatedAt' && (ts.operator === 'lt' || ts.operator === 'lte')) {
|
|
1692
|
+
params.updatedBefore = ts.value;
|
|
1693
|
+
} else if (ts.field === 'createdAt' && (ts.operator === 'gt' || ts.operator === 'gte')) {
|
|
1694
|
+
params.createdAfter = ts.value;
|
|
1695
|
+
} else if (ts.field === 'createdAt' && (ts.operator === 'lt' || ts.operator === 'lte')) {
|
|
1696
|
+
params.createdBefore = ts.value;
|
|
812
1697
|
}
|
|
1698
|
+
}
|
|
1699
|
+
return params;
|
|
1700
|
+
};
|
|
813
1701
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1702
|
+
function filterContainsTimestamp(filter: QueryAST.Filter): boolean {
|
|
1703
|
+
if (filter.type === 'timestamp') {
|
|
1704
|
+
return true;
|
|
1705
|
+
}
|
|
1706
|
+
if (filter.type === 'and' || filter.type === 'or') {
|
|
1707
|
+
return filter.filters.some(filterContainsTimestamp);
|
|
1708
|
+
}
|
|
1709
|
+
if (filter.type === 'not') {
|
|
1710
|
+
return filterContainsTimestamp(filter.filter);
|
|
820
1711
|
}
|
|
1712
|
+
return false;
|
|
821
1713
|
}
|