@dxos/index-core 0.0.0 → 0.8.4-main.52d7546f51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/neutral/index.mjs +658 -0
- package/dist/lib/neutral/index.mjs.map +7 -0
- package/dist/lib/neutral/meta.json +1 -0
- package/dist/types/src/index-engine.d.ts +83 -0
- package/dist/types/src/index-engine.d.ts.map +1 -0
- package/dist/types/src/index-engine.test.d.ts +2 -0
- package/dist/types/src/index-engine.test.d.ts.map +1 -0
- package/dist/types/src/index-tracker.d.ts +44 -0
- package/dist/types/src/index-tracker.d.ts.map +1 -0
- package/dist/types/src/index-tracker.test.d.ts +2 -0
- package/dist/types/src/index-tracker.test.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +8 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/indexes/fts-index.d.ts +63 -0
- package/dist/types/src/indexes/fts-index.d.ts.map +1 -0
- package/dist/types/src/indexes/fts-index.test.d.ts +2 -0
- package/dist/types/src/indexes/fts-index.test.d.ts.map +1 -0
- package/dist/types/src/indexes/fts5.test.d.ts +2 -0
- package/dist/types/src/indexes/fts5.test.d.ts.map +1 -0
- package/dist/types/src/indexes/index.d.ts +5 -0
- package/dist/types/src/indexes/index.d.ts.map +1 -0
- package/dist/types/src/indexes/interface.d.ts +47 -0
- package/dist/types/src/indexes/interface.d.ts.map +1 -0
- package/dist/types/src/indexes/object-meta-index.d.ts +60 -0
- package/dist/types/src/indexes/object-meta-index.d.ts.map +1 -0
- package/dist/types/src/indexes/object-meta-index.test.d.ts +2 -0
- package/dist/types/src/indexes/object-meta-index.test.d.ts.map +1 -0
- package/dist/types/src/indexes/reverse-ref-index.d.ts +37 -0
- package/dist/types/src/indexes/reverse-ref-index.d.ts.map +1 -0
- package/dist/types/src/indexes/reverse-ref-index.test.d.ts +2 -0
- package/dist/types/src/indexes/reverse-ref-index.test.d.ts.map +1 -0
- package/dist/types/src/utils.d.ts +17 -0
- package/dist/types/src/utils.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +22 -18
- package/src/index-engine.test.ts +8 -5
- package/src/index-engine.ts +53 -9
- package/src/index.ts +3 -2
- package/src/indexes/fts-index.ts +41 -2
- package/src/indexes/object-meta-index.test.ts +179 -0
- package/src/indexes/object-meta-index.ts +148 -13
- package/src/utils.ts +1 -1
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
// src/index-engine.ts
|
|
2
|
+
import * as Effect5 from "effect/Effect";
|
|
3
|
+
import * as SqlTransaction from "@dxos/sql-sqlite/SqlTransaction";
|
|
4
|
+
|
|
5
|
+
// src/index-tracker.ts
|
|
6
|
+
import * as SqlClient from "@effect/sql/SqlClient";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
import * as Schema from "effect/Schema";
|
|
9
|
+
import { SpaceId } from "@dxos/keys";
|
|
10
|
+
var IndexCursor = Schema.Struct({
|
|
11
|
+
/**
|
|
12
|
+
* Name of the index owning this cursor.
|
|
13
|
+
*/
|
|
14
|
+
indexName: Schema.String,
|
|
15
|
+
/**
|
|
16
|
+
* Space id.
|
|
17
|
+
*/
|
|
18
|
+
spaceId: Schema.NullOr(SpaceId),
|
|
19
|
+
/**
|
|
20
|
+
* Source name.
|
|
21
|
+
* 'automerge' / 'queue' / 'index' (for secondary indexes)
|
|
22
|
+
*/
|
|
23
|
+
sourceName: Schema.String,
|
|
24
|
+
/**
|
|
25
|
+
* Document id or queue id.
|
|
26
|
+
* doc_id, queue_id, '' <empty string> (if indexing entire namespace)
|
|
27
|
+
*/
|
|
28
|
+
resourceId: Schema.NullOr(Schema.String),
|
|
29
|
+
/**
|
|
30
|
+
* Heads, queue position, version.
|
|
31
|
+
*/
|
|
32
|
+
cursor: Schema.Union(Schema.Number, Schema.String)
|
|
33
|
+
});
|
|
34
|
+
var IndexTracker = class {
|
|
35
|
+
migrate = Effect.fn("IndexTracker.migrate")(function* () {
|
|
36
|
+
const sql = yield* SqlClient.SqlClient;
|
|
37
|
+
yield* sql`CREATE TABLE IF NOT EXISTS indexCursor (
|
|
38
|
+
indexName TEXT NOT NULL,
|
|
39
|
+
spaceId TEXT NOT NULL DEFAULT '',
|
|
40
|
+
sourceName TEXT NOT NULL,
|
|
41
|
+
resourceId TEXT NOT NULL DEFAULT '',
|
|
42
|
+
cursor,
|
|
43
|
+
PRIMARY KEY (indexName, spaceId, sourceName, resourceId)
|
|
44
|
+
)`;
|
|
45
|
+
});
|
|
46
|
+
queryCursors = Effect.fn("IndexTracker.queryCursors")((query) => Effect.gen(function* () {
|
|
47
|
+
const sql = yield* SqlClient.SqlClient;
|
|
48
|
+
const spaceIdParam = query.spaceId === void 0 ? null : query.spaceId ?? "";
|
|
49
|
+
const sourceNameParam = query.sourceName === void 0 ? null : query.sourceName;
|
|
50
|
+
const resourceIdParam = query.resourceId === void 0 ? null : query.resourceId ?? "";
|
|
51
|
+
const rows = yield* sql`
|
|
52
|
+
SELECT * FROM indexCursor
|
|
53
|
+
WHERE indexName = ${query.indexName}
|
|
54
|
+
AND (${spaceIdParam} IS NULL OR spaceId = ${spaceIdParam})
|
|
55
|
+
AND (${sourceNameParam} IS NULL OR sourceName = ${sourceNameParam})
|
|
56
|
+
AND (${resourceIdParam} IS NULL OR resourceId = ${resourceIdParam})
|
|
57
|
+
`;
|
|
58
|
+
return rows.map((row) => ({
|
|
59
|
+
indexName: row.indexName,
|
|
60
|
+
spaceId: row.spaceId === "" ? null : Schema.decodeSync(SpaceId)(row.spaceId),
|
|
61
|
+
sourceName: row.sourceName,
|
|
62
|
+
resourceId: row.resourceId === "" ? null : row.resourceId,
|
|
63
|
+
cursor: row.cursor
|
|
64
|
+
}));
|
|
65
|
+
}));
|
|
66
|
+
updateCursors = Effect.fn("IndexTracker.updateCursors")((cursors) => Effect.gen(function* () {
|
|
67
|
+
const sql = yield* SqlClient.SqlClient;
|
|
68
|
+
yield* Effect.forEach(cursors, (cursor) => {
|
|
69
|
+
const spaceId = cursor.spaceId ?? "";
|
|
70
|
+
const resourceId = cursor.resourceId ?? "";
|
|
71
|
+
return sql`
|
|
72
|
+
INSERT INTO indexCursor (indexName, spaceId, sourceName, resourceId, cursor)
|
|
73
|
+
VALUES (${cursor.indexName}, ${spaceId}, ${cursor.sourceName}, ${resourceId}, ${cursor.cursor})
|
|
74
|
+
ON CONFLICT(indexName, spaceId, sourceName, resourceId) DO UPDATE SET cursor = excluded.cursor
|
|
75
|
+
`;
|
|
76
|
+
}, {
|
|
77
|
+
discard: true
|
|
78
|
+
});
|
|
79
|
+
}));
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// src/indexes/fts-index.ts
|
|
83
|
+
import * as SqlClient3 from "@effect/sql/SqlClient";
|
|
84
|
+
import * as Effect2 from "effect/Effect";
|
|
85
|
+
var escapeFts5Query = (text) => {
|
|
86
|
+
return text.split(/\s+/).filter(Boolean).map((term) => `"${term.replace(/"/g, '""')}"`).join(" ");
|
|
87
|
+
};
|
|
88
|
+
var FtsIndex = class {
|
|
89
|
+
migrate = Effect2.fn("FtsIndex.migrate")(function* () {
|
|
90
|
+
const sql = yield* SqlClient3.SqlClient;
|
|
91
|
+
yield* sql`CREATE VIRTUAL TABLE IF NOT EXISTS ftsIndex USING fts5(snapshot, tokenize='trigram')`;
|
|
92
|
+
});
|
|
93
|
+
query({ query, spaceId, includeAllQueues, queueIds }) {
|
|
94
|
+
return Effect2.gen(function* () {
|
|
95
|
+
const trimmed = query.trim();
|
|
96
|
+
if (trimmed.length === 0) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
const sql = yield* SqlClient3.SqlClient;
|
|
100
|
+
const terms = trimmed.split(/\s+/).filter(Boolean);
|
|
101
|
+
const minTermLength = Math.min(...terms.map((t) => t.length));
|
|
102
|
+
const useBm25 = minTermLength >= 3;
|
|
103
|
+
const conditions = minTermLength < 3 ? terms.map((term) => sql`f.snapshot LIKE ${"%" + term + "%"}`) : [
|
|
104
|
+
sql`f.snapshot MATCH ${escapeFts5Query(trimmed)}`
|
|
105
|
+
];
|
|
106
|
+
const sourceConditions = [];
|
|
107
|
+
if (spaceId && spaceId.length > 0) {
|
|
108
|
+
if (includeAllQueues) {
|
|
109
|
+
sourceConditions.push(sql`m.spaceId IN ${sql.in(spaceId)}`);
|
|
110
|
+
} else {
|
|
111
|
+
sourceConditions.push(sql`(m.spaceId IN ${sql.in(spaceId)} AND m.queueId = '')`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (queueIds && queueIds.length > 0) {
|
|
115
|
+
sourceConditions.push(sql`m.queueId IN ${sql.in(queueIds)}`);
|
|
116
|
+
}
|
|
117
|
+
if (sourceConditions.length > 0) {
|
|
118
|
+
conditions.push(sql`(${sql.or(sourceConditions)})`);
|
|
119
|
+
}
|
|
120
|
+
if (useBm25) {
|
|
121
|
+
const rows = yield* sql`
|
|
122
|
+
SELECT m.*, -bm25(ftsIndex) AS rank
|
|
123
|
+
FROM ftsIndex AS f
|
|
124
|
+
JOIN objectMeta AS m ON f.rowid = m.recordId
|
|
125
|
+
WHERE ${sql.and(conditions)}
|
|
126
|
+
ORDER BY rank DESC
|
|
127
|
+
`;
|
|
128
|
+
return rows;
|
|
129
|
+
} else {
|
|
130
|
+
const rows = yield* sql`
|
|
131
|
+
SELECT m.*
|
|
132
|
+
FROM ftsIndex AS f
|
|
133
|
+
JOIN objectMeta AS m ON f.rowid = m.recordId
|
|
134
|
+
WHERE ${sql.and(conditions)}
|
|
135
|
+
`;
|
|
136
|
+
return rows.map((row) => ({
|
|
137
|
+
...row,
|
|
138
|
+
rank: 1
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Query snapshots by recordIds.
|
|
145
|
+
* Returns the parsed JSON snapshots for queue objects.
|
|
146
|
+
*/
|
|
147
|
+
querySnapshotsJSON(recordIds) {
|
|
148
|
+
return Effect2.gen(function* () {
|
|
149
|
+
if (recordIds.length === 0) {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
const sql = yield* SqlClient3.SqlClient;
|
|
153
|
+
const results = yield* sql`SELECT rowid, snapshot FROM ftsIndex WHERE rowid IN ${sql.in(recordIds)}`;
|
|
154
|
+
return results.map((r) => ({
|
|
155
|
+
recordId: r.rowid,
|
|
156
|
+
snapshot: JSON.parse(r.snapshot)
|
|
157
|
+
}));
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
update = Effect2.fn("FtsIndex.update")((objects) => Effect2.gen(function* () {
|
|
161
|
+
const sql = yield* SqlClient3.SqlClient;
|
|
162
|
+
yield* Effect2.forEach(objects, (object) => Effect2.gen(function* () {
|
|
163
|
+
const { recordId, data } = object;
|
|
164
|
+
if (recordId === null) {
|
|
165
|
+
return yield* Effect2.die(new Error("FtsIndex.update requires recordId to be set"));
|
|
166
|
+
}
|
|
167
|
+
const snapshot = JSON.stringify(data);
|
|
168
|
+
const existing = yield* sql`SELECT rowid FROM ftsIndex WHERE rowid = ${recordId}`;
|
|
169
|
+
if (existing.length > 0) {
|
|
170
|
+
yield* sql`DELETE FROM ftsIndex WHERE rowid = ${recordId}`;
|
|
171
|
+
}
|
|
172
|
+
yield* sql`INSERT INTO ftsIndex (rowid, snapshot) VALUES (${recordId}, ${snapshot})`;
|
|
173
|
+
}), {
|
|
174
|
+
discard: true
|
|
175
|
+
});
|
|
176
|
+
}));
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// src/indexes/object-meta-index.ts
|
|
180
|
+
import * as SqlClient5 from "@effect/sql/SqlClient";
|
|
181
|
+
import * as Effect3 from "effect/Effect";
|
|
182
|
+
import * as Schema2 from "effect/Schema";
|
|
183
|
+
import { ATTR_DELETED, ATTR_PARENT, ATTR_RELATION_SOURCE, ATTR_RELATION_TARGET, ATTR_TYPE } from "@dxos/echo/internal";
|
|
184
|
+
import { DXN } from "@dxos/keys";
|
|
185
|
+
var _escapeLikePrefix = (prefix) => {
|
|
186
|
+
const escaped = prefix.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
|
|
187
|
+
return `${escaped}:%`;
|
|
188
|
+
};
|
|
189
|
+
var ObjectMeta = Schema2.Struct({
|
|
190
|
+
recordId: Schema2.Number,
|
|
191
|
+
objectId: Schema2.String,
|
|
192
|
+
queueId: Schema2.String,
|
|
193
|
+
spaceId: Schema2.String,
|
|
194
|
+
documentId: Schema2.String,
|
|
195
|
+
entityKind: Schema2.String,
|
|
196
|
+
/** The versioned DXN of the type of the object. */
|
|
197
|
+
typeDxn: Schema2.String,
|
|
198
|
+
deleted: Schema2.Boolean,
|
|
199
|
+
source: Schema2.NullOr(Schema2.String),
|
|
200
|
+
target: Schema2.NullOr(Schema2.String),
|
|
201
|
+
/** Parent object id (nullable). */
|
|
202
|
+
parent: Schema2.NullOr(Schema2.String),
|
|
203
|
+
/** Monotonically increasing sequence number assigned on insert/update for tracking indexing order. */
|
|
204
|
+
version: Schema2.Number
|
|
205
|
+
});
|
|
206
|
+
var ObjectMetaIndex = class {
|
|
207
|
+
migrate = Effect3.fn("ObjectMetaIndex.runMigrations")(function* () {
|
|
208
|
+
const sql = yield* SqlClient5.SqlClient;
|
|
209
|
+
yield* sql`CREATE TABLE IF NOT EXISTS objectMeta (
|
|
210
|
+
recordId INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
211
|
+
objectId TEXT NOT NULL,
|
|
212
|
+
queueId TEXT NOT NULL DEFAULT '',
|
|
213
|
+
spaceId TEXT NOT NULL,
|
|
214
|
+
documentId TEXT NOT NULL DEFAULT '',
|
|
215
|
+
entityKind TEXT NOT NULL,
|
|
216
|
+
typeDxn TEXT NOT NULL,
|
|
217
|
+
deleted INTEGER NOT NULL,
|
|
218
|
+
source TEXT,
|
|
219
|
+
target TEXT,
|
|
220
|
+
parent TEXT,
|
|
221
|
+
version INTEGER NOT NULL
|
|
222
|
+
)`;
|
|
223
|
+
yield* Effect3.catchAll(sql`ALTER TABLE objectMeta ADD COLUMN parent TEXT`, () => Effect3.void);
|
|
224
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_object_index_objectId ON objectMeta(spaceId, objectId)`;
|
|
225
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_object_index_typeDxn ON objectMeta(spaceId, typeDxn)`;
|
|
226
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_object_index_version ON objectMeta(version)`;
|
|
227
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_object_index_parent ON objectMeta(spaceId, parent)`;
|
|
228
|
+
});
|
|
229
|
+
query = Effect3.fn("ObjectMetaIndex.query")((query) => Effect3.gen(function* () {
|
|
230
|
+
const sql = yield* SqlClient5.SqlClient;
|
|
231
|
+
const parsedType = DXN.tryParse(query.typeDxn)?.asTypeDXN();
|
|
232
|
+
const rows = parsedType && parsedType.version === void 0 ? yield* sql`SELECT * FROM objectMeta WHERE spaceId = ${query.spaceId} AND (typeDxn = ${query.typeDxn} OR typeDxn LIKE ${_escapeLikePrefix(query.typeDxn)} ESCAPE '\\')` : yield* sql`SELECT * FROM objectMeta WHERE spaceId = ${query.spaceId} AND typeDxn = ${query.typeDxn}`;
|
|
233
|
+
return rows.map((row) => ({
|
|
234
|
+
...row,
|
|
235
|
+
deleted: !!row.deleted
|
|
236
|
+
}));
|
|
237
|
+
}));
|
|
238
|
+
queryAll = Effect3.fn("ObjectMetaIndex.queryAll")((query) => Effect3.gen(function* () {
|
|
239
|
+
if (query.spaceIds.length === 0) {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
const sql = yield* SqlClient5.SqlClient;
|
|
243
|
+
const rows = yield* sql`SELECT * FROM objectMeta WHERE ${sql.in("spaceId", query.spaceIds)}`;
|
|
244
|
+
return rows.map((row) => ({
|
|
245
|
+
...row,
|
|
246
|
+
deleted: !!row.deleted
|
|
247
|
+
}));
|
|
248
|
+
}));
|
|
249
|
+
queryTypes = Effect3.fn("ObjectMetaIndex.queryTypes")(({ spaceIds, typeDxns, inverted = false }) => Effect3.gen(function* () {
|
|
250
|
+
if (spaceIds.length === 0) {
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
if (typeDxns.length === 0) {
|
|
254
|
+
if (!inverted) {
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
const sql2 = yield* SqlClient5.SqlClient;
|
|
258
|
+
const rows2 = yield* sql2`SELECT * FROM objectMeta WHERE ${sql2.in("spaceId", spaceIds)}`;
|
|
259
|
+
return rows2.map((row) => ({
|
|
260
|
+
...row,
|
|
261
|
+
deleted: !!row.deleted
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
const sql = yield* SqlClient5.SqlClient;
|
|
265
|
+
const spaceWhere = sql.in("spaceId", spaceIds);
|
|
266
|
+
const typeWhere = sql.or(typeDxns.map((typeDxn) => {
|
|
267
|
+
const parsedType = DXN.tryParse(typeDxn)?.asTypeDXN();
|
|
268
|
+
return parsedType && parsedType.version === void 0 ? sql.or([
|
|
269
|
+
sql`typeDxn = ${typeDxn}`,
|
|
270
|
+
sql`typeDxn LIKE ${_escapeLikePrefix(typeDxn)} ESCAPE '\\'`
|
|
271
|
+
]) : sql`typeDxn = ${typeDxn}`;
|
|
272
|
+
}));
|
|
273
|
+
const rows = inverted ? yield* sql`SELECT * FROM objectMeta WHERE ${spaceWhere} AND NOT ${typeWhere}` : yield* sql`SELECT * FROM objectMeta WHERE ${spaceWhere} AND ${typeWhere}`;
|
|
274
|
+
return rows.map((row) => ({
|
|
275
|
+
...row,
|
|
276
|
+
deleted: !!row.deleted
|
|
277
|
+
}));
|
|
278
|
+
}));
|
|
279
|
+
queryRelations = Effect3.fn("ObjectMetaIndex.queryRelations")(({ endpoint, anchorDxns }) => Effect3.gen(function* () {
|
|
280
|
+
if (anchorDxns.length === 0) {
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
const sql = yield* SqlClient5.SqlClient;
|
|
284
|
+
const column = endpoint === "source" ? "source" : "target";
|
|
285
|
+
const rows = yield* sql`SELECT * FROM objectMeta WHERE entityKind = 'relation' AND ${sql.in(column, anchorDxns)}`;
|
|
286
|
+
return rows.map((row) => ({
|
|
287
|
+
...row,
|
|
288
|
+
deleted: !!row.deleted
|
|
289
|
+
}));
|
|
290
|
+
}));
|
|
291
|
+
// TODO(dmaretskyi): Update recordId on objects so that we don't need to look it up separately.
|
|
292
|
+
update = Effect3.fn("ObjectMetaIndex.update")((objects) => Effect3.gen(function* () {
|
|
293
|
+
const sql = yield* SqlClient5.SqlClient;
|
|
294
|
+
yield* Effect3.forEach(objects, (object) => Effect3.gen(function* () {
|
|
295
|
+
const { spaceId, queueId, documentId, data } = object;
|
|
296
|
+
const castData = data;
|
|
297
|
+
const objectId = castData.id;
|
|
298
|
+
let existing;
|
|
299
|
+
if (documentId) {
|
|
300
|
+
existing = yield* sql`SELECT recordId FROM objectMeta WHERE spaceId = ${spaceId} AND documentId = ${documentId} AND objectId = ${objectId} LIMIT 1`;
|
|
301
|
+
} else if (queueId) {
|
|
302
|
+
existing = yield* sql`SELECT recordId FROM objectMeta WHERE spaceId = ${spaceId} AND queueId = ${queueId} AND objectId = ${objectId} LIMIT 1`;
|
|
303
|
+
} else {
|
|
304
|
+
existing = [];
|
|
305
|
+
}
|
|
306
|
+
const result = yield* sql`SELECT MAX(version) as v FROM objectMeta`;
|
|
307
|
+
const [{ v }] = result;
|
|
308
|
+
const version = (v ?? 0) + 1;
|
|
309
|
+
const entityKind = castData[ATTR_RELATION_SOURCE] ? "relation" : "object";
|
|
310
|
+
const typeDxn = castData[ATTR_TYPE] ? String(castData[ATTR_TYPE]) : "type";
|
|
311
|
+
const deleted = castData[ATTR_DELETED] ? 1 : 0;
|
|
312
|
+
const source = entityKind === "relation" ? castData[ATTR_RELATION_SOURCE] ?? null : null;
|
|
313
|
+
const target = entityKind === "relation" ? castData[ATTR_RELATION_TARGET] ?? null : null;
|
|
314
|
+
const parent = castData[ATTR_PARENT] ?? null;
|
|
315
|
+
if (existing.length > 0) {
|
|
316
|
+
yield* sql`
|
|
317
|
+
UPDATE objectMeta SET
|
|
318
|
+
version = ${version},
|
|
319
|
+
entityKind = ${entityKind},
|
|
320
|
+
typeDxn = ${typeDxn},
|
|
321
|
+
deleted = ${deleted},
|
|
322
|
+
source = ${source},
|
|
323
|
+
target = ${target},
|
|
324
|
+
parent = ${parent}
|
|
325
|
+
WHERE recordId = ${existing[0].recordId}
|
|
326
|
+
`;
|
|
327
|
+
} else {
|
|
328
|
+
yield* sql`
|
|
329
|
+
INSERT INTO objectMeta (
|
|
330
|
+
objectId, queueId, spaceId, documentId,
|
|
331
|
+
entityKind, typeDxn, deleted, source, target, parent, version
|
|
332
|
+
) VALUES (
|
|
333
|
+
${objectId}, ${queueId ?? ""}, ${spaceId}, ${documentId ?? ""},
|
|
334
|
+
${entityKind}, ${typeDxn}, ${deleted},
|
|
335
|
+
${source}, ${target}, ${parent}, ${version}
|
|
336
|
+
)
|
|
337
|
+
`;
|
|
338
|
+
}
|
|
339
|
+
}), {
|
|
340
|
+
discard: true
|
|
341
|
+
});
|
|
342
|
+
}));
|
|
343
|
+
/**
|
|
344
|
+
* Look up `recordIds` for objects that are already stored in the ObjectMetaIndex.
|
|
345
|
+
* Mutates the objects in place.
|
|
346
|
+
*/
|
|
347
|
+
lookupRecordIds = Effect3.fn("ObjectMetaIndex.lookupRecordIds")((objects) => Effect3.gen(function* () {
|
|
348
|
+
const sql = yield* SqlClient5.SqlClient;
|
|
349
|
+
for (const object of objects) {
|
|
350
|
+
const { spaceId, queueId, documentId, data } = object;
|
|
351
|
+
const objectId = data.id;
|
|
352
|
+
let result;
|
|
353
|
+
if (documentId) {
|
|
354
|
+
result = yield* sql`SELECT recordId FROM objectMeta WHERE spaceId = ${spaceId} AND documentId = ${documentId} AND objectId = ${objectId} LIMIT 1`;
|
|
355
|
+
} else if (queueId) {
|
|
356
|
+
result = yield* sql`SELECT recordId FROM objectMeta WHERE spaceId = ${spaceId} AND queueId = ${queueId} AND objectId = ${objectId} LIMIT 1`;
|
|
357
|
+
} else {
|
|
358
|
+
result = [];
|
|
359
|
+
}
|
|
360
|
+
if (result.length === 0) {
|
|
361
|
+
return yield* Effect3.die(new Error(`Object not found in ObjectMetaIndex: ${spaceId}/${documentId ?? queueId}/${objectId}`));
|
|
362
|
+
}
|
|
363
|
+
object.recordId = result[0].recordId;
|
|
364
|
+
}
|
|
365
|
+
}));
|
|
366
|
+
/**
|
|
367
|
+
* Look up object metadata by recordIds.
|
|
368
|
+
*/
|
|
369
|
+
lookupByRecordIds = Effect3.fn("ObjectMetaIndex.lookupByRecordIds")((recordIds) => Effect3.gen(function* () {
|
|
370
|
+
if (recordIds.length === 0) {
|
|
371
|
+
return [];
|
|
372
|
+
}
|
|
373
|
+
const sql = yield* SqlClient5.SqlClient;
|
|
374
|
+
const rows = yield* sql`SELECT * FROM objectMeta WHERE ${sql.in("recordId", recordIds)}`;
|
|
375
|
+
return rows.map((row) => ({
|
|
376
|
+
...row,
|
|
377
|
+
deleted: !!row.deleted
|
|
378
|
+
}));
|
|
379
|
+
}));
|
|
380
|
+
/**
|
|
381
|
+
* Query children by parent object ids.
|
|
382
|
+
*/
|
|
383
|
+
queryChildren = Effect3.fn("ObjectMetaIndex.queryChildren")((query) => Effect3.gen(function* () {
|
|
384
|
+
if (query.parentIds.length === 0) {
|
|
385
|
+
return [];
|
|
386
|
+
}
|
|
387
|
+
const sql = yield* SqlClient5.SqlClient;
|
|
388
|
+
const rows = yield* sql`SELECT * FROM objectMeta WHERE spaceId IN ${sql.in(query.spaceId)} AND parent IN ${sql.in(query.parentIds.map((id) => DXN.fromLocalObjectId(id).toString()))}`;
|
|
389
|
+
return rows.map((row) => ({
|
|
390
|
+
...row,
|
|
391
|
+
deleted: !!row.deleted
|
|
392
|
+
}));
|
|
393
|
+
}));
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
// src/indexes/reverse-ref-index.ts
|
|
397
|
+
import * as SqlClient7 from "@effect/sql/SqlClient";
|
|
398
|
+
import * as Effect4 from "effect/Effect";
|
|
399
|
+
import * as Schema4 from "effect/Schema";
|
|
400
|
+
import { EncodedReference, isEncodedReference } from "@dxos/echo-protocol";
|
|
401
|
+
|
|
402
|
+
// src/utils.ts
|
|
403
|
+
import * as Schema3 from "effect/Schema";
|
|
404
|
+
import { invariant } from "@dxos/invariant";
|
|
405
|
+
var __dxlog_file = "/__w/dxos/dxos/packages/core/echo/index-core/src/utils.ts";
|
|
406
|
+
var EscapedPropPath = class extends Schema3.String.annotations({
|
|
407
|
+
title: "EscapedPropPath"
|
|
408
|
+
}) {
|
|
409
|
+
static escape(path) {
|
|
410
|
+
return path.map((p) => p.toString().replaceAll("\\", "\\\\").replaceAll(".", "\\.")).join(".");
|
|
411
|
+
}
|
|
412
|
+
static unescape(path) {
|
|
413
|
+
const parts = [];
|
|
414
|
+
let current = "";
|
|
415
|
+
for (let i = 0; i < path.length; i++) {
|
|
416
|
+
if (path[i] === "\\") {
|
|
417
|
+
invariant(i + 1 < path.length && (path[i + 1] === "." || path[i + 1] === "\\"), "Malformed escaping.", {
|
|
418
|
+
F: __dxlog_file,
|
|
419
|
+
L: 34,
|
|
420
|
+
S: this,
|
|
421
|
+
A: [
|
|
422
|
+
"i + 1 < path.length && (path[i + 1] === '.' || path[i + 1] === '\\\\')",
|
|
423
|
+
"'Malformed escaping.'"
|
|
424
|
+
]
|
|
425
|
+
});
|
|
426
|
+
current = current + path[i + 1];
|
|
427
|
+
i++;
|
|
428
|
+
} else if (path[i] === ".") {
|
|
429
|
+
parts.push(current);
|
|
430
|
+
current = "";
|
|
431
|
+
} else {
|
|
432
|
+
current += path[i];
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
parts.push(current);
|
|
436
|
+
return parts;
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// src/indexes/reverse-ref-index.ts
|
|
441
|
+
var extractReferences = (data) => {
|
|
442
|
+
const refs = [];
|
|
443
|
+
const visit = (path, value) => {
|
|
444
|
+
if (isEncodedReference(value)) {
|
|
445
|
+
const dxn = EncodedReference.toDXN(value);
|
|
446
|
+
const echoId = dxn.asEchoDXN()?.echoId;
|
|
447
|
+
if (!echoId) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
refs.push({
|
|
451
|
+
path,
|
|
452
|
+
targetDxn: dxn.toString()
|
|
453
|
+
});
|
|
454
|
+
} else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
455
|
+
for (const [key, v] of Object.entries(value)) {
|
|
456
|
+
visit([
|
|
457
|
+
...path,
|
|
458
|
+
key
|
|
459
|
+
], v);
|
|
460
|
+
}
|
|
461
|
+
} else if (Array.isArray(value)) {
|
|
462
|
+
for (let i = 0; i < value.length; i++) {
|
|
463
|
+
visit([
|
|
464
|
+
...path,
|
|
465
|
+
String(i)
|
|
466
|
+
], value[i]);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
visit([], data);
|
|
471
|
+
return refs;
|
|
472
|
+
};
|
|
473
|
+
var ReverseRef = Schema4.Struct({
|
|
474
|
+
recordId: Schema4.Number,
|
|
475
|
+
targetDxn: Schema4.String,
|
|
476
|
+
/**
|
|
477
|
+
* Escaped property path within an object.
|
|
478
|
+
*
|
|
479
|
+
* Escaping rules:
|
|
480
|
+
*
|
|
481
|
+
* - '.' -> '\.'
|
|
482
|
+
* - '\' -> '\\'
|
|
483
|
+
* - contact with .
|
|
484
|
+
*/
|
|
485
|
+
propPath: Schema4.String
|
|
486
|
+
});
|
|
487
|
+
var ReverseRefIndex = class {
|
|
488
|
+
migrate = Effect4.fn("ReverseRefIndex.migrate")(function* () {
|
|
489
|
+
const sql = yield* SqlClient7.SqlClient;
|
|
490
|
+
yield* sql`CREATE TABLE IF NOT EXISTS reverseRef (
|
|
491
|
+
recordId INTEGER NOT NULL,
|
|
492
|
+
targetDxn TEXT NOT NULL,
|
|
493
|
+
propPath TEXT NOT NULL,
|
|
494
|
+
PRIMARY KEY (recordId, targetDxn, propPath)
|
|
495
|
+
)`;
|
|
496
|
+
yield* sql`CREATE INDEX IF NOT EXISTS idx_reverse_ref_target ON reverseRef(targetDxn)`;
|
|
497
|
+
});
|
|
498
|
+
/**
|
|
499
|
+
* Query all references pointing to a target DXN.
|
|
500
|
+
*/
|
|
501
|
+
query = Effect4.fn("ReverseRefIndex.query")(({ targetDxn }) => Effect4.gen(function* () {
|
|
502
|
+
const sql = yield* SqlClient7.SqlClient;
|
|
503
|
+
const rows = yield* sql`SELECT * FROM reverseRef WHERE targetDxn = ${targetDxn}`;
|
|
504
|
+
return rows;
|
|
505
|
+
}));
|
|
506
|
+
update = Effect4.fn("ReverseRefIndex.update")((objects) => Effect4.gen(function* () {
|
|
507
|
+
const sql = yield* SqlClient7.SqlClient;
|
|
508
|
+
yield* Effect4.forEach(objects, (object) => Effect4.gen(function* () {
|
|
509
|
+
const { recordId, data } = object;
|
|
510
|
+
if (recordId === null) {
|
|
511
|
+
yield* Effect4.die(new Error("ReverseRefIndex.update requires recordId to be set"));
|
|
512
|
+
}
|
|
513
|
+
yield* sql`DELETE FROM reverseRef WHERE recordId = ${recordId}`;
|
|
514
|
+
const refs = extractReferences(data);
|
|
515
|
+
yield* Effect4.forEach(refs, (ref) => sql`INSERT INTO reverseRef (recordId, targetDxn, propPath) VALUES (${recordId}, ${ref.targetDxn}, ${EscapedPropPath.escape(ref.path)})`, {
|
|
516
|
+
discard: true
|
|
517
|
+
});
|
|
518
|
+
}), {
|
|
519
|
+
discard: true
|
|
520
|
+
});
|
|
521
|
+
}));
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// src/index-engine.ts
|
|
525
|
+
var IndexEngine = class {
|
|
526
|
+
#tracker;
|
|
527
|
+
#objectMetaIndex;
|
|
528
|
+
#ftsIndex;
|
|
529
|
+
#reverseRefIndex;
|
|
530
|
+
constructor(params) {
|
|
531
|
+
this.#tracker = params?.tracker ?? new IndexTracker();
|
|
532
|
+
this.#objectMetaIndex = params?.objectMetaIndex ?? new ObjectMetaIndex();
|
|
533
|
+
this.#ftsIndex = params?.ftsIndex ?? new FtsIndex();
|
|
534
|
+
this.#reverseRefIndex = params?.reverseRefIndex ?? new ReverseRefIndex();
|
|
535
|
+
}
|
|
536
|
+
migrate() {
|
|
537
|
+
return Effect5.gen(this, function* () {
|
|
538
|
+
yield* this.#tracker.migrate();
|
|
539
|
+
yield* this.#objectMetaIndex.migrate();
|
|
540
|
+
yield* this.#ftsIndex.migrate();
|
|
541
|
+
yield* this.#reverseRefIndex.migrate();
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Query text index and return full object metadata with rank.
|
|
546
|
+
*/
|
|
547
|
+
queryText(query) {
|
|
548
|
+
return Effect5.gen(this, function* () {
|
|
549
|
+
return yield* this.#ftsIndex.query(query);
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
queryReverseRef(query) {
|
|
553
|
+
return this.#reverseRefIndex.query(query);
|
|
554
|
+
}
|
|
555
|
+
queryAll(query) {
|
|
556
|
+
return this.#objectMetaIndex.queryAll(query);
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Query snapshots by recordIds.
|
|
560
|
+
* Used to load queue objects from indexed snapshots.
|
|
561
|
+
*/
|
|
562
|
+
querySnapshotsJSON(recordIds) {
|
|
563
|
+
return this.#ftsIndex.querySnapshotsJSON(recordIds);
|
|
564
|
+
}
|
|
565
|
+
queryType(query) {
|
|
566
|
+
return this.#objectMetaIndex.query(query);
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Query children by parent object ids.
|
|
570
|
+
*/
|
|
571
|
+
queryChildren(query) {
|
|
572
|
+
return this.#objectMetaIndex.queryChildren(query);
|
|
573
|
+
}
|
|
574
|
+
queryTypes(query) {
|
|
575
|
+
return this.#objectMetaIndex.queryTypes(query);
|
|
576
|
+
}
|
|
577
|
+
queryRelations(query) {
|
|
578
|
+
return this.#objectMetaIndex.queryRelations(query);
|
|
579
|
+
}
|
|
580
|
+
lookupByRecordIds(recordIds) {
|
|
581
|
+
return this.#objectMetaIndex.lookupByRecordIds(recordIds);
|
|
582
|
+
}
|
|
583
|
+
update(dataSource, opts) {
|
|
584
|
+
return Effect5.gen(this, function* () {
|
|
585
|
+
let updated = 0;
|
|
586
|
+
const { updated: updatedFtsIndex, done: doneFtsIndex } = yield* this.#update(this.#ftsIndex, dataSource, {
|
|
587
|
+
indexName: "fts",
|
|
588
|
+
spaceId: opts.spaceId,
|
|
589
|
+
limit: opts.limit
|
|
590
|
+
});
|
|
591
|
+
updated += updatedFtsIndex;
|
|
592
|
+
const { updated: updatedReverseRefIndex, done: doneReverseRefIndex } = yield* this.#update(this.#reverseRefIndex, dataSource, {
|
|
593
|
+
indexName: "reverseRef",
|
|
594
|
+
spaceId: opts.spaceId,
|
|
595
|
+
limit: opts.limit
|
|
596
|
+
});
|
|
597
|
+
updated += updatedReverseRefIndex;
|
|
598
|
+
return {
|
|
599
|
+
updated,
|
|
600
|
+
done: doneFtsIndex && doneReverseRefIndex
|
|
601
|
+
};
|
|
602
|
+
}).pipe(Effect5.withSpan("IndexEngine.update"));
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Update a dependent index that requires recordId enrichment.
|
|
606
|
+
* This method:
|
|
607
|
+
* 1. Gets changed objects from the source.
|
|
608
|
+
* 2. Ensures those objects exist in ObjectMetaIndex.
|
|
609
|
+
* 3. Looks up recordIds for those objects.
|
|
610
|
+
* 4. Enriches objects with recordIds.
|
|
611
|
+
* 5. Updates the dependent index.
|
|
612
|
+
*/
|
|
613
|
+
#update(index, source, opts) {
|
|
614
|
+
return Effect5.gen(this, function* () {
|
|
615
|
+
const sqlTransaction = yield* SqlTransaction.SqlTransaction;
|
|
616
|
+
return yield* sqlTransaction.withTransaction(Effect5.gen(this, function* () {
|
|
617
|
+
const cursors = yield* this.#tracker.queryCursors({
|
|
618
|
+
indexName: opts.indexName,
|
|
619
|
+
sourceName: source.sourceName,
|
|
620
|
+
// Pass undefined to get all cursors when spaceId is null.
|
|
621
|
+
spaceId: opts.spaceId ?? void 0
|
|
622
|
+
});
|
|
623
|
+
const { objects, cursors: updatedCursors } = yield* source.getChangedObjects(cursors, {
|
|
624
|
+
limit: opts.limit
|
|
625
|
+
});
|
|
626
|
+
if (objects.length === 0) {
|
|
627
|
+
return {
|
|
628
|
+
updated: 0,
|
|
629
|
+
done: true
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
yield* this.#objectMetaIndex.update(objects);
|
|
633
|
+
yield* this.#objectMetaIndex.lookupRecordIds(objects);
|
|
634
|
+
yield* index.update(objects);
|
|
635
|
+
yield* this.#tracker.updateCursors(updatedCursors.map((_) => ({
|
|
636
|
+
indexName: opts.indexName,
|
|
637
|
+
spaceId: _.spaceId,
|
|
638
|
+
sourceName: source.sourceName,
|
|
639
|
+
resourceId: _.resourceId,
|
|
640
|
+
cursor: _.cursor
|
|
641
|
+
})));
|
|
642
|
+
return {
|
|
643
|
+
updated: objects.length,
|
|
644
|
+
done: false
|
|
645
|
+
};
|
|
646
|
+
}));
|
|
647
|
+
}).pipe(Effect5.withSpan("IndexEngine.#update"));
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
export {
|
|
651
|
+
EscapedPropPath,
|
|
652
|
+
FtsIndex,
|
|
653
|
+
IndexEngine,
|
|
654
|
+
IndexTracker,
|
|
655
|
+
ObjectMetaIndex,
|
|
656
|
+
ReverseRefIndex
|
|
657
|
+
};
|
|
658
|
+
//# sourceMappingURL=index.mjs.map
|