@minnowdb/core 0.2.1 → 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 (145) 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 +2 -0
  15. package/dist/engine/client.d.ts.map +1 -1
  16. package/dist/engine/client.js +20 -10
  17. package/dist/engine/client.js.map +1 -1
  18. package/dist/engine/database.d.ts +67 -5
  19. package/dist/engine/database.d.ts.map +1 -1
  20. package/dist/engine/database.js +3508 -369
  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.map +1 -1
  34. package/dist/engine/live.js +10 -2
  35. package/dist/engine/live.js.map +1 -1
  36. package/dist/engine/optimizer.d.ts.map +1 -1
  37. package/dist/engine/optimizer.js +258 -23
  38. package/dist/engine/optimizer.js.map +1 -1
  39. package/dist/engine/query-cache.d.ts +1 -1
  40. package/dist/engine/query-cache.d.ts.map +1 -1
  41. package/dist/engine/query-cache.js +3 -1
  42. package/dist/engine/query-cache.js.map +1 -1
  43. package/dist/engine/query.d.ts +130 -22
  44. package/dist/engine/query.d.ts.map +1 -1
  45. package/dist/engine/query.js +1344 -241
  46. package/dist/engine/query.js.map +1 -1
  47. package/dist/engine/schema-wire.d.ts +6 -2
  48. package/dist/engine/schema-wire.d.ts.map +1 -1
  49. package/dist/engine/schema-wire.js +40 -33
  50. package/dist/engine/schema-wire.js.map +1 -1
  51. package/dist/engine/schema.d.ts +157 -76
  52. package/dist/engine/schema.d.ts.map +1 -1
  53. package/dist/engine/schema.js +548 -103
  54. package/dist/engine/schema.js.map +1 -1
  55. package/dist/engine/sort-keys.d.ts +4 -3
  56. package/dist/engine/sort-keys.d.ts.map +1 -1
  57. package/dist/engine/sort-keys.js +42 -27
  58. package/dist/engine/sort-keys.js.map +1 -1
  59. package/dist/engine/sql-domains.d.ts +26 -0
  60. package/dist/engine/sql-domains.d.ts.map +1 -0
  61. package/dist/engine/sql-domains.js +421 -0
  62. package/dist/engine/sql-domains.js.map +1 -0
  63. package/dist/engine/sql-driver.d.ts +19 -0
  64. package/dist/engine/sql-driver.d.ts.map +1 -0
  65. package/dist/engine/sql-driver.js +2 -0
  66. package/dist/engine/sql-driver.js.map +1 -0
  67. package/dist/engine/sql-json.d.ts +7 -8
  68. package/dist/engine/sql-json.d.ts.map +1 -1
  69. package/dist/engine/sql-json.js +28 -14
  70. package/dist/engine/sql-json.js.map +1 -1
  71. package/dist/engine/sql-semantics.d.ts +2 -0
  72. package/dist/engine/sql-semantics.d.ts.map +1 -1
  73. package/dist/engine/sql-semantics.js +69 -3
  74. package/dist/engine/sql-semantics.js.map +1 -1
  75. package/dist/engine/vector.d.ts +5 -1
  76. package/dist/engine/vector.d.ts.map +1 -1
  77. package/dist/engine/vector.js +433 -61
  78. package/dist/engine/vector.js.map +1 -1
  79. package/dist/engine/worker-host.d.ts +1 -0
  80. package/dist/engine/worker-host.d.ts.map +1 -1
  81. package/dist/engine/worker-host.js +4 -0
  82. package/dist/engine/worker-host.js.map +1 -1
  83. package/dist/plan/index.d.ts +3 -3
  84. package/dist/plan/index.js +3 -3
  85. package/dist/storage/indexeddb.d.ts +44 -5
  86. package/dist/storage/indexeddb.d.ts.map +1 -1
  87. package/dist/storage/indexeddb.js +738 -175
  88. package/dist/storage/indexeddb.js.map +1 -1
  89. package/dist/storage/memory.d.ts +27 -10
  90. package/dist/storage/memory.d.ts.map +1 -1
  91. package/dist/storage/memory.js +67 -18
  92. package/dist/storage/memory.js.map +1 -1
  93. package/dist/storage/opfs/leader.d.ts +28 -6
  94. package/dist/storage/opfs/leader.d.ts.map +1 -1
  95. package/dist/storage/opfs/leader.js +301 -34
  96. package/dist/storage/opfs/leader.js.map +1 -1
  97. package/dist/storage/opfs/rpc.d.ts.map +1 -1
  98. package/dist/storage/opfs/rpc.js +2 -1
  99. package/dist/storage/opfs/rpc.js.map +1 -1
  100. package/dist/storage/opfs/store.d.ts +34 -5
  101. package/dist/storage/opfs/store.d.ts.map +1 -1
  102. package/dist/storage/opfs/store.js +41 -0
  103. package/dist/storage/opfs/store.js.map +1 -1
  104. package/dist/storage/snapshot.d.ts +14 -0
  105. package/dist/storage/snapshot.d.ts.map +1 -1
  106. package/dist/storage/snapshot.js +44 -0
  107. package/dist/storage/snapshot.js.map +1 -1
  108. package/dist/storage/toolkit/index.d.ts +1 -1
  109. package/dist/storage/toolkit/index.d.ts.map +1 -1
  110. package/dist/storage/toolkit/index.js +1 -1
  111. package/dist/storage/toolkit/index.js.map +1 -1
  112. package/dist/storage/toolkit/record-core.d.ts +19 -5
  113. package/dist/storage/toolkit/record-core.d.ts.map +1 -1
  114. package/dist/storage/toolkit/record-core.js +353 -78
  115. package/dist/storage/toolkit/record-core.js.map +1 -1
  116. package/dist/storage/toolkit/wire.d.ts +15 -0
  117. package/dist/storage/toolkit/wire.d.ts.map +1 -1
  118. package/dist/storage/toolkit/wire.js +134 -0
  119. package/dist/storage/toolkit/wire.js.map +1 -1
  120. package/dist/storage/types.d.ts +213 -31
  121. package/dist/storage/types.d.ts.map +1 -1
  122. package/dist/storage/types.js +291 -20
  123. package/dist/storage/types.js.map +1 -1
  124. package/dist/testing/block-store-conformance.d.ts.map +1 -1
  125. package/dist/testing/block-store-conformance.js +153 -2
  126. package/dist/testing/block-store-conformance.js.map +1 -1
  127. package/dist/testing/index.d.ts +9 -0
  128. package/dist/testing/index.d.ts.map +1 -1
  129. package/dist/testing/index.js +23 -0
  130. package/dist/testing/index.js.map +1 -1
  131. package/dist/testing/simulator.d.ts +98 -0
  132. package/dist/testing/simulator.d.ts.map +1 -0
  133. package/dist/testing/simulator.js +560 -0
  134. package/dist/testing/simulator.js.map +1 -0
  135. package/dist/testing/sqllogictest.d.ts +90 -0
  136. package/dist/testing/sqllogictest.d.ts.map +1 -0
  137. package/dist/testing/sqllogictest.js +435 -0
  138. package/dist/testing/sqllogictest.js.map +1 -0
  139. package/dist/transactions/index.d.ts +18 -3
  140. package/dist/transactions/index.d.ts.map +1 -1
  141. package/dist/transactions/index.js +64 -8
  142. package/dist/transactions/index.js.map +1 -1
  143. package/package.json +5 -3
  144. package/postgres-feature-profile.json +223 -0
  145. package/sql-feature-matrix.json +172 -252
