@dxos/echo-pipeline 0.8.4-main.ae835ea → 0.8.4-main.bc674ce
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/browser/{chunk-WJJXJTNS.mjs → chunk-FJPXA75J.mjs} +7 -12
- package/dist/lib/browser/chunk-FJPXA75J.mjs.map +7 -0
- package/dist/lib/browser/{chunk-FR6GJ4S4.mjs → chunk-WIQUYO7H.mjs} +39 -29
- package/dist/lib/browser/chunk-WIQUYO7H.mjs.map +7 -0
- package/dist/lib/browser/filter/index.mjs +1 -2
- package/dist/lib/browser/index.mjs +1933 -620
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/testing/index.mjs +28 -201
- package/dist/lib/browser/testing/index.mjs.map +4 -4
- package/dist/lib/node-esm/{chunk-JQCCVDLT.mjs → chunk-7PL6UK4B.mjs} +39 -29
- package/dist/lib/node-esm/chunk-7PL6UK4B.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-LEQ77KAT.mjs → chunk-S5KGVQFZ.mjs} +7 -12
- package/dist/lib/node-esm/chunk-S5KGVQFZ.mjs.map +7 -0
- package/dist/lib/node-esm/filter/index.mjs +1 -2
- package/dist/lib/node-esm/index.mjs +1933 -620
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/testing/index.mjs +28 -201
- package/dist/lib/node-esm/testing/index.mjs.map +4 -4
- package/dist/types/src/automerge/automerge-host.d.ts +36 -9
- 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 +3 -2
- package/dist/types/src/automerge/collection-synchronizer.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-network-adapter.d.ts +11 -6
- package/dist/types/src/automerge/echo-network-adapter.d.ts.map +1 -1
- package/dist/types/src/automerge/echo-replicator.d.ts +9 -6
- 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/leveldb-storage-adapter.d.ts +4 -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 +7 -7
- package/dist/types/src/automerge/mesh-echo-replicator-connection.d.ts.map +1 -1
- package/dist/types/src/automerge/mesh-echo-replicator.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 +4 -3
- package/dist/types/src/db-host/data-service.d.ts.map +1 -1
- package/dist/types/src/db-host/documents-iterator.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 +43 -11
- 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 +15 -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 +7 -2
- 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 +1 -0
- 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 +2 -2
- package/dist/types/src/edge/echo-edge-replicator.d.ts.map +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/pipeline.d.ts +2 -2
- package/dist/types/src/pipeline/pipeline.d.ts.map +1 -1
- package/dist/types/src/query/errors.d.ts +11 -15
- package/dist/types/src/query/errors.d.ts.map +1 -1
- package/dist/types/src/query/plan.d.ts +37 -3
- package/dist/types/src/query/plan.d.ts.map +1 -1
- package/dist/types/src/query/query-executor.d.ts +39 -3
- package/dist/types/src/query/query-executor.d.ts.map +1 -1
- package/dist/types/src/query/query-planner.d.ts +6 -0
- package/dist/types/src/query/query-planner.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 +2 -2
- package/dist/types/src/space/control-pipeline.d.ts.map +1 -1
- package/dist/types/src/space/space-manager.d.ts +6 -6
- package/dist/types/src/space/space-manager.d.ts.map +1 -1
- package/dist/types/src/space/space-protocol.d.ts +5 -5
- package/dist/types/src/space/space-protocol.d.ts.map +1 -1
- package/dist/types/src/space/space.d.ts +3 -3
- 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-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 +6 -6
- 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 +48 -38
- package/src/automerge/automerge-host.test.ts +37 -14
- package/src/automerge/automerge-host.ts +130 -36
- package/src/automerge/automerge-repo.test.ts +147 -30
- package/src/automerge/collection-synchronizer.test.ts +16 -10
- package/src/automerge/collection-synchronizer.ts +41 -11
- package/src/automerge/echo-network-adapter.ts +23 -11
- package/src/automerge/echo-replicator.ts +10 -6
- package/src/automerge/heads-store.ts +14 -2
- package/src/automerge/leveldb-storage-adapter.ts +4 -4
- package/src/automerge/mesh-echo-replicator-connection.ts +7 -7
- package/src/automerge/mesh-echo-replicator.ts +2 -2
- package/src/db-host/automerge-data-source.test.ts +294 -0
- package/src/db-host/automerge-data-source.ts +144 -0
- package/src/db-host/data-service.ts +10 -3
- package/src/db-host/documents-iterator.ts +3 -1
- package/src/db-host/documents-synchronizer.test.ts +77 -13
- package/src/db-host/documents-synchronizer.ts +51 -50
- package/src/db-host/echo-host.ts +191 -28
- package/src/db-host/index.ts +1 -0
- package/src/db-host/local-queue-service.ts +127 -0
- package/src/db-host/query-service.ts +17 -4
- package/src/db-host/queue-data-source.ts +132 -0
- package/src/db-host/queue-service.test.ts +141 -0
- package/src/db-host/space-state-manager.ts +4 -0
- package/src/db-host/stub.ts +28 -0
- package/src/edge/echo-edge-replicator.ts +12 -8
- package/src/filter/filter-match.test.ts +31 -25
- package/src/filter/filter-match.ts +8 -14
- package/src/metadata/metadata-store.ts +5 -2
- package/src/pipeline/pipeline.ts +2 -2
- package/src/query/errors.ts +2 -2
- package/src/query/plan.ts +48 -3
- package/src/query/query-executor.ts +612 -131
- package/src/query/query-planner.test.ts +509 -35
- package/src/query/query-planner.ts +187 -13
- package/src/space/admission-discovery-extension.ts +1 -1
- package/src/space/auth.ts +6 -6
- package/src/space/control-pipeline.ts +2 -2
- package/src/space/space-manager.ts +6 -6
- package/src/space/space-protocol.ts +11 -11
- package/src/space/space.ts +3 -3
- package/src/testing/index.ts +0 -2
- package/src/testing/test-network-adapter.ts +16 -3
- package/src/testing/test-replicator.ts +8 -7
- 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/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.map +0 -7
- package/dist/lib/node-esm/chunk-LEQ77KAT.mjs.map +0 -7
- 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/testing/test-data.ts +0 -127
- package/src/testing/test-schema.ts +0 -53
|
@@ -3,11 +3,23 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import type { AutomergeUrl, DocumentId } from '@automerge/automerge-repo';
|
|
6
|
+
import type * as SqlClient from '@effect/sql/SqlClient';
|
|
7
|
+
import type * as Effect from 'effect/Effect';
|
|
6
8
|
import * as Match from 'effect/Match';
|
|
7
|
-
import * as
|
|
9
|
+
import * as Runtime from 'effect/Runtime';
|
|
8
10
|
|
|
9
11
|
import { Context, ContextDisposedError, LifecycleState, Resource } from '@dxos/context';
|
|
10
|
-
import
|
|
12
|
+
import type { Obj } from '@dxos/echo';
|
|
13
|
+
import { ATTR_RELATION_SOURCE, ATTR_RELATION_TARGET } from '@dxos/echo/internal';
|
|
14
|
+
import {
|
|
15
|
+
DatabaseDirectory,
|
|
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 { type IndexEngine, type ObjectMeta, type ReverseRef } from '@dxos/index-core';
|
|
11
23
|
import { EscapedPropPath, type FindResult, type Indexer } from '@dxos/indexing';
|
|
12
24
|
import { invariant } from '@dxos/invariant';
|
|
13
25
|
import { DXN, type ObjectId, PublicKey, type SpaceId } from '@dxos/keys';
|
|
@@ -19,15 +31,15 @@ import { getDeep, isNonNullable } from '@dxos/util';
|
|
|
19
31
|
import type { AutomergeHost } from '../automerge';
|
|
20
32
|
import { createIdFromSpaceKey } from '../common';
|
|
21
33
|
import type { SpaceStateManager } from '../db-host';
|
|
22
|
-
import { filterMatchObject } from '../filter';
|
|
34
|
+
import { filterMatchObject, filterMatchObjectJSON } from '../filter';
|
|
23
35
|
|
|
24
36
|
import type { QueryPlan } from './plan';
|
|
25
37
|
import { QueryPlanner } from './query-planner';
|
|
26
38
|
|
|
27
|
-
const isNullable: Predicate.Refinement<unknown, null | undefined> = Predicate.isNullable;
|
|
28
|
-
|
|
29
39
|
type QueryExecutorOptions = {
|
|
30
40
|
indexer: Indexer;
|
|
41
|
+
indexer2?: IndexEngine;
|
|
42
|
+
runtime?: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient>;
|
|
31
43
|
automergeHost: AutomergeHost;
|
|
32
44
|
spaceStateManager: SpaceStateManager;
|
|
33
45
|
|
|
@@ -48,11 +60,70 @@ type QueryExecutionResult = {
|
|
|
48
60
|
*/
|
|
49
61
|
type QueryItem = {
|
|
50
62
|
objectId: ObjectId;
|
|
51
|
-
|
|
63
|
+
|
|
52
64
|
spaceId: SpaceId;
|
|
53
|
-
|
|
65
|
+
queueId: ObjectId | null;
|
|
66
|
+
queueNamespace: string | null;
|
|
67
|
+
|
|
68
|
+
// For objects from automerge documents.
|
|
69
|
+
documentId: DocumentId | null;
|
|
70
|
+
|
|
71
|
+
// For objects from automerge documents.
|
|
72
|
+
doc: ObjectStructure | null;
|
|
73
|
+
|
|
74
|
+
// For objects from queues.
|
|
75
|
+
data: Obj.JSON | null;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Relevance rank for this item.
|
|
79
|
+
* Higher values indicate better matches for FTS/vector searches.
|
|
80
|
+
* Defaults to 1 for non-ranked queries (predicate matches).
|
|
81
|
+
*/
|
|
82
|
+
rank: number;
|
|
54
83
|
};
|
|
55
84
|
|
|
85
|
+
const QueryItem = Object.freeze({
|
|
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
|
+
getRelationSource: (item: QueryItem): DXN.String => {
|
|
107
|
+
if (item.doc) {
|
|
108
|
+
return ObjectStructure.getRelationSource(item.doc)?.['/'] as DXN.String;
|
|
109
|
+
} else if (item.data) {
|
|
110
|
+
return item.data[ATTR_RELATION_SOURCE] as DXN.String;
|
|
111
|
+
} else {
|
|
112
|
+
throw new Error('Invalid query item');
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
getRelationTarget: (item: QueryItem): DXN.String => {
|
|
117
|
+
if (item.doc) {
|
|
118
|
+
return ObjectStructure.getRelationTarget(item.doc)?.['/'] as DXN.String;
|
|
119
|
+
} else if (item.data) {
|
|
120
|
+
return item.data[ATTR_RELATION_TARGET] as DXN.String;
|
|
121
|
+
} else {
|
|
122
|
+
throw new Error('Invalid query item');
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
56
127
|
/**
|
|
57
128
|
* Recursive data structure that represents the execution trace of a query.
|
|
58
129
|
*/
|
|
@@ -149,6 +220,8 @@ const TRACE_QUERY_EXECUTION = false;
|
|
|
149
220
|
*/
|
|
150
221
|
export class QueryExecutor extends Resource {
|
|
151
222
|
private readonly _indexer: Indexer;
|
|
223
|
+
private readonly _indexer2?: IndexEngine;
|
|
224
|
+
private readonly _runtime?: RuntimeProvider.RuntimeProvider<SqlClient.SqlClient>;
|
|
152
225
|
private readonly _automergeHost: AutomergeHost;
|
|
153
226
|
private readonly _spaceStateManager: SpaceStateManager;
|
|
154
227
|
/**
|
|
@@ -167,6 +240,8 @@ export class QueryExecutor extends Resource {
|
|
|
167
240
|
super();
|
|
168
241
|
|
|
169
242
|
this._indexer = options.indexer;
|
|
243
|
+
this._indexer2 = options.indexer2;
|
|
244
|
+
this._runtime = options.runtime;
|
|
170
245
|
this._automergeHost = options.automergeHost;
|
|
171
246
|
this._spaceStateManager = options.spaceStateManager;
|
|
172
247
|
|
|
@@ -190,19 +265,18 @@ export class QueryExecutor extends Resource {
|
|
|
190
265
|
return this._trace;
|
|
191
266
|
}
|
|
192
267
|
|
|
193
|
-
protected override async _open(ctx: Context): Promise<void> {}
|
|
194
|
-
|
|
195
|
-
protected override async _close(ctx: Context): Promise<void> {}
|
|
196
|
-
|
|
197
268
|
getResults(): QueryResult[] {
|
|
198
269
|
return this._lastResultSet.map(
|
|
199
270
|
(item): QueryResult => ({
|
|
200
271
|
id: item.objectId,
|
|
201
|
-
documentId: item.documentId,
|
|
272
|
+
documentId: item.documentId ?? undefined,
|
|
273
|
+
queueId: item.queueId ?? undefined,
|
|
274
|
+
queueNamespace: item.queueNamespace ?? undefined,
|
|
202
275
|
spaceId: item.spaceId,
|
|
203
276
|
|
|
204
|
-
|
|
205
|
-
|
|
277
|
+
rank: item.rank,
|
|
278
|
+
|
|
279
|
+
documentJson: item.doc ? JSON.stringify(item.doc) : item.data ? JSON.stringify(item.data) : undefined,
|
|
206
280
|
}),
|
|
207
281
|
);
|
|
208
282
|
}
|
|
@@ -230,6 +304,7 @@ export class QueryExecutor extends Resource {
|
|
|
230
304
|
// ExecutionTrace.putOnPerformanceTimeline(trace);
|
|
231
305
|
|
|
232
306
|
if (TRACE_QUERY_EXECUTION) {
|
|
307
|
+
// eslint-disable-next-line no-console
|
|
233
308
|
console.log(ExecutionTrace.format(trace));
|
|
234
309
|
}
|
|
235
310
|
|
|
@@ -240,7 +315,6 @@ export class QueryExecutor extends Resource {
|
|
|
240
315
|
|
|
241
316
|
private async _execPlan(plan: QueryPlan.Plan, workingSet: QueryItem[]): Promise<StepExecutionResult> {
|
|
242
317
|
const trace = ExecutionTrace.makeEmpty();
|
|
243
|
-
const begin = performance.now();
|
|
244
318
|
for (const step of plan.steps) {
|
|
245
319
|
if (this._ctx.disposed) {
|
|
246
320
|
throw new ContextDisposedError();
|
|
@@ -288,6 +362,9 @@ export class QueryExecutor extends Resource {
|
|
|
288
362
|
case 'OrderStep':
|
|
289
363
|
({ workingSet: newWorkingSet, trace } = await this._execOrderStep(step, workingSet));
|
|
290
364
|
break;
|
|
365
|
+
case 'LimitStep':
|
|
366
|
+
({ workingSet: newWorkingSet, trace } = await this._execLimitStep(step, workingSet));
|
|
367
|
+
break;
|
|
291
368
|
default:
|
|
292
369
|
throw new Error(`Unknown step type: ${(step as any)._tag}`);
|
|
293
370
|
}
|
|
@@ -309,24 +386,42 @@ export class QueryExecutor extends Resource {
|
|
|
309
386
|
|
|
310
387
|
switch (step.selector._tag) {
|
|
311
388
|
case 'WildcardSelector': {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
389
|
+
if (this._supportsSqlIndexing()) {
|
|
390
|
+
const beginIndexQuery = performance.now();
|
|
391
|
+
const metas = await this._queryAllFromSqlIndex(step.spaces);
|
|
392
|
+
trace.indexHits = metas.length;
|
|
393
|
+
trace.indexQueryTime += performance.now() - beginIndexQuery;
|
|
394
|
+
|
|
395
|
+
if (this._ctx.disposed) {
|
|
396
|
+
return { workingSet, trace };
|
|
397
|
+
}
|
|
319
398
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
399
|
+
const documentLoadStart = performance.now();
|
|
400
|
+
const results = await this._loadDocumentsAfterSqlQuery(metas);
|
|
401
|
+
trace.documentsLoaded += results.length;
|
|
402
|
+
trace.documentLoadTime += performance.now() - documentLoadStart;
|
|
403
|
+
|
|
404
|
+
workingSet.push(...results.filter(isNonNullable));
|
|
405
|
+
} else {
|
|
406
|
+
const beginIndexQuery = performance.now();
|
|
407
|
+
const indexHits = await this._indexer.execQuery({
|
|
408
|
+
typenames: [],
|
|
409
|
+
inverted: false,
|
|
410
|
+
});
|
|
411
|
+
trace.indexHits = +indexHits.length;
|
|
412
|
+
trace.indexQueryTime += performance.now() - beginIndexQuery;
|
|
413
|
+
|
|
414
|
+
if (this._ctx.disposed) {
|
|
415
|
+
return { workingSet, trace };
|
|
416
|
+
}
|
|
323
417
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
418
|
+
const documentLoadStart = performance.now();
|
|
419
|
+
const results = await this._loadDocumentsAfterIndexQuery(indexHits);
|
|
420
|
+
trace.documentsLoaded += results.length;
|
|
421
|
+
trace.documentLoadTime += performance.now() - documentLoadStart;
|
|
328
422
|
|
|
329
|
-
|
|
423
|
+
workingSet.push(...results.filter(isNonNullable).filter((item) => step.spaces.includes(item.spaceId)));
|
|
424
|
+
}
|
|
330
425
|
trace.objectCount = workingSet.length;
|
|
331
426
|
|
|
332
427
|
break;
|
|
@@ -347,57 +442,161 @@ export class QueryExecutor extends Resource {
|
|
|
347
442
|
}
|
|
348
443
|
|
|
349
444
|
case 'TypeSelector': {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
445
|
+
if (this._supportsSqlIndexing()) {
|
|
446
|
+
const beginIndexQuery = performance.now();
|
|
447
|
+
const metas = await this._queryTypesFromSqlIndex(step.spaces, step.selector.typename, step.selector.inverted);
|
|
448
|
+
trace.indexHits = metas.length;
|
|
449
|
+
trace.indexQueryTime += performance.now() - beginIndexQuery;
|
|
450
|
+
|
|
451
|
+
if (this._ctx.disposed) {
|
|
452
|
+
return { workingSet, trace };
|
|
453
|
+
}
|
|
357
454
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
455
|
+
const documentLoadStart = performance.now();
|
|
456
|
+
const results = await this._loadDocumentsAfterSqlQuery(metas);
|
|
457
|
+
trace.documentsLoaded += results.length;
|
|
458
|
+
trace.documentLoadTime += performance.now() - documentLoadStart;
|
|
459
|
+
|
|
460
|
+
workingSet.push(...results.filter(isNonNullable));
|
|
461
|
+
} else {
|
|
462
|
+
const beginIndexQuery = performance.now();
|
|
463
|
+
const indexHits = await this._indexer.execQuery({
|
|
464
|
+
typenames: step.selector.typename,
|
|
465
|
+
inverted: step.selector.inverted,
|
|
466
|
+
});
|
|
467
|
+
trace.indexHits = +indexHits.length;
|
|
468
|
+
trace.indexQueryTime += performance.now() - beginIndexQuery;
|
|
469
|
+
|
|
470
|
+
if (this._ctx.disposed) {
|
|
471
|
+
return { workingSet, trace };
|
|
472
|
+
}
|
|
361
473
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
474
|
+
const documentLoadStart = performance.now();
|
|
475
|
+
const results = await this._loadDocumentsAfterIndexQuery(indexHits);
|
|
476
|
+
trace.documentsLoaded += results.length;
|
|
477
|
+
trace.documentLoadTime += performance.now() - documentLoadStart;
|
|
366
478
|
|
|
367
|
-
|
|
479
|
+
workingSet.push(...results.filter(isNonNullable).filter((item) => step.spaces.includes(item.spaceId)));
|
|
480
|
+
}
|
|
368
481
|
trace.objectCount = workingSet.length;
|
|
369
482
|
|
|
370
483
|
break;
|
|
371
484
|
}
|
|
372
485
|
|
|
373
486
|
case 'TextSelector': {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
text
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
487
|
+
// TODO(dmaretskyi): type + FTS queries would be very common so we should support those, maybe chunk the fts index.
|
|
488
|
+
// TODO(dmaretskyi): nice to have matched text snippets/highlighting.
|
|
489
|
+
if (step.selector.searchKind === 'full-text' && this._supportsSqlIndexing()) {
|
|
490
|
+
// Use indexer2 for full-text search.
|
|
491
|
+
const beginIndexQuery = performance.now();
|
|
492
|
+
invariant(this._indexer2, 'SQL indexer is required.');
|
|
493
|
+
invariant(step.spaces.length <= 1, 'Multiple spaces are not supported for full-text search');
|
|
494
|
+
// Extract queue IDs from DXN strings.
|
|
495
|
+
const queueIds =
|
|
496
|
+
step.queues.length > 0
|
|
497
|
+
? (step.queues.map((dxnStr) => DXN.parse(dxnStr).asQueueDXN()?.queueId).filter(Boolean) as ObjectId[])
|
|
498
|
+
: null;
|
|
499
|
+
const textResults = await this._runInRuntime(
|
|
500
|
+
this._indexer2.queryText({
|
|
501
|
+
query: step.selector.text,
|
|
502
|
+
spaceId: step.spaces,
|
|
503
|
+
includeAllQueues: step.allQueuesFromSpaces,
|
|
504
|
+
queueIds,
|
|
505
|
+
}),
|
|
506
|
+
);
|
|
507
|
+
trace.indexHits = textResults.length;
|
|
508
|
+
trace.indexQueryTime += performance.now() - beginIndexQuery;
|
|
509
|
+
|
|
510
|
+
if (this._ctx.disposed) {
|
|
511
|
+
return { workingSet, trace };
|
|
512
|
+
}
|
|
389
513
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
}
|
|
514
|
+
// Load documents from the results.
|
|
515
|
+
const documentLoadStart = performance.now();
|
|
393
516
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
trace.documentLoadTime += performance.now() - documentLoadStart;
|
|
517
|
+
// Separate queue items from space items.
|
|
518
|
+
const queueResults = textResults.filter((r) => r.queueId);
|
|
519
|
+
const spaceResults = textResults.filter((r) => !r.queueId);
|
|
398
520
|
|
|
399
|
-
|
|
400
|
-
|
|
521
|
+
// Build a map from recordId to rank for all FTS results.
|
|
522
|
+
const rankMap = new Map(textResults.map((r) => [r.recordId, r.rank]));
|
|
523
|
+
|
|
524
|
+
// Load queue items from indexed snapshots.
|
|
525
|
+
let queueItems: QueryItem[] = [];
|
|
526
|
+
if (queueResults.length > 0) {
|
|
527
|
+
const snapshots = await this._runInRuntime(
|
|
528
|
+
this._indexer2.querySnapshotsJSON(queueResults.map((r) => r.recordId)),
|
|
529
|
+
);
|
|
530
|
+
const snapshotMap = new Map(snapshots.map((s) => [s.recordId, s.snapshot]));
|
|
531
|
+
queueItems = queueResults
|
|
532
|
+
.map((result): QueryItem | null => {
|
|
533
|
+
const snapshot = snapshotMap.get(result.recordId);
|
|
534
|
+
if (!snapshot || typeof snapshot !== 'object') {
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
return {
|
|
538
|
+
objectId: result.objectId as ObjectId,
|
|
539
|
+
spaceId: result.spaceId as SpaceId,
|
|
540
|
+
queueId: result.queueId as ObjectId,
|
|
541
|
+
queueNamespace: 'data',
|
|
542
|
+
documentId: null,
|
|
543
|
+
doc: null,
|
|
544
|
+
data: snapshot as Obj.JSON,
|
|
545
|
+
rank: rankMap.get(result.recordId) ?? 1,
|
|
546
|
+
};
|
|
547
|
+
})
|
|
548
|
+
.filter(isNonNullable);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Load space items from documents.
|
|
552
|
+
const spaceItems = await Promise.all(
|
|
553
|
+
spaceResults.map(async (result): Promise<QueryItem | null> => {
|
|
554
|
+
const dxn = DXN.fromLocalObjectId(result.objectId);
|
|
555
|
+
const item = await this._loadFromDXN(dxn, { sourceSpaceId: result.spaceId as SpaceId });
|
|
556
|
+
if (item) {
|
|
557
|
+
// Override the default rank with the FTS rank.
|
|
558
|
+
item.rank = rankMap.get(result.recordId) ?? 1;
|
|
559
|
+
}
|
|
560
|
+
return item;
|
|
561
|
+
}),
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
const items = [...queueItems, ...spaceItems.filter(isNonNullable)];
|
|
565
|
+
trace.documentsLoaded += items.length;
|
|
566
|
+
trace.documentLoadTime += performance.now() - documentLoadStart;
|
|
567
|
+
|
|
568
|
+
workingSet.push(...items.filter((item) => step.spaces.includes(item.spaceId)));
|
|
569
|
+
trace.objectCount = workingSet.length;
|
|
570
|
+
} else {
|
|
571
|
+
// Fall back to old indexer for vector search.
|
|
572
|
+
const beginIndexQuery = performance.now();
|
|
573
|
+
const indexHits = await this._indexer.execQuery({
|
|
574
|
+
typenames: [],
|
|
575
|
+
text: {
|
|
576
|
+
query: step.selector.text,
|
|
577
|
+
kind: Match.type<QueryPlan.TextSearchKind>().pipe(
|
|
578
|
+
Match.withReturnType<'text' | 'vector'>(),
|
|
579
|
+
Match.when('full-text', () => 'text'),
|
|
580
|
+
Match.when('vector', () => 'vector'),
|
|
581
|
+
Match.orElseAbsurd,
|
|
582
|
+
)(step.selector.searchKind),
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
trace.indexHits = +indexHits.length;
|
|
586
|
+
trace.indexQueryTime += performance.now() - beginIndexQuery;
|
|
587
|
+
|
|
588
|
+
if (this._ctx.disposed) {
|
|
589
|
+
return { workingSet, trace };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const documentLoadStart = performance.now();
|
|
593
|
+
const results = await this._loadDocumentsAfterIndexQuery(indexHits);
|
|
594
|
+
trace.documentsLoaded += results.length;
|
|
595
|
+
trace.documentLoadTime += performance.now() - documentLoadStart;
|
|
596
|
+
|
|
597
|
+
workingSet.push(...results.filter(isNonNullable).filter((item) => step.spaces.includes(item.spaceId)));
|
|
598
|
+
trace.objectCount = workingSet.length;
|
|
599
|
+
}
|
|
401
600
|
break;
|
|
402
601
|
}
|
|
403
602
|
|
|
@@ -405,17 +604,29 @@ export class QueryExecutor extends Resource {
|
|
|
405
604
|
throw new Error(`Unknown selector type: ${(step.selector as any)._tag}`);
|
|
406
605
|
}
|
|
407
606
|
|
|
607
|
+
// Apply limit if specified on the select step.
|
|
608
|
+
if (step.limit !== undefined && workingSet.length > step.limit) {
|
|
609
|
+
workingSet = workingSet.slice(0, step.limit);
|
|
610
|
+
trace.objectCount = workingSet.length;
|
|
611
|
+
}
|
|
612
|
+
|
|
408
613
|
return { workingSet, trace };
|
|
409
614
|
}
|
|
410
615
|
|
|
411
616
|
private async _execFilterStep(step: QueryPlan.FilterStep, workingSet: QueryItem[]): Promise<StepExecutionResult> {
|
|
412
|
-
const result = workingSet.filter((item) =>
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
617
|
+
const result = workingSet.filter((item) => {
|
|
618
|
+
if (item.doc) {
|
|
619
|
+
return filterMatchObject(step.filter, {
|
|
620
|
+
id: item.objectId,
|
|
621
|
+
spaceId: item.spaceId,
|
|
622
|
+
doc: item.doc,
|
|
623
|
+
});
|
|
624
|
+
} else if (item.data) {
|
|
625
|
+
return filterMatchObjectJSON(step.filter, item.data);
|
|
626
|
+
} else {
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
});
|
|
419
630
|
|
|
420
631
|
return {
|
|
421
632
|
workingSet: result,
|
|
@@ -433,7 +644,8 @@ export class QueryExecutor extends Resource {
|
|
|
433
644
|
workingSet: QueryItem[],
|
|
434
645
|
): Promise<StepExecutionResult> {
|
|
435
646
|
const expected = step.mode === 'only-deleted';
|
|
436
|
-
|
|
647
|
+
// 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.
|
|
648
|
+
const result = workingSet.filter((item) => QueryItem.isDeleted(item) === expected);
|
|
437
649
|
return {
|
|
438
650
|
workingSet: result,
|
|
439
651
|
trace: {
|
|
@@ -459,11 +671,12 @@ export class QueryExecutor extends Resource {
|
|
|
459
671
|
case 'ReferenceTraversal': {
|
|
460
672
|
switch (step.traversal.direction) {
|
|
461
673
|
case 'outgoing': {
|
|
674
|
+
invariant(step.traversal.property !== null, 'Outgoing reference traversal requires a property');
|
|
462
675
|
const property = EscapedPropPath.unescape(step.traversal.property);
|
|
463
676
|
|
|
464
677
|
const refs = workingSet
|
|
465
678
|
.flatMap((item) => {
|
|
466
|
-
const ref =
|
|
679
|
+
const ref = QueryItem.getProperty(item, property);
|
|
467
680
|
const refs = Array.isArray(ref) ? ref : [ref];
|
|
468
681
|
return refs.map((ref) => {
|
|
469
682
|
try {
|
|
@@ -493,24 +706,39 @@ export class QueryExecutor extends Resource {
|
|
|
493
706
|
break;
|
|
494
707
|
}
|
|
495
708
|
case 'incoming': {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
709
|
+
if (this._supportsSqlIndexing()) {
|
|
710
|
+
const beginIndexQuery = performance.now();
|
|
711
|
+
const metas = await this._queryIncomingReferencesFromSqlIndex(workingSet, step.traversal.property);
|
|
712
|
+
trace.indexHits += metas.length;
|
|
713
|
+
trace.indexQueryTime += performance.now() - beginIndexQuery;
|
|
714
|
+
|
|
715
|
+
const documentLoadStart = performance.now();
|
|
716
|
+
const results = await this._loadDocumentsAfterSqlQuery(metas);
|
|
717
|
+
trace.documentsLoaded += results.length;
|
|
718
|
+
trace.documentLoadTime += performance.now() - documentLoadStart;
|
|
719
|
+
|
|
720
|
+
newWorkingSet.push(...results.filter(isNonNullable));
|
|
721
|
+
trace.objectCount = newWorkingSet.length;
|
|
722
|
+
} else {
|
|
723
|
+
const indexHits = await this._indexer.execQuery({
|
|
724
|
+
typenames: [],
|
|
725
|
+
inverted: false,
|
|
726
|
+
graph: {
|
|
727
|
+
kind: 'inbound-reference',
|
|
728
|
+
property: step.traversal.property,
|
|
729
|
+
anchors: workingSet.map((item) => item.objectId),
|
|
730
|
+
},
|
|
731
|
+
});
|
|
732
|
+
trace.indexHits += indexHits.length;
|
|
733
|
+
|
|
734
|
+
const documentLoadStart = performance.now();
|
|
735
|
+
const results = await this._loadDocumentsAfterIndexQuery(indexHits);
|
|
736
|
+
trace.documentsLoaded += results.length;
|
|
737
|
+
trace.documentLoadTime += performance.now() - documentLoadStart;
|
|
738
|
+
|
|
739
|
+
newWorkingSet.push(...results.filter(isNonNullable));
|
|
740
|
+
trace.objectCount = newWorkingSet.length;
|
|
741
|
+
}
|
|
514
742
|
|
|
515
743
|
break;
|
|
516
744
|
}
|
|
@@ -523,21 +751,17 @@ export class QueryExecutor extends Resource {
|
|
|
523
751
|
case 'relation-to-target': {
|
|
524
752
|
const refs = workingSet
|
|
525
753
|
.map((item) => {
|
|
526
|
-
const
|
|
754
|
+
const dxn =
|
|
527
755
|
step.traversal.direction === 'relation-to-source'
|
|
528
|
-
?
|
|
529
|
-
:
|
|
530
|
-
|
|
531
|
-
if (!isEncodedReference(ref)) {
|
|
532
|
-
return null;
|
|
533
|
-
}
|
|
756
|
+
? QueryItem.getRelationSource(item)
|
|
757
|
+
: QueryItem.getRelationTarget(item);
|
|
534
758
|
try {
|
|
535
759
|
return {
|
|
536
|
-
ref: DXN.parse(
|
|
760
|
+
ref: DXN.parse(dxn),
|
|
537
761
|
spaceId: item.spaceId,
|
|
538
762
|
};
|
|
539
763
|
} catch {
|
|
540
|
-
log.warn('invalid reference', { ref:
|
|
764
|
+
log.warn('invalid reference', { ref: dxn });
|
|
541
765
|
return null;
|
|
542
766
|
}
|
|
543
767
|
})
|
|
@@ -557,25 +781,43 @@ export class QueryExecutor extends Resource {
|
|
|
557
781
|
|
|
558
782
|
case 'source-to-relation':
|
|
559
783
|
case 'target-to-relation': {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
784
|
+
if (this._supportsSqlIndexing()) {
|
|
785
|
+
const beginIndexQuery = performance.now();
|
|
786
|
+
const metas = await this._queryRelationsFromSqlIndex(
|
|
787
|
+
workingSet,
|
|
788
|
+
step.traversal.direction === 'source-to-relation' ? 'source' : 'target',
|
|
789
|
+
);
|
|
790
|
+
trace.indexHits += metas.length;
|
|
791
|
+
trace.indexQueryTime += performance.now() - beginIndexQuery;
|
|
792
|
+
|
|
793
|
+
const documentLoadStart = performance.now();
|
|
794
|
+
const results = await this._loadDocumentsAfterSqlQuery(metas);
|
|
795
|
+
trace.documentsLoaded += results.length;
|
|
796
|
+
trace.documentLoadTime += performance.now() - documentLoadStart;
|
|
797
|
+
|
|
798
|
+
newWorkingSet.push(...results.filter(isNonNullable));
|
|
799
|
+
trace.objectCount = newWorkingSet.length;
|
|
800
|
+
} else {
|
|
801
|
+
const indexHits = await this._indexer.execQuery({
|
|
802
|
+
typenames: [],
|
|
803
|
+
inverted: false,
|
|
804
|
+
graph: {
|
|
805
|
+
kind: step.traversal.direction === 'source-to-relation' ? 'relation-source' : 'relation-target',
|
|
806
|
+
anchors: workingSet.map((item) => item.objectId),
|
|
807
|
+
property: null,
|
|
808
|
+
},
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
trace.indexHits += indexHits.length;
|
|
812
|
+
|
|
813
|
+
const documentLoadStart = performance.now();
|
|
814
|
+
const results = await this._loadDocumentsAfterIndexQuery(indexHits);
|
|
815
|
+
trace.documentsLoaded += results.length;
|
|
816
|
+
trace.documentLoadTime += performance.now() - documentLoadStart;
|
|
817
|
+
|
|
818
|
+
newWorkingSet.push(...results.filter(isNonNullable));
|
|
819
|
+
trace.objectCount = newWorkingSet.length;
|
|
820
|
+
}
|
|
579
821
|
|
|
580
822
|
break;
|
|
581
823
|
}
|
|
@@ -637,19 +879,38 @@ export class QueryExecutor extends Resource {
|
|
|
637
879
|
}
|
|
638
880
|
|
|
639
881
|
private async _execOrderStep(step: QueryPlan.OrderStep, workingSet: QueryItem[]): Promise<StepExecutionResult> {
|
|
640
|
-
|
|
882
|
+
let sortedWorkingSet = [...workingSet].sort((a, b) => this._compareMultiOrder(a, b, step.order));
|
|
883
|
+
|
|
884
|
+
// Apply limit if specified on the order step.
|
|
885
|
+
if (step.limit !== undefined && sortedWorkingSet.length > step.limit) {
|
|
886
|
+
sortedWorkingSet = sortedWorkingSet.slice(0, step.limit);
|
|
887
|
+
}
|
|
641
888
|
|
|
642
889
|
return {
|
|
643
890
|
workingSet: sortedWorkingSet,
|
|
644
891
|
trace: {
|
|
645
892
|
...ExecutionTrace.makeEmpty(),
|
|
646
893
|
name: 'Order',
|
|
647
|
-
details: JSON.stringify(step.order),
|
|
894
|
+
details: JSON.stringify({ order: step.order, limit: step.limit }),
|
|
648
895
|
objectCount: sortedWorkingSet.length,
|
|
649
896
|
},
|
|
650
897
|
};
|
|
651
898
|
}
|
|
652
899
|
|
|
900
|
+
private async _execLimitStep(step: QueryPlan.LimitStep, workingSet: QueryItem[]): Promise<StepExecutionResult> {
|
|
901
|
+
const limitedWorkingSet = workingSet.slice(0, step.limit);
|
|
902
|
+
|
|
903
|
+
return {
|
|
904
|
+
workingSet: limitedWorkingSet,
|
|
905
|
+
trace: {
|
|
906
|
+
...ExecutionTrace.makeEmpty(),
|
|
907
|
+
name: 'Limit',
|
|
908
|
+
details: JSON.stringify({ limit: step.limit }),
|
|
909
|
+
objectCount: limitedWorkingSet.length,
|
|
910
|
+
},
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
|
|
653
914
|
private _compareMultiOrder(a: QueryItem, b: QueryItem, orders: readonly QueryAST.Order[]): number {
|
|
654
915
|
// Short circuit for common cases.
|
|
655
916
|
if (orders.length === 0) {
|
|
@@ -675,15 +936,20 @@ export class QueryExecutor extends Resource {
|
|
|
675
936
|
const comparison = this._compareByProperty(a, b, order.property);
|
|
676
937
|
return order.direction === 'desc' ? -comparison : comparison;
|
|
677
938
|
}
|
|
939
|
+
case 'rank': {
|
|
940
|
+
// Higher rank = better match. By default, descending order (best first).
|
|
941
|
+
const comparison = a.rank - b.rank;
|
|
942
|
+
return order.direction === 'desc' ? -comparison : comparison;
|
|
943
|
+
}
|
|
678
944
|
default:
|
|
679
|
-
// Should never reach here with proper TypeScript types
|
|
945
|
+
// Should never reach here with proper TypeScript types.
|
|
680
946
|
return 0;
|
|
681
947
|
}
|
|
682
948
|
}
|
|
683
949
|
|
|
684
950
|
private _compareByProperty(a: QueryItem, b: QueryItem, property: string): number {
|
|
685
|
-
const aValue = a
|
|
686
|
-
const bValue = b
|
|
951
|
+
const aValue = QueryItem.getProperty(a, [property]);
|
|
952
|
+
const bValue = QueryItem.getProperty(b, [property]);
|
|
687
953
|
|
|
688
954
|
// Both null or undefined
|
|
689
955
|
if (aValue == null && bValue == null) {
|
|
@@ -727,6 +993,205 @@ export class QueryExecutor extends Resource {
|
|
|
727
993
|
);
|
|
728
994
|
}
|
|
729
995
|
|
|
996
|
+
private _supportsSqlIndexing(): boolean {
|
|
997
|
+
return !!this._indexer2 && !!this._runtime;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
private async _runInRuntime<T>(effect: Effect.Effect<T, unknown, SqlClient.SqlClient>): Promise<T> {
|
|
1001
|
+
const runtimeProvider = this._runtime;
|
|
1002
|
+
invariant(runtimeProvider, 'SQL runtime is required.');
|
|
1003
|
+
const runtime = await runAndForwardErrors(runtimeProvider);
|
|
1004
|
+
return await unwrapExit(await effect.pipe(Runtime.runPromiseExit(runtime)));
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
private async _queryAllFromSqlIndex(spaceIds: readonly SpaceId[]): Promise<readonly ObjectMeta[]> {
|
|
1008
|
+
invariant(this._indexer2, 'SQL indexer is required.');
|
|
1009
|
+
return await this._runInRuntime(this._indexer2.queryAll({ spaceIds }));
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
private async _queryTypesFromSqlIndex(
|
|
1013
|
+
spaceIds: readonly SpaceId[],
|
|
1014
|
+
typeDxns: readonly string[],
|
|
1015
|
+
inverted: boolean,
|
|
1016
|
+
): Promise<readonly ObjectMeta[]> {
|
|
1017
|
+
invariant(this._indexer2, 'SQL indexer is required.');
|
|
1018
|
+
return await this._runInRuntime(this._indexer2.queryTypes({ spaceIds, typeDxns, inverted }));
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
private async _queryIncomingReferencesFromSqlIndex(
|
|
1022
|
+
workingSet: QueryItem[],
|
|
1023
|
+
property: EscapedPropPath | null,
|
|
1024
|
+
): Promise<readonly ObjectMeta[]> {
|
|
1025
|
+
const anchorDxns = workingSet.map((item) => DXN.fromLocalObjectId(item.objectId).toString());
|
|
1026
|
+
invariant(this._indexer2, 'SQL indexer is required.');
|
|
1027
|
+
const rows: readonly ReverseRef[] = (
|
|
1028
|
+
await Promise.all(
|
|
1029
|
+
anchorDxns.map((targetDxn) => this._runInRuntime(this._indexer2!.queryReverseRef({ targetDxn }))),
|
|
1030
|
+
)
|
|
1031
|
+
).flat();
|
|
1032
|
+
|
|
1033
|
+
const recordIds = rows
|
|
1034
|
+
.filter((row) => {
|
|
1035
|
+
if (property === null) {
|
|
1036
|
+
return true;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const queryPath = EscapedPropPath.unescape(property);
|
|
1040
|
+
const rowPath = EscapedPropPath.unescape(row.propPath);
|
|
1041
|
+
return QueryExecutor._matchesReferencePropertyPath(rowPath, queryPath);
|
|
1042
|
+
})
|
|
1043
|
+
.map((row) => row.recordId);
|
|
1044
|
+
|
|
1045
|
+
const uniqueRecordIds = Array.from(new Set<number>(recordIds));
|
|
1046
|
+
return await this._runInRuntime(this._indexer2.lookupByRecordIds(uniqueRecordIds));
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Matches a reverse-reference row path against a query property path.
|
|
1051
|
+
* Allows numeric segments in the row path (array indices) that are not present in the query.
|
|
1052
|
+
*
|
|
1053
|
+
* Examples:
|
|
1054
|
+
* - query: ['assignee'] matches row: ['assignee'] and ['assignee', '0'].
|
|
1055
|
+
* - query: ['items', 'assignee'] matches row: ['items', '0', 'assignee'].
|
|
1056
|
+
* - query: ['a', 'b'] does NOT match row: ['a'].
|
|
1057
|
+
* - query: ['a'] does NOT match row: ['a', 'b'].
|
|
1058
|
+
*/
|
|
1059
|
+
private static _matchesReferencePropertyPath(rowPath: readonly string[], queryPath: readonly string[]): boolean {
|
|
1060
|
+
const isNumericSegment = (segment: string) => /^[0-9]+$/.test(segment);
|
|
1061
|
+
|
|
1062
|
+
let i = 0; // queryPath index.
|
|
1063
|
+
let j = 0; // rowPath index.
|
|
1064
|
+
|
|
1065
|
+
while (i < queryPath.length && j < rowPath.length) {
|
|
1066
|
+
if (rowPath[j] === queryPath[i]) {
|
|
1067
|
+
i++;
|
|
1068
|
+
j++;
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// Row may contain array indices that aren't present in the query path.
|
|
1073
|
+
if (isNumericSegment(rowPath[j])) {
|
|
1074
|
+
j++;
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// Must consume full query path.
|
|
1082
|
+
if (i !== queryPath.length) {
|
|
1083
|
+
return false;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Any remaining row segments must be numeric (array indices).
|
|
1087
|
+
for (; j < rowPath.length; j++) {
|
|
1088
|
+
if (!isNumericSegment(rowPath[j])) {
|
|
1089
|
+
return false;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
return true;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
private async _queryRelationsFromSqlIndex(
|
|
1097
|
+
workingSet: QueryItem[],
|
|
1098
|
+
endpoint: 'source' | 'target',
|
|
1099
|
+
): Promise<readonly ObjectMeta[]> {
|
|
1100
|
+
invariant(this._indexer2, 'SQL indexer is required.');
|
|
1101
|
+
const anchorDxns = workingSet.map((item) => DXN.fromLocalObjectId(item.objectId).toString());
|
|
1102
|
+
return await this._runInRuntime(this._indexer2.queryRelations({ endpoint, anchorDxns }));
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
private async _loadDocumentsAfterSqlQuery(metas: readonly ObjectMeta[]): Promise<(QueryItem | null)[]> {
|
|
1106
|
+
const snapshotMap = await this._loadQueueSnapshotMap(metas);
|
|
1107
|
+
return await Promise.all(
|
|
1108
|
+
metas.map(async (meta) => {
|
|
1109
|
+
// Branch 1: Document-backed object.
|
|
1110
|
+
if (meta.documentId) {
|
|
1111
|
+
return this._loadFromAutomerge(meta);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Branch 2: Queue-backed object.
|
|
1115
|
+
if (meta.queueId) {
|
|
1116
|
+
return this._loadFromQueue(meta, snapshotMap);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
return null;
|
|
1120
|
+
}),
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
private async _loadQueueSnapshotMap(metas: readonly ObjectMeta[]): Promise<Map<number, unknown>> {
|
|
1125
|
+
const queueMetas = metas.filter((meta) => !meta.documentId && !!meta.queueId);
|
|
1126
|
+
if (queueMetas.length === 0) {
|
|
1127
|
+
return new Map();
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
invariant(this._indexer2, 'SQL indexer is required.');
|
|
1131
|
+
const snapshots = await this._runInRuntime(
|
|
1132
|
+
this._indexer2.querySnapshotsJSON(queueMetas.map((meta) => meta.recordId)),
|
|
1133
|
+
);
|
|
1134
|
+
return new Map(snapshots.map((s) => [s.recordId, s.snapshot]));
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* Loads a queue-backed object from an indexed snapshot (by `recordId`).
|
|
1139
|
+
* Returns `null` when the snapshot is missing or is not a JSON object.
|
|
1140
|
+
*/
|
|
1141
|
+
private _loadFromQueue(meta: ObjectMeta, snapshotMap: Map<number, unknown>): QueryItem | null {
|
|
1142
|
+
const snapshot = snapshotMap.get(meta.recordId);
|
|
1143
|
+
if (!snapshot || typeof snapshot !== 'object') {
|
|
1144
|
+
return null;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
return {
|
|
1148
|
+
objectId: meta.objectId as ObjectId,
|
|
1149
|
+
spaceId: meta.spaceId as SpaceId,
|
|
1150
|
+
queueId: meta.queueId as ObjectId,
|
|
1151
|
+
queueNamespace: 'data',
|
|
1152
|
+
documentId: null,
|
|
1153
|
+
doc: null,
|
|
1154
|
+
data: snapshot as Obj.JSON,
|
|
1155
|
+
rank: 1,
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* Loads a document-backed object from an Automerge `DatabaseDirectory`.
|
|
1161
|
+
* Returns `null` if the document can't be loaded, the inline object isn't present, or if the meta does not have a
|
|
1162
|
+
* document id (e.g. queue-backed objects).
|
|
1163
|
+
*/
|
|
1164
|
+
private async _loadFromAutomerge(meta: ObjectMeta): Promise<QueryItem | null> {
|
|
1165
|
+
if (!meta.documentId) {
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
const handle = await this._automergeHost.loadDoc<DatabaseDirectory>(
|
|
1169
|
+
Context.default(),
|
|
1170
|
+
meta.documentId as DocumentId,
|
|
1171
|
+
{
|
|
1172
|
+
fetchFromNetwork: true,
|
|
1173
|
+
},
|
|
1174
|
+
);
|
|
1175
|
+
const doc = handle.doc();
|
|
1176
|
+
if (!doc) {
|
|
1177
|
+
return null;
|
|
1178
|
+
}
|
|
1179
|
+
const object = DatabaseDirectory.getInlineObject(doc, meta.objectId);
|
|
1180
|
+
if (!object) {
|
|
1181
|
+
return null;
|
|
1182
|
+
}
|
|
1183
|
+
return {
|
|
1184
|
+
objectId: meta.objectId,
|
|
1185
|
+
documentId: meta.documentId as DocumentId,
|
|
1186
|
+
spaceId: meta.spaceId as SpaceId,
|
|
1187
|
+
queueId: null,
|
|
1188
|
+
queueNamespace: null,
|
|
1189
|
+
doc: object,
|
|
1190
|
+
data: null,
|
|
1191
|
+
rank: 1,
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
|
|
730
1195
|
/**
|
|
731
1196
|
* Space key hex -> SpaceId.
|
|
732
1197
|
*/
|
|
@@ -735,7 +1200,9 @@ export class QueryExecutor extends Resource {
|
|
|
735
1200
|
private async _loadFromIndexHit(hit: FindResult): Promise<QueryItem | null> {
|
|
736
1201
|
const { objectId, documentId, spaceKey: spaceKeyInIndex } = objectPointerCodec.decode(hit.id);
|
|
737
1202
|
|
|
738
|
-
const handle = await this._automergeHost.loadDoc<DatabaseDirectory>(Context.default(), documentId as DocumentId
|
|
1203
|
+
const handle = await this._automergeHost.loadDoc<DatabaseDirectory>(Context.default(), documentId as DocumentId, {
|
|
1204
|
+
fetchFromNetwork: true,
|
|
1205
|
+
});
|
|
739
1206
|
const doc = handle.doc();
|
|
740
1207
|
if (!doc) {
|
|
741
1208
|
return null;
|
|
@@ -761,7 +1228,11 @@ export class QueryExecutor extends Resource {
|
|
|
761
1228
|
objectId,
|
|
762
1229
|
documentId: documentId as DocumentId,
|
|
763
1230
|
spaceId,
|
|
1231
|
+
queueId: null,
|
|
1232
|
+
queueNamespace: null,
|
|
1233
|
+
data: null,
|
|
764
1234
|
doc: object,
|
|
1235
|
+
rank: hit.rank,
|
|
765
1236
|
};
|
|
766
1237
|
}
|
|
767
1238
|
|
|
@@ -791,7 +1262,11 @@ export class QueryExecutor extends Resource {
|
|
|
791
1262
|
objectId: echoDxn.echoId,
|
|
792
1263
|
documentId: spaceRoot.documentId,
|
|
793
1264
|
spaceId,
|
|
1265
|
+
queueId: null,
|
|
1266
|
+
queueNamespace: null,
|
|
1267
|
+
data: null,
|
|
794
1268
|
doc: inlineObject,
|
|
1269
|
+
rank: 1,
|
|
795
1270
|
};
|
|
796
1271
|
}
|
|
797
1272
|
|
|
@@ -800,7 +1275,9 @@ export class QueryExecutor extends Resource {
|
|
|
800
1275
|
return null;
|
|
801
1276
|
}
|
|
802
1277
|
|
|
803
|
-
const handle = await this._automergeHost.loadDoc<DatabaseDirectory>(Context.default(), link as AutomergeUrl
|
|
1278
|
+
const handle = await this._automergeHost.loadDoc<DatabaseDirectory>(Context.default(), link as AutomergeUrl, {
|
|
1279
|
+
fetchFromNetwork: true,
|
|
1280
|
+
});
|
|
804
1281
|
const doc = handle.doc();
|
|
805
1282
|
if (!doc) {
|
|
806
1283
|
return null;
|
|
@@ -815,7 +1292,11 @@ export class QueryExecutor extends Resource {
|
|
|
815
1292
|
objectId: echoDxn.echoId,
|
|
816
1293
|
documentId: handle.documentId,
|
|
817
1294
|
spaceId,
|
|
1295
|
+
queueId: null,
|
|
1296
|
+
queueNamespace: null,
|
|
1297
|
+
data: null,
|
|
818
1298
|
doc: object,
|
|
1299
|
+
rank: 1,
|
|
819
1300
|
};
|
|
820
1301
|
}
|
|
821
1302
|
}
|