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