@minnowdb/core 0.6.6 → 0.6.7

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.
@@ -16,14 +16,6 @@ export interface CompressionMemoryBound {
16
16
  /** Additional JavaScript-owned byte storage retained while producing the result. */
17
17
  readonly scratchBytes: number;
18
18
  }
19
- /**
20
- * The raw codec returns its input view unchanged in both directions: callers treat compressed
21
- * and decompressed payloads as read-only (encode copies the payload into the block envelope,
22
- * decode consumers only read or copy out), so a full-payload defensive copy per block would be
23
- * pure overhead on the default compression path. The returned view may share the caller's
24
- * buffer at a non-zero byte offset.
25
- */
26
- export declare const rawCodec: CompressionCodec;
27
19
  export declare function getCompressionMemoryBound(compression: Compression, inputLength: number): CompressionMemoryBound;
28
20
  export declare const gzipCodec: CompressionCodec;
29
21
  export declare function getCodec(id: Compression): CompressionCodec;
@@ -16,7 +16,7 @@ export class CompressionOutputLimitError extends RangeError {
16
16
  * pure overhead on the default compression path. The returned view may share the caller's
17
17
  * buffer at a non-zero byte offset.
18
18
  */
19
- export const rawCodec = {
19
+ const rawCodec = {
20
20
  id: "raw",
21
21
  async compress(bytes, maximumOutputLength) {
22
22
  if (maximumOutputLength !== undefined) {
@@ -29,3 +29,5 @@ export declare function hashScratch(length: number): number;
29
29
  export declare function equalsScratch(arena: Uint8Array, offset: number, length: number): boolean;
30
30
  /** Copies the scratch arena's first `length` bytes into owned storage. Exported for the join index. */
31
31
  export declare function copyScratchKey(length: number): Uint8Array;
32
+ export declare function safeDouble(value: number, label: string): number;
33
+ export declare function safeProduct(left: number, right: number, label: string): number;
@@ -49,7 +49,7 @@ export class ByteGroupIndex {
49
49
  return this.get([]);
50
50
  }
51
51
  getOne(key) {
52
- const length = encodeSingleGroupKey(key);
52
+ const length = encodeSingleScalarKey(key);
53
53
  const index = this.#find(length, hashScratch(length));
54
54
  return index < 0 ? undefined : this.#values[index];
55
55
  }
@@ -68,7 +68,7 @@ export class ByteGroupIndex {
68
68
  return this.#getOrInsertScratch(length, create);
69
69
  }
70
70
  getOrInsertOne(key, create) {
71
- const length = encodeSingleGroupKey(key);
71
+ const length = encodeSingleScalarKey(key);
72
72
  return this.#getOrInsertScratch(length, create);
73
73
  }
74
74
  /**
@@ -231,10 +231,6 @@ export function encodeSingleScalarKey(key) {
231
231
  reclaimScratch();
232
232
  return writeGroupKey(key, 0);
233
233
  }
234
- function encodeSingleGroupKey(key) {
235
- reclaimScratch();
236
- return writeGroupKey(key, 0);
237
- }
238
234
  function writeGroupKey(key, offset) {
239
235
  if (key === null) {
240
236
  ensureScratchCapacity(offset + 1);
@@ -341,13 +337,13 @@ export function equalsScratch(arena, offset, length) {
341
337
  export function copyScratchKey(length) {
342
338
  return scratch.slice(0, length);
343
339
  }
344
- function safeDouble(value, label) {
340
+ export function safeDouble(value, label) {
345
341
  const doubled = value * 2;
346
342
  if (!Number.isSafeInteger(doubled))
347
343
  throw new RangeError(`${label} exceeds the safe integer range`);
348
344
  return doubled;
349
345
  }
350
- function safeProduct(left, right, label) {
346
+ export function safeProduct(left, right, label) {
351
347
  const product = left * right;
352
348
  if (!Number.isSafeInteger(product))
353
349
  throw new RangeError(`${label} exceeds the safe integer range`);
@@ -1,5 +1,5 @@
1
1
  import { dateMilliseconds } from "../date-value.js";
2
- import { copyScratchKey, encodeSingleScalarKey, equalsScratch, hashScratch, } from "./group-index.js";
2
+ import { copyScratchKey, encodeSingleScalarKey, equalsScratch, hashScratch, safeDouble, safeProduct, } from "./group-index.js";
3
3
  const INITIAL_ENTRY_CAPACITY = 4;
4
4
  const INITIAL_BUCKET_CAPACITY = 8;
5
5
  const INITIAL_KEY_CAPACITY = 32;
@@ -201,17 +201,3 @@ function encodeJoinKey(value) {
201
201
  }
202
202
  throw new TypeError("Join keys must be SQL scalar values");
203
203
  }
204
- function safeDouble(value, label) {
205
- const doubled = value * 2;
206
- if (!Number.isSafeInteger(doubled)) {
207
- throw new RangeError(`${label} exceeds the safe integer range`);
208
- }
209
- return doubled;
210
- }
211
- function safeProduct(left, right, label) {
212
- const product = left * right;
213
- if (!Number.isSafeInteger(product)) {
214
- throw new RangeError(`${label} exceeds the safe integer range`);
215
- }
216
- return product;
217
- }
@@ -1,39 +1,5 @@
1
1
  import { dateMilliseconds } from "../date-value.js";
2
- function sameValue(left, right) {
3
- if (Object.is(left, right))
4
- return true;
5
- if (left instanceof Date || right instanceof Date) {
6
- return (left instanceof Date &&
7
- right instanceof Date &&
8
- Object.is(dateMilliseconds(left), dateMilliseconds(right)));
9
- }
10
- if (Array.isArray(left) || Array.isArray(right)) {
11
- if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length)
12
- return false;
13
- for (let index = 0; index < left.length; index += 1) {
14
- if (!sameValue(left[index], right[index]))
15
- return false;
16
- }
17
- return true;
18
- }
19
- if (typeof left !== "object" || left === null || typeof right !== "object" || right === null) {
20
- return false;
21
- }
22
- const leftRecord = left;
23
- const rightRecord = right;
24
- const leftKeys = Object.keys(leftRecord);
25
- const rightKeys = Object.keys(rightRecord);
26
- if (leftKeys.length !== rightKeys.length)
27
- return false;
28
- for (let index = 0; index < leftKeys.length; index += 1) {
29
- const key = leftKeys[index];
30
- if (key === undefined || key !== rightKeys[index])
31
- return false;
32
- if (!sameValue(leftRecord[key], rightRecord[key]))
33
- return false;
34
- }
35
- return true;
36
- }
2
+ import { sameLiveValue } from "./live-equal.js";
37
3
  function keyToken(value, name) {
38
4
  if (typeof value === "string")
39
5
  return `s:${String(value.length)}:${value}`;
@@ -84,7 +50,7 @@ function diffRows(previousRows, rows, key) {
84
50
  changes.push({ type: "insert", row: next.row, index: next.index });
85
51
  continue;
86
52
  }
87
- if (!sameValue(old.row, next.row)) {
53
+ if (!sameLiveValue(old.row, next.row)) {
88
54
  changes.push({ type: "update", row: next.row, previous: old.row, index: next.index });
89
55
  }
90
56
  if (old.index !== next.index) {
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Structural equality over live result values: dates by instant, arrays by element, objects by
3
+ * own keys in insertion order. Shared by the typed live store's exact suppression and the keyed
4
+ * live window's diffing; internal on purpose — live-api re-exports its modules wholesale, and
5
+ * this helper is not part of the live API.
6
+ */
7
+ export declare function sameLiveValue(left: unknown, right: unknown): boolean;
@@ -0,0 +1,42 @@
1
+ import { dateMilliseconds } from "../date-value.js";
2
+ /**
3
+ * Structural equality over live result values: dates by instant, arrays by element, objects by
4
+ * own keys in insertion order. Shared by the typed live store's exact suppression and the keyed
5
+ * live window's diffing; internal on purpose — live-api re-exports its modules wholesale, and
6
+ * this helper is not part of the live API.
7
+ */
8
+ export function sameLiveValue(left, right) {
9
+ if (Object.is(left, right))
10
+ return true;
11
+ if (left instanceof Date || right instanceof Date) {
12
+ return (left instanceof Date &&
13
+ right instanceof Date &&
14
+ Object.is(dateMilliseconds(left), dateMilliseconds(right)));
15
+ }
16
+ if (Array.isArray(left) || Array.isArray(right)) {
17
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length)
18
+ return false;
19
+ for (let index = 0; index < left.length; index += 1) {
20
+ if (!sameLiveValue(left[index], right[index]))
21
+ return false;
22
+ }
23
+ return true;
24
+ }
25
+ if (typeof left !== "object" || left === null || typeof right !== "object" || right === null) {
26
+ return false;
27
+ }
28
+ const leftRecord = left;
29
+ const rightRecord = right;
30
+ const leftKeys = Object.keys(leftRecord);
31
+ const rightKeys = Object.keys(rightRecord);
32
+ if (leftKeys.length !== rightKeys.length)
33
+ return false;
34
+ for (let index = 0; index < leftKeys.length; index += 1) {
35
+ const key = leftKeys[index];
36
+ if (key === undefined || key !== rightKeys[index])
37
+ return false;
38
+ if (!sameLiveValue(leftRecord[key], rightRecord[key]))
39
+ return false;
40
+ }
41
+ return true;
42
+ }
@@ -1,4 +1,5 @@
1
1
  import { dateIsoString, dateMilliseconds } from "../date-value.js";
2
+ import { crossJoinPlan } from "../plan/model.js";
2
3
  import { blockHasSubqueries, childExpressions, DUAL_TABLE, expressionAliases, forEachBlockExpression, forEachNestedBlock, hasAggregate, isAggregateCall, isScalarFunctionName, mapBlockExpressions, mapChildExpressions, parseQuantified, scalarFunctionNames, scalarFunctionValue, splitCondition, volatileScalarFunctionNames, } from "./query.js";
3
4
  import { concatenatedSqlValue, isSqlDomainValue } from "./sql-domains.js";
4
5
  /**
@@ -1776,20 +1777,7 @@ function decorrelateExistsWithProbes(block, exists, scope, nextAlias) {
1776
1777
  reference: `${probesAlias}.${correlationProbeAlias(index)}`,
1777
1778
  }));
1778
1779
  replacePlanReferences(inner, new Map(references.map((reference, index) => [reference, probeReferences[index] ?? null])));
1779
- inner.joins.push({
1780
- table: probesAlias,
1781
- alias: probesAlias,
1782
- derived: probes,
1783
- kind: "inner",
1784
- left: { kind: "literal", value: null },
1785
- right: { kind: "literal", value: null },
1786
- on: {
1787
- kind: "condition",
1788
- operator: "=",
1789
- left: { kind: "literal", value: 1 },
1790
- right: { kind: "literal", value: 1 },
1791
- },
1792
- });
1780
+ inner.joins.push(crossJoinPlan({ table: probesAlias, alias: probesAlias, derived: probes }));
1793
1781
  const flagsAlias = nextAlias();
1794
1782
  const flags = {
1795
1783
  sql: "(probe-lifted correlated exists flags)",
@@ -1,4 +1,5 @@
1
1
  import { copyDate, dateIsoString, dateMilliseconds, dateUtcDate, dateUtcDay, dateUtcFullYear, dateUtcHours, dateUtcMinutes, dateUtcMonth, dateUtcSeconds, setDateUtcDate, setDateUtcMonth, } from "../date-value.js";
2
+ import { crossJoinPlan } from "../plan/model.js";
2
3
  import { assertWellFormedString, wellFormedUtf8ByteLength } from "../block-format/unicode.js";
3
4
  import { MAX_SQL_NESTING_DEPTH, MAX_SQL_PARAMETERS, MAX_SQL_SCALAR_RESULT_CHARACTERS, MAX_SQL_TEXT_CHARACTERS, MAX_SQL_TOKENS, } from "./cache-limits.js";
4
5
  import { SqlCompileError } from "./errors.js";
@@ -2549,6 +2550,9 @@ function createPreparedRowQuery(plan, tables, memory) {
2549
2550
  },
2550
2551
  };
2551
2552
  }
2553
+ // protectText defaults OFF here — the row engine's own tables are already inside the SQL
2554
+ // boundary — but ON in vector.ts's columnarTableFromRows, which ingests caller-owned rows.
2555
+ // A new call site must pick deliberately, not inherit whichever default is nearest.
2552
2556
  function cloneRowTables(tables, protectText = false) {
2553
2557
  return new Map([...tables].map(([name, rows]) => [
2554
2558
  name,
@@ -6163,19 +6167,6 @@ class Parser {
6163
6167
  }
6164
6168
  const joins = [];
6165
6169
  let rightJoins = 0;
6166
- /** A cross join: the nested-loop path with a condition every row pair satisfies. */
6167
- const crossJoin = (source) => ({
6168
- ...source,
6169
- kind: "inner",
6170
- left: { kind: "literal", value: null },
6171
- right: { kind: "literal", value: null },
6172
- on: {
6173
- kind: "condition",
6174
- operator: "=",
6175
- left: { kind: "literal", value: 1 },
6176
- right: { kind: "literal", value: 1 },
6177
- },
6178
- });
6179
6170
  while (this.#peek().text === "," ||
6180
6171
  this.#isKeyword("NATURAL") ||
6181
6172
  this.#isKeyword("JOIN") ||
@@ -6186,13 +6177,13 @@ class Parser {
6186
6177
  this.#isKeyword("CROSS")) {
6187
6178
  if (this.#punctuation(",")) {
6188
6179
  // F041-07: a comma between table references is a cross join.
6189
- joins.push(crossJoin(this.#source()));
6180
+ joins.push(crossJoinPlan(this.#source()));
6190
6181
  continue;
6191
6182
  }
6192
6183
  if (this.#isKeyword("CROSS")) {
6193
6184
  this.#keyword("CROSS");
6194
6185
  this.#keyword("JOIN");
6195
- joins.push(crossJoin(this.#source()));
6186
+ joins.push(crossJoinPlan(this.#source()));
6196
6187
  continue;
6197
6188
  }
6198
6189
  let kind = "inner";
@@ -48,13 +48,6 @@ export interface TableForeignKey<TColumnName extends string = string> {
48
48
  * derives for an unnamed inline REFERENCES, so a table built either way has the same catalog.
49
49
  */
50
50
  export declare function foreignKeyName(tableName: string, columnName: string): string;
51
- /**
52
- * A flavored value: a column whose slot the engine can fill, so inserts may omit it. The brand
53
- * is an optional phantom property — plain values stay assignable in both directions.
54
- */
55
- export type HasDefault<TValue> = TValue & {
56
- readonly __minnowHasDefault?: true;
57
- };
58
51
  /**
59
52
  * A JSON column's select value carrying a declared document shape. The value is still JSON
60
53
  * text — the brand is an optional phantom property, so plain strings stay assignable in both
@@ -171,11 +164,6 @@ interface AnyColumn {
171
164
  readonly renamedFromName?: string;
172
165
  readonly reference?: ColumnReferenceSpec;
173
166
  }
174
- /**
175
- * Rebuilds a column carrying an exact default spec — the wire layer's escape hatch, since the
176
- * public `.default()` accepts literals and cannot express auto-increment catalog metadata.
177
- */
178
- export declare function columnWithDefaultSpec(base: Pick<AnyColumn, "type" | "isNullable" | "isUnique" | "renamedFromName" | "reference" | "enumValues"> & Partial<Pick<AnyColumn, "integer" | "sqlDomain">>, spec: ColumnDefault): AnyColumn;
179
167
  /** Rebuilds a fluent column from structured-clone-safe metadata. */
180
168
  export declare function columnFromState(state: Pick<AnyColumn, "type" | "isNullable" | "isUnique"> & Partial<Pick<AnyColumn, "integer" | "sqlDomain" | "renamedFromName" | "reference" | "defaultSpec" | "generatedSpec" | "enumValues" | "backfillValue">>): AnyColumn;
181
169
  export declare const column: {
@@ -184,23 +184,6 @@ function validateSchemaName(name, kind) {
184
184
  throw new TypeError(`${kind} name cannot start or end with whitespace: ${JSON.stringify(name)}`);
185
185
  }
186
186
  }
187
- /**
188
- * Rebuilds a column carrying an exact default spec — the wire layer's escape hatch, since the
189
- * public `.default()` accepts literals and cannot express auto-increment catalog metadata.
190
- */
191
- export function columnWithDefaultSpec(base, spec) {
192
- return columnFromState({
193
- type: base.type,
194
- isNullable: base.isNullable,
195
- isUnique: base.isUnique,
196
- integer: base.integer ?? false,
197
- ...(base.sqlDomain === undefined ? {} : { sqlDomain: base.sqlDomain }),
198
- ...(base.renamedFromName === undefined ? {} : { renamedFromName: base.renamedFromName }),
199
- ...(base.reference === undefined ? {} : { reference: base.reference }),
200
- ...(base.enumValues === undefined ? {} : { enumValues: base.enumValues }),
201
- defaultSpec: spec,
202
- });
203
- }
204
187
  /** Rebuilds a fluent column from structured-clone-safe metadata. */
205
188
  export function columnFromState(state) {
206
189
  return createColumn(state.type, {
@@ -1,42 +1,4 @@
1
- import { dateMilliseconds } from "../date-value.js";
2
- function sameLiveValue(left, right) {
3
- if (Object.is(left, right))
4
- return true;
5
- if (left instanceof Date || right instanceof Date) {
6
- return (left instanceof Date &&
7
- right instanceof Date &&
8
- Object.is(dateMilliseconds(left), dateMilliseconds(right)));
9
- }
10
- if (Array.isArray(left) || Array.isArray(right)) {
11
- if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length)
12
- return false;
13
- for (let index = 0; index < left.length; index += 1) {
14
- if (!sameLiveValue(left[index], right[index]))
15
- return false;
16
- }
17
- return true;
18
- }
19
- if (typeof left !== "object" || left === null || typeof right !== "object" || right === null) {
20
- return false;
21
- }
22
- const leftRecord = left;
23
- const rightRecord = right;
24
- const leftKeys = Object.keys(leftRecord);
25
- const rightKeys = Object.keys(rightRecord);
26
- if (leftKeys.length !== rightKeys.length)
27
- return false;
28
- for (let index = 0; index < leftKeys.length; index += 1) {
29
- const key = leftKeys[index];
30
- if (key === undefined || key !== rightKeys[index])
31
- return false;
32
- if (!sameLiveValue(leftRecord[key], rightRecord[key]))
33
- return false;
34
- }
35
- return true;
36
- }
37
- function sameRows(left, right) {
38
- return sameLiveValue(left, right);
39
- }
1
+ import { sameLiveValue } from "./live-equal.js";
40
2
  function immutableRows(rows) {
41
3
  // Adapters own their values. Copy the array so mutating the returned builder result cannot
42
4
  // change the snapshot identity retained for exact suppression.
@@ -191,10 +153,10 @@ export class LiveQuery {
191
153
  const previous = this.#snapshot.rows;
192
154
  if (this.#snapshot.status === "ready" &&
193
155
  this.#snapshot.version === invalidation.manifestVersion &&
194
- sameRows(previous, rows)) {
156
+ sameLiveValue(previous, rows)) {
195
157
  continue;
196
158
  }
197
- if (sameRows(previous, rows) && this.#snapshot.status !== "loading") {
159
+ if (sameLiveValue(previous, rows) && this.#snapshot.status !== "loading") {
198
160
  // Advance the version without replacing the immutable row array.
199
161
  this.#snapshot = {
200
162
  status: "ready",
@@ -1,4 +1,5 @@
1
1
  import { dateMilliseconds } from "../date-value.js";
2
+ import { crossJoinPlan, isCrossJoinPlan } from "../plan/model.js";
2
3
  import { MAX_TEMP_RUN_BATCH_BYTES, MAX_TEMP_RUN_PAGE_BYTES, MAX_TEMP_RUN_PAGES_PER_BATCH, } from "../storage/types.js";
3
4
  import { throwIfAborted } from "./cancellation.js";
4
5
  import { cachedListMembership, childExpressions, distinctFromComparison, nullOrder, isScalarFunctionName, likeMatches, orderOutputName, parseQuantified, quantifiedComparison, scalarFunctionValue, unknownColumnDomains, } from "./query.js";
@@ -409,7 +410,7 @@ function disjunctiveNormalForm(predicate) {
409
410
  function orderCartesianJoins(plan, tables) {
410
411
  if (plan.joins.length < 2 ||
411
412
  plan.select.some((item) => item.expression.kind === "wildcard") ||
412
- plan.joins.some((join) => !isCartesianJoin(join))) {
413
+ plan.joins.some((join) => !isCrossJoinPlan(join))) {
413
414
  return plan;
414
415
  }
415
416
  const entries = [
@@ -454,21 +455,11 @@ function orderCartesianJoins(plan, tables) {
454
455
  return plan;
455
456
  const keyIndex = predicates.findIndex((predicate) => predicateJoinsSource(predicate, sourceIndex, available, entries, sourceTables));
456
457
  const key = keyIndex < 0 ? undefined : predicates.splice(keyIndex, 1)[0];
457
- joins.push(key === undefined ? cartesianJoin(source) : keyedInnerJoin(source, key));
458
+ joins.push(key === undefined ? crossJoinPlan(source) : keyedInnerJoin(source, key));
458
459
  available.add(sourceIndex);
459
460
  }
460
461
  return { ...plan, base: plan.base, joins, predicates };
461
462
  }
462
- function isCartesianJoin(join) {
463
- const condition = join.on;
464
- return (join.kind === "inner" &&
465
- condition?.kind === "condition" &&
466
- condition.operator === "=" &&
467
- condition.left.kind === "literal" &&
468
- condition.left.value === 1 &&
469
- condition.right.kind === "literal" &&
470
- condition.right.value === 1);
471
- }
472
463
  function tableSourceOf(join) {
473
464
  const { kind, left, right, on, full, natural, ...source } = join;
474
465
  void kind;
@@ -479,20 +470,6 @@ function tableSourceOf(join) {
479
470
  void natural;
480
471
  return source;
481
472
  }
482
- function cartesianJoin(source) {
483
- return {
484
- ...source,
485
- kind: "inner",
486
- left: { kind: "literal", value: null },
487
- right: { kind: "literal", value: null },
488
- on: {
489
- kind: "condition",
490
- operator: "=",
491
- left: { kind: "literal", value: 1 },
492
- right: { kind: "literal", value: 1 },
493
- },
494
- };
495
- }
496
473
  function keyedInnerJoin(source, predicate) {
497
474
  return {
498
475
  ...source,
@@ -190,6 +190,13 @@ export interface JoinPlan extends TableSource {
190
190
  full?: boolean;
191
191
  natural?: boolean;
192
192
  }
193
+ /**
194
+ * A cross join rides the nested-loop inner-join path with a condition every row pair satisfies:
195
+ * 1 = 1 over null literal key expressions. Producers build the shape with crossJoinPlan and
196
+ * consumers recognize it with isCrossJoinPlan, so the encoding lives in exactly one place.
197
+ */
198
+ export declare function crossJoinPlan(source: TableSource): JoinPlan;
199
+ export declare function isCrossJoinPlan(join: JoinPlan): boolean;
193
200
  export type SetOperator = "union" | "union all" | "intersect" | "intersect all" | "except" | "except all";
194
201
  export interface RecursiveCte {
195
202
  reference: string;
@@ -1 +1,29 @@
1
- export {};
1
+ /**
2
+ * A cross join rides the nested-loop inner-join path with a condition every row pair satisfies:
3
+ * 1 = 1 over null literal key expressions. Producers build the shape with crossJoinPlan and
4
+ * consumers recognize it with isCrossJoinPlan, so the encoding lives in exactly one place.
5
+ */
6
+ export function crossJoinPlan(source) {
7
+ return {
8
+ ...source,
9
+ kind: "inner",
10
+ left: { kind: "literal", value: null },
11
+ right: { kind: "literal", value: null },
12
+ on: {
13
+ kind: "condition",
14
+ operator: "=",
15
+ left: { kind: "literal", value: 1 },
16
+ right: { kind: "literal", value: 1 },
17
+ },
18
+ };
19
+ }
20
+ export function isCrossJoinPlan(join) {
21
+ const condition = join.on;
22
+ return (join.kind === "inner" &&
23
+ condition?.kind === "condition" &&
24
+ condition.operator === "=" &&
25
+ condition.left.kind === "literal" &&
26
+ condition.left.value === 1 &&
27
+ condition.right.kind === "literal" &&
28
+ condition.right.value === 1);
29
+ }
@@ -1,4 +1,4 @@
1
- import { CompactionJobConflictError, assertCompactionOutputProvenance, compactionOutputSegmentIds, CompactionBacklogError, createManifest, createGarbageCollectionJobRecord, storeNames, advanceGarbageCollectionJobRecord as advanceGarbageCollectionJobRecordUnchecked, BlockReadBatchTooLargeError, activePostingStorageColumnIds, assertTempRunPageBatchLimits, assertStorageBulkReadItems, assertTransactionArtifactBatchLimits, assertTransactionArtifactJournalLimits, boundedMaintenanceBatchItems, canonicalManifestChangedTableIds, catalogRecordRetainedBytes, manifestRecordRetainedReservationBytes, segmentRecordRetainedBytes, collectFtsPostings, ftsPostingQueryMatches, invalidateUncoveredFtsColumns, invalidateUncoveredSecondaryIndexes, GarbageCollectionJobConflictError, LeaseConflictError, LeaseExpiredError, LeaseOwnerConflictError, MAX_FTS_CANDIDATE_ROW_IDS, MAX_ACTIVE_UNIQUE_KEY_BUILDS, MAX_UNIQUE_KEY_BUILD_STAGED_BYTES, MAX_UNIQUE_KEY_BUILD_STAGED_BYTES_TOTAL, MAX_UNIQUE_KEY_BUILD_CHUNK_BYTES, MAX_UNIQUE_KEY_BUILD_TOKENS_PER_CHUNK, MAX_UNIQUE_KEY_BUILD_TTL_MS, MAX_FTS_BASE_CHUNKS, MAX_FTS_DELTA_CHUNKS, MAX_FTS_POSTINGS_PER_CHUNK, MAX_FTS_POSTING_ROW_IDS_PER_CHUNK, MAX_FTS_POSTING_TERM_CHARACTERS, MAX_FTS_TOKENS_PER_DOCUMENT, MAX_FTS_ORDERED_READ_BYTES, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_MANIFEST_RECORDS, MAX_MANIFEST_RETAINED_BYTES, MAX_BLOCK_READ_BATCH_BYTES, MAX_STORAGE_ID_CHARACTERS, MAX_LEASE_TTL_MS, MAX_ACTIVE_LEASES, MAX_ACTIVE_TRANSACTIONS, MAX_GLOBAL_STAGED_ARTIFACT_BYTES, MAX_GLOBAL_STAGED_BLOCKS, MAX_GLOBAL_STAGED_SEGMENTS, MAX_RETIRED_HISTORY_BYTES, MAX_PINNED_MANIFEST_VERSION_LAG, MAX_PINNED_RETIRED_BLOCKS, MAX_PINNED_RETIRED_BYTES, MAX_TERMINAL_TRANSACTION_RECORDS, MAX_ACTIVE_COMPACTION_JOBS, MAX_ACTIVE_GARBAGE_COLLECTION_JOBS, MAX_TERMINAL_COMPACTION_JOB_RECORDS, MAX_COMPLETED_GARBAGE_COLLECTION_JOB_RECORDS, MAX_CATALOG_RECORDS, MAX_CATALOG_RETAINED_BYTES, MAX_SEGMENT_RECORDS, MAX_SEGMENT_RETAINED_BYTES, MAX_ACTIVE_FTS_BASE_BUILDS, MAX_ACTIVE_SECONDARY_INDEX_BUILDS, MAX_ACCELERATOR_BUILD_STAGED_BYTES_TOTAL, MAX_ACCELERATOR_BUILD_STAGED_ENTRIES_TOTAL, MAX_POSTING_BUILD_TTL_MS, MAX_SNAPSHOT_SESSION_TTL_MS, MAX_SNAPSHOT_FRAME_BATCH_BYTES, MAX_SNAPSHOT_FRAME_BATCH_ITEMS, MAX_SNAPSHOT_METADATA_BATCH_BYTES, MAX_SNAPSHOT_METADATA_FRAME_BYTES, SNAPSHOT_FRAME_KINDS, MAX_LEVEL_ZERO_SEGMENTS, MAX_AUTO_INCREMENT_EXCLUSIVE_END, MAX_ROW_ID, MAX_ROW_ID_EXCLUSIVE_END, StorageCorruptionError, StorageFormatVersionError, IndexedDbSchemaUpgradeBlockedError, StorageResourceLimitError, SnapshotManifestMissingError, SnapshotImportConflictError, PostingBuildConflictError, validateTableForeignKey, TableInUseError, TableRecordConflictError, TempOwnerConflictError, MAX_TEMP_OWNER_TTL_MS, MAX_ACTIVE_TEMP_OWNERS, MAX_TEMP_RUNS_PER_OWNER, MAX_TEMP_PAGES_PER_OWNER, MAX_TEMP_RUNS_TOTAL, MAX_TEMP_PAGES_TOTAL, MAX_TEMP_BYTES_PER_OWNER, MAX_TEMP_BYTES_TOTAL, TransactionRecordConflictError, UniqueKeyConflictError, UniqueKeyBuildConflictError, UniqueIndexCoverageError, SchemaConflictError, normalizeCompactionJobRecord, normalizeGarbageCollectionJobRecord, normalizeSegmentRecord, updateCompactionJobRecord as updateCompactionJobRecordUnchecked, updateGarbageCollectionPlanningRecord, updateTransactionRecord as updateTransactionRecordUnchecked, validateColumnDefault, validateCatalogName, validateCanonicalManifestChangedTableIds, validateEnumValues, validateFtsOrderedReadLimits, validateFtsPostingQueries, validateStorageId, validateStorageDatabaseName, validateTableColumns, validateSecondaryIndexes, validateTableRecordBounds, secondaryIndexColumnIds, secondaryIndexWriteContractChanged, secondaryUniqueKeyNamespace, transactionCommitDeltaRetainedBytes, uniqueKeyBuildChunkRetainedBytes, WriteConflictError, } from "./types.js";
1
+ import { CompactionJobConflictError, assertCompactionOutputProvenance, compactionOutputSegmentIds, CompactionBacklogError, createManifest, createGarbageCollectionJobRecord, storeNames, advanceGarbageCollectionJobRecord as advanceGarbageCollectionJobRecordUnchecked, BlockReadBatchTooLargeError, activePostingStorageColumnIds, assertTempRunPageBatchLimits, assertStorageBulkReadItems, assertTransactionArtifactBatchLimits, assertTransactionArtifactJournalLimits, boundedMaintenanceBatchItems, canonicalManifestChangedTableIds, catalogRecordRetainedBytes, manifestRecordRetainedReservationBytes, segmentRecordRetainedBytes, collectFtsPostings, ftsPostingQueryMatches, invalidateUncoveredFtsColumns, invalidateUncoveredSecondaryIndexes, GarbageCollectionJobConflictError, LeaseConflictError, LeaseExpiredError, LeaseOwnerConflictError, MAX_FTS_CANDIDATE_ROW_IDS, MAX_ACTIVE_UNIQUE_KEY_BUILDS, MAX_UNIQUE_KEY_BUILD_STAGED_BYTES, MAX_UNIQUE_KEY_BUILD_STAGED_BYTES_TOTAL, MAX_UNIQUE_KEY_BUILD_CHUNK_BYTES, MAX_UNIQUE_KEY_BUILD_TOKENS_PER_CHUNK, MAX_UNIQUE_KEY_BUILD_TTL_MS, MAX_FTS_BASE_CHUNKS, MAX_FTS_DELTA_CHUNKS, MAX_FTS_POSTINGS_PER_CHUNK, MAX_FTS_POSTING_ROW_IDS_PER_CHUNK, MAX_FTS_POSTING_TERM_CHARACTERS, MAX_FTS_TOKENS_PER_DOCUMENT, MAX_FTS_ORDERED_READ_BYTES, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_MANIFEST_RECORDS, MAX_MANIFEST_RETAINED_BYTES, MAX_BLOCK_READ_BATCH_BYTES, MAX_STORAGE_ID_CHARACTERS, MAX_LEASE_TTL_MS, MAX_ACTIVE_LEASES, MAX_ACTIVE_TRANSACTIONS, MAX_GLOBAL_STAGED_ARTIFACT_BYTES, MAX_GLOBAL_STAGED_BLOCKS, MAX_GLOBAL_STAGED_SEGMENTS, MAX_RETIRED_HISTORY_BYTES, MAX_PINNED_MANIFEST_VERSION_LAG, MAX_PINNED_RETIRED_BLOCKS, MAX_PINNED_RETIRED_BYTES, MAX_TERMINAL_TRANSACTION_RECORDS, MAX_ACTIVE_COMPACTION_JOBS, MAX_ACTIVE_GARBAGE_COLLECTION_JOBS, MAX_TERMINAL_COMPACTION_JOB_RECORDS, MAX_COMPLETED_GARBAGE_COLLECTION_JOB_RECORDS, MAX_CATALOG_RECORDS, MAX_CATALOG_RETAINED_BYTES, MAX_SEGMENT_RECORDS, MAX_SEGMENT_RETAINED_BYTES, MAX_ACTIVE_FTS_BASE_BUILDS, MAX_ACTIVE_SECONDARY_INDEX_BUILDS, MAX_ACCELERATOR_BUILD_STAGED_BYTES_TOTAL, MAX_ACCELERATOR_BUILD_STAGED_ENTRIES_TOTAL, MAX_POSTING_BUILD_TTL_MS, MAX_SNAPSHOT_SESSION_TTL_MS, MAX_SNAPSHOT_FRAME_BATCH_BYTES, MAX_SNAPSHOT_FRAME_BATCH_ITEMS, MAX_SNAPSHOT_METADATA_BATCH_BYTES, MAX_SNAPSHOT_METADATA_FRAME_BYTES, SNAPSHOT_FRAME_KINDS, MAX_LEVEL_ZERO_SEGMENTS, MAX_AUTO_INCREMENT_EXCLUSIVE_END, MAX_ROW_ID, MAX_ROW_ID_EXCLUSIVE_END, StorageCorruptionError, StorageFormatVersionError, IndexedDbSchemaUpgradeBlockedError, StorageResourceLimitError, SnapshotManifestMissingError, SnapshotImportConflictError, PostingBuildConflictError, validateTableForeignKey, TableInUseError, TableRecordConflictError, TempOwnerConflictError, MAX_TEMP_OWNER_TTL_MS, MAX_ACTIVE_TEMP_OWNERS, MAX_TEMP_RUNS_PER_OWNER, MAX_TEMP_PAGES_PER_OWNER, MAX_TEMP_RUNS_TOTAL, MAX_TEMP_PAGES_TOTAL, MAX_TEMP_BYTES_PER_OWNER, MAX_TEMP_BYTES_TOTAL, TransactionRecordConflictError, UniqueKeyConflictError, UniqueKeyBuildConflictError, UniqueIndexCoverageError, SchemaConflictError, normalizeCompactionJobRecord, normalizeGarbageCollectionJobRecord, normalizeSegmentRecord, updateCompactionJobRecord as updateCompactionJobRecordUnchecked, updateGarbageCollectionPlanningRecord, updateTransactionRecord as updateTransactionRecordUnchecked, validateColumnDefault, validateCatalogName, validateCanonicalManifestChangedTableIds, validateEnumValues, validateFtsCandidateLimit, validateFtsOrderedReadLimits, validateFtsReadVersion, validateFtsPostingQueries, validateStorageId, validateStorageDatabaseName, validateTableColumns, validateSecondaryIndexes, validateTableRecordBounds, secondaryIndexColumnIds, secondaryIndexWriteContractChanged, secondaryUniqueKeyNamespace, transactionCommitDeltaRetainedBytes, uniqueKeyBuildChunkRetainedBytes, WriteConflictError, } from "./types.js";
2
2
  import { crc32, verifyStoredBlock } from "../block-format/index.js";
3
3
  import { dateIsoString } from "../date-value.js";
4
4
  import { decodeSnapshotMetadataItems, encodeSnapshotMetadataPage, extendSnapshotFrameStreamChecksum, prepareSnapshotFrameStreamHeader, snapshotFrameEnvelopeParts, snapshotFrameStreamHeaderIdentity, } from "./snapshot-stream.js";
@@ -1769,11 +1769,8 @@ export class IndexedDbBlockStore {
1769
1769
  validateId(tableId, "Table ID");
1770
1770
  validateId(columnId, "Column ID");
1771
1771
  validateFtsPostingQueries(terms);
1772
- if (!Number.isSafeInteger(maxRowIds) ||
1773
- maxRowIds < 1 ||
1774
- maxRowIds > MAX_FTS_CANDIDATE_ROW_IDS) {
1775
- throw new RangeError(`Full-text candidate limit must be between 1 and ${String(MAX_FTS_CANDIDATE_ROW_IDS)}`);
1776
- }
1772
+ validateFtsReadVersion(upToVersion);
1773
+ validateFtsCandidateLimit(maxRowIds);
1777
1774
  const transaction = this.#transaction("catalog", "readonly");
1778
1775
  const store = transaction.objectStore("catalog");
1779
1776
  const [rawToc, rawDeltaIndex] = await Promise.all([
@@ -1870,9 +1867,7 @@ export class IndexedDbBlockStore {
1870
1867
  async readFtsPostings(tableId, columnId, upToVersion, maxRowIds = MAX_FTS_CANDIDATE_ROW_IDS, maxRetainedBytes = MAX_FTS_ORDERED_READ_BYTES) {
1871
1868
  validateId(tableId, "Table ID");
1872
1869
  validateId(columnId, "Column ID");
1873
- if (!Number.isSafeInteger(upToVersion) || upToVersion < 0) {
1874
- throw new RangeError("Full-text snapshot version must be a non-negative safe integer");
1875
- }
1870
+ validateFtsReadVersion(upToVersion);
1876
1871
  validateFtsOrderedReadLimits(maxRowIds, maxRetainedBytes);
1877
1872
  const transaction = this.#transaction("catalog", "readonly");
1878
1873
  const store = transaction.objectStore("catalog");
@@ -6983,20 +6978,15 @@ function createCurrentIndexedDbSchema(database, upgrade) {
6983
6978
  for (const storeName of storeNames)
6984
6979
  database.createObjectStore(storeName);
6985
6980
  database.createObjectStore(SNAPSHOT_HEADER_STORE);
6986
- upgrade.objectStore("segments").createIndex(SEGMENT_TABLE_INDEX, "tableId");
6987
- upgrade.objectStore("leases").createIndex(LEASE_EXPIRY_INDEX, ["expiresAt", "id"]);
6988
- upgrade.objectStore("transactions").createIndex(TRANSACTION_STATUS_INDEX, "status");
6989
- upgrade.objectStore("temp").createIndex(TEMP_OWNER_EXPIRY_INDEX, ["expiresAt", "ownerId"]);
6990
- upgrade.objectStore("catalog").createIndex(CATALOG_FTS_BUILD_UPDATED_INDEX, "updatedAt");
6991
- upgrade.objectStore("catalog").createIndex(CATALOG_FTS_BUILD_EXPIRY_INDEX, "ftsBuildExpiry");
6992
- upgrade
6993
- .objectStore("catalog")
6994
- .createIndex(CATALOG_FTS_RETIREMENT_UPDATED_INDEX, "retirementUpdatedAt");
6995
- upgrade.objectStore("catalog").createIndex(UNIQUE_KEY_BUILD_ACTIVE_INDEX, "activeBuildState");
6996
- upgrade.objectStore("catalog").createIndex(UNIQUE_KEY_BUILD_EXPIRY_INDEX, "activeExpiry");
6997
- upgrade.objectStore("catalog").createIndex(MANIFEST_BLOCK_ID_INDEX, "blockId", {
6998
- unique: true,
6999
- });
6981
+ // Creation is driven by the same declaration validateCurrentIndexedDbSchema verifies
6982
+ // against, so the two can never disagree about what the current schema is.
6983
+ for (const [storeName, indexes] of Object.entries(indexedDbIndexSchema)) {
6984
+ const store = upgrade.objectStore(storeName);
6985
+ for (const index of indexes) {
6986
+ const keyPath = typeof index.keyPath === "string" ? index.keyPath : [...index.keyPath];
6987
+ store.createIndex(index.name, keyPath, { unique: index.unique ?? false });
6988
+ }
6989
+ }
7000
6990
  upgrade.objectStore("gc").add(emptyMaintenanceQuota(), MAINTENANCE_QUOTA_KEY);
7001
6991
  upgrade.objectStore("statistics").add(emptyResourceLedger(), RESOURCE_LEDGER_KEY);
7002
6992
  upgrade.objectStore("statistics").add(emptyCatalogResourceLedger(), CATALOG_RESOURCE_LEDGER_KEY);
@@ -1,4 +1,4 @@
1
- import { activePostingStorageColumnIds, assertStorageBulkReadItems, assertTempRunPageBatchLimits, BlockReadBatchTooLargeError, collectFtsPostingsBounded, ftsPostingQueryMatches, MAX_BLOCK_READ_BATCH_BYTES, MAX_FTS_BASE_CHUNKS, MAX_POSTING_BUILD_TTL_MS, MAX_FTS_POSTING_TERM_CHARACTERS, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_STORAGE_ID_CHARACTERS, MAX_TEMP_RUN_PAGE_BYTES, MAX_TEMP_RUN_PAGES_PER_BATCH, MAX_TEMP_RUN_BATCH_BYTES, MAX_TEMP_RUNS_PER_OWNER, MAX_TEMP_PAGES_PER_OWNER, MAX_TEMP_RUNS_TOTAL, MAX_TEMP_PAGES_TOTAL, MAX_TEMP_BYTES_PER_OWNER, MAX_TEMP_BYTES_TOTAL, MAX_ACTIVE_FTS_BASE_BUILDS, MAX_ACTIVE_SECONDARY_INDEX_BUILDS, MAX_ACCELERATOR_BUILD_STAGED_BYTES_TOTAL, MAX_ACCELERATOR_BUILD_STAGED_ENTRIES_TOTAL, MAX_FTS_CANDIDATE_ROW_IDS, MAX_FTS_ORDERED_READ_BYTES, MAX_SNAPSHOT_SESSION_TTL_MS, MAX_SNAPSHOT_FRAME_BATCH_BYTES, MAX_SNAPSHOT_FRAME_BATCH_ITEMS, MAX_SNAPSHOT_METADATA_BATCH_BYTES, SNAPSHOT_FRAME_KINDS, MAX_TRANSACTION_STAGE_BLOCKS, MAX_TRANSACTION_STAGE_SEGMENTS, SnapshotImportConflictError, PostingBuildConflictError, StorageResourceLimitError, StorageCorruptionError, StorageFormatVersionError, validateFtsPostingQueries, validateFtsOrderedReadLimits, uniqueKeyBuildChunkRetainedBytes, } from "../types.js";
1
+ import { activePostingStorageColumnIds, assertStorageBulkReadItems, assertTempRunPageBatchLimits, BlockReadBatchTooLargeError, collectFtsPostingsBounded, ftsPostingQueryMatches, MAX_BLOCK_READ_BATCH_BYTES, MAX_FTS_BASE_CHUNKS, MAX_POSTING_BUILD_TTL_MS, MAX_FTS_POSTING_TERM_CHARACTERS, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_STORAGE_ID_CHARACTERS, MAX_TEMP_RUN_PAGE_BYTES, MAX_TEMP_RUN_PAGES_PER_BATCH, MAX_TEMP_RUN_BATCH_BYTES, MAX_TEMP_RUNS_PER_OWNER, MAX_TEMP_PAGES_PER_OWNER, MAX_TEMP_RUNS_TOTAL, MAX_TEMP_PAGES_TOTAL, MAX_TEMP_BYTES_PER_OWNER, MAX_TEMP_BYTES_TOTAL, MAX_ACTIVE_FTS_BASE_BUILDS, MAX_ACTIVE_SECONDARY_INDEX_BUILDS, MAX_ACCELERATOR_BUILD_STAGED_BYTES_TOTAL, MAX_ACCELERATOR_BUILD_STAGED_ENTRIES_TOTAL, MAX_FTS_CANDIDATE_ROW_IDS, MAX_FTS_ORDERED_READ_BYTES, MAX_SNAPSHOT_SESSION_TTL_MS, MAX_SNAPSHOT_FRAME_BATCH_BYTES, MAX_SNAPSHOT_FRAME_BATCH_ITEMS, MAX_SNAPSHOT_METADATA_BATCH_BYTES, SNAPSHOT_FRAME_KINDS, MAX_TRANSACTION_STAGE_BLOCKS, MAX_TRANSACTION_STAGE_SEGMENTS, SnapshotImportConflictError, PostingBuildConflictError, StorageResourceLimitError, StorageCorruptionError, StorageFormatVersionError, validateFtsPostingQueries, validateFtsCandidateLimit, validateFtsOrderedReadLimits, validateFtsReadVersion, uniqueKeyBuildChunkRetainedBytes, } from "../types.js";
2
2
  import { dateIsoString } from "../../date-value.js";
3
3
  import { decodeSnapshotMetadataItems, encodeSnapshotMetadataPage, extendSnapshotFrameStreamChecksum, prepareSnapshotFrameStreamHeader, snapshotFrameEnvelopeParts, snapshotFrameStreamHeaderIdentity, } from "../snapshot-stream.js";
4
4
  import { RecordCore, validateBeginTransactionInput, validateBlockWriteBytes, validateFtsBaseInput, validateFtsPostingChunks, validateId, validateTempRunPage, validateTempRunPageIdentity, } from "../toolkit/record-core.js";
@@ -2485,9 +2485,7 @@ export class OpfsLeader {
2485
2485
  validateId(tableId);
2486
2486
  validateId(columnId);
2487
2487
  validateFtsPostingQueries(terms);
2488
- if (!Number.isSafeInteger(upToVersion) || upToVersion < -1) {
2489
- throw new RangeError("Full-text query version must be a safe integer at least -1");
2490
- }
2488
+ validateFtsReadVersion(upToVersion);
2491
2489
  validateFtsCandidateLimit(maxRowIds);
2492
2490
  return this.#run(async () => {
2493
2491
  // Unlike immutable table blocks, derived-index extents are not protected by reader leases:
@@ -2553,9 +2551,7 @@ export class OpfsLeader {
2553
2551
  async readFtsPostings(tableId, columnId, upToVersion, maxRowIds = MAX_FTS_CANDIDATE_ROW_IDS, maxRetainedBytes = MAX_FTS_ORDERED_READ_BYTES) {
2554
2552
  validateId(tableId);
2555
2553
  validateId(columnId);
2556
- if (!Number.isSafeInteger(upToVersion) || upToVersion < -1) {
2557
- throw new RangeError("Full-text query version must be a safe integer at least -1");
2558
- }
2554
+ validateFtsReadVersion(upToVersion);
2559
2555
  validateFtsOrderedReadLimits(maxRowIds, maxRetainedBytes);
2560
2556
  return this.#run(async () => {
2561
2557
  const key = postingStorageKey(tableId, columnId);
@@ -4041,11 +4037,6 @@ function requireCoverageVersion(value, label) {
4041
4037
  if (!Number.isSafeInteger(value) || value < -1)
4042
4038
  throw new Error(`Invalid ${label}`);
4043
4039
  }
4044
- function validateFtsCandidateLimit(value) {
4045
- if (!Number.isSafeInteger(value) || value < 1 || value > MAX_FTS_CANDIDATE_ROW_IDS) {
4046
- throw new RangeError(`Full-text candidate limit must be between 1 and ${String(MAX_FTS_CANDIDATE_ROW_IDS)}`);
4047
- }
4048
- }
4049
4040
  function requirePositiveInteger(value, label) {
4050
4041
  if (!Number.isSafeInteger(value) || value < 1)
4051
4042
  throw new Error(`Invalid ${label}`);
@@ -1,4 +1,4 @@
1
- import { CompactionBacklogError, CompactionJobConflictError, assertCompactionOutputProvenance, compactionOutputSegmentIds, createManifest, createGarbageCollectionJobRecord, advanceGarbageCollectionJobRecord, collectFtsCandidates, collectFtsPostingsBounded, activePostingStorageColumnIds, invalidateUncoveredFtsColumns, invalidateUncoveredSecondaryIndexes, GarbageCollectionJobConflictError, LeaseConflictError, LeaseExpiredError, LeaseOwnerConflictError, MAX_LEASE_TTL_MS, MAX_ACTIVE_LEASES, MAX_ACTIVE_TEMP_OWNERS, MAX_ACTIVE_COMPACTION_JOBS, MAX_ACTIVE_UNIQUE_KEY_BUILDS, MAX_ACTIVE_TRANSACTIONS, MAX_TERMINAL_TRANSACTION_RECORDS, MAX_TERMINAL_COMPACTION_JOB_RECORDS, MAX_COMPLETED_GARBAGE_COLLECTION_JOB_RECORDS, MAX_PINNED_MANIFEST_VERSION_LAG, MAX_PINNED_RETIRED_BLOCKS, MAX_PINNED_RETIRED_BYTES, MAX_RETIRED_HISTORY_BYTES, MAX_GLOBAL_STAGED_ARTIFACT_BYTES, MAX_GLOBAL_STAGED_BLOCKS, MAX_GLOBAL_STAGED_SEGMENTS, MAX_TEMP_OWNER_TTL_MS, MAX_ROW_ID, MAX_ROW_ID_EXCLUSIVE_END, MAX_AUTO_INCREMENT_EXCLUSIVE_END, MAX_LEVEL_ZERO_SEGMENTS, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_STORAGE_ID_CHARACTERS, MAX_CATALOG_RECORDS, MAX_CATALOG_RETAINED_BYTES, MAX_MANIFEST_RECORDS, MAX_MANIFEST_RETAINED_BYTES, MAX_SEGMENT_RECORDS, MAX_SEGMENT_RETAINED_BYTES, MAX_FTS_CANDIDATE_ROW_IDS, MAX_FTS_POSTINGS_PER_CHUNK, MAX_FTS_POSTING_ROW_IDS_PER_CHUNK, MAX_FTS_POSTING_TERM_CHARACTERS, MAX_FTS_TOKENS_PER_DOCUMENT, MAX_FTS_BASE_CHUNKS, MAX_FTS_DELTA_CHUNKS, SnapshotManifestMissingError, StorageResourceLimitError, TableInUseError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueKeyConflictError, UniqueKeyBuildConflictError, UniqueIndexCoverageError, assertTransactionArtifactBatchLimits, assertTransactionArtifactJournalLimits, assertTempRunPageBatchLimits, assertStorageBulkReadItems, boundedMaintenanceBatchItems, normalizeCompactionJobRecord, normalizeGarbageCollectionJobRecord, normalizeSegmentRecord, updateCompactionJobRecord, updateTransactionRecord, updateGarbageCollectionPlanningRecord, validateTableColumns, validateTableForeignKey, validateSecondaryIndexes, secondaryIndexColumnIds, secondaryIndexWriteContractChanged, secondaryUniqueKeyNamespace, transactionCommitDeltaRetainedBytes, uniqueKeyBuildChunkRetainedBytes, MAX_UNIQUE_KEY_BUILD_TTL_MS, MAX_UNIQUE_KEY_BUILD_STAGED_BYTES, MAX_UNIQUE_KEY_BUILD_STAGED_BYTES_TOTAL, MAX_UNIQUE_KEY_BUILD_TOKENS_PER_CHUNK, validateFtsPostingQueries, validateCanonicalManifestChangedTableIds, catalogRecordRetainedBytes, manifestRecordRetainedReservationBytes, segmentRecordRetainedBytes, snapshotAcceleratorItemRetainedUsage, assertSnapshotImportAcceleratorUsage, SchemaConflictError, WriteConflictError, } from "../types.js";
1
+ import { CompactionBacklogError, CompactionJobConflictError, assertCompactionOutputProvenance, compactionOutputSegmentIds, createManifest, createGarbageCollectionJobRecord, advanceGarbageCollectionJobRecord, collectFtsCandidates, collectFtsPostingsBounded, validateFtsCandidateLimit, validateFtsReadVersion, activePostingStorageColumnIds, invalidateUncoveredFtsColumns, invalidateUncoveredSecondaryIndexes, GarbageCollectionJobConflictError, LeaseConflictError, LeaseExpiredError, LeaseOwnerConflictError, MAX_LEASE_TTL_MS, MAX_ACTIVE_LEASES, MAX_ACTIVE_TEMP_OWNERS, MAX_ACTIVE_COMPACTION_JOBS, MAX_ACTIVE_UNIQUE_KEY_BUILDS, MAX_ACTIVE_TRANSACTIONS, MAX_TERMINAL_TRANSACTION_RECORDS, MAX_TERMINAL_COMPACTION_JOB_RECORDS, MAX_COMPLETED_GARBAGE_COLLECTION_JOB_RECORDS, MAX_PINNED_MANIFEST_VERSION_LAG, MAX_PINNED_RETIRED_BLOCKS, MAX_PINNED_RETIRED_BYTES, MAX_RETIRED_HISTORY_BYTES, MAX_GLOBAL_STAGED_ARTIFACT_BYTES, MAX_GLOBAL_STAGED_BLOCKS, MAX_GLOBAL_STAGED_SEGMENTS, MAX_TEMP_OWNER_TTL_MS, MAX_ROW_ID, MAX_ROW_ID_EXCLUSIVE_END, MAX_AUTO_INCREMENT_EXCLUSIVE_END, MAX_LEVEL_ZERO_SEGMENTS, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_STORAGE_ID_CHARACTERS, MAX_CATALOG_RECORDS, MAX_CATALOG_RETAINED_BYTES, MAX_MANIFEST_RECORDS, MAX_MANIFEST_RETAINED_BYTES, MAX_SEGMENT_RECORDS, MAX_SEGMENT_RETAINED_BYTES, MAX_FTS_CANDIDATE_ROW_IDS, MAX_FTS_POSTINGS_PER_CHUNK, MAX_FTS_POSTING_ROW_IDS_PER_CHUNK, MAX_FTS_POSTING_TERM_CHARACTERS, MAX_FTS_TOKENS_PER_DOCUMENT, MAX_FTS_BASE_CHUNKS, MAX_FTS_DELTA_CHUNKS, SnapshotManifestMissingError, StorageResourceLimitError, TableInUseError, TableRecordConflictError, TempOwnerConflictError, TransactionRecordConflictError, UniqueKeyConflictError, UniqueKeyBuildConflictError, UniqueIndexCoverageError, assertTransactionArtifactBatchLimits, assertTransactionArtifactJournalLimits, assertTempRunPageBatchLimits, assertStorageBulkReadItems, boundedMaintenanceBatchItems, normalizeCompactionJobRecord, normalizeGarbageCollectionJobRecord, normalizeSegmentRecord, updateCompactionJobRecord, updateTransactionRecord, updateGarbageCollectionPlanningRecord, validateTableColumns, validateTableForeignKey, validateSecondaryIndexes, secondaryIndexColumnIds, secondaryIndexWriteContractChanged, secondaryUniqueKeyNamespace, transactionCommitDeltaRetainedBytes, uniqueKeyBuildChunkRetainedBytes, MAX_UNIQUE_KEY_BUILD_TTL_MS, MAX_UNIQUE_KEY_BUILD_STAGED_BYTES, MAX_UNIQUE_KEY_BUILD_STAGED_BYTES_TOTAL, MAX_UNIQUE_KEY_BUILD_TOKENS_PER_CHUNK, validateFtsPostingQueries, validateCanonicalManifestChangedTableIds, catalogRecordRetainedBytes, manifestRecordRetainedReservationBytes, segmentRecordRetainedBytes, snapshotAcceleratorItemRetainedUsage, assertSnapshotImportAcceleratorUsage, SchemaConflictError, WriteConflictError, } from "../types.js";
2
2
  import { dateIsoString } from "../../date-value.js";
3
3
  import { assertWellFormedString, crc32, MAX_STORED_BLOCK_BYTE_LENGTH, } from "../../block-format/index.js";
4
4
  /**
@@ -1729,9 +1729,7 @@ export class RecordCore {
1729
1729
  validateId(tableId);
1730
1730
  validateId(columnId);
1731
1731
  validateFtsPostingQueries(terms);
1732
- if (!Number.isSafeInteger(upToVersion) || upToVersion < -1) {
1733
- throw new RangeError("Full-text query version must be a safe integer at least -1");
1734
- }
1732
+ validateFtsReadVersion(upToVersion);
1735
1733
  validateFtsCandidateLimit(maxRowIds);
1736
1734
  const key = `${tableId}/${columnId}`;
1737
1735
  const base = this.#ftsBases.get(key);
@@ -1834,9 +1832,7 @@ export class RecordCore {
1834
1832
  readFtsPostings(tableId, columnId, upToVersion, maxRowIds = MAX_FTS_CANDIDATE_ROW_IDS, maxRetainedBytes) {
1835
1833
  validateId(tableId);
1836
1834
  validateId(columnId);
1837
- if (!Number.isSafeInteger(upToVersion) || upToVersion < -1) {
1838
- throw new RangeError("Full-text query version must be a safe integer at least -1");
1839
- }
1835
+ validateFtsReadVersion(upToVersion);
1840
1836
  const base = this.#ftsBases.get(`${tableId}/${columnId}`);
1841
1837
  const coversVersion = base?.coversVersion ?? -1;
1842
1838
  const delta = this.readFtsDeltas(tableId, columnId, coversVersion, upToVersion);
@@ -6489,11 +6485,6 @@ function sortedVersionInInterval(versions, addedVersion, removedVersion) {
6489
6485
  const version = versions[low];
6490
6486
  return version !== undefined && (removedVersion === null || version < removedVersion);
6491
6487
  }
6492
- function validateFtsCandidateLimit(value) {
6493
- if (!Number.isSafeInteger(value) || value < 1 || value > MAX_FTS_CANDIDATE_ROW_IDS) {
6494
- throw new RangeError(`Full-text candidate limit must be between 1 and ${String(MAX_FTS_CANDIDATE_ROW_IDS)}`);
6495
- }
6496
- }
6497
6488
  function varuintByteLength(value) {
6498
6489
  let length = 1;
6499
6490
  while (value >= 0x80n) {
@@ -534,8 +534,6 @@ export interface CompactionJobCursor {
534
534
  sourceSegmentIndex: number;
535
535
  sourceBlockIndex: number;
536
536
  }
537
- export declare const compactionRewritePlanKinds: readonly ["copy-v1", "rechunk-v1", "merge-v1"];
538
- export type CompactionRewritePlanKind = (typeof compactionRewritePlanKinds)[number];
539
537
  export interface CopyCompactionRewritePlan {
540
538
  readonly kind: "copy-v1";
541
539
  }
@@ -1669,6 +1667,10 @@ export declare function ftsPostingQueryMatches(term: string, query: FtsPostingQu
1669
1667
  * never drift between backends — pruning would silently differ per store.
1670
1668
  */
1671
1669
  export declare function collectFtsCandidates(chunkLists: Iterable<readonly FtsPosting[]>, terms: readonly FtsPostingQuery[], maxRowIds?: number): FtsCandidates;
1670
+ /** Validates the snapshot bound of one full-text read; -1 selects the base view alone. */
1671
+ export declare function validateFtsReadVersion(upToVersion: number): void;
1672
+ /** Validates the candidate row-ID ceiling accepted by one full-text candidate read. */
1673
+ export declare function validateFtsCandidateLimit(maxRowIds: number): void;
1672
1674
  /** Validates the fixed memory ceilings accepted by one ordered postings read. */
1673
1675
  export declare function validateFtsOrderedReadLimits(maxRowIds?: number, maxRetainedBytes?: number): void;
1674
1676
  /**
@@ -2422,7 +2424,6 @@ export declare function createManifest(input: CreateManifestInput): Manifest;
2422
2424
  export declare function normalizeSegmentRecord(record: SegmentRecord): SegmentRecord;
2423
2425
  export declare function updateTransactionRecord(record: TransactionRecord, update: TransactionRecordUpdate): TransactionRecord;
2424
2426
  export declare function createGarbageCollectionJobRecord(input: CreateGarbageCollectionJobInput): GarbageCollectionJobRecord;
2425
- export declare function normalizeGarbageCollectionDiscovery(discovery: GarbageCollectionDiscovery): GarbageCollectionDiscovery;
2426
2427
  export declare function updateGarbageCollectionPlanningRecord(record: GarbageCollectionJobRecord, input: UpdateGarbageCollectionPlanningInput): GarbageCollectionJobRecord;
2427
2428
  export declare function normalizeGarbageCollectionJobRecord(record: GarbageCollectionJobRecord): GarbageCollectionJobRecord;
2428
2429
  export declare function advanceGarbageCollectionJobRecord(record: GarbageCollectionJobRecord, accounting: GarbageCollectionStepAccounting): GarbageCollectionJobRecord;
@@ -726,7 +726,6 @@ export const compactionJobStates = [
726
726
  "cancelled",
727
727
  "aborted",
728
728
  ];
729
- export const compactionRewritePlanKinds = ["copy-v1", "rechunk-v1", "merge-v1"];
730
729
  export const compactionOutputCompressions = ["raw", "gzip"];
731
730
  /** Every segment ID owned by a compaction job, including merge partition outputs. */
732
731
  export function compactionOutputSegmentIds(job) {
@@ -1427,6 +1426,18 @@ export function collectFtsCandidates(chunkLists, terms, maxRowIds = MAX_FTS_CAND
1427
1426
  overflow: false,
1428
1427
  };
1429
1428
  }
1429
+ /** Validates the snapshot bound of one full-text read; -1 selects the base view alone. */
1430
+ export function validateFtsReadVersion(upToVersion) {
1431
+ if (!Number.isSafeInteger(upToVersion) || upToVersion < -1) {
1432
+ throw new RangeError("Full-text query version must be a safe integer at least -1");
1433
+ }
1434
+ }
1435
+ /** Validates the candidate row-ID ceiling accepted by one full-text candidate read. */
1436
+ export function validateFtsCandidateLimit(maxRowIds) {
1437
+ if (!Number.isSafeInteger(maxRowIds) || maxRowIds < 1 || maxRowIds > MAX_FTS_CANDIDATE_ROW_IDS) {
1438
+ throw new RangeError(`Full-text candidate limit must be between 1 and ${String(MAX_FTS_CANDIDATE_ROW_IDS)}`);
1439
+ }
1440
+ }
1430
1441
  /** Validates the fixed memory ceilings accepted by one ordered postings read. */
1431
1442
  export function validateFtsOrderedReadLimits(maxRowIds = MAX_FTS_CANDIDATE_ROW_IDS, maxRetainedBytes = MAX_FTS_ORDERED_READ_BYTES) {
1432
1443
  if (!Number.isSafeInteger(maxRowIds) || maxRowIds < 1 || maxRowIds > MAX_FTS_CANDIDATE_ROW_IDS) {
@@ -1940,7 +1951,7 @@ export function createGarbageCollectionJobRecord(input) {
1940
1951
  ...(discovery === undefined ? {} : { discovery }),
1941
1952
  };
1942
1953
  }
1943
- export function normalizeGarbageCollectionDiscovery(discovery) {
1954
+ function normalizeGarbageCollectionDiscovery(discovery) {
1944
1955
  const runtime = discovery;
1945
1956
  if (typeof runtime !== "object" || runtime === null) {
1946
1957
  throw new TypeError("Garbage collection discovery must be an object");
@@ -1008,6 +1008,34 @@ export function blockStoreConformanceCases() {
1008
1008
  { term: "minnows", rowIds: [2n], tf: [1] },
1009
1009
  { term: "shark", rowIds: [4n], tf: [1] },
1010
1010
  ], "ordered postings must merge into canonical term and row-ID order");
1011
+ // The read-argument contract is uniform across adapters: -1 selects the base view
1012
+ // alone, anything below it (or non-integral) is refused, and the candidate limit is
1013
+ // validated on every read path.
1014
+ const baseOnly = await store.readFtsPostings("table-t", "col-v", -1);
1015
+ check(baseOnly.hasBase && baseOnly.postings.length === 3, "an upToVersion of -1 must serve the base view alone");
1016
+ for (const version of [-2, Number.NaN, 1.5]) {
1017
+ try {
1018
+ await store.readFtsCandidates("table-t", "col-v", [{ term: "minnow", prefix: false }], version);
1019
+ throw new Error(`candidate read accepted invalid version ${String(version)}`);
1020
+ }
1021
+ catch (error) {
1022
+ check(error instanceof RangeError, "invalid candidate versions must throw RangeError");
1023
+ }
1024
+ try {
1025
+ await store.readFtsPostings("table-t", "col-v", version);
1026
+ throw new Error(`postings read accepted invalid version ${String(version)}`);
1027
+ }
1028
+ catch (error) {
1029
+ check(error instanceof RangeError, "invalid postings versions must throw RangeError");
1030
+ }
1031
+ }
1032
+ try {
1033
+ await store.readFtsCandidates("table-t", "col-v", [{ term: "minnow", prefix: false }], 10, 0);
1034
+ throw new Error("candidate read accepted a zero row-ID limit");
1035
+ }
1036
+ catch (error) {
1037
+ check(error instanceof RangeError, "invalid candidate limits must throw RangeError");
1038
+ }
1011
1039
  store.close();
1012
1040
  },
1013
1041
  },
@@ -1,38 +1,6 @@
1
1
  export declare const protocolVersion: 3;
2
2
  /** Outstanding request/response pairs retained by either side of one database RPC connection. */
3
3
  export declare const MAX_DATABASE_RPC_IN_FLIGHT = 256;
4
- export type WorkerOperation = "benchmark" | "cancelBenchmark" | "datasetList" | "datasetCreate" | "datasetDelete" | "runQuery" | "suiteReference" | "suiteWrite" | "suiteFeatureMatrix" | "suiteLive";
5
- export interface WorkerRequest<T = unknown> {
6
- version: typeof protocolVersion;
7
- requestId: string;
8
- operation: WorkerOperation;
9
- payload: T;
10
- }
11
- export interface SuccessResponse<T = unknown> {
12
- version: typeof protocolVersion;
13
- requestId: string;
14
- kind: "success";
15
- result: T;
16
- }
17
- export interface FailureResponse {
18
- version: typeof protocolVersion;
19
- requestId: string;
20
- kind: "failure";
21
- error: {
22
- name: string;
23
- message: string;
24
- };
25
- }
26
- export interface ProgressResponse<T = unknown> {
27
- version: typeof protocolVersion;
28
- requestId: string;
29
- kind: "progress";
30
- progress: T;
31
- }
32
- export type WorkerResponse<T = unknown> = SuccessResponse<T> | FailureResponse | ProgressResponse;
33
- export declare function parseRequest(value: unknown): WorkerRequest;
34
- export declare function success<T>(requestId: string, result: T): SuccessResponse<T>;
35
- export declare function failure(requestId: string, error: unknown): FailureResponse;
36
4
  export type RpcRequest = {
37
5
  version: typeof protocolVersion;
38
6
  requestId: string;
@@ -1,31 +1,6 @@
1
1
  export const protocolVersion = 3;
2
2
  /** Outstanding request/response pairs retained by either side of one database RPC connection. */
3
3
  export const MAX_DATABASE_RPC_IN_FLIGHT = 256;
4
- export function parseRequest(value) {
5
- if (typeof value !== "object" || value === null)
6
- throw new TypeError("Request must be an object");
7
- const candidate = value;
8
- if (candidate.version !== protocolVersion)
9
- throw new Error("Unsupported protocol version");
10
- if (typeof candidate.requestId !== "string" || candidate.requestId.length === 0) {
11
- throw new TypeError("Request ID must be a non-empty string");
12
- }
13
- if (!isOperation(candidate.operation))
14
- throw new Error("Unsupported worker operation");
15
- return candidate;
16
- }
17
- export function success(requestId, result) {
18
- return { version: protocolVersion, requestId, kind: "success", result };
19
- }
20
- export function failure(requestId, error) {
21
- const normalized = error instanceof Error ? error : new Error(String(error));
22
- return {
23
- version: protocolVersion,
24
- requestId,
25
- kind: "failure",
26
- error: { name: normalized.name, message: normalized.message },
27
- };
28
- }
29
4
  export function serializeError(error) {
30
5
  if (!(error instanceof Error)) {
31
6
  return { name: "Error", message: String(error) };
@@ -101,17 +76,3 @@ export function parseRpcResponse(value) {
101
76
  throw new Error("Unsupported protocol version");
102
77
  return candidate;
103
78
  }
104
- function isOperation(value) {
105
- return [
106
- "benchmark",
107
- "cancelBenchmark",
108
- "datasetList",
109
- "datasetCreate",
110
- "datasetDelete",
111
- "runQuery",
112
- "suiteReference",
113
- "suiteWrite",
114
- "suiteFeatureMatrix",
115
- "suiteLive",
116
- ].includes(String(value));
117
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minnowdb/core",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
4
4
  "description": "A columnar SQL database for the browser: PostgreSQL-style SQL over durable IndexedDB or OPFS data, with no server or WebAssembly module.",
5
5
  "license": "MIT",
6
6
  "author": "Eric Wilhite",