@@ -25,6 +25,29 @@ export function applyManifestRecord(blockIds, record) {
25
25
  blockIds.add(id);
26
26
  }
27
27
  export const simpleDataTypes = ["boolean", "number", "string", "datetime"];
28
+ /** Validates the logical metadata shared by SQL DDL, the schema DSL, and restored catalogs. */
29
+ export function validateSqlDomain(domain, context) {
30
+ if (domain.kind === "numeric") {
31
+ const { precision, scale } = domain;
32
+ if ((precision !== undefined &&
33
+ (!Number.isSafeInteger(precision) || precision < 1 || precision > 100_000)) ||
34
+ (scale !== undefined && (!Number.isSafeInteger(scale) || scale < 0 || scale > 100_000)) ||
35
+ (precision !== undefined && scale !== undefined && scale > precision)) {
36
+ throw new TypeError(`Invalid NUMERIC domain metadata: ${context}`);
37
+ }
38
+ }
39
+ if (domain.kind === "array" &&
40
+ (domain.element.trim().length === 0 || domain.element.trim() !== domain.element)) {
41
+ throw new TypeError(`ARRAY element type must be a trimmed non-empty name: ${context}`);
42
+ }
43
+ if (domain.kind === "enum") {
44
+ if (domain.name.trim().length === 0 || domain.name.trim() !== domain.name) {
45
+ throw new TypeError(`Enum type name must be a trimmed non-empty name: ${context}`);
46
+ }
47
+ validateEnumValues(domain.values, domain.name);
48
+ }
49
+ return structuredClone(domain);
50
+ }
28
51
  /**
29
52
  * The single authority on which enum declarations are legal, shared by the schema DSL's
30
53
  * `column.enum()` and the engine's `createTable`: at least one value, every value a non-empty
@@ -46,22 +69,45 @@ export function validateEnumValues(values, context) {
46
69
  }
47
70
  return [...values];
48
71
  }
72
+ /**
73
+ * Validates the catalog invariants owned by a table's column list. Storage adapters call this
74
+ * for ordinary catalog writes and before restoring snapshots or checkpoints, so malformed
75
+ * metadata cannot enter through a less common persistence path.
76
+ */
77
+ export function validateTableColumns(columns) {
78
+ if (columns.length === 0)
79
+ throw new TypeError("A table needs at least one column");
80
+ for (const column of columns) {
81
+ const integer = column.integer;
82
+ if (integer !== undefined && (integer !== true || column.type !== "number")) {
83
+ throw new TypeError(`Integer domain requires a number column: ${column.name}`);
84
+ }
85
+ if (column.sqlDomain !== undefined && column.type !== "string") {
86
+ throw new TypeError(`SQL domain columns must use string storage: ${column.name}`);
87
+ }
88
+ if (column.sqlDomain !== undefined)
89
+ validateSqlDomain(column.sqlDomain, column.name);
90
+ if (column.integer === true && column.sqlDomain !== undefined) {
91
+ throw new TypeError(`A column cannot have both integer and SQL-domain metadata: ${column.name}`);
92
+ }
93
+ if (column.enumValues !== undefined && column.sqlDomain !== undefined) {
94
+ throw new TypeError(`A column cannot have both enum restrictions and a SQL domain: ${column.name}`);
95
+ }
96
+ }
97
+ const ids = new Set(columns.map(({ id }) => id));
98
+ const names = new Set(columns.map(({ name }) => name));
99
+ if (ids.size !== columns.length || names.size !== columns.length) {
100
+ throw new TypeError("Table columns must have unique IDs and names");
101
+ }
102
+ }
49
103
  /**
50
104
  * The single authority on which default declarations are legal, shared by the schema DSL's
51
105
  * `table()` and the engine's `createTable` so the two entry points (and the wire path between
52
- * them) can never drift: defaults require non-nullable columns, "now" is datetime-only,
53
- * auto-increment is the number unique key, and the unique key never defaults to a constant.
106
+ * them) can never drift. Storage owns structural and literal validation; the engine additionally
107
+ * parses and type-checks SQL expressions before catalog mutation.
54
108
  */
