@minnowdb/core 0.2.0 → 0.3.0
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/README.md +21 -32
- package/dist/engine/artifact-cache.d.ts +2 -0
- package/dist/engine/artifact-cache.d.ts.map +1 -1
- package/dist/engine/artifact-cache.js +5 -0
- package/dist/engine/artifact-cache.js.map +1 -1
- package/dist/engine/batch.d.ts +6 -1
- package/dist/engine/batch.d.ts.map +1 -1
- package/dist/engine/batch.js +17 -6
- package/dist/engine/batch.js.map +1 -1
- package/dist/engine/catalog.d.ts +22 -2
- package/dist/engine/catalog.d.ts.map +1 -1
- package/dist/engine/catalog.js +30 -3
- package/dist/engine/catalog.js.map +1 -1
- package/dist/engine/client.d.ts +3 -0
- package/dist/engine/client.d.ts.map +1 -1
- package/dist/engine/client.js +21 -10
- package/dist/engine/client.js.map +1 -1
- package/dist/engine/database.d.ts +71 -5
- package/dist/engine/database.d.ts.map +1 -1
- package/dist/engine/database.js +3619 -421
- package/dist/engine/database.js.map +1 -1
- package/dist/engine/defaults.d.ts +4 -7
- package/dist/engine/defaults.d.ts.map +1 -1
- package/dist/engine/defaults.js +47 -21
- package/dist/engine/defaults.js.map +1 -1
- package/dist/engine/fts.d.ts.map +1 -1
- package/dist/engine/fts.js +2 -0
- package/dist/engine/fts.js.map +1 -1
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +1 -0
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/live.d.ts +6 -2
- package/dist/engine/live.d.ts.map +1 -1
- package/dist/engine/live.js +10 -2
- package/dist/engine/live.js.map +1 -1
- package/dist/engine/optimizer.d.ts.map +1 -1
- package/dist/engine/optimizer.js +258 -23
- package/dist/engine/optimizer.js.map +1 -1
- package/dist/engine/query-cache.d.ts +1 -1
- package/dist/engine/query-cache.d.ts.map +1 -1
- package/dist/engine/query-cache.js +3 -1
- package/dist/engine/query-cache.js.map +1 -1
- package/dist/engine/query.d.ts +132 -22
- package/dist/engine/query.d.ts.map +1 -1
- package/dist/engine/query.js +1368 -246
- package/dist/engine/query.js.map +1 -1
- package/dist/engine/schema-wire.d.ts +6 -2
- package/dist/engine/schema-wire.d.ts.map +1 -1
- package/dist/engine/schema-wire.js +40 -33
- package/dist/engine/schema-wire.js.map +1 -1
- package/dist/engine/schema.d.ts +157 -76
- package/dist/engine/schema.d.ts.map +1 -1
- package/dist/engine/schema.js +548 -103
- package/dist/engine/schema.js.map +1 -1
- package/dist/engine/sort-keys.d.ts +4 -3
- package/dist/engine/sort-keys.d.ts.map +1 -1
- package/dist/engine/sort-keys.js +42 -27
- package/dist/engine/sort-keys.js.map +1 -1
- package/dist/engine/sql-domains.d.ts +26 -0
- package/dist/engine/sql-domains.d.ts.map +1 -0
- package/dist/engine/sql-domains.js +421 -0
- package/dist/engine/sql-domains.js.map +1 -0
- package/dist/engine/sql-driver.d.ts +19 -0
- package/dist/engine/sql-driver.d.ts.map +1 -0
- package/dist/engine/sql-driver.js +2 -0
- package/dist/engine/sql-driver.js.map +1 -0
- package/dist/engine/sql-json.d.ts +7 -8
- package/dist/engine/sql-json.d.ts.map +1 -1
- package/dist/engine/sql-json.js +28 -14
- package/dist/engine/sql-json.js.map +1 -1
- package/dist/engine/sql-semantics.d.ts +2 -0
- package/dist/engine/sql-semantics.d.ts.map +1 -1
- package/dist/engine/sql-semantics.js +69 -3
- package/dist/engine/sql-semantics.js.map +1 -1
- package/dist/engine/vector.d.ts +5 -1
- package/dist/engine/vector.d.ts.map +1 -1
- package/dist/engine/vector.js +433 -61
- package/dist/engine/vector.js.map +1 -1
- package/dist/engine/worker-host.d.ts +1 -0
- package/dist/engine/worker-host.d.ts.map +1 -1
- package/dist/engine/worker-host.js +8 -0
- package/dist/engine/worker-host.js.map +1 -1
- package/dist/plan/index.d.ts +3 -3
- package/dist/plan/index.js +3 -3
- package/dist/storage/indexeddb.d.ts +44 -5
- package/dist/storage/indexeddb.d.ts.map +1 -1
- package/dist/storage/indexeddb.js +738 -175
- package/dist/storage/indexeddb.js.map +1 -1
- package/dist/storage/memory.d.ts +27 -10
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +67 -18
- package/dist/storage/memory.js.map +1 -1
- package/dist/storage/opfs/leader.d.ts +28 -6
- package/dist/storage/opfs/leader.d.ts.map +1 -1
- package/dist/storage/opfs/leader.js +301 -34
- package/dist/storage/opfs/leader.js.map +1 -1
- package/dist/storage/opfs/rpc.d.ts.map +1 -1
- package/dist/storage/opfs/rpc.js +2 -1
- package/dist/storage/opfs/rpc.js.map +1 -1
- package/dist/storage/opfs/store.d.ts +34 -5
- package/dist/storage/opfs/store.d.ts.map +1 -1
- package/dist/storage/opfs/store.js +41 -0
- package/dist/storage/opfs/store.js.map +1 -1
- package/dist/storage/snapshot.d.ts +14 -0
- package/dist/storage/snapshot.d.ts.map +1 -1
- package/dist/storage/snapshot.js +44 -0
- package/dist/storage/snapshot.js.map +1 -1
- package/dist/storage/toolkit/index.d.ts +1 -1
- package/dist/storage/toolkit/index.d.ts.map +1 -1
- package/dist/storage/toolkit/index.js +1 -1
- package/dist/storage/toolkit/index.js.map +1 -1
- package/dist/storage/toolkit/record-core.d.ts +19 -5
- package/dist/storage/toolkit/record-core.d.ts.map +1 -1
- package/dist/storage/toolkit/record-core.js +353 -78
- package/dist/storage/toolkit/record-core.js.map +1 -1
- package/dist/storage/toolkit/wire.d.ts +15 -0
- package/dist/storage/toolkit/wire.d.ts.map +1 -1
- package/dist/storage/toolkit/wire.js +134 -0
- package/dist/storage/toolkit/wire.js.map +1 -1
- package/dist/storage/types.d.ts +213 -31
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/storage/types.js +291 -20
- package/dist/storage/types.js.map +1 -1
- package/dist/testing/block-store-conformance.d.ts.map +1 -1
- package/dist/testing/block-store-conformance.js +153 -2
- package/dist/testing/block-store-conformance.js.map +1 -1
- package/dist/testing/index.d.ts +9 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +23 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/simulator.d.ts +98 -0
- package/dist/testing/simulator.d.ts.map +1 -0
- package/dist/testing/simulator.js +560 -0
- package/dist/testing/simulator.js.map +1 -0
- package/dist/testing/sqllogictest.d.ts +90 -0
- package/dist/testing/sqllogictest.d.ts.map +1 -0
- package/dist/testing/sqllogictest.js +435 -0
- package/dist/testing/sqllogictest.js.map +1 -0
- package/dist/transactions/index.d.ts +18 -3
- package/dist/transactions/index.d.ts.map +1 -1
- package/dist/transactions/index.js +64 -8
- package/dist/transactions/index.js.map +1 -1
- package/package.json +5 -3
- package/postgres-feature-profile.json +223 -0
- package/sql-feature-matrix.json +179 -251
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { collectFtsCandidates, } from "../types.js";
|
|
1
|
+
import { activePostingStorageColumnIds, collectFtsCandidates, collectFtsPostings, } from "../types.js";
|
|
2
|
+
import { validateSnapshotCatalog, } from "../snapshot.js";
|
|
2
3
|
import { RecordCore, validateBeginTransactionInput, validateId, validateTempRunPage, validateTempRunPageIdentity, } from "../toolkit/record-core.js";
|
|
3
4
|
import { encodeSegment, decodeSegment } from "./files.js";
|
|
4
|
-
import { decodeChunk, decodeSyncCheckpoint,
|
|
5
|
+
import { decodeChunk, decodePostingChunk, decodeSyncCheckpoint, encodePostingChunk, encodeSyncCheckpoint, } from "../toolkit/wire.js";
|
|
5
6
|
import { WalWriter, replayWalFrames } from "../toolkit/wal.js";
|
|
6
7
|
import { ExtentPool } from "../toolkit/extents.js";
|
|
7
8
|
/** Checkpoint when the WAL passes either bound; both trade replay time against pause time. */
|
|
@@ -9,6 +10,8 @@ const CHECKPOINT_WAL_BYTES = 4 * 1024 * 1024;
|
|
|
9
10
|
const CHECKPOINT_ENTRIES = 1024;
|
|
10
11
|
/** Decoded full-text base chunks kept in memory; least recently loaded goes first. */
|
|
11
12
|
const FTS_CHUNK_CACHE_SIZE = 64;
|
|
13
|
+
/** Extent reads in flight for a range or ordered postings scan. */
|
|
14
|
+
const FTS_CHUNK_READ_CONCURRENCY = 8;
|
|
12
15
|
/**
|
|
13
16
|
* The leader: the one instance holding the database's write-ahead log handle, and with it
|
|
14
17
|
* every other handle — checkpoint slots, the tail extent, a small cache of sealed extents.
|
|
@@ -32,6 +35,7 @@ export class OpfsLeader {
|
|
|
32
35
|
#core;
|
|
33
36
|
#blockIndex = new Map();
|
|
34
37
|
#ftsBases = new Map();
|
|
38
|
+
#ftsBuilds = new Map();
|
|
35
39
|
#ftsChunkCache = new Map();
|
|
36
40
|
#extents;
|
|
37
41
|
#wal;
|
|
@@ -98,6 +102,7 @@ export class OpfsLeader {
|
|
|
98
102
|
});
|
|
99
103
|
this.#blockIndex.clear();
|
|
100
104
|
this.#ftsBases.clear();
|
|
105
|
+
this.#ftsBuilds.clear();
|
|
101
106
|
this.#ftsChunkCache.clear();
|
|
102
107
|
this.#extents?.close();
|
|
103
108
|
this.#extents = await ExtentPool.open(this.#tree, checkpoint?.extents);
|
|
@@ -111,6 +116,8 @@ export class OpfsLeader {
|
|
|
111
116
|
this.#blockIndex.set(id, placement);
|
|
112
117
|
for (const [key, pointer] of checkpoint.ftsBases)
|
|
113
118
|
this.#ftsBases.set(key, pointer);
|
|
119
|
+
for (const [key, pointer] of checkpoint.ftsBuilds ?? [])
|
|
120
|
+
this.#ftsBuilds.set(key, pointer);
|
|
114
121
|
this.#seq = checkpoint.lastSeq;
|
|
115
122
|
}
|
|
116
123
|
const newest = this.#slots[this.#newestSlot];
|
|
@@ -141,6 +148,16 @@ export class OpfsLeader {
|
|
|
141
148
|
this.#wal = new WalWriter(this.#walHandle, endOffset);
|
|
142
149
|
this.#entriesSinceCheckpoint = applied;
|
|
143
150
|
this.#poisoned = false;
|
|
151
|
+
// A staged postings generation has no reader-visible pointer and no builder can prove it
|
|
152
|
+
// survived a leader death. Reclaim it now rather than retain unreachable extents. A
|
|
153
|
+
// still-live caller receives "build changed", marks its catalog record invalid, and a later
|
|
154
|
+
// query restarts from a fresh snapshot. The next ordinary checkpoint persists the
|
|
155
|
+
// reclamation; until then another crash simply replays and reclaims the same unpublished
|
|
156
|
+
// generation.
|
|
157
|
+
if (this.#ftsBuilds.size > 0) {
|
|
158
|
+
this.#pool.release([...this.#ftsBuilds.values()].flatMap((build) => build.chunks));
|
|
159
|
+
this.#ftsBuilds.clear();
|
|
160
|
+
}
|
|
144
161
|
// Extents fully drained before the crash but never deleted: finish the job.
|
|
145
162
|
for (const id of this.#pool.release([]))
|
|
146
163
|
await this.#pool.deleteExtent(id);
|
|
@@ -240,6 +257,13 @@ export class OpfsLeader {
|
|
|
240
257
|
throw new Error(`OPFS full-text chunk moved before relocation: ${move.key}`);
|
|
241
258
|
}
|
|
242
259
|
}
|
|
260
|
+
for (const move of body.ftsBuildChunks ?? []) {
|
|
261
|
+
const build = this.#ftsBuilds.get(move.key);
|
|
262
|
+
if (build?.buildId !== move.buildId ||
|
|
263
|
+
!samePlacement(build.chunks[move.ordinal], move.from)) {
|
|
264
|
+
throw new Error(`OPFS staged postings chunk moved before relocation: ${move.key}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
243
267
|
for (const move of body.blocks)
|
|
244
268
|
this.#blockIndex.set(move.id, move.placement);
|
|
245
269
|
for (const move of body.ftsChunks) {
|
|
@@ -249,16 +273,53 @@ export class OpfsLeader {
|
|
|
249
273
|
pointer.chunks[move.ordinal] = move.placement;
|
|
250
274
|
this.#dropFtsChunkCache(move.key);
|
|
251
275
|
}
|
|
276
|
+
for (const move of body.ftsBuildChunks ?? []) {
|
|
277
|
+
const build = this.#ftsBuilds.get(move.key);
|
|
278
|
+
if (build === undefined) {
|
|
279
|
+
throw new Error(`OPFS staged postings build is missing: ${move.key}`);
|
|
280
|
+
}
|
|
281
|
+
build.chunks[move.ordinal] = move.placement;
|
|
282
|
+
}
|
|
252
283
|
this.#pool.release([
|
|
253
284
|
...body.blocks.map((move) => move.from),
|
|
254
285
|
...body.ftsChunks.map((move) => move.from),
|
|
286
|
+
...(body.ftsBuildChunks ?? []).map((move) => move.from),
|
|
255
287
|
]);
|
|
256
288
|
return undefined;
|
|
257
289
|
}
|
|
258
290
|
case "addTable":
|
|
259
291
|
return this.#core.addTable(body.record);
|
|
260
|
-
case "updateTable":
|
|
261
|
-
|
|
292
|
+
case "updateTable": {
|
|
293
|
+
const before = this.#core.getTable(body.id);
|
|
294
|
+
const updated = this.#core.updateTable(body.id, body.expectedRevision, body.update);
|
|
295
|
+
if (before !== undefined) {
|
|
296
|
+
const beforeStorageIds = new Set([
|
|
297
|
+
...Object.keys(before.ftsColumns ?? {}),
|
|
298
|
+
...Object.values(before.secondaryIndexes ?? {}).map((index) => index.storageColumnId),
|
|
299
|
+
]);
|
|
300
|
+
const retainedStorageIds = new Set([
|
|
301
|
+
...Object.keys(updated.ftsColumns ?? {}),
|
|
302
|
+
...Object.values(updated.secondaryIndexes ?? {}).map((index) => index.storageColumnId),
|
|
303
|
+
]);
|
|
304
|
+
for (const storageColumnId of beforeStorageIds) {
|
|
305
|
+
if (retainedStorageIds.has(storageColumnId))
|
|
306
|
+
continue;
|
|
307
|
+
const key = `${before.id}/${storageColumnId}`;
|
|
308
|
+
const pointer = this.#ftsBases.get(key);
|
|
309
|
+
if (pointer !== undefined) {
|
|
310
|
+
this.#ftsBases.delete(key);
|
|
311
|
+
this.#dropFtsChunkCache(key);
|
|
312
|
+
this.#pool.release(pointer.chunks);
|
|
313
|
+
}
|
|
314
|
+
const build = this.#ftsBuilds.get(key);
|
|
315
|
+
if (build !== undefined) {
|
|
316
|
+
this.#ftsBuilds.delete(key);
|
|
317
|
+
this.#pool.release(build.chunks);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return updated;
|
|
322
|
+
}
|
|
262
323
|
case "removeTable": {
|
|
263
324
|
this.#core.removeTable(body.id, body.expectedRevision);
|
|
264
325
|
const owned = `${body.id}/`;
|
|
@@ -269,6 +330,28 @@ export class OpfsLeader {
|
|
|
269
330
|
this.#dropFtsChunkCache(key);
|
|
270
331
|
this.#pool.release(pointer.chunks);
|
|
271
332
|
}
|
|
333
|
+
for (const [key, build] of [...this.#ftsBuilds]) {
|
|
334
|
+
if (!key.startsWith(owned))
|
|
335
|
+
continue;
|
|
336
|
+
this.#ftsBuilds.delete(key);
|
|
337
|
+
this.#pool.release(build.chunks);
|
|
338
|
+
}
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
case "removeFtsColumn": {
|
|
342
|
+
const key = `${body.tableId}/${body.columnId}`;
|
|
343
|
+
const pointer = this.#ftsBases.get(key);
|
|
344
|
+
if (pointer !== undefined) {
|
|
345
|
+
this.#ftsBases.delete(key);
|
|
346
|
+
this.#dropFtsChunkCache(key);
|
|
347
|
+
this.#pool.release(pointer.chunks);
|
|
348
|
+
}
|
|
349
|
+
const build = this.#ftsBuilds.get(key);
|
|
350
|
+
if (build !== undefined) {
|
|
351
|
+
this.#ftsBuilds.delete(key);
|
|
352
|
+
this.#pool.release(build.chunks);
|
|
353
|
+
}
|
|
354
|
+
this.#core.removeFtsColumn(body.tableId, body.columnId);
|
|
272
355
|
return undefined;
|
|
273
356
|
}
|
|
274
357
|
case "addSegment":
|
|
@@ -347,6 +430,11 @@ export class OpfsLeader {
|
|
|
347
430
|
return this.#core.removeTempOwner(body.ownerId);
|
|
348
431
|
case "writeFtsBase": {
|
|
349
432
|
const key = `${body.tableId}/${body.columnId}`;
|
|
433
|
+
const build = this.#ftsBuilds.get(key);
|
|
434
|
+
if (build !== undefined) {
|
|
435
|
+
this.#ftsBuilds.delete(key);
|
|
436
|
+
this.#pool.release(build.chunks);
|
|
437
|
+
}
|
|
350
438
|
const previous = this.#ftsBases.get(key);
|
|
351
439
|
if (previous !== undefined) {
|
|
352
440
|
this.#pool.release(previous.chunks);
|
|
@@ -356,6 +444,67 @@ export class OpfsLeader {
|
|
|
356
444
|
this.#core.pruneFtsDeltas(body.tableId, body.columnId, body.pointer.coversVersion);
|
|
357
445
|
return undefined;
|
|
358
446
|
}
|
|
447
|
+
case "beginFtsBaseBuild": {
|
|
448
|
+
const table = this.#core.getTable(body.tableId);
|
|
449
|
+
if (table === undefined || !activePostingStorageColumnIds(table).has(body.columnId)) {
|
|
450
|
+
throw new Error(`Postings index is no longer active: ${body.tableId}/${body.columnId}`);
|
|
451
|
+
}
|
|
452
|
+
const key = `${body.tableId}/${body.columnId}`;
|
|
453
|
+
const previous = this.#ftsBuilds.get(key);
|
|
454
|
+
if (previous !== undefined)
|
|
455
|
+
this.#pool.release(previous.chunks);
|
|
456
|
+
this.#ftsBuilds.set(key, { buildId: body.buildId, chunks: [], chunkBounds: [] });
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
459
|
+
case "writeFtsBaseBuildChunk": {
|
|
460
|
+
const key = `${body.tableId}/${body.columnId}`;
|
|
461
|
+
const build = this.#ftsBuilds.get(key);
|
|
462
|
+
if (build?.buildId !== body.buildId) {
|
|
463
|
+
throw new Error(`Full-text base build changed: ${body.buildId}`);
|
|
464
|
+
}
|
|
465
|
+
if (body.ordinal !== build.chunks.length) {
|
|
466
|
+
throw new Error(`Full-text base chunk is out of order: ${String(body.ordinal)}`);
|
|
467
|
+
}
|
|
468
|
+
build.chunks.push(body.placement);
|
|
469
|
+
build.chunkBounds.push(body.bounds);
|
|
470
|
+
return undefined;
|
|
471
|
+
}
|
|
472
|
+
case "finishFtsBaseBuild": {
|
|
473
|
+
const key = `${body.tableId}/${body.columnId}`;
|
|
474
|
+
const build = this.#ftsBuilds.get(key);
|
|
475
|
+
if (build?.buildId !== body.buildId || build.chunks.length !== body.input.chunkCount) {
|
|
476
|
+
throw new Error(`Full-text base build is incomplete: ${body.buildId}`);
|
|
477
|
+
}
|
|
478
|
+
const table = this.#core.getTable(body.tableId);
|
|
479
|
+
if (table === undefined || !activePostingStorageColumnIds(table).has(body.columnId)) {
|
|
480
|
+
this.#ftsBuilds.delete(key);
|
|
481
|
+
this.#pool.release(build.chunks);
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
const previous = this.#ftsBases.get(key);
|
|
485
|
+
if (previous !== undefined) {
|
|
486
|
+
this.#pool.release(previous.chunks);
|
|
487
|
+
this.#dropFtsChunkCache(key);
|
|
488
|
+
}
|
|
489
|
+
this.#ftsBases.set(key, {
|
|
490
|
+
coversVersion: body.input.coversVersion,
|
|
491
|
+
totalTokens: body.input.totalTokens,
|
|
492
|
+
chunks: build.chunks,
|
|
493
|
+
chunkBounds: build.chunkBounds,
|
|
494
|
+
});
|
|
495
|
+
this.#ftsBuilds.delete(key);
|
|
496
|
+
this.#core.pruneFtsDeltas(body.tableId, body.columnId, body.input.coversVersion);
|
|
497
|
+
return undefined;
|
|
498
|
+
}
|
|
499
|
+
case "abortFtsBaseBuild": {
|
|
500
|
+
const key = `${body.tableId}/${body.columnId}`;
|
|
501
|
+
const build = this.#ftsBuilds.get(key);
|
|
502
|
+
if (build?.buildId === body.buildId) {
|
|
503
|
+
this.#ftsBuilds.delete(key);
|
|
504
|
+
this.#pool.release(build.chunks);
|
|
505
|
+
}
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
359
508
|
case "importSnapshot": {
|
|
360
509
|
if (this.#core.getCurrentManifestVersion() !== null || this.#core.listTables().length > 0) {
|
|
361
510
|
throw new Error("This store already holds a database");
|
|
@@ -395,6 +544,7 @@ export class OpfsLeader {
|
|
|
395
544
|
core: this.#core.dump(),
|
|
396
545
|
blockIndex: [...this.#blockIndex.entries()],
|
|
397
546
|
ftsBases: [...this.#ftsBases.entries()],
|
|
547
|
+
ftsBuilds: [...this.#ftsBuilds.entries()],
|
|
398
548
|
extents: this.#pool.meta(),
|
|
399
549
|
};
|
|
400
550
|
const bytes = encodeSyncCheckpoint(state);
|
|
@@ -423,11 +573,13 @@ export class OpfsLeader {
|
|
|
423
573
|
.filter(([, placement]) => placement.extent === extent)
|
|
424
574
|
.map(([id, from]) => ({ id, from }));
|
|
425
575
|
const ftsSources = [...this.#ftsBases.entries()].flatMap(([key, pointer]) => pointer.chunks.flatMap((from, ordinal) => from.extent === extent ? [{ key, ordinal, from }] : []));
|
|
426
|
-
|
|
576
|
+
const ftsBuildSources = [...this.#ftsBuilds.entries()].flatMap(([key, build]) => build.chunks.flatMap((from, ordinal) => from.extent === extent ? [{ key, buildId: build.buildId, ordinal, from }] : []));
|
|
577
|
+
if (blockSources.length === 0 && ftsSources.length === 0 && ftsBuildSources.length === 0) {
|
|
427
578
|
throw new Error(`OPFS extent ${String(extent)} has live bytes but no indexed payload`);
|
|
428
579
|
}
|
|
429
580
|
const blocks = [];
|
|
430
581
|
const ftsChunks = [];
|
|
582
|
+
const ftsBuildChunks = [];
|
|
431
583
|
const appended = [];
|
|
432
584
|
try {
|
|
433
585
|
for (const source of blockSources) {
|
|
@@ -440,7 +592,12 @@ export class OpfsLeader {
|
|
|
440
592
|
appended.push(placement);
|
|
441
593
|
ftsChunks.push({ ...source, placement });
|
|
442
594
|
}
|
|
443
|
-
|
|
595
|
+
for (const source of ftsBuildSources) {
|
|
596
|
+
const placement = await this.#pool.append(await this.#pool.read(source.from), this.#strict);
|
|
597
|
+
appended.push(placement);
|
|
598
|
+
ftsBuildChunks.push({ ...source, placement });
|
|
599
|
+
}
|
|
600
|
+
this.#logged({ op: "relocatePayloads", blocks, ftsChunks, ftsBuildChunks });
|
|
444
601
|
}
|
|
445
602
|
catch (error) {
|
|
446
603
|
this.#pool.release(appended);
|
|
@@ -574,6 +731,10 @@ export class OpfsLeader {
|
|
|
574
731
|
await this.#healthy();
|
|
575
732
|
return this.#core.listSegments(tableId);
|
|
576
733
|
}
|
|
734
|
+
async listSegmentPage(afterId, limit) {
|
|
735
|
+
await this.#healthy();
|
|
736
|
+
return this.#core.listSegmentPage(afterId, limit);
|
|
737
|
+
}
|
|
577
738
|
async getExistingUniqueKeys(tableId, keyTokens) {
|
|
578
739
|
await this.#healthy();
|
|
579
740
|
return this.#core.getExistingUniqueKeys(tableId, keyTokens);
|
|
@@ -657,7 +818,21 @@ export class OpfsLeader {
|
|
|
657
818
|
await this.#run(() => this.#logged({ op: "addTable", record }));
|
|
658
819
|
}
|
|
659
820
|
async updateTable(id, expectedRevision, update) {
|
|
660
|
-
return this.#run(() =>
|
|
821
|
+
return this.#run(async () => {
|
|
822
|
+
const updated = this.#logged({
|
|
823
|
+
op: "updateTable",
|
|
824
|
+
id,
|
|
825
|
+
expectedRevision,
|
|
826
|
+
update,
|
|
827
|
+
});
|
|
828
|
+
if (update.columns !== undefined) {
|
|
829
|
+
// DROP COLUMN may have released full-text chunks. Reclaim their extents before this
|
|
830
|
+
// mutation resolves so repeated schema evolution cannot grow the live process's disk.
|
|
831
|
+
await this.#deleteDrainedExtents();
|
|
832
|
+
await this.#compactFragmentedExtents();
|
|
833
|
+
}
|
|
834
|
+
return updated;
|
|
835
|
+
});
|
|
661
836
|
}
|
|
662
837
|
async removeTable(id, expectedRevision) {
|
|
663
838
|
await this.#run(async () => {
|
|
@@ -666,6 +841,13 @@ export class OpfsLeader {
|
|
|
666
841
|
await this.#compactFragmentedExtents();
|
|
667
842
|
});
|
|
668
843
|
}
|
|
844
|
+
async removeFtsColumn(tableId, columnId) {
|
|
845
|
+
await this.#run(async () => {
|
|
846
|
+
this.#logged({ op: "removeFtsColumn", tableId, columnId });
|
|
847
|
+
await this.#deleteDrainedExtents();
|
|
848
|
+
await this.#compactFragmentedExtents();
|
|
849
|
+
});
|
|
850
|
+
}
|
|
669
851
|
async addSegment(record) {
|
|
670
852
|
await this.#run(() => this.#logged({ op: "addSegment", record }));
|
|
671
853
|
}
|
|
@@ -884,21 +1066,95 @@ export class OpfsLeader {
|
|
|
884
1066
|
await this.#compactFragmentedExtents();
|
|
885
1067
|
});
|
|
886
1068
|
}
|
|
1069
|
+
async beginFtsBaseBuild(tableId, columnId, buildId) {
|
|
1070
|
+
await this.#run(() => {
|
|
1071
|
+
const table = this.#core.getTable(tableId);
|
|
1072
|
+
if (table === undefined || !activePostingStorageColumnIds(table).has(columnId)) {
|
|
1073
|
+
throw new Error(`Postings index is no longer active: ${tableId}/${columnId}`);
|
|
1074
|
+
}
|
|
1075
|
+
this.#logged({ op: "beginFtsBaseBuild", tableId, columnId, buildId });
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
async writeFtsBaseBuildChunk(tableId, columnId, buildId, ordinal, chunk) {
|
|
1079
|
+
await this.#run(async () => {
|
|
1080
|
+
const build = this.#ftsBuilds.get(`${tableId}/${columnId}`);
|
|
1081
|
+
if (build?.buildId !== buildId)
|
|
1082
|
+
throw new Error(`Full-text base build changed: ${buildId}`);
|
|
1083
|
+
if (ordinal !== build.chunks.length) {
|
|
1084
|
+
throw new Error(`Full-text base chunk is out of order: ${String(ordinal)}`);
|
|
1085
|
+
}
|
|
1086
|
+
const placement = await this.#pool.append(encodeFtsChunk(chunk), this.#strict);
|
|
1087
|
+
this.#logged({
|
|
1088
|
+
op: "writeFtsBaseBuildChunk",
|
|
1089
|
+
tableId,
|
|
1090
|
+
columnId,
|
|
1091
|
+
buildId,
|
|
1092
|
+
ordinal,
|
|
1093
|
+
placement,
|
|
1094
|
+
bounds: { first: chunk[0]?.term ?? "", last: chunk[chunk.length - 1]?.term ?? "" },
|
|
1095
|
+
});
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
async finishFtsBaseBuild(tableId, columnId, buildId, input) {
|
|
1099
|
+
await this.#run(async () => {
|
|
1100
|
+
this.#logged({ op: "finishFtsBaseBuild", tableId, columnId, buildId, input });
|
|
1101
|
+
await this.#deleteDrainedExtents();
|
|
1102
|
+
await this.#compactFragmentedExtents();
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
async abortFtsBaseBuild(tableId, columnId, buildId) {
|
|
1106
|
+
await this.#run(async () => {
|
|
1107
|
+
this.#logged({ op: "abortFtsBaseBuild", tableId, columnId, buildId });
|
|
1108
|
+
await this.#deleteDrainedExtents();
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
887
1111
|
async readFtsCandidates(tableId, columnId, terms, upToVersion) {
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1112
|
+
return this.#run(async () => {
|
|
1113
|
+
// Unlike immutable table blocks, derived-index extents are not protected by reader leases:
|
|
1114
|
+
// DROP INDEX may reclaim them immediately. Hold the leader queue until every selected
|
|
1115
|
+
// chunk is copied so a concurrent drop yields either the complete old base or no base,
|
|
1116
|
+
// never a missing file halfway through a query.
|
|
1117
|
+
const key = `${tableId}/${columnId}`;
|
|
1118
|
+
const pointer = this.#ftsBases.get(key);
|
|
1119
|
+
const coversVersion = pointer?.coversVersion ?? -1;
|
|
1120
|
+
const deltas = this.#core.readFtsDeltas(tableId, columnId, coversVersion, upToVersion);
|
|
1121
|
+
const baseChunks = pointer === undefined
|
|
1122
|
+
? []
|
|
1123
|
+
: await this.#loadFtsChunks(key, pointer, selectFtsChunks(pointer.chunkBounds, terms));
|
|
1124
|
+
return {
|
|
1125
|
+
...collectFtsCandidates([...baseChunks, ...deltas.chunkLists], terms),
|
|
1126
|
+
deltaChunkCount: deltas.deltaChunkCount,
|
|
1127
|
+
totalTokens: (pointer?.totalTokens ?? 0) + deltas.deltaTokens,
|
|
1128
|
+
coversVersion,
|
|
1129
|
+
hasBase: pointer !== undefined,
|
|
1130
|
+
};
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
async readFtsPostings(tableId, columnId, upToVersion) {
|
|
1134
|
+
return this.#run(async () => {
|
|
1135
|
+
const key = `${tableId}/${columnId}`;
|
|
1136
|
+
const pointer = this.#ftsBases.get(key);
|
|
1137
|
+
const coversVersion = pointer?.coversVersion ?? -1;
|
|
1138
|
+
const deltas = this.#core.readFtsDeltas(tableId, columnId, coversVersion, upToVersion);
|
|
1139
|
+
const baseChunks = pointer === undefined
|
|
1140
|
+
? []
|
|
1141
|
+
: await this.#loadFtsChunks(key, pointer, pointer.chunks.map((_, ordinal) => ordinal));
|
|
1142
|
+
return {
|
|
1143
|
+
postings: collectFtsPostings([...baseChunks, ...deltas.chunkLists]),
|
|
1144
|
+
deltaChunkCount: deltas.deltaChunkCount,
|
|
1145
|
+
coversVersion,
|
|
1146
|
+
hasBase: pointer !== undefined,
|
|
1147
|
+
};
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
async #loadFtsChunks(key, pointer, ordinals) {
|
|
1151
|
+
const chunks = [];
|
|
1152
|
+
for (let start = 0; start < ordinals.length; start += FTS_CHUNK_READ_CONCURRENCY) {
|
|
1153
|
+
chunks.push(...(await Promise.all(ordinals
|
|
1154
|
+
.slice(start, start + FTS_CHUNK_READ_CONCURRENCY)
|
|
1155
|
+
.map((ordinal) => this.#loadFtsChunk(key, pointer, ordinal)))));
|
|
1156
|
+
}
|
|
1157
|
+
return chunks;
|
|
902
1158
|
}
|
|
903
1159
|
async #loadFtsChunk(key, pointer, ordinal) {
|
|
904
1160
|
const placement = pointer.chunks[ordinal];
|
|
@@ -944,6 +1200,7 @@ export class OpfsLeader {
|
|
|
944
1200
|
});
|
|
945
1201
|
}
|
|
946
1202
|
async importSnapshot(snapshot, options = {}) {
|
|
1203
|
+
validateSnapshotCatalog(snapshot.tables);
|
|
947
1204
|
const totalBytes = snapshot.blocks.reduce((total, block) => total + block.bytes.byteLength, 0);
|
|
948
1205
|
options.onProgress?.({ phase: "blocks", writtenBytes: 0, totalBytes });
|
|
949
1206
|
await this.#run(async () => {
|
|
@@ -1052,12 +1309,15 @@ function placementsOf(entry) {
|
|
|
1052
1309
|
return entry.blocks.map(({ placement }) => placement);
|
|
1053
1310
|
case "writeFtsBase":
|
|
1054
1311
|
return entry.pointer.chunks;
|
|
1312
|
+
case "writeFtsBaseBuildChunk":
|
|
1313
|
+
return [entry.placement];
|
|
1055
1314
|
case "importSnapshot":
|
|
1056
1315
|
return entry.blockPlacements.map(({ placement }) => placement);
|
|
1057
1316
|
case "relocatePayloads":
|
|
1058
1317
|
return [
|
|
1059
1318
|
...entry.blocks.map(({ placement }) => placement),
|
|
1060
1319
|
...entry.ftsChunks.map(({ placement }) => placement),
|
|
1320
|
+
...(entry.ftsBuildChunks ?? []).map(({ placement }) => placement),
|
|
1061
1321
|
];
|
|
1062
1322
|
default:
|
|
1063
1323
|
return [];
|
|
@@ -1067,10 +1327,12 @@ function samePlacement(left, right) {
|
|
|
1067
1327
|
return (left?.extent === right.extent && left.offset === right.offset && left.length === right.length);
|
|
1068
1328
|
}
|
|
1069
1329
|
function encodeFtsChunk(chunk) {
|
|
1070
|
-
return
|
|
1330
|
+
return encodePostingChunk(chunk);
|
|
1071
1331
|
}
|
|
1072
1332
|
function decodeFtsChunk(bytes) {
|
|
1073
|
-
|
|
1333
|
+
// Generic JSON chunks were written by format-v2 releases before the compact postings codec.
|
|
1334
|
+
// Keep reading them in place; the next rebuild naturally writes the compact representation.
|
|
1335
|
+
const chunk = decodePostingChunk(bytes) ?? decodeChunk(bytes);
|
|
1074
1336
|
if (chunk === undefined)
|
|
1075
1337
|
throw new Error("Full-text base chunk is unreadable");
|
|
1076
1338
|
return chunk;
|
|
@@ -1079,13 +1341,11 @@ function decodeFtsChunk(bytes) {
|
|
|
1079
1341
|
function selectFtsChunks(chunkBounds, terms) {
|
|
1080
1342
|
const ordinals = [];
|
|
1081
1343
|
for (const [ordinal, bounds] of chunkBounds.entries()) {
|
|
1082
|
-
const needed = terms.some((
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
}
|
|
1088
|
-
return bounds.first <= term;
|
|
1344
|
+
const needed = terms.some((query) => {
|
|
1345
|
+
const lower = "term" in query ? query.term : query.lower;
|
|
1346
|
+
const upper = "term" in query ? (query.prefix ? `${query.term}` : query.term) : query.upper;
|
|
1347
|
+
return ((lower === undefined || lower <= bounds.last) &&
|
|
1348
|
+
(upper === undefined || upper >= bounds.first));
|
|
1089
1349
|
});
|
|
1090
1350
|
if (needed)
|
|
1091
1351
|
ordinals.push(ordinal);
|
|
@@ -1093,12 +1353,19 @@ function selectFtsChunks(chunkBounds, terms) {
|
|
|
1093
1353
|
return ordinals;
|
|
1094
1354
|
}
|
|
1095
1355
|
function invalidateFtsColumns(record) {
|
|
1096
|
-
|
|
1097
|
-
return record;
|
|
1098
|
-
const ftsColumns = Object.fromEntries(Object.entries(record.ftsColumns).map(([columnId, state]) => [
|
|
1356
|
+
const ftsColumns = Object.fromEntries(Object.entries(record.ftsColumns ?? {}).map(([columnId, state]) => [
|
|
1099
1357
|
columnId,
|
|
1100
1358
|
{ ...state, state: "invalid" },
|
|
1101
1359
|
]));
|
|
1102
|
-
|
|
1360
|
+
const secondaryIndexes = Object.fromEntries(Object.entries(record.secondaryIndexes ?? {}).map(([indexId, state]) => {
|
|
1361
|
+
const { buildId: _abandonedBuild, ...invalid } = state;
|
|
1362
|
+
void _abandonedBuild;
|
|
1363
|
+
return [indexId, { ...invalid, state: "invalid" }];
|
|
1364
|
+
}));
|
|
1365
|
+
return {
|
|
1366
|
+
...record,
|
|
1367
|
+
...(Object.keys(ftsColumns).length === 0 ? {} : { ftsColumns }),
|
|
1368
|
+
...(Object.keys(secondaryIndexes).length === 0 ? {} : { secondaryIndexes }),
|
|
1369
|
+
};
|
|
1103
1370
|
}
|
|
1104
1371
|
//# sourceMappingURL=leader.js.map
|