@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.
Files changed (146) hide show
  1. package/README.md +21 -32
  2. package/dist/engine/artifact-cache.d.ts +2 -0
  3. package/dist/engine/artifact-cache.d.ts.map +1 -1
  4. package/dist/engine/artifact-cache.js +5 -0
  5. package/dist/engine/artifact-cache.js.map +1 -1
  6. package/dist/engine/batch.d.ts +6 -1
  7. package/dist/engine/batch.d.ts.map +1 -1
  8. package/dist/engine/batch.js +17 -6
  9. package/dist/engine/batch.js.map +1 -1
  10. package/dist/engine/catalog.d.ts +22 -2
  11. package/dist/engine/catalog.d.ts.map +1 -1
  12. package/dist/engine/catalog.js +30 -3
  13. package/dist/engine/catalog.js.map +1 -1
  14. package/dist/engine/client.d.ts +3 -0
  15. package/dist/engine/client.d.ts.map +1 -1
  16. package/dist/engine/client.js +21 -10
  17. package/dist/engine/client.js.map +1 -1
  18. package/dist/engine/database.d.ts +71 -5
  19. package/dist/engine/database.d.ts.map +1 -1
  20. package/dist/engine/database.js +3619 -421
  21. package/dist/engine/database.js.map +1 -1
  22. package/dist/engine/defaults.d.ts +4 -7
  23. package/dist/engine/defaults.d.ts.map +1 -1
  24. package/dist/engine/defaults.js +47 -21
  25. package/dist/engine/defaults.js.map +1 -1
  26. package/dist/engine/fts.d.ts.map +1 -1
  27. package/dist/engine/fts.js +2 -0
  28. package/dist/engine/fts.js.map +1 -1
  29. package/dist/engine/index.d.ts +1 -0
  30. package/dist/engine/index.d.ts.map +1 -1
  31. package/dist/engine/index.js +1 -0
  32. package/dist/engine/index.js.map +1 -1
  33. package/dist/engine/live.d.ts +6 -2
  34. package/dist/engine/live.d.ts.map +1 -1
  35. package/dist/engine/live.js +10 -2
  36. package/dist/engine/live.js.map +1 -1
  37. package/dist/engine/optimizer.d.ts.map +1 -1
  38. package/dist/engine/optimizer.js +258 -23
  39. package/dist/engine/optimizer.js.map +1 -1
  40. package/dist/engine/query-cache.d.ts +1 -1
  41. package/dist/engine/query-cache.d.ts.map +1 -1
  42. package/dist/engine/query-cache.js +3 -1
  43. package/dist/engine/query-cache.js.map +1 -1
  44. package/dist/engine/query.d.ts +132 -22
  45. package/dist/engine/query.d.ts.map +1 -1
  46. package/dist/engine/query.js +1368 -246
  47. package/dist/engine/query.js.map +1 -1
  48. package/dist/engine/schema-wire.d.ts +6 -2
  49. package/dist/engine/schema-wire.d.ts.map +1 -1
  50. package/dist/engine/schema-wire.js +40 -33
  51. package/dist/engine/schema-wire.js.map +1 -1
  52. package/dist/engine/schema.d.ts +157 -76
  53. package/dist/engine/schema.d.ts.map +1 -1
  54. package/dist/engine/schema.js +548 -103
  55. package/dist/engine/schema.js.map +1 -1
  56. package/dist/engine/sort-keys.d.ts +4 -3
  57. package/dist/engine/sort-keys.d.ts.map +1 -1
  58. package/dist/engine/sort-keys.js +42 -27
  59. package/dist/engine/sort-keys.js.map +1 -1
  60. package/dist/engine/sql-domains.d.ts +26 -0
  61. package/dist/engine/sql-domains.d.ts.map +1 -0
  62. package/dist/engine/sql-domains.js +421 -0
  63. package/dist/engine/sql-domains.js.map +1 -0
  64. package/dist/engine/sql-driver.d.ts +19 -0
  65. package/dist/engine/sql-driver.d.ts.map +1 -0
  66. package/dist/engine/sql-driver.js +2 -0
  67. package/dist/engine/sql-driver.js.map +1 -0
  68. package/dist/engine/sql-json.d.ts +7 -8
  69. package/dist/engine/sql-json.d.ts.map +1 -1
  70. package/dist/engine/sql-json.js +28 -14
  71. package/dist/engine/sql-json.js.map +1 -1
  72. package/dist/engine/sql-semantics.d.ts +2 -0
  73. package/dist/engine/sql-semantics.d.ts.map +1 -1
  74. package/dist/engine/sql-semantics.js +69 -3
  75. package/dist/engine/sql-semantics.js.map +1 -1
  76. package/dist/engine/vector.d.ts +5 -1
  77. package/dist/engine/vector.d.ts.map +1 -1
  78. package/dist/engine/vector.js +433 -61
  79. package/dist/engine/vector.js.map +1 -1
  80. package/dist/engine/worker-host.d.ts +1 -0
  81. package/dist/engine/worker-host.d.ts.map +1 -1
  82. package/dist/engine/worker-host.js +8 -0
  83. package/dist/engine/worker-host.js.map +1 -1
  84. package/dist/plan/index.d.ts +3 -3
  85. package/dist/plan/index.js +3 -3
  86. package/dist/storage/indexeddb.d.ts +44 -5
  87. package/dist/storage/indexeddb.d.ts.map +1 -1
  88. package/dist/storage/indexeddb.js +738 -175
  89. package/dist/storage/indexeddb.js.map +1 -1
  90. package/dist/storage/memory.d.ts +27 -10
  91. package/dist/storage/memory.d.ts.map +1 -1
  92. package/dist/storage/memory.js +67 -18
  93. package/dist/storage/memory.js.map +1 -1
  94. package/dist/storage/opfs/leader.d.ts +28 -6
  95. package/dist/storage/opfs/leader.d.ts.map +1 -1
  96. package/dist/storage/opfs/leader.js +301 -34
  97. package/dist/storage/opfs/leader.js.map +1 -1
  98. package/dist/storage/opfs/rpc.d.ts.map +1 -1
  99. package/dist/storage/opfs/rpc.js +2 -1
  100. package/dist/storage/opfs/rpc.js.map +1 -1
  101. package/dist/storage/opfs/store.d.ts +34 -5
  102. package/dist/storage/opfs/store.d.ts.map +1 -1
  103. package/dist/storage/opfs/store.js +41 -0
  104. package/dist/storage/opfs/store.js.map +1 -1
  105. package/dist/storage/snapshot.d.ts +14 -0
  106. package/dist/storage/snapshot.d.ts.map +1 -1
  107. package/dist/storage/snapshot.js +44 -0
  108. package/dist/storage/snapshot.js.map +1 -1
  109. package/dist/storage/toolkit/index.d.ts +1 -1
  110. package/dist/storage/toolkit/index.d.ts.map +1 -1
  111. package/dist/storage/toolkit/index.js +1 -1
  112. package/dist/storage/toolkit/index.js.map +1 -1
  113. package/dist/storage/toolkit/record-core.d.ts +19 -5
  114. package/dist/storage/toolkit/record-core.d.ts.map +1 -1
  115. package/dist/storage/toolkit/record-core.js +353 -78
  116. package/dist/storage/toolkit/record-core.js.map +1 -1
  117. package/dist/storage/toolkit/wire.d.ts +15 -0
  118. package/dist/storage/toolkit/wire.d.ts.map +1 -1
  119. package/dist/storage/toolkit/wire.js +134 -0
  120. package/dist/storage/toolkit/wire.js.map +1 -1
  121. package/dist/storage/types.d.ts +213 -31
  122. package/dist/storage/types.d.ts.map +1 -1
  123. package/dist/storage/types.js +291 -20
  124. package/dist/storage/types.js.map +1 -1
  125. package/dist/testing/block-store-conformance.d.ts.map +1 -1
  126. package/dist/testing/block-store-conformance.js +153 -2
  127. package/dist/testing/block-store-conformance.js.map +1 -1
  128. package/dist/testing/index.d.ts +9 -0
  129. package/dist/testing/index.d.ts.map +1 -1
  130. package/dist/testing/index.js +23 -0
  131. package/dist/testing/index.js.map +1 -1
  132. package/dist/testing/simulator.d.ts +98 -0
  133. package/dist/testing/simulator.d.ts.map +1 -0
  134. package/dist/testing/simulator.js +560 -0
  135. package/dist/testing/simulator.js.map +1 -0
  136. package/dist/testing/sqllogictest.d.ts +90 -0
  137. package/dist/testing/sqllogictest.d.ts.map +1 -0
  138. package/dist/testing/sqllogictest.js +435 -0
  139. package/dist/testing/sqllogictest.js.map +1 -0
  140. package/dist/transactions/index.d.ts +18 -3
  141. package/dist/transactions/index.d.ts.map +1 -1
  142. package/dist/transactions/index.js +64 -8
  143. package/dist/transactions/index.js.map +1 -1
  144. package/package.json +5 -3
  145. package/postgres-feature-profile.json +223 -0
  146. 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, encodeChunk, encodeSyncCheckpoint, } from "../toolkit/wire.js";
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
- return this.#core.updateTable(body.id, body.expectedRevision, body.update);
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
- if (blockSources.length === 0 && ftsSources.length === 0) {
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
- this.#logged({ op: "relocatePayloads", blocks, ftsChunks });
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(() => this.#logged({ op: "updateTable", id, expectedRevision, update }));
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
- await this.#healthy();
889
- const key = `${tableId}/${columnId}`;
890
- const pointer = this.#ftsBases.get(key);
891
- const coversVersion = pointer?.coversVersion ?? -1;
892
- const deltas = this.#core.readFtsDeltas(tableId, columnId, coversVersion, upToVersion);
893
- const baseChunks = pointer === undefined
894
- ? []
895
- : await Promise.all(selectFtsChunks(pointer.chunkBounds, terms).map((ordinal) => this.#loadFtsChunk(key, pointer, ordinal)));
896
- return {
897
- ...collectFtsCandidates([...baseChunks, ...deltas.chunkLists], terms),
898
- deltaChunkCount: deltas.deltaChunkCount,
899
- totalTokens: (pointer?.totalTokens ?? 0) + deltas.deltaTokens,
900
- coversVersion,
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 encodeChunk(chunk);
1330
+ return encodePostingChunk(chunk);
1071
1331
  }
1072
1332
  function decodeFtsChunk(bytes) {
1073
- const chunk = decodeChunk(bytes);
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(({ term, prefix }) => {
1083
- if (bounds.last < term)
1084
- return false;
1085
- if (prefix) {
1086
- return bounds.first <= `${term}￿` || bounds.first.startsWith(term);
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
- if (record.ftsColumns === undefined)
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
- return { ...record, ftsColumns };
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