55
109
  export function validateColumnDefault(column, defaultValue) {
56
- if (column.nullable) {
57
- throw new TypeError(`Defaults require a non-nullable column: ${column.name}`);
58
- }
59
110
  switch (defaultValue.kind) {
60
- case "now":
61
- if (column.type !== "datetime") {
62
- throw new TypeError(`Default now requires a datetime column: ${column.name}`);
63
- }
64
- return;
65
111
  case "autoincrement":
66
112
  if (column.type !== "number") {
67
113
  throw new TypeError(`Auto-increment requires a number column: ${column.name}`);
@@ -70,32 +116,110 @@ export function validateColumnDefault(column, defaultValue) {
70
116
  throw new TypeError(`Auto-increment requires the unique key column: ${column.name}`);
71
117
  }
72
118
  return;
119
+ case "expression":
120
+ if (typeof defaultValue.sql !== "string" ||
121
+ defaultValue.sql.length === 0 ||
122
+ defaultValue.sql.trim() !== defaultValue.sql) {
123
+ throw new TypeError(`Default SQL must be a trimmed non-empty expression: ${column.name}`);
124
+ }
125
+ return;
73
126
  case "literal": {
74
127
  const value = defaultValue.value;
75
- if (column.type === "datetime") {
76
- throw new TypeError(`Datetime columns default with now, not a literal: ${column.name}`);
77
- }
78
- if (typeof value !== column.type) {
128
+ const numericDomain = column.sqlDomain?.kind === "numeric";
129
+ const correctType = column.type === "datetime"
130
+ ? value instanceof Date && Number.isFinite(value.getTime())
131
+ : numericDomain
132
+ ? typeof value === "number" || typeof value === "string"
133
+ : typeof value === column.type;
134
+ if (!correctType) {
79
135
  throw new TypeError(`Default literal must be a ${column.type}: ${column.name}`);
80
136
  }
81
137
  if (typeof value === "number" && !Number.isFinite(value)) {
82
138
  throw new TypeError(`Default literal must be finite: ${column.name}`);
83
139
  }
140
+ if (column.integer === true && !Number.isSafeInteger(value)) {
141
+ throw new TypeError(`Default literal must be a safe integer: ${column.name}`);
142
+ }
84
143
  if (column.enumValues !== undefined &&
85
144
  typeof value === "string" &&
86
145
  !column.enumValues.includes(value)) {
87
146
  throw new TypeError(`Default must be one of the enum values: ${column.name}`);
88
147
  }
89
- if (column.isUniqueKey) {
90
- throw new TypeError(`Unique key cannot default to a constant: ${column.name}`);
148
+ if (column.sqlDomain?.kind === "enum" &&
149
+ typeof value === "string" &&
150
+ !column.sqlDomain.values.includes(value)) {
151
+ throw new TypeError(`Default must be one of the enum values: ${column.name}`);
91
152
  }
92
153
  return;
93
154
  }
94
- // The wire path can hand this untyped data (including specs from removed generator kinds).
155
+ // Storage restoration and untyped wire callers can still supply malformed catalog data.
95
156
  default:
96
157
  throw new TypeError(`Unknown default kind: ${String(defaultValue.kind)}`);
97
158
  }
98
159
  }
160
+ /** Validates the durable identities and ownership rules of one table's secondary indexes. */
161
+ export function validateSecondaryIndexes(record) {
162
+ const columnIds = new Set(record.columns.map((column) => column.id));
163
+ const names = new Set();
164
+ const storageIds = new Set(Object.keys(record.ftsColumns ?? {}));
165
+ for (const [indexId, index] of Object.entries(record.secondaryIndexes ?? {})) {
166
+ if (indexId.length === 0 || index.name.length === 0 || index.storageColumnId.length === 0) {
167
+ throw new TypeError("Secondary-index IDs and names must be non-empty");
168
+ }
169
+ const indexedColumnIds = index.columnIds ?? [index.columnId];
170
+ if (indexedColumnIds.length === 0 ||
171
+ indexedColumnIds[0] !== index.columnId ||
172
+ new Set(indexedColumnIds).size !== indexedColumnIds.length ||
173
+ indexedColumnIds.some((columnId) => !columnIds.has(columnId))) {
174
+ throw new TypeError(`Secondary index ${index.name} references an unknown column`);
175
+ }
176
+ const directions = index.directions ?? indexedColumnIds.map(() => "asc");
177
+ const termEncoding = index.termEncoding;
178
+ if (directions.length !== indexedColumnIds.length ||
179
+ directions.some((direction) => direction !== "asc" && direction !== "desc") ||
180
+ (termEncoding !== undefined && termEncoding !== "tuple-v1") ||
181
+ (termEncoding !== "tuple-v1" &&
182
+ (indexedColumnIds.length > 1 ||
183
+ directions.some((direction) => direction !== "asc") ||
184
+ index.unique === true))) {
185
+ throw new TypeError(`Secondary index ${index.name} has invalid key metadata`);
186
+ }
187
+ if (index.uniqueEnforced === true && index.unique !== true) {
188
+ throw new TypeError(`Secondary index ${index.name} enforces uniqueness without UNIQUE`);
189
+ }
190
+ if (names.has(index.name))
191
+ throw new TypeError(`Index already exists: ${index.name}`);
192
+ names.add(index.name);
193
+ if (storageIds.has(index.storageColumnId)) {
194
+ throw new TypeError(`Secondary-index storage ID is already used: ${index.storageColumnId}`);
195
+ }
196
+ storageIds.add(index.storageColumnId);
197
+ const expectedLocator = record.uniqueKeyColumnId === undefined ? "row-id" : "key-hash-v1";
198
+ const storage = index.storage;
199
+ if (storage !== "postings-v1" || index.locator !== expectedLocator) {
200
+ throw new TypeError(`Secondary index ${index.name} has incompatible storage metadata`);
201
+ }
202
+ const state = index.state;
203
+ if ((state !== "building" && state !== "ready" && state !== "invalid") ||
204
+ !Number.isSafeInteger(index.buildFromVersion) ||
205
+ index.buildFromVersion < -1 ||
206
+ (index.state === "building") !== (index.buildId !== undefined)) {
207
+ throw new TypeError(`Secondary index ${index.name} has invalid build metadata`);
208
+ }
209
+ }
210
+ }
211
+ /** Ordered catalog column IDs for legacy and current secondary-index records. */
212
+ export function secondaryIndexColumnIds(index) {
213
+ return index.columnIds ?? [index.columnId];
214
+ }
215
+ /** Declared directions for legacy and current secondary-index records. */
216
+ export function secondaryIndexDirections(index) {
217
+ return index.directions ?? secondaryIndexColumnIds(index).map(() => "asc");
218
+ }
219
+ /** Unique-membership namespace owned by one physical secondary index. */
220
+ export function secondaryUniqueKeyNamespace(tableId, indexId) {
221
+ return `${tableId}\u0000secondary-index\u0000${indexId}`;
222
+ }
99
223
  export class TableRecordConflictError extends Error {
100
224
  tableId;
101
225
  expectedRevision;
@@ -163,6 +287,20 @@ export class LeaseConflictError extends Error {
163
287
  this.actualRevision = actualRevision;
164
288
  }
165
289
  }
290
+ /** Whether a stored term belongs to one exact, prefix, or range lookup. */
291
+ export function ftsPostingQueryMatches(term, query) {
292
+ if ("term" in query)
293
+ return query.prefix ? term.startsWith(query.term) : term === query.term;
294
+ if (query.lower !== undefined) {
295
+ if (term < query.lower || (term === query.lower && query.lowerInclusive === false))
296
+ return false;
297
+ }
298
+ if (query.upper !== undefined) {
299
+ if (term > query.upper || (term === query.upper && query.upperInclusive === false))
300
+ return false;
301
+ }
302
+ return true;
303
+ }
166
304
  /**
167
305
  * Shared candidate-merge core for both stores: fetching chunks is store-specific, but the
168
306
  * term-match rule (exact, or prefix as a term range) and the sorted-unique row-id shape must
@@ -176,9 +314,7 @@ export function collectFtsCandidates(chunkLists, terms) {
176
314
  const term = terms[index];
177
315
  if (term === undefined)
178
316
  continue;
179
- const matches = term.prefix
180
- ? posting.term.startsWith(term.term)
181
- : posting.term === term.term;
317
+ const matches = ftsPostingQueryMatches(posting.term, term);
182
318
  if (!matches)
183
319
  continue;
184
320
  const set = sets[index];
@@ -192,6 +328,97 @@ export function collectFtsCandidates(chunkLists, terms) {
192
328
  rowIdsByTerm: sets.map((set) => [...set].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0))),
193
329
  };
194
330
  }
331
+ /**
332
+ * K-way merges sorted base/delta chunks into canonical term order. Only one term's row map and
333
+ * one cursor per chunk stay live beyond the returned postings, avoiding a second index-sized map.
334
+ */
335
+ export function collectFtsPostings(chunkLists) {
336
+ const heap = [];
337
+ const before = (left, right) => {
338
+ const leftTerm = left.postings[left.position]?.term ?? "";
339
+ const rightTerm = right.postings[right.position]?.term ?? "";
340
+ return leftTerm < rightTerm || (leftTerm === rightTerm && left.ordinal < right.ordinal);
341
+ };
342
+ const cursorAt = (position) => {
343
+ const cursor = heap[position];
344
+ if (cursor === undefined)
345
+ throw new Error("Posting merge heap is inconsistent");
346
+ return cursor;
347
+ };
348
+ const push = (cursor) => {
349
+ heap.push(cursor);
350
+ let child = heap.length - 1;
351
+ while (child > 0) {
352
+ const parent = (child - 1) >>> 1;
353
+ const childCursor = cursorAt(child);
354
+ const parentCursor = cursorAt(parent);
355
+ if (!before(childCursor, parentCursor))
356
+ break;
357
+ heap[parent] = childCursor;
358
+ heap[child] = parentCursor;
359
+ child = parent;
360
+ }
361
+ };
362
+ const pop = () => {
363
+ const first = heap[0];
364
+ const last = heap.pop();
365
+ if (first === undefined || last === undefined)
366
+ return first;
367
+ if (heap.length === 0)
368
+ return first;
369
+ heap[0] = last;
370
+ let parent = 0;
371
+ for (;;) {
372
+ const left = parent * 2 + 1;
373
+ if (left >= heap.length)
374
+ break;
375
+ const right = left + 1;
376
+ const child = right < heap.length && before(cursorAt(right), cursorAt(left)) ? right : left;
377
+ const childCursor = cursorAt(child);
378
+ const parentCursor = cursorAt(parent);
379
+ if (!before(childCursor, parentCursor))
380
+ break;
381
+ heap[parent] = childCursor;
382
+ heap[child] = parentCursor;
383
+ parent = child;
384
+ }
385
+ return first;
386
+ };
387
+ let ordinal = 0;
388
+ for (const postings of chunkLists) {
389
+ if (postings.length > 0)
390
+ push({ postings, position: 0, ordinal });
391
+ ordinal += 1;
392
+ }
393
+ const output = [];
394
+ while (heap.length > 0) {
395
+ const term = heap[0]?.postings[heap[0].position]?.term;
396
+ if (term === undefined)
397
+ break;
398
+ const rows = new Map();
399
+ while (heap[0]?.postings[heap[0].position]?.term === term) {
400
+ const cursor = pop();
401
+ if (cursor === undefined)
402
+ break;
403
+ const posting = cursor.postings[cursor.position];
404
+ if (posting === undefined)
405
+ continue;
406
+ posting.rowIds.forEach((rowId, index) => {
407
+ rows.set(rowId, Math.max(rows.get(rowId) ?? 0, posting.tf[index] ?? 1));
408
+ });
409
+ cursor.position += 1;
410
+ if (cursor.position < cursor.postings.length)
411
+ push(cursor);
412
+ }
413
+ const ordered = [...rows].sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
414
+ output.push({
415
+ term,
416
+ rowIds: ordered.map(([rowId]) => rowId),
417
+ tf: ordered.map(([, frequency]) => frequency),
418
+ });
419
+ }
420
+ return output;
421
+ }
195
422
  /**
196
423
  * Shared stale-writer policy for both stores' commit steps: a commit that adds segments to a
197
424
  * table with active full-text columns but no covering delta flips the uncovered columns to
@@ -217,6 +444,39 @@ export function invalidateUncoveredFtsColumns(record, coveredColumnIds) {
217
444
  return undefined;
218
445
  return { ...record, ftsColumns: next, revision: (record.revision ?? 0) + 1 };
219
446
  }
447
+ /**
448
+ * Scalar-index half of the stale-writer rule. A writer that staged table data from catalog
449
+ * metadata older than a newly building/ready index cannot provide its postings, so the commit
450
+ * invalidates that index atomically. Readers then scan until a rebuild closes the gap.
451
+ */
452
+ export function invalidateUncoveredSecondaryIndexes(record, coveredStorageColumnIds) {
453
+ const indexes = record.secondaryIndexes;
454
+ if (indexes === undefined)
455
+ return undefined;
456
+ let invalidated = false;
457
+ const next = {};
458
+ for (const [indexId, index] of Object.entries(indexes)) {
459
+ if (index.state !== "invalid" && !coveredStorageColumnIds.has(index.storageColumnId)) {
460
+ const { buildId: _abandonedBuild, ...invalid } = index;
461
+ void _abandonedBuild;
462
+ next[indexId] = { ...invalid, state: "invalid" };
463
+ invalidated = true;
464
+ }
465
+ else {
466
+ next[indexId] = { ...index };
467
+ }
468
+ }
469
+ if (!invalidated)
470
+ return undefined;
471
+ return { ...record, secondaryIndexes: next, revision: (record.revision ?? 0) + 1 };
472
+ }
473
+ /** Physical posting IDs a current catalog record authorizes a commit to write. */
474
+ export function activePostingStorageColumnIds(record) {
475
+ return new Set([
476
+ ...Object.entries(record.ftsColumns ?? {}).flatMap(([columnId, index]) => index.state === "invalid" ? [] : [columnId]),
477
+ ...Object.values(record.secondaryIndexes ?? {}).flatMap((index) => index.state === "invalid" ? [] : [index.storageColumnId]),
478
+ ]);
479
+ }
220
480
  export class UniqueKeyConflictError extends Error {
221
481
  tableId;
222
482
  keyToken;
@@ -227,6 +487,17 @@ export class UniqueKeyConflictError extends Error {
227
487
  this.keyToken = keyToken;
228
488
  }
229
489
  }
490
+ /** A writer prepared before an enforced UNIQUE index existed and therefore cannot enforce it. */
491
+ export class UniqueIndexCoverageError extends Error {
492
+ tableId;
493
+ indexName;
494
+ name = "UniqueIndexCoverageError";
495
+ constructor(tableId, indexName) {
496
+ super(`Write did not cover enforced UNIQUE index ${indexName} on table ${tableId}`);
497
+ this.tableId = tableId;
498
+ this.indexName = indexName;
499
+ }
500
+ }
230
501
  export class WriteConflictError extends Error {
231
502
  expectedVersion;
232
503
  actualVersion;