@dxos/echo-pipeline 0.8.3 → 0.8.4-main.1068cf700f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/neutral/chunk-FJPXA75J.mjs +235 -0
- package/dist/lib/neutral/chunk-FJPXA75J.mjs.map +7 -0
- package/dist/lib/{browser/chunk-35I6ERLG.mjs → neutral/chunk-WIQUYO7H.mjs} +300 -213
- package/dist/lib/neutral/chunk-WIQUYO7H.mjs.map +7 -0
- package/dist/lib/{browser → neutral}/filter/index.mjs +3 -2
- package/dist/lib/{browser → neutral}/index.mjs +2832 -946
- 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 +73 -224
- package/dist/lib/neutral/testing/index.mjs.map +7 -0
- package/dist/types/src/automerge/automerge-host.d.ts +53 -42
- package/dist/types/src/automerge/automerge-host.d.ts.map +1 -1
- 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/collection-synchronizer.d.ts +6 -4
- package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-network-adapter.d.ts +22 -10
- package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-replicator.d.ts +37 -15
- package/dist/types/src/automerge/echo-replicator.d.ts.map +1 -1
- 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 +1 -1
- package/dist/types/src/automerge/index.d.ts.map +1 -1
- package/dist/types/src/automerge/leveldb-storage-adapter.d.ts +5 -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 +5 -5
- package/dist/types/src/automerge/mesh-echo-replicator.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/db-host/automerge-data-source.d.ts +34 -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/data-service.d.ts +5 -4
- package/dist/types/src/db-host/data-service.d.ts.map +1 -1
- package/dist/types/src/db-host/database-root.d.ts.map +1 -1
- package/dist/types/src/db-host/documents-synchronizer.d.ts +6 -5
- package/dist/types/src/db-host/documents-synchronizer.d.ts.map +1 -1
- package/dist/types/src/db-host/echo-host.d.ts +44 -23
- package/dist/types/src/db-host/echo-host.d.ts.map +1 -1
- package/dist/types/src/db-host/index.d.ts +1 -0
- package/dist/types/src/db-host/index.d.ts.map +1 -1
- package/dist/types/src/db-host/local-queue-service.d.ts +16 -0
- package/dist/types/src/db-host/local-queue-service.d.ts.map +1 -0
- package/dist/types/src/db-host/query-service.d.ts +14 -9
- package/dist/types/src/db-host/query-service.d.ts.map +1 -1
- package/dist/types/src/db-host/queue-data-source.d.ts +28 -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 +2 -1
- package/dist/types/src/db-host/space-state-manager.d.ts.map +1 -1
- package/dist/types/src/db-host/stub.d.ts +10 -0
- package/dist/types/src/db-host/stub.d.ts.map +1 -0
- package/dist/types/src/edge/echo-edge-replicator.d.ts +8 -6
- package/dist/types/src/edge/echo-edge-replicator.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/pipeline.d.ts +3 -3
- package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
- package/dist/types/src/query/errors.d.ts +21 -9
- package/dist/types/src/query/errors.d.ts.map +1 -1
- package/dist/types/src/query/plan.d.ts +57 -5
- package/dist/types/src/query/plan.d.ts.map +1 -1
- package/dist/types/src/query/query-executor.d.ts +49 -10
- package/dist/types/src/query/query-executor.d.ts.map +1 -1
- package/dist/types/src/query/query-planner.d.ts +9 -0
- 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 +3 -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 +6 -6
- package/dist/types/src/space/space-protocol.d.ts.map +1 -1
- package/dist/types/src/space/space.d.ts +4 -4
- package/dist/types/src/space/space.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-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 +53 -45
- package/src/automerge/automerge-host.test.ts +55 -24
- package/src/automerge/automerge-host.ts +399 -126
- package/src/automerge/automerge-repo.test.ts +214 -46
- package/src/automerge/collection-synchronizer.test.ts +22 -15
- package/src/automerge/collection-synchronizer.ts +57 -22
- package/src/automerge/echo-data-monitor.ts +1 -1
- package/src/automerge/echo-network-adapter.test.ts +3 -3
- package/src/automerge/echo-network-adapter.ts +73 -28
- package/src/automerge/echo-replicator.ts +40 -15
- package/src/automerge/heads-store.ts +42 -3
- package/src/automerge/index.ts +1 -1
- package/src/automerge/leveldb-storage-adapter.ts +11 -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 +292 -0
- package/src/db-host/automerge-data-source.ts +144 -0
- package/src/db-host/data-service.ts +19 -20
- package/src/db-host/database-root.ts +2 -2
- package/src/db-host/documents-synchronizer.test.ts +73 -13
- package/src/db-host/documents-synchronizer.ts +58 -44
- package/src/db-host/echo-host.ts +194 -84
- package/src/db-host/index.ts +1 -0
- package/src/db-host/local-queue-service.ts +147 -0
- package/src/db-host/query-service.ts +28 -95
- package/src/db-host/queue-data-source.ts +132 -0
- package/src/db-host/queue-service.test.ts +145 -0
- package/src/db-host/space-state-manager.ts +6 -2
- package/src/db-host/stub.ts +28 -0
- package/src/edge/echo-edge-replicator.test.ts +8 -6
- package/src/edge/echo-edge-replicator.ts +124 -42
- package/src/filter/filter-match.test.ts +58 -32
- package/src/filter/filter-match.ts +163 -15
- package/src/metadata/metadata-store.ts +8 -5
- package/src/pipeline/pipeline-stress.test.ts +4 -2
- package/src/pipeline/pipeline.test.ts +3 -2
- package/src/pipeline/pipeline.ts +10 -7
- package/src/query/errors.ts +3 -1
- package/src/query/plan.ts +75 -5
- package/src/query/query-executor.ts +812 -117
- package/src/query/query-planner.test.ts +652 -34
- package/src/query/query-planner.ts +276 -13
- package/src/space/admission-discovery-extension.ts +3 -3
- package/src/space/auth.ts +6 -6
- package/src/space/control-pipeline.test.ts +4 -3
- package/src/space/control-pipeline.ts +11 -8
- package/src/space/space-manager.browser.test.ts +1 -1
- package/src/space/space-manager.ts +11 -10
- package/src/space/space-protocol.browser.test.ts +2 -2
- package/src/space/space-protocol.test.ts +3 -2
- package/src/space/space-protocol.ts +17 -14
- package/src/space/space.test.ts +1 -1
- package/src/space/space.ts +6 -5
- package/src/testing/index.ts +0 -2
- package/src/testing/test-agent-builder.ts +4 -3
- package/src/testing/test-network-adapter.ts +16 -3
- package/src/testing/test-replicator.ts +23 -18
- 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.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
|
@@ -3,27 +3,39 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import type { AutomergeUrl, DocumentId } from '@automerge/automerge-repo';
|
|
6
|
-
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';
|
|
7
9
|
|
|
8
10
|
import { Context, ContextDisposedError, LifecycleState, Resource } from '@dxos/context';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
+
import type { Obj } from '@dxos/echo';
|
|
12
|
+
import { ATTR_PARENT, ATTR_RELATION_SOURCE, ATTR_RELATION_TARGET } from '@dxos/echo/internal';
|
|
13
|
+
import {
|
|
14
|
+
DatabaseDirectory,
|
|
15
|
+
EncodedReference,
|
|
16
|
+
type ObjectPropPath,
|
|
17
|
+
ObjectStructure,
|
|
18
|
+
type QueryAST,
|
|
19
|
+
isEncodedReference,
|
|
20
|
+
} from '@dxos/echo-protocol';
|
|
21
|
+
import { type RuntimeProvider, runAndForwardErrors, unwrapExit } from '@dxos/effect';
|
|
22
|
+
import { EscapedPropPath, type IndexEngine, type ObjectMeta, type ReverseRef } from '@dxos/index-core';
|
|
11
23
|
import { invariant } from '@dxos/invariant';
|
|
12
|
-
import { DXN, type ObjectId,
|
|
24
|
+
import { DXN, type ObjectId, type SpaceId } from '@dxos/keys';
|
|
13
25
|
import { log } from '@dxos/log';
|
|
14
|
-
import { objectPointerCodec } from '@dxos/protocols';
|
|
15
26
|
import { type QueryReactivity, type QueryResult } from '@dxos/protocols/proto/dxos/echo/query';
|
|
16
27
|
import { getDeep, isNonNullable } from '@dxos/util';
|
|
17
28
|
|
|
18
|
-
import type { QueryPlan } from './plan';
|
|
19
|
-
import { QueryPlanner } from './query-planner';
|
|
20
29
|
import type { AutomergeHost } from '../automerge';
|
|
21
|
-
import { createIdFromSpaceKey } from '../common';
|
|
22
30
|
import type { SpaceStateManager } from '../db-host';
|
|
23
|
-
import { filterMatchObject } from '../filter';
|
|
31
|
+
import { filterMatchObject, filterMatchObjectJSON } from '../filter';
|
|
32
|
+
|
|
33
|
+
import type { QueryPlan } from './plan';
|
|
34
|
+
import { QueryPlanner } from './query-planner';
|
|
24
35
|
|
|
25
36
|
type QueryExecutorOptions = {
|
|
26
|
-
|
|
37
|
+
indexEngine: IndexEngine;
|
|
38
|
+
runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient>;
|
|
27
39
|
automergeHost: AutomergeHost;
|
|
28
40
|
spaceStateManager: SpaceStateManager;
|
|
29
41
|
|
|
@@ -44,11 +56,84 @@ type QueryExecutionResult = {
|
|
|
44
56
|
*/
|
|
45
57
|
type QueryItem = {
|
|
46
58
|
objectId: ObjectId;
|
|
47
|
-
|
|
59
|
+
|
|
48
60
|
spaceId: SpaceId;
|
|
49
|
-
|
|
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;
|
|
50
79
|
};
|
|
51
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
|
+
|
|
52
137
|
/**
|
|
53
138
|
* Recursive data structure that represents the execution trace of a query.
|
|
54
139
|
*/
|
|
@@ -60,6 +145,9 @@ export type ExecutionTrace = {
|
|
|
60
145
|
documentsLoaded: number;
|
|
61
146
|
indexHits: number;
|
|
62
147
|
|
|
148
|
+
beginTs: number;
|
|
149
|
+
endTs: number;
|
|
150
|
+
|
|
63
151
|
executionTime: number;
|
|
64
152
|
indexQueryTime: number;
|
|
65
153
|
documentLoadTime: number;
|
|
@@ -74,11 +162,42 @@ export const ExecutionTrace = Object.freeze({
|
|
|
74
162
|
objectCount: 0,
|
|
75
163
|
documentsLoaded: 0,
|
|
76
164
|
indexHits: 0,
|
|
165
|
+
beginTs: 0,
|
|
166
|
+
endTs: 0,
|
|
77
167
|
indexQueryTime: 0,
|
|
78
168
|
documentLoadTime: 0,
|
|
79
169
|
executionTime: 0,
|
|
80
170
|
children: [],
|
|
81
171
|
}),
|
|
172
|
+
markEnd: (trace: ExecutionTrace) => {
|
|
173
|
+
trace.endTs = performance.now();
|
|
174
|
+
trace.executionTime = trace.endTs - trace.beginTs;
|
|
175
|
+
},
|
|
176
|
+
putOnPerformanceTimeline: (trace: ExecutionTrace) => {
|
|
177
|
+
performance.measure(trace.name, {
|
|
178
|
+
start: trace.beginTs,
|
|
179
|
+
end: trace.endTs,
|
|
180
|
+
detail: {
|
|
181
|
+
devtools: {
|
|
182
|
+
dataType: 'track-entry',
|
|
183
|
+
track: 'Query Execution',
|
|
184
|
+
trackGroup: 'ECHO', // Group related tracks together
|
|
185
|
+
color: 'tertiary-dark',
|
|
186
|
+
properties: [
|
|
187
|
+
['objectCount', trace.objectCount],
|
|
188
|
+
['documentsLoaded', trace.documentsLoaded],
|
|
189
|
+
['index hits', trace.indexHits],
|
|
190
|
+
['indexQueryTime', trace.indexQueryTime],
|
|
191
|
+
['documentLoadTime', trace.documentLoadTime],
|
|
192
|
+
],
|
|
193
|
+
tooltipText: trace.details,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
for (const child of trace.children) {
|
|
198
|
+
ExecutionTrace.putOnPerformanceTimeline(child);
|
|
199
|
+
}
|
|
200
|
+
},
|
|
82
201
|
format: (trace: ExecutionTrace): string => {
|
|
83
202
|
const go = (trace: ExecutionTrace, indent: number): string => {
|
|
84
203
|
return [
|
|
@@ -99,8 +218,10 @@ type StepExecutionResult = {
|
|
|
99
218
|
|
|
100
219
|
const TRACE_QUERY_EXECUTION = false;
|
|
101
220
|
|
|
221
|
+
const MAX_DEPTH_FOR_DELETION_TRACING = 10;
|
|
222
|
+
|
|
102
223
|
/**
|
|
103
|
-
* Executes query plans against the
|
|
224
|
+
* Executes query plans against the IndexEngine and AutomergeHost.
|
|
104
225
|
*
|
|
105
226
|
* The QueryExecutor is responsible for:
|
|
106
227
|
* - Executing query plans step by step
|
|
@@ -110,7 +231,8 @@ const TRACE_QUERY_EXECUTION = false;
|
|
|
110
231
|
* - Handling different types of query operations (select, filter, traverse, etc.)
|
|
111
232
|
*/
|
|
112
233
|
export class QueryExecutor extends Resource {
|
|
113
|
-
private readonly
|
|
234
|
+
private readonly _indexEngine: IndexEngine;
|
|
235
|
+
private readonly _runtime: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient>;
|
|
114
236
|
private readonly _automergeHost: AutomergeHost;
|
|
115
237
|
private readonly _spaceStateManager: SpaceStateManager;
|
|
116
238
|
/**
|
|
@@ -128,7 +250,8 @@ export class QueryExecutor extends Resource {
|
|
|
128
250
|
constructor(options: QueryExecutorOptions) {
|
|
129
251
|
super();
|
|
130
252
|
|
|
131
|
-
this.
|
|
253
|
+
this._indexEngine = options.indexEngine;
|
|
254
|
+
this._runtime = options.runtime;
|
|
132
255
|
this._automergeHost = options.automergeHost;
|
|
133
256
|
this._spaceStateManager = options.spaceStateManager;
|
|
134
257
|
|
|
@@ -152,19 +275,18 @@ export class QueryExecutor extends Resource {
|
|
|
152
275
|
return this._trace;
|
|
153
276
|
}
|
|
154
277
|
|
|
155
|
-
protected override async _open(ctx: Context): Promise<void> {}
|
|
156
|
-
|
|
157
|
-
protected override async _close(ctx: Context): Promise<void> {}
|
|
158
|
-
|
|
159
278
|
getResults(): QueryResult[] {
|
|
160
279
|
return this._lastResultSet.map(
|
|
161
280
|
(item): QueryResult => ({
|
|
162
281
|
id: item.objectId,
|
|
163
|
-
documentId: item.documentId,
|
|
282
|
+
documentId: item.documentId ?? undefined,
|
|
283
|
+
queueId: item.queueId ?? undefined,
|
|
284
|
+
queueNamespace: item.queueNamespace ?? undefined,
|
|
164
285
|
spaceId: item.spaceId,
|
|
165
286
|
|
|
166
|
-
|
|
167
|
-
|
|
287
|
+
rank: item.rank,
|
|
288
|
+
|
|
289
|
+
documentJson: item.doc ? JSON.stringify(item.doc) : item.data ? JSON.stringify(item.data) : undefined,
|
|
168
290
|
}),
|
|
169
291
|
);
|
|
170
292
|
}
|
|
@@ -176,7 +298,7 @@ export class QueryExecutor extends Resource {
|
|
|
176
298
|
const { workingSet, trace } = await this._execPlan(this._plan, []);
|
|
177
299
|
this._lastResultSet = workingSet;
|
|
178
300
|
trace.name = 'Root';
|
|
179
|
-
trace.details = JSON.stringify({ id: this._id });
|
|
301
|
+
trace.details = JSON.stringify({ id: this._id, query: prettyQuery(this._query) });
|
|
180
302
|
this._trace = trace;
|
|
181
303
|
|
|
182
304
|
const changed =
|
|
@@ -188,6 +310,9 @@ export class QueryExecutor extends Resource {
|
|
|
188
310
|
workingSet[index].documentId !== item.documentId,
|
|
189
311
|
);
|
|
190
312
|
|
|
313
|
+
// Disabled because concurrent queries don't print hierarchies correctly.
|
|
314
|
+
// ExecutionTrace.putOnPerformanceTimeline(trace);
|
|
315
|
+
|
|
191
316
|
if (TRACE_QUERY_EXECUTION) {
|
|
192
317
|
// eslint-disable-next-line no-console
|
|
193
318
|
console.log(ExecutionTrace.format(trace));
|
|
@@ -200,7 +325,6 @@ export class QueryExecutor extends Resource {
|
|
|
200
325
|
|
|
201
326
|
private async _execPlan(plan: QueryPlan.Plan, workingSet: QueryItem[]): Promise<StepExecutionResult> {
|
|
202
327
|
const trace = ExecutionTrace.makeEmpty();
|
|
203
|
-
const begin = performance.now();
|
|
204
328
|
for (const step of plan.steps) {
|
|
205
329
|
if (this._ctx.disposed) {
|
|
206
330
|
throw new ContextDisposedError();
|
|
@@ -211,7 +335,7 @@ export class QueryExecutor extends Resource {
|
|
|
211
335
|
trace.children.push(result.trace);
|
|
212
336
|
}
|
|
213
337
|
trace.objectCount = workingSet.length;
|
|
214
|
-
|
|
338
|
+
ExecutionTrace.markEnd(trace);
|
|
215
339
|
return { workingSet, trace };
|
|
216
340
|
}
|
|
217
341
|
|
|
@@ -245,10 +369,17 @@ export class QueryExecutor extends Resource {
|
|
|
245
369
|
case 'TraverseStep':
|
|
246
370
|
({ workingSet: newWorkingSet, trace } = await this._execTraverseStep(step, workingSet));
|
|
247
371
|
break;
|
|
372
|
+
case 'OrderStep':
|
|
373
|
+
({ workingSet: newWorkingSet, trace } = await this._execOrderStep(step, workingSet));
|
|
374
|
+
break;
|
|
375
|
+
case 'LimitStep':
|
|
376
|
+
({ workingSet: newWorkingSet, trace } = await this._execLimitStep(step, workingSet));
|
|
377
|
+
break;
|
|
248
378
|
default:
|
|
249
379
|
throw new Error(`Unknown step type: ${(step as any)._tag}`);
|
|
250
380
|
}
|
|
251
|
-
trace.
|
|
381
|
+
trace.beginTs = begin;
|
|
382
|
+
ExecutionTrace.markEnd(trace);
|
|
252
383
|
|
|
253
384
|
return { workingSet: newWorkingSet, trace };
|
|
254
385
|
}
|
|
@@ -260,16 +391,14 @@ export class QueryExecutor extends Resource {
|
|
|
260
391
|
...ExecutionTrace.makeEmpty(),
|
|
261
392
|
name: 'Select',
|
|
262
393
|
details: JSON.stringify(step.selector),
|
|
394
|
+
beginTs: performance.now(),
|
|
263
395
|
};
|
|
264
396
|
|
|
265
397
|
switch (step.selector._tag) {
|
|
266
398
|
case 'WildcardSelector': {
|
|
267
399
|
const beginIndexQuery = performance.now();
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
inverted: false,
|
|
271
|
-
});
|
|
272
|
-
trace.indexHits = +indexHits.length;
|
|
400
|
+
const metas = await this._queryAllFromSqlIndex(step.spaces);
|
|
401
|
+
trace.indexHits = metas.length;
|
|
273
402
|
trace.indexQueryTime += performance.now() - beginIndexQuery;
|
|
274
403
|
|
|
275
404
|
if (this._ctx.disposed) {
|
|
@@ -277,15 +406,16 @@ export class QueryExecutor extends Resource {
|
|
|
277
406
|
}
|
|
278
407
|
|
|
279
408
|
const documentLoadStart = performance.now();
|
|
280
|
-
const results = await this.
|
|
409
|
+
const results = await this._loadDocumentsAfterSqlQuery(metas);
|
|
281
410
|
trace.documentsLoaded += results.length;
|
|
282
411
|
trace.documentLoadTime += performance.now() - documentLoadStart;
|
|
283
412
|
|
|
284
|
-
workingSet.push(...results.filter(isNonNullable)
|
|
413
|
+
workingSet.push(...results.filter(isNonNullable));
|
|
285
414
|
trace.objectCount = workingSet.length;
|
|
286
415
|
|
|
287
416
|
break;
|
|
288
417
|
}
|
|
418
|
+
|
|
289
419
|
case 'IdSelector': {
|
|
290
420
|
const beginLoad = performance.now();
|
|
291
421
|
const items = await Promise.all(
|
|
@@ -299,13 +429,11 @@ export class QueryExecutor extends Resource {
|
|
|
299
429
|
trace.objectCount = workingSet.length;
|
|
300
430
|
break;
|
|
301
431
|
}
|
|
432
|
+
|
|
302
433
|
case 'TypeSelector': {
|
|
303
434
|
const beginIndexQuery = performance.now();
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
inverted: step.selector.inverted,
|
|
307
|
-
});
|
|
308
|
-
trace.indexHits = +indexHits.length;
|
|
435
|
+
const metas = await this._queryTypesFromSqlIndex(step.spaces, step.selector.typename, step.selector.inverted);
|
|
436
|
+
trace.indexHits = metas.length;
|
|
309
437
|
trace.indexQueryTime += performance.now() - beginIndexQuery;
|
|
310
438
|
|
|
311
439
|
if (this._ctx.disposed) {
|
|
@@ -313,60 +441,135 @@ export class QueryExecutor extends Resource {
|
|
|
313
441
|
}
|
|
314
442
|
|
|
315
443
|
const documentLoadStart = performance.now();
|
|
316
|
-
const results = await this.
|
|
444
|
+
const results = await this._loadDocumentsAfterSqlQuery(metas);
|
|
317
445
|
trace.documentsLoaded += results.length;
|
|
318
446
|
trace.documentLoadTime += performance.now() - documentLoadStart;
|
|
319
447
|
|
|
320
|
-
workingSet.push(...results.filter(isNonNullable)
|
|
448
|
+
workingSet.push(...results.filter(isNonNullable));
|
|
321
449
|
trace.objectCount = workingSet.length;
|
|
322
450
|
|
|
323
451
|
break;
|
|
324
452
|
}
|
|
453
|
+
|
|
325
454
|
case 'TextSelector': {
|
|
455
|
+
// TODO(dmaretskyi): type + FTS queries would be very common so we should support those, maybe chunk the fts index.
|
|
456
|
+
// TODO(dmaretskyi): nice to have matched text snippets/highlighting.
|
|
457
|
+
if (step.selector.searchKind === 'vector') {
|
|
458
|
+
// Vector search is not currently supported.
|
|
459
|
+
log.warn('Vector search is not supported');
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Full-text search using SQLite FTS5.
|
|
326
464
|
const beginIndexQuery = performance.now();
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
465
|
+
invariant(step.spaces.length <= 1, 'Multiple spaces are not supported for full-text search');
|
|
466
|
+
// Extract queue IDs from DXN strings.
|
|
467
|
+
const queueIds =
|
|
468
|
+
step.queues.length > 0
|
|
469
|
+
? (step.queues.map((dxnStr) => DXN.parse(dxnStr).asQueueDXN()?.queueId).filter(Boolean) as ObjectId[])
|
|
470
|
+
: null;
|
|
471
|
+
const textResults = await this._runInRuntime(
|
|
472
|
+
this._indexEngine.queryText({
|
|
330
473
|
query: step.selector.text,
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
},
|
|
338
|
-
});
|
|
339
|
-
trace.indexHits = +indexHits.length;
|
|
474
|
+
spaceId: step.spaces,
|
|
475
|
+
includeAllQueues: step.allQueuesFromSpaces,
|
|
476
|
+
queueIds,
|
|
477
|
+
}),
|
|
478
|
+
);
|
|
479
|
+
trace.indexHits = textResults.length;
|
|
340
480
|
trace.indexQueryTime += performance.now() - beginIndexQuery;
|
|
341
481
|
|
|
342
482
|
if (this._ctx.disposed) {
|
|
343
483
|
return { workingSet, trace };
|
|
344
484
|
}
|
|
345
485
|
|
|
486
|
+
// Load documents from the results.
|
|
346
487
|
const documentLoadStart = performance.now();
|
|
347
|
-
|
|
348
|
-
|
|
488
|
+
|
|
489
|
+
// Separate queue items from space items.
|
|
490
|
+
const queueResults = textResults.filter((r) => r.queueId);
|
|
491
|
+
const spaceResults = textResults.filter((r) => !r.queueId);
|
|
492
|
+
|
|
493
|
+
// Build a map from recordId to rank for all FTS results.
|
|
494
|
+
const rankMap = new Map(textResults.map((r) => [r.recordId, r.rank]));
|
|
495
|
+
|
|
496
|
+
// Load queue items from indexed snapshots.
|
|
497
|
+
let queueItems: QueryItem[] = [];
|
|
498
|
+
if (queueResults.length > 0) {
|
|
499
|
+
const snapshots = await this._runInRuntime(
|
|
500
|
+
this._indexEngine.querySnapshotsJSON(queueResults.map((r) => r.recordId)),
|
|
501
|
+
);
|
|
502
|
+
const snapshotMap = new Map(snapshots.map((s) => [s.recordId, s.snapshot]));
|
|
503
|
+
queueItems = queueResults
|
|
504
|
+
.map((result): QueryItem | null => {
|
|
505
|
+
const snapshot = snapshotMap.get(result.recordId);
|
|
506
|
+
if (!snapshot || typeof snapshot !== 'object') {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
objectId: result.objectId as ObjectId,
|
|
511
|
+
spaceId: result.spaceId as SpaceId,
|
|
512
|
+
queueId: result.queueId as ObjectId,
|
|
513
|
+
queueNamespace: 'data',
|
|
514
|
+
documentId: null,
|
|
515
|
+
doc: null,
|
|
516
|
+
data: snapshot as Obj.JSON,
|
|
517
|
+
rank: rankMap.get(result.recordId) ?? 1,
|
|
518
|
+
};
|
|
519
|
+
})
|
|
520
|
+
.filter(isNonNullable);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Load space items from documents.
|
|
524
|
+
const spaceItems = await Promise.all(
|
|
525
|
+
spaceResults.map(async (result): Promise<QueryItem | null> => {
|
|
526
|
+
const dxn = DXN.fromLocalObjectId(result.objectId);
|
|
527
|
+
const item = await this._loadFromDXN(dxn, { sourceSpaceId: result.spaceId as SpaceId });
|
|
528
|
+
if (item) {
|
|
529
|
+
// Override the default rank with the FTS rank.
|
|
530
|
+
item.rank = rankMap.get(result.recordId) ?? 1;
|
|
531
|
+
}
|
|
532
|
+
return item;
|
|
533
|
+
}),
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
const items = [...queueItems, ...spaceItems.filter(isNonNullable)];
|
|
537
|
+
trace.documentsLoaded += items.length;
|
|
349
538
|
trace.documentLoadTime += performance.now() - documentLoadStart;
|
|
350
539
|
|
|
351
|
-
workingSet.push(...
|
|
540
|
+
workingSet.push(...items.filter((item) => step.spaces.includes(item.spaceId)));
|
|
352
541
|
trace.objectCount = workingSet.length;
|
|
353
542
|
break;
|
|
354
543
|
}
|
|
544
|
+
|
|
355
545
|
default:
|
|
356
546
|
throw new Error(`Unknown selector type: ${(step.selector as any)._tag}`);
|
|
357
547
|
}
|
|
358
548
|
|
|
549
|
+
// Apply limit if specified on the select step.
|
|
550
|
+
if (step.limit !== undefined && workingSet.length > step.limit) {
|
|
551
|
+
workingSet = workingSet.slice(0, step.limit);
|
|
552
|
+
trace.objectCount = workingSet.length;
|
|
553
|
+
}
|
|
554
|
+
|
|
359
555
|
return { workingSet, trace };
|
|
360
556
|
}
|
|
361
557
|
|
|
362
558
|
private async _execFilterStep(step: QueryPlan.FilterStep, workingSet: QueryItem[]): Promise<StepExecutionResult> {
|
|
363
|
-
const result = workingSet.filter((item) =>
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
559
|
+
const result = workingSet.filter((item) => {
|
|
560
|
+
if (item.doc) {
|
|
561
|
+
return filterMatchObject(step.filter, {
|
|
562
|
+
id: item.objectId,
|
|
563
|
+
spaceId: item.spaceId,
|
|
564
|
+
doc: item.doc,
|
|
565
|
+
});
|
|
566
|
+
} else if (item.data) {
|
|
567
|
+
return filterMatchObjectJSON(step.filter, item.data);
|
|
568
|
+
} else {
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
|
|
370
573
|
return {
|
|
371
574
|
workingSet: result,
|
|
372
575
|
trace: {
|
|
@@ -382,12 +585,21 @@ export class QueryExecutor extends Resource {
|
|
|
382
585
|
step: QueryPlan.FilterDeletedStep,
|
|
383
586
|
workingSet: QueryItem[],
|
|
384
587
|
): Promise<StepExecutionResult> {
|
|
385
|
-
if (workingSet.length === 6) {
|
|
386
|
-
log.info('FilterDeletedStep', { step, workingSet });
|
|
387
|
-
}
|
|
388
|
-
|
|
389
588
|
const expected = step.mode === 'only-deleted';
|
|
390
|
-
|
|
589
|
+
|
|
590
|
+
const deletedState = workingSet.map((item) => QueryItem.isDeleted(item));
|
|
591
|
+
await Promise.all(
|
|
592
|
+
workingSet.map(async (item, index) => {
|
|
593
|
+
if (deletedState[index]) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
deletedState[index] ||= await this._getTransitiveDeletionState(item, MAX_DEPTH_FOR_DELETION_TRACING);
|
|
597
|
+
}),
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
const result = workingSet.filter((item, index) => deletedState[index] === expected);
|
|
601
|
+
|
|
602
|
+
// 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.
|
|
391
603
|
return {
|
|
392
604
|
workingSet: result,
|
|
393
605
|
trace: {
|
|
@@ -413,11 +625,12 @@ export class QueryExecutor extends Resource {
|
|
|
413
625
|
case 'ReferenceTraversal': {
|
|
414
626
|
switch (step.traversal.direction) {
|
|
415
627
|
case 'outgoing': {
|
|
628
|
+
invariant(step.traversal.property !== null, 'Outgoing reference traversal requires a property');
|
|
416
629
|
const property = EscapedPropPath.unescape(step.traversal.property);
|
|
417
630
|
|
|
418
631
|
const refs = workingSet
|
|
419
632
|
.flatMap((item) => {
|
|
420
|
-
const ref =
|
|
633
|
+
const ref = QueryItem.getProperty(item, property);
|
|
421
634
|
const refs = Array.isArray(ref) ? ref : [ref];
|
|
422
635
|
return refs.map((ref) => {
|
|
423
636
|
try {
|
|
@@ -428,7 +641,7 @@ export class QueryExecutor extends Resource {
|
|
|
428
641
|
}
|
|
429
642
|
: null;
|
|
430
643
|
} catch {
|
|
431
|
-
log.warn('
|
|
644
|
+
log.warn('invalid reference', { ref: ref['/'] });
|
|
432
645
|
return null;
|
|
433
646
|
}
|
|
434
647
|
});
|
|
@@ -447,19 +660,13 @@ export class QueryExecutor extends Resource {
|
|
|
447
660
|
break;
|
|
448
661
|
}
|
|
449
662
|
case 'incoming': {
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
kind: 'inbound-reference',
|
|
455
|
-
property: step.traversal.property,
|
|
456
|
-
anchors: workingSet.map((item) => item.objectId),
|
|
457
|
-
},
|
|
458
|
-
});
|
|
459
|
-
trace.indexHits += indexHits.length;
|
|
663
|
+
const beginIndexQuery = performance.now();
|
|
664
|
+
const metas = await this._queryIncomingReferencesFromSqlIndex(workingSet, step.traversal.property);
|
|
665
|
+
trace.indexHits += metas.length;
|
|
666
|
+
trace.indexQueryTime += performance.now() - beginIndexQuery;
|
|
460
667
|
|
|
461
668
|
const documentLoadStart = performance.now();
|
|
462
|
-
const results = await this.
|
|
669
|
+
const results = await this._loadDocumentsAfterSqlQuery(metas);
|
|
463
670
|
trace.documentsLoaded += results.length;
|
|
464
671
|
trace.documentLoadTime += performance.now() - documentLoadStart;
|
|
465
672
|
|
|
@@ -477,21 +684,20 @@ export class QueryExecutor extends Resource {
|
|
|
477
684
|
case 'relation-to-target': {
|
|
478
685
|
const refs = workingSet
|
|
479
686
|
.map((item) => {
|
|
480
|
-
const
|
|
687
|
+
const dxn =
|
|
481
688
|
step.traversal.direction === 'relation-to-source'
|
|
482
|
-
?
|
|
483
|
-
:
|
|
484
|
-
|
|
485
|
-
if (!isEncodedReference(ref)) {
|
|
689
|
+
? QueryItem.getRelationSource(item)
|
|
690
|
+
: QueryItem.getRelationTarget(item);
|
|
691
|
+
if (!dxn) {
|
|
486
692
|
return null;
|
|
487
693
|
}
|
|
488
694
|
try {
|
|
489
695
|
return {
|
|
490
|
-
ref: DXN.parse(
|
|
696
|
+
ref: DXN.parse(dxn),
|
|
491
697
|
spaceId: item.spaceId,
|
|
492
698
|
};
|
|
493
699
|
} catch {
|
|
494
|
-
log.warn('
|
|
700
|
+
log.warn('invalid reference', { ref: dxn });
|
|
495
701
|
return null;
|
|
496
702
|
}
|
|
497
703
|
})
|
|
@@ -511,20 +717,16 @@ export class QueryExecutor extends Resource {
|
|
|
511
717
|
|
|
512
718
|
case 'source-to-relation':
|
|
513
719
|
case 'target-to-relation': {
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
},
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
trace.indexHits += indexHits.length;
|
|
720
|
+
const beginIndexQuery = performance.now();
|
|
721
|
+
const metas = await this._queryRelationsFromSqlIndex(
|
|
722
|
+
workingSet,
|
|
723
|
+
step.traversal.direction === 'source-to-relation' ? 'source' : 'target',
|
|
724
|
+
);
|
|
725
|
+
trace.indexHits += metas.length;
|
|
726
|
+
trace.indexQueryTime += performance.now() - beginIndexQuery;
|
|
525
727
|
|
|
526
728
|
const documentLoadStart = performance.now();
|
|
527
|
-
const results = await this.
|
|
729
|
+
const results = await this._loadDocumentsAfterSqlQuery(metas);
|
|
528
730
|
trace.documentsLoaded += results.length;
|
|
529
731
|
trace.documentLoadTime += performance.now() - documentLoadStart;
|
|
530
732
|
|
|
@@ -536,6 +738,85 @@ export class QueryExecutor extends Resource {
|
|
|
536
738
|
}
|
|
537
739
|
break;
|
|
538
740
|
}
|
|
741
|
+
case 'HierarchyTraversal': {
|
|
742
|
+
switch (step.traversal.direction) {
|
|
743
|
+
case 'to-parent': {
|
|
744
|
+
// Traverse from child to parent using the parent reference in the document.
|
|
745
|
+
const refs = workingSet
|
|
746
|
+
.map((item) => {
|
|
747
|
+
if (!item.doc) {
|
|
748
|
+
return null; // TODO(dmaretskyi): Queue items not supported here.
|
|
749
|
+
}
|
|
750
|
+
const ref = ObjectStructure.getParent(item.doc);
|
|
751
|
+
if (!EncodedReference.isEncodedReference(ref)) {
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
try {
|
|
755
|
+
return {
|
|
756
|
+
ref: DXN.parse(ref['/']),
|
|
757
|
+
spaceId: item.spaceId,
|
|
758
|
+
};
|
|
759
|
+
} catch {
|
|
760
|
+
log.warn('invalid parent reference', { ref: ref['/'] });
|
|
761
|
+
return null;
|
|
762
|
+
}
|
|
763
|
+
})
|
|
764
|
+
.filter(isNonNullable);
|
|
765
|
+
|
|
766
|
+
const beginLoad = performance.now();
|
|
767
|
+
const items = await Promise.all(
|
|
768
|
+
refs.map(({ ref, spaceId }) => this._loadFromDXN(ref, { sourceSpaceId: spaceId })),
|
|
769
|
+
);
|
|
770
|
+
trace.documentLoadTime += performance.now() - beginLoad;
|
|
771
|
+
|
|
772
|
+
newWorkingSet.push(...items.filter(isNonNullable));
|
|
773
|
+
trace.objectCount = newWorkingSet.length;
|
|
774
|
+
break;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
case 'to-children': {
|
|
778
|
+
// Traverse from parent to children using the SQL index.
|
|
779
|
+
// Group working set by spaceId.
|
|
780
|
+
const bySpace = new Map<SpaceId, ObjectId[]>();
|
|
781
|
+
for (const item of workingSet) {
|
|
782
|
+
const existing = bySpace.get(item.spaceId);
|
|
783
|
+
if (existing) {
|
|
784
|
+
existing.push(item.objectId);
|
|
785
|
+
} else {
|
|
786
|
+
bySpace.set(item.spaceId, [item.objectId]);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Query children for each space.
|
|
791
|
+
const allChildren: { spaceId: SpaceId; objectId: ObjectId }[] = [];
|
|
792
|
+
for (const [spaceId, parentIds] of bySpace) {
|
|
793
|
+
const children = await this._runInRuntime(
|
|
794
|
+
this._indexEngine.queryChildren({ spaceId: [spaceId], parentIds }),
|
|
795
|
+
);
|
|
796
|
+
|
|
797
|
+
for (const child of children) {
|
|
798
|
+
allChildren.push({ spaceId, objectId: child.objectId as ObjectId });
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
trace.indexHits += allChildren.length;
|
|
803
|
+
|
|
804
|
+
const documentLoadStart = performance.now();
|
|
805
|
+
const results = await Promise.all(
|
|
806
|
+
allChildren.map(({ spaceId, objectId }) =>
|
|
807
|
+
this._loadFromDXN(DXN.fromLocalObjectId(objectId), { sourceSpaceId: spaceId }),
|
|
808
|
+
),
|
|
809
|
+
);
|
|
810
|
+
trace.documentsLoaded += results.filter(isNonNullable).length;
|
|
811
|
+
trace.documentLoadTime += performance.now() - documentLoadStart;
|
|
812
|
+
|
|
813
|
+
newWorkingSet.push(...results.filter(isNonNullable));
|
|
814
|
+
trace.objectCount = newWorkingSet.length;
|
|
815
|
+
break;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
539
820
|
default:
|
|
540
821
|
throw new Error(`Unknown traversal type: ${(step.traversal as any)._tag}`);
|
|
541
822
|
}
|
|
@@ -590,38 +871,300 @@ export class QueryExecutor extends Resource {
|
|
|
590
871
|
};
|
|
591
872
|
}
|
|
592
873
|
|
|
593
|
-
private async
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
874
|
+
private async _execOrderStep(step: QueryPlan.OrderStep, workingSet: QueryItem[]): Promise<StepExecutionResult> {
|
|
875
|
+
let sortedWorkingSet = [...workingSet].sort((a, b) => this._compareMultiOrder(a, b, step.order));
|
|
876
|
+
|
|
877
|
+
// Apply limit if specified on the order step.
|
|
878
|
+
if (step.limit !== undefined && sortedWorkingSet.length > step.limit) {
|
|
879
|
+
sortedWorkingSet = sortedWorkingSet.slice(0, step.limit);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
return {
|
|
883
|
+
workingSet: sortedWorkingSet,
|
|
884
|
+
trace: {
|
|
885
|
+
...ExecutionTrace.makeEmpty(),
|
|
886
|
+
name: 'Order',
|
|
887
|
+
details: JSON.stringify({ order: step.order, limit: step.limit }),
|
|
888
|
+
objectCount: sortedWorkingSet.length,
|
|
889
|
+
},
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
private async _execLimitStep(step: QueryPlan.LimitStep, workingSet: QueryItem[]): Promise<StepExecutionResult> {
|
|
894
|
+
const limitedWorkingSet = workingSet.slice(0, step.limit);
|
|
895
|
+
|
|
896
|
+
return {
|
|
897
|
+
workingSet: limitedWorkingSet,
|
|
898
|
+
trace: {
|
|
899
|
+
...ExecutionTrace.makeEmpty(),
|
|
900
|
+
name: 'Limit',
|
|
901
|
+
details: JSON.stringify({ limit: step.limit }),
|
|
902
|
+
objectCount: limitedWorkingSet.length,
|
|
903
|
+
},
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
private _compareMultiOrder(a: QueryItem, b: QueryItem, orders: readonly QueryAST.Order[]): number {
|
|
908
|
+
// Short circuit for common cases.
|
|
909
|
+
if (orders.length === 0) {
|
|
910
|
+
return 0;
|
|
911
|
+
} else if (orders.length === 1) {
|
|
912
|
+
return this._compareByOrder(a, b, orders[0]);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
for (const order of orders) {
|
|
916
|
+
const comparison = this._compareByOrder(a, b, order);
|
|
917
|
+
if (comparison !== 0) {
|
|
918
|
+
return comparison;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return 0;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
private _compareByOrder(a: QueryItem, b: QueryItem, order: QueryAST.Order): number {
|
|
925
|
+
switch (order.kind) {
|
|
926
|
+
case 'natural':
|
|
927
|
+
return a.objectId.localeCompare(b.objectId);
|
|
928
|
+
case 'property': {
|
|
929
|
+
const comparison = this._compareByProperty(a, b, order.property);
|
|
930
|
+
return order.direction === 'desc' ? -comparison : comparison;
|
|
931
|
+
}
|
|
932
|
+
case 'rank': {
|
|
933
|
+
// Higher rank = better match. By default, descending order (best first).
|
|
934
|
+
const comparison = a.rank - b.rank;
|
|
935
|
+
return order.direction === 'desc' ? -comparison : comparison;
|
|
936
|
+
}
|
|
937
|
+
default:
|
|
938
|
+
// Should never reach here with proper TypeScript types.
|
|
939
|
+
return 0;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
private _compareByProperty(a: QueryItem, b: QueryItem, property: string): number {
|
|
944
|
+
const aValue = QueryItem.getProperty(a, [property]);
|
|
945
|
+
const bValue = QueryItem.getProperty(b, [property]);
|
|
946
|
+
|
|
947
|
+
// Both null or undefined
|
|
948
|
+
if (aValue == null && bValue == null) {
|
|
949
|
+
return 0;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// Only a is null/undefined
|
|
953
|
+
if (aValue == null) {
|
|
954
|
+
return 1;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Only b is null/undefined
|
|
958
|
+
if (bValue == null) {
|
|
959
|
+
return -1;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Both strings
|
|
963
|
+
if (typeof aValue === 'string' && typeof bValue === 'string') {
|
|
964
|
+
return aValue.localeCompare(bValue);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Both numbers
|
|
968
|
+
if (typeof aValue === 'number' && typeof bValue === 'number') {
|
|
969
|
+
return aValue - bValue;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Both booleans
|
|
973
|
+
if (typeof aValue === 'boolean' && typeof bValue === 'boolean') {
|
|
974
|
+
return aValue === bValue ? 0 : aValue ? 1 : -1;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// Fallback: convert to strings and compare
|
|
978
|
+
return String(aValue).localeCompare(String(bValue));
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
private async _runInRuntime<T>(effect: Effect.Effect<T, unknown, SqlClient.SqlClient>): Promise<T> {
|
|
982
|
+
const runtimeProvider = this._runtime;
|
|
983
|
+
invariant(runtimeProvider, 'SQL runtime is required.');
|
|
984
|
+
const runtime = await runAndForwardErrors(runtimeProvider);
|
|
985
|
+
return await unwrapExit(await effect.pipe(Runtime.runPromiseExit(runtime)));
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
private async _queryAllFromSqlIndex(spaceIds: readonly SpaceId[]): Promise<readonly ObjectMeta[]> {
|
|
989
|
+
return await this._runInRuntime(this._indexEngine.queryAll({ spaceIds }));
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
private async _queryTypesFromSqlIndex(
|
|
993
|
+
spaceIds: readonly SpaceId[],
|
|
994
|
+
typeDxns: readonly string[],
|
|
995
|
+
inverted: boolean,
|
|
996
|
+
): Promise<readonly ObjectMeta[]> {
|
|
997
|
+
return await this._runInRuntime(this._indexEngine.queryTypes({ spaceIds, typeDxns, inverted }));
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
private async _queryIncomingReferencesFromSqlIndex(
|
|
1001
|
+
workingSet: QueryItem[],
|
|
1002
|
+
property: EscapedPropPath | null,
|
|
1003
|
+
): Promise<readonly ObjectMeta[]> {
|
|
1004
|
+
const anchorDxns = workingSet.map((item) => DXN.fromLocalObjectId(item.objectId).toString());
|
|
1005
|
+
const rows: readonly ReverseRef[] = (
|
|
1006
|
+
await Promise.all(
|
|
1007
|
+
anchorDxns.map((targetDxn) => this._runInRuntime(this._indexEngine.queryReverseRef({ targetDxn }))),
|
|
1008
|
+
)
|
|
1009
|
+
).flat();
|
|
1010
|
+
|
|
1011
|
+
const recordIds = rows
|
|
1012
|
+
.filter((row) => {
|
|
1013
|
+
if (property === null) {
|
|
1014
|
+
return true;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const queryPath = EscapedPropPath.unescape(property);
|
|
1018
|
+
const rowPath = EscapedPropPath.unescape(row.propPath);
|
|
1019
|
+
return QueryExecutor._matchesReferencePropertyPath(rowPath, queryPath);
|
|
1020
|
+
})
|
|
1021
|
+
.map((row) => row.recordId);
|
|
1022
|
+
|
|
1023
|
+
const uniqueRecordIds = Array.from(new Set<number>(recordIds));
|
|
1024
|
+
return await this._runInRuntime(this._indexEngine.lookupByRecordIds(uniqueRecordIds));
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Matches a reverse-reference row path against a query property path.
|
|
1029
|
+
* Allows numeric segments in the row path (array indices) that are not present in the query.
|
|
1030
|
+
*
|
|
1031
|
+
* Examples:
|
|
1032
|
+
* - query: ['assignee'] matches row: ['assignee'] and ['assignee', '0'].
|
|
1033
|
+
* - query: ['items', 'assignee'] matches row: ['items', '0', 'assignee'].
|
|
1034
|
+
* - query: ['a', 'b'] does NOT match row: ['a'].
|
|
1035
|
+
* - query: ['a'] does NOT match row: ['a', 'b'].
|
|
1036
|
+
*/
|
|
1037
|
+
private static _matchesReferencePropertyPath(rowPath: readonly string[], queryPath: readonly string[]): boolean {
|
|
1038
|
+
const isNumericSegment = (segment: string) => /^[0-9]+$/.test(segment);
|
|
1039
|
+
|
|
1040
|
+
let i = 0; // queryPath index.
|
|
1041
|
+
let j = 0; // rowPath index.
|
|
1042
|
+
|
|
1043
|
+
while (i < queryPath.length && j < rowPath.length) {
|
|
1044
|
+
if (rowPath[j] === queryPath[i]) {
|
|
1045
|
+
i++;
|
|
1046
|
+
j++;
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// Row may contain array indices that aren't present in the query path.
|
|
1051
|
+
if (isNumericSegment(rowPath[j])) {
|
|
1052
|
+
j++;
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
return false;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// Must consume full query path.
|
|
1060
|
+
if (i !== queryPath.length) {
|
|
1061
|
+
return false;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Any remaining row segments must be numeric (array indices).
|
|
1065
|
+
for (; j < rowPath.length; j++) {
|
|
1066
|
+
if (!isNumericSegment(rowPath[j])) {
|
|
1067
|
+
return false;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
return true;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
private async _queryRelationsFromSqlIndex(
|
|
1075
|
+
workingSet: QueryItem[],
|
|
1076
|
+
endpoint: 'source' | 'target',
|
|
1077
|
+
): Promise<readonly ObjectMeta[]> {
|
|
1078
|
+
const anchorDxns = workingSet.map((item) => DXN.fromLocalObjectId(item.objectId).toString());
|
|
1079
|
+
return await this._runInRuntime(this._indexEngine.queryRelations({ endpoint, anchorDxns }));
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
private async _loadDocumentsAfterSqlQuery(metas: readonly ObjectMeta[]): Promise<(QueryItem | null)[]> {
|
|
1083
|
+
const snapshotMap = await this._loadQueueSnapshotMap(metas);
|
|
1084
|
+
return await Promise.all(
|
|
1085
|
+
metas.map(async (meta) => {
|
|
1086
|
+
// Branch 1: Document-backed object.
|
|
1087
|
+
if (meta.documentId) {
|
|
1088
|
+
return this._loadFromAutomerge(meta);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// Branch 2: Queue-backed object.
|
|
1092
|
+
if (meta.queueId) {
|
|
1093
|
+
return this._loadFromQueue(meta, snapshotMap);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
return null;
|
|
597
1097
|
}),
|
|
598
1098
|
);
|
|
599
1099
|
}
|
|
600
1100
|
|
|
601
|
-
private async
|
|
602
|
-
const
|
|
1101
|
+
private async _loadQueueSnapshotMap(metas: readonly ObjectMeta[]): Promise<Map<number, unknown>> {
|
|
1102
|
+
const queueMetas = metas.filter((meta) => !meta.documentId && !!meta.queueId);
|
|
1103
|
+
if (queueMetas.length === 0) {
|
|
1104
|
+
return new Map();
|
|
1105
|
+
}
|
|
603
1106
|
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
1107
|
+
const snapshots = await this._runInRuntime(
|
|
1108
|
+
this._indexEngine.querySnapshotsJSON(queueMetas.map((meta) => meta.recordId)),
|
|
1109
|
+
);
|
|
1110
|
+
return new Map(snapshots.map((s) => [s.recordId, s.snapshot]));
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/**
|
|
1114
|
+
* Loads a queue-backed object from an indexed snapshot (by `recordId`).
|
|
1115
|
+
* Returns `null` when the snapshot is missing or is not a JSON object.
|
|
1116
|
+
*/
|
|
1117
|
+
private _loadFromQueue(meta: ObjectMeta, snapshotMap: Map<number, unknown>): QueryItem | null {
|
|
1118
|
+
const snapshot = snapshotMap.get(meta.recordId);
|
|
1119
|
+
if (!snapshot || typeof snapshot !== 'object') {
|
|
607
1120
|
return null;
|
|
608
1121
|
}
|
|
609
1122
|
|
|
610
|
-
|
|
611
|
-
|
|
1123
|
+
return {
|
|
1124
|
+
objectId: meta.objectId as ObjectId,
|
|
1125
|
+
spaceId: meta.spaceId as SpaceId,
|
|
1126
|
+
queueId: meta.queueId as ObjectId,
|
|
1127
|
+
queueNamespace: 'data',
|
|
1128
|
+
documentId: null,
|
|
1129
|
+
doc: null,
|
|
1130
|
+
data: snapshot as Obj.JSON,
|
|
1131
|
+
rank: 1,
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* Loads a document-backed object from an Automerge `DatabaseDirectory`.
|
|
1137
|
+
* Returns `null` if the document can't be loaded, the inline object isn't present, or if the meta does not have a
|
|
1138
|
+
* document id (e.g. queue-backed objects).
|
|
1139
|
+
*/
|
|
1140
|
+
private async _loadFromAutomerge(meta: ObjectMeta): Promise<QueryItem | null> {
|
|
1141
|
+
if (!meta.documentId) {
|
|
612
1142
|
return null;
|
|
613
1143
|
}
|
|
614
|
-
|
|
615
|
-
|
|
1144
|
+
const handle = await this._automergeHost.loadDoc<DatabaseDirectory>(
|
|
1145
|
+
Context.default(),
|
|
1146
|
+
meta.documentId as DocumentId,
|
|
1147
|
+
{
|
|
1148
|
+
fetchFromNetwork: true,
|
|
1149
|
+
},
|
|
1150
|
+
);
|
|
1151
|
+
const doc = handle.doc();
|
|
1152
|
+
if (!doc) {
|
|
1153
|
+
return null;
|
|
1154
|
+
}
|
|
1155
|
+
const object = DatabaseDirectory.getInlineObject(doc, meta.objectId);
|
|
616
1156
|
if (!object) {
|
|
617
1157
|
return null;
|
|
618
1158
|
}
|
|
619
|
-
|
|
620
1159
|
return {
|
|
621
|
-
objectId,
|
|
622
|
-
documentId: documentId as DocumentId,
|
|
623
|
-
spaceId:
|
|
1160
|
+
objectId: meta.objectId,
|
|
1161
|
+
documentId: meta.documentId as DocumentId,
|
|
1162
|
+
spaceId: meta.spaceId as SpaceId,
|
|
1163
|
+
queueId: null,
|
|
1164
|
+
queueNamespace: null,
|
|
624
1165
|
doc: object,
|
|
1166
|
+
data: null,
|
|
1167
|
+
rank: 1,
|
|
625
1168
|
};
|
|
626
1169
|
}
|
|
627
1170
|
|
|
@@ -651,7 +1194,11 @@ export class QueryExecutor extends Resource {
|
|
|
651
1194
|
objectId: echoDxn.echoId,
|
|
652
1195
|
documentId: spaceRoot.documentId,
|
|
653
1196
|
spaceId,
|
|
1197
|
+
queueId: null,
|
|
1198
|
+
queueNamespace: null,
|
|
1199
|
+
data: null,
|
|
654
1200
|
doc: inlineObject,
|
|
1201
|
+
rank: 1,
|
|
655
1202
|
};
|
|
656
1203
|
}
|
|
657
1204
|
|
|
@@ -660,7 +1207,9 @@ export class QueryExecutor extends Resource {
|
|
|
660
1207
|
return null;
|
|
661
1208
|
}
|
|
662
1209
|
|
|
663
|
-
const handle = await this._automergeHost.loadDoc<DatabaseDirectory>(Context.default(), link as AutomergeUrl
|
|
1210
|
+
const handle = await this._automergeHost.loadDoc<DatabaseDirectory>(Context.default(), link as AutomergeUrl, {
|
|
1211
|
+
fetchFromNetwork: true,
|
|
1212
|
+
});
|
|
664
1213
|
const doc = handle.doc();
|
|
665
1214
|
if (!doc) {
|
|
666
1215
|
return null;
|
|
@@ -675,7 +1224,153 @@ export class QueryExecutor extends Resource {
|
|
|
675
1224
|
objectId: echoDxn.echoId,
|
|
676
1225
|
documentId: handle.documentId,
|
|
677
1226
|
spaceId,
|
|
1227
|
+
queueId: null,
|
|
1228
|
+
queueNamespace: null,
|
|
1229
|
+
data: null,
|
|
678
1230
|
doc: object,
|
|
1231
|
+
rank: 1,
|
|
679
1232
|
};
|
|
680
1233
|
}
|
|
1234
|
+
|
|
1235
|
+
private async _getTransitiveDeletionState(item: QueryItem, remainingDepth: number): Promise<boolean> {
|
|
1236
|
+
const strongDeps = [
|
|
1237
|
+
QueryItem.getParent(item),
|
|
1238
|
+
QueryItem.getRelationSource(item),
|
|
1239
|
+
QueryItem.getRelationTarget(item),
|
|
1240
|
+
].filter((x) => x !== undefined);
|
|
1241
|
+
|
|
1242
|
+
if (strongDeps.length === 0) {
|
|
1243
|
+
return false;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// TODO(dmaretskyi): This could be optimized to bail early if any of the dependencies are deleted.
|
|
1247
|
+
const strongDepStates = await Promise.all(
|
|
1248
|
+
strongDeps.map(async (dxn) => {
|
|
1249
|
+
const dep = await this._loadFromDXN(DXN.parse(dxn), { sourceSpaceId: item.spaceId });
|
|
1250
|
+
if (!dep) {
|
|
1251
|
+
return false;
|
|
1252
|
+
}
|
|
1253
|
+
if (QueryItem.isDeleted(dep)) {
|
|
1254
|
+
return true;
|
|
1255
|
+
}
|
|
1256
|
+
if (remainingDepth > 0) {
|
|
1257
|
+
return this._getTransitiveDeletionState(dep, remainingDepth - 1);
|
|
1258
|
+
}
|
|
1259
|
+
return false;
|
|
1260
|
+
}),
|
|
1261
|
+
);
|
|
1262
|
+
|
|
1263
|
+
return strongDepStates.some((x) => x);
|
|
1264
|
+
}
|
|
681
1265
|
}
|
|
1266
|
+
|
|
1267
|
+
const prettyFilter = (filter: QueryAST.Filter): string => {
|
|
1268
|
+
switch (filter.type) {
|
|
1269
|
+
case 'object': {
|
|
1270
|
+
const parts: string[] = [];
|
|
1271
|
+
if (filter.typename !== null) {
|
|
1272
|
+
parts.push(String(filter.typename));
|
|
1273
|
+
}
|
|
1274
|
+
if (filter.id !== undefined && filter.id.length > 0) {
|
|
1275
|
+
parts.push(`id: [${filter.id.join(', ')}]`);
|
|
1276
|
+
}
|
|
1277
|
+
const propEntries = Object.entries(filter.props);
|
|
1278
|
+
if (propEntries.length > 0) {
|
|
1279
|
+
const propsStr = propEntries.map(([k, v]) => `${k}: ${prettyFilter(v)}`).join(', ');
|
|
1280
|
+
parts.push(`{ ${propsStr} }`);
|
|
1281
|
+
}
|
|
1282
|
+
if (filter.foreignKeys !== undefined && filter.foreignKeys.length > 0) {
|
|
1283
|
+
parts.push(`foreignKeys: [${filter.foreignKeys.map((fk) => JSON.stringify(fk)).join(', ')}]`);
|
|
1284
|
+
}
|
|
1285
|
+
return parts.length > 0 ? `Filter.type(${parts.join(', ')})` : 'Filter.everything()';
|
|
1286
|
+
}
|
|
1287
|
+
case 'compare':
|
|
1288
|
+
return `Filter.${filter.operator}(${JSON.stringify(filter.value)})`;
|
|
1289
|
+
case 'in':
|
|
1290
|
+
return `Filter.in([${filter.values.map((v) => JSON.stringify(v)).join(', ')}])`;
|
|
1291
|
+
case 'contains':
|
|
1292
|
+
return `Filter.contains(${JSON.stringify(filter.value)})`;
|
|
1293
|
+
case 'tag':
|
|
1294
|
+
return `Filter.tag(${JSON.stringify(filter.tag)})`;
|
|
1295
|
+
case 'range':
|
|
1296
|
+
return `Filter.range(${JSON.stringify(filter.from)}, ${JSON.stringify(filter.to)})`;
|
|
1297
|
+
case 'text-search':
|
|
1298
|
+
return filter.searchKind
|
|
1299
|
+
? `Filter.textSearch(${JSON.stringify(filter.text)}, ${JSON.stringify(filter.searchKind)})`
|
|
1300
|
+
: `Filter.textSearch(${JSON.stringify(filter.text)})`;
|
|
1301
|
+
case 'not':
|
|
1302
|
+
return `Filter.not(${prettyFilter(filter.filter)})`;
|
|
1303
|
+
case 'and':
|
|
1304
|
+
return `Filter.and(${filter.filters.map(prettyFilter).join(', ')})`;
|
|
1305
|
+
case 'or':
|
|
1306
|
+
return `Filter.or(${filter.filters.map(prettyFilter).join(', ')})`;
|
|
1307
|
+
}
|
|
1308
|
+
};
|
|
1309
|
+
|
|
1310
|
+
const prettyQuery = (query: QueryAST.Query): string => {
|
|
1311
|
+
switch (query.type) {
|
|
1312
|
+
case 'select':
|
|
1313
|
+
return `Query.select(${prettyFilter(query.filter)})`;
|
|
1314
|
+
case 'filter':
|
|
1315
|
+
return `${prettyQuery(query.selection)}.select(${prettyFilter(query.filter)})`;
|
|
1316
|
+
case 'reference-traversal':
|
|
1317
|
+
return `${prettyQuery(query.anchor)}.reference(${JSON.stringify(query.property)})`;
|
|
1318
|
+
case 'incoming-references': {
|
|
1319
|
+
const args: string[] = [];
|
|
1320
|
+
if (query.typename !== null) {
|
|
1321
|
+
args.push(String(query.typename));
|
|
1322
|
+
}
|
|
1323
|
+
if (query.property !== null) {
|
|
1324
|
+
args.push(JSON.stringify(query.property));
|
|
1325
|
+
}
|
|
1326
|
+
return `${prettyQuery(query.anchor)}.referencedBy(${args.join(', ')})`;
|
|
1327
|
+
}
|
|
1328
|
+
case 'relation': {
|
|
1329
|
+
const method =
|
|
1330
|
+
query.direction === 'outgoing' ? 'sourceOf' : query.direction === 'incoming' ? 'targetOf' : 'relationOf';
|
|
1331
|
+
const filterStr = query.filter !== undefined ? prettyFilter(query.filter) : '';
|
|
1332
|
+
return `${prettyQuery(query.anchor)}.${method}(${filterStr})`;
|
|
1333
|
+
}
|
|
1334
|
+
case 'relation-traversal':
|
|
1335
|
+
return `${prettyQuery(query.anchor)}.${query.direction}()`;
|
|
1336
|
+
case 'hierarchy-traversal':
|
|
1337
|
+
return query.direction === 'to-parent'
|
|
1338
|
+
? `${prettyQuery(query.anchor)}.parent()`
|
|
1339
|
+
: `${prettyQuery(query.anchor)}.children()`;
|
|
1340
|
+
case 'union':
|
|
1341
|
+
return `Query.all(${query.queries.map(prettyQuery).join(', ')})`;
|
|
1342
|
+
case 'set-difference':
|
|
1343
|
+
return `Query.without(${prettyQuery(query.source)}, ${prettyQuery(query.exclude)})`;
|
|
1344
|
+
case 'order': {
|
|
1345
|
+
const orders = query.order.map((o) => {
|
|
1346
|
+
if (o.kind === 'natural') {
|
|
1347
|
+
return 'Order.natural()';
|
|
1348
|
+
} else if (o.kind === 'rank') {
|
|
1349
|
+
return `Order.rank(${JSON.stringify(o.direction)})`;
|
|
1350
|
+
} else {
|
|
1351
|
+
return `Order.property(${JSON.stringify(o.property)}, ${JSON.stringify(o.direction)})`;
|
|
1352
|
+
}
|
|
1353
|
+
});
|
|
1354
|
+
return `${prettyQuery(query.query)}.orderBy(${orders.join(', ')})`;
|
|
1355
|
+
}
|
|
1356
|
+
case 'options': {
|
|
1357
|
+
const opts = query.options;
|
|
1358
|
+
const parts: string[] = [];
|
|
1359
|
+
if (opts.spaceIds !== undefined) {
|
|
1360
|
+
parts.push(`spaceIds: [${opts.spaceIds.map((s) => JSON.stringify(s)).join(', ')}]`);
|
|
1361
|
+
}
|
|
1362
|
+
if (opts.queues !== undefined) {
|
|
1363
|
+
parts.push(`queues: [${opts.queues.map(String).join(', ')}]`);
|
|
1364
|
+
}
|
|
1365
|
+
if (opts.deleted !== undefined) {
|
|
1366
|
+
parts.push(`deleted: ${JSON.stringify(opts.deleted)}`);
|
|
1367
|
+
}
|
|
1368
|
+
if (opts.allQueuesFromSpaces !== undefined) {
|
|
1369
|
+
parts.push(`allQueuesFromSpaces: ${opts.allQueuesFromSpaces}`);
|
|
1370
|
+
}
|
|
1371
|
+
return `${prettyQuery(query.query)}.options({ ${parts.join(', ')} })`;
|
|
1372
|
+
}
|
|
1373
|
+
case 'limit':
|
|
1374
|
+
return `${prettyQuery(query.query)}.limit(${query.limit})`;
|
|
1375
|
+
}
|
|
1376
|
+
};
|