@minnowdb/core 0.4.1 → 0.5.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.
@@ -1,6 +1,7 @@
1
1
  import { dateMilliseconds } from "../date-value.js";
2
+ import { unknownColumnDomains } from "./query.js";
2
3
  export function encodeQueryResult(result) {
3
- return encodeRows(result.columns, result.rows);
4
+ return encodeRows(result.columns, result.rows, result.columnDomains);
4
5
  }
5
6
  /**
6
7
  * Encodes a bare row array (`run()` returns rows without a column list). Every row of one result
@@ -8,13 +9,18 @@ export function encodeQueryResult(result) {
8
9
  */
9
10
  export function encodeQueryRows(rows) {
10
11
  const first = rows[0];
11
- return encodeRows(first === undefined ? [] : Object.keys(first), rows);
12
+ const columns = first === undefined ? [] : Object.keys(first);
13
+ return encodeRows(columns, rows, unknownColumnDomains(columns));
12
14
  }
13
15
  export function decodeQueryResult(payload) {
14
16
  if (!isWireQueryResult(payload)) {
15
17
  throw new TypeError("Expected a columnar query result frame");
16
18
  }
17
19
  const columns = [...payload.columns];
20
+ if (payload.values.length !== columns.length || payload.columnDomains.length !== columns.length) {
21
+ throw new TypeError("Columnar result frame metadata is not aligned");
22
+ }
23
+ const columnDomains = structuredClone(payload.columnDomains);
18
24
  const rows = [];
19
25
  for (let index = 0; index < payload.rowCount; index += 1)
20
26
  rows.push({});
@@ -26,17 +32,21 @@ export function decodeQueryResult(payload) {
26
32
  throw new TypeError("Columnar result frame is missing a column");
27
33
  fillColumn(rows, name, column);
28
34
  }
29
- return { columns, rows };
35
+ return { columns, columnDomains, rows };
30
36
  }
31
37
  export function isWireQueryResult(value) {
32
38
  return (typeof value === "object" &&
33
39
  value !== null &&
34
40
  value.kind === "columnar-result" &&
35
41
  Array.isArray(value.columns) &&
42
+ Array.isArray(value.columnDomains) &&
36
43
  Array.isArray(value.values) &&
37
44
  typeof value.rowCount === "number");
38
45
  }
39
- function encodeRows(columns, rows) {
46
+ function encodeRows(columns, rows, columnDomains) {
47
+ if (columnDomains.length !== columns.length) {
48
+ throw new TypeError("Query result column domains must align with the result columns");
49
+ }
40
50
  const transfer = [];
41
51
  const values = columns.map((name) => {
42
52
  const column = encodeColumn(name, rows);
@@ -53,7 +63,13 @@ function encodeRows(columns, rows) {
53
63
  return column;
54
64
  });
55
65
  return {
56
- payload: { kind: "columnar-result", columns: [...columns], rowCount: rows.length, values },
66
+ payload: {
67
+ kind: "columnar-result",
68
+ columns: [...columns],
69
+ columnDomains: structuredClone([...columnDomains]),
70
+ rowCount: rows.length,
71
+ values,
72
+ },
57
73
  transfer,
58
74
  };
59
75
  }
@@ -17,6 +17,7 @@ export interface WireColumn {
17
17
  table: string;
18
18
  column: string;
19
19
  onDelete: ReferentialAction;
20
+ enforced?: boolean;
20
21
  };
21
22
  readonly enumValues?: readonly string[];
22
23
  /** Already frozen by planning, so the wire carries a value and never a generator. */
@@ -83,6 +84,12 @@ export type WireMigrationStep = {
83
84
  tableName: string;
84
85
  columnName: string;
85
86
  defaultValue: ColumnDefault | null;
87
+ } | {
88
+ kind: "alter-foreign-keys";
89
+ tableName: string;
90
+ foreignKeys: Extract<MigrationStep, {
91
+ kind: "alter-foreign-keys";
92
+ }>["foreignKeys"];
86
93
  } | {
87
94
  kind: "replace-view";
88
95
  view: WireView;
@@ -96,7 +96,9 @@ function deserializeColumn(wire) {
96
96
  ...(wire.sqlDomain === undefined ? {} : { sqlDomain: structuredClone(wire.sqlDomain) }),
97
97
  ...(wire.defaultSpec === undefined ? {} : { defaultSpec: wire.defaultSpec }),
98
98
  ...(wire.renamedFromName === undefined ? {} : { renamedFromName: wire.renamedFromName }),
99
- ...(wire.reference === undefined ? {} : { reference: wire.reference }),
99
+ ...(wire.reference === undefined
100
+ ? {}
101
+ : { reference: { ...wire.reference, enforced: wire.reference.enforced !== false } }),
100
102
  ...(wire.enumValues === undefined ? {} : { enumValues: wire.enumValues }),
101
103
  ...(wire.backfillValue === undefined ? {} : { backfillValue: wire.backfillValue }),
102
104
  });
@@ -16,6 +16,7 @@ export interface ColumnReferenceSpec {
16
16
  readonly table: string;
17
17
  readonly column: string;
18
18
  readonly onDelete: ReferentialAction;
19
+ readonly enforced: boolean;
19
20
  }
20
21
  /** A row condition every write must satisfy (E141-06); `sql` is a boolean expression. */
21
22
  export interface TableCheck {
@@ -31,6 +32,8 @@ export interface TableForeignKey<TColumnName extends string = string> {
31
32
  readonly columns: readonly string[];
32
33
  };
33
34
  readonly onDelete?: ReferentialAction;
35
+ /** False keeps the relationship in the catalog without validating or cascading rows. */
36
+ readonly enforced?: boolean;
34
37
  }
35
38
  /**
36
39
  * The constraint name `migrate()` gives a declared relation. It matches the name the SQL parser
@@ -91,9 +94,14 @@ export interface ColumnBuilder<TValue extends SchemaValue, TNullable extends boo
91
94
  */
92
95
  references(this: ColumnBuilder<TValue, TNullable, TUnique, THasDefault, TInput>, table: string, column: string, options?: {
93
96
  onDelete?: Exclude<ReferentialAction, "set null">;
97
+ enforced?: true;
98
+ } | {
99
+ enforced: false;
100
+ onDelete?: never;
94
101
  }): ColumnBuilder<TValue, TNullable, TUnique, THasDefault, TInput>;
95
102
  references(this: ColumnBuilder<TValue, true, TUnique, THasDefault, TInput>, table: string, column: string, options: {
96
103
  onDelete: "set null";
104
+ enforced?: true;
97
105
  }): ColumnBuilder<TValue, TNullable, TUnique, THasDefault, TInput>;
98
106
  /** Declares a literal SQL default. Omission or SQL `DEFAULT` invokes it; NULL does not. */
99
107
  default(value: TInput): ColumnBuilder<TValue, TNullable, TUnique, true, TInput>;
@@ -152,6 +160,8 @@ export declare const column: {
152
160
  json: () => ColumnBuilder<string, false, false, false, string>;
153
161
  jsonb: () => ColumnBuilder<string, false, false, false, string>;
154
162
  uuid: () => ColumnBuilder<string, false, false, false, string>;
163
+ /** A zoneless calendar date, represented publicly as canonical `YYYY-MM-DD` text. */
164
+ date: () => ColumnBuilder<string, false, false, false, string>;
155
165
  time: () => ColumnBuilder<string, false, false, false, string>;
156
166
  interval: () => ColumnBuilder<string, false, false, false, string>;
157
167
  /** JSON array text at the JavaScript boundary, with the SQL element type retained in metadata. */
@@ -234,6 +244,7 @@ export declare function declaredForeignKeys(definition: AnyTable): Array<{
234
244
  parentTable: string;
235
245
  parentColumns: string[];
236
246
  onDelete: ReferentialAction;
247
+ enforced: boolean;
237
248
  }>;
238
249
  /**
239
250
  * A view declared in the schema: the query it stands for, plus the column shape the author
@@ -366,6 +377,12 @@ export type MigrationStep = {
366
377
  columnName: string;
367
378
  defaultValue: ColumnDefault | null;
368
379
  }
380
+ /** Informational relationships are catalog-only and may change without scanning stored rows. */
381
+ | {
382
+ kind: "alter-foreign-keys";
383
+ tableName: string;
384
+ foreignKeys: ReturnType<typeof declaredForeignKeys>;
385
+ }
369
386
  /**
370
387
  * A view is derived and disposable: nothing is stored under it, so replacing its body loses no
371
388
  * data and needs none of the proofs a table alteration needs. `replace` covers both creating a
@@ -71,12 +71,16 @@ function createColumn(type, state = {}) {
71
71
  references: (table, referencedColumn, options = {}) => {
72
72
  validateSchemaName(table, "Referenced table");
73
73
  validateSchemaName(referencedColumn, "Referenced column");
74
+ if (options.enforced === false && options.onDelete !== undefined) {
75
+ throw new TypeError("An informational FOREIGN KEY cannot declare ON DELETE behavior");
76
+ }
74
77
  return createColumn(type, {
75
78
  ...state,
76
79
  reference: {
77
80
  table,
78
81
  column: referencedColumn,
79
82
  onDelete: options.onDelete ?? "restrict",
83
+ enforced: options.enforced !== false,
80
84
  },
81
85
  });
82
86
  },
@@ -160,6 +164,8 @@ export const column = {
160
164
  json: () => createColumn("string", { sqlDomain: { kind: "json" } }),
161
165
  jsonb: () => createColumn("string", { sqlDomain: { kind: "jsonb" } }),
162
166
  uuid: () => createColumn("string", { sqlDomain: { kind: "uuid" } }),
167
+ /** A zoneless calendar date, represented publicly as canonical `YYYY-MM-DD` text. */
168
+ date: () => createColumn("string", { sqlDomain: { kind: "date" } }),
163
169
  time: () => createColumn("string", { sqlDomain: { kind: "time" } }),
164
170
  interval: () => createColumn("string", { sqlDomain: { kind: "interval" } }),
165
171
  /** JSON array text at the JavaScript boundary, with the SQL element type retained in metadata. */
@@ -301,6 +307,9 @@ export function table(name, columns, options = {}) {
301
307
  const foreignKeyNames = new Set();
302
308
  for (const key of foreignKeys) {
303
309
  validateSchemaName(key.name, "FOREIGN KEY");
310
+ if (key.enforced === false && key.onDelete !== undefined) {
311
+ throw new TypeError(`Informational FOREIGN KEY ${key.name} cannot declare ON DELETE behavior`);
312
+ }
304
313
  if (foreignKeyNames.has(key.name)) {
305
314
  throw new TypeError(`Duplicate FOREIGN KEY in table ${name}: ${key.name}`);
306
315
  }
@@ -431,6 +440,7 @@ export function declaredForeignKeys(definition) {
431
440
  parentTable: reference.table,
432
441
  parentColumns: [reference.column],
433
442
  onDelete: reference.onDelete,
443
+ enforced: reference.enforced,
434
444
  });
435
445
  }
436
446
  for (const key of definition.foreignKeys) {
@@ -442,6 +452,7 @@ export function declaredForeignKeys(definition) {
442
452
  parentTable: key.references.table,
443
453
  parentColumns,
444
454
  onDelete: key.onDelete ?? "restrict",
455
+ enforced: key.enforced !== false,
445
456
  });
446
457
  }
447
458
  return keys;
@@ -675,34 +686,58 @@ function backfillsEqual(left, right) {
675
686
  return externalLeft === externalRight;
676
687
  }
677
688
  /**
678
- * Constraints on an existing table cannot change through a metadata-only step. Attaching a
679
- * FOREIGN KEY or CHECK to a table that already holds rows would claim something about those rows
689
+ * Enforced constraints on an existing table cannot change through a metadata-only step. Attaching
690
+ * a FOREIGN KEY or CHECK to a table that already holds rows would claim something about those rows
680
691
  * that nobody has verified, and no validation scan exists; dropping one is refused for the mirror
681
- * reason, so that a constraint never disappears because a schema file drifted. Both get the same
682
- * discipline every other unprovable change gets: an explicit error naming the fix.
692
+ * reason, so that a constraint never disappears because a schema file drifted. Informational
693
+ * foreign keys are catalog metadata only, so adding, dropping, or remapping one is safe.
683
694
  */
684
- function assertConstraintsUnchanged(record, definition) {
695
+ function planConstraintChanges(record, definition, steps) {
685
696
  const describeKey = (key) => `${key.columns.join(",")} -> ${key.parentTable}.` +
686
697
  `${key.parentColumns.join(",")} ON DELETE ${key.onDelete}`;
698
+ const declaredList = declaredForeignKeys(definition);
687
699
  const existingKeys = new Map(record.foreignKeys.map((key) => [key.name, key]));
688
- const declaredKeys = new Map(declaredForeignKeys(definition).map((key) => [key.name, key]));
700
+ const declaredKeys = new Map(declaredList.map((key) => [key.name, key]));
701
+ let informationalChanged = false;
689
702
  for (const [name, declared] of declaredKeys) {
690
703
  const existing = existingKeys.get(name);
691
704
  if (existing === undefined) {
705
+ if (!declared.enforced) {
706
+ informationalChanged = true;
707
+ continue;
708
+ }
692
709
  throw new TypeError(`FOREIGN KEY cannot be added after creation: ${definition.name}.${declared.columns.join(",")}. ` +
693
710
  `Existing rows are not known to satisfy it; recreate the table to add a relation.`);
694
711
  }
712
+ if (existing.enforced !== declared.enforced) {
713
+ throw new TypeError(`FOREIGN KEY enforcement cannot change: ${definition.name}.${declared.columns.join(",")}`);
714
+ }
695
715
  if (describeKey(existing) !== describeKey(declared)) {
716
+ if (!declared.enforced) {
717
+ informationalChanged = true;
718
+ continue;
719
+ }
696
720
  throw new TypeError(`FOREIGN KEY cannot change: ${definition.name}.${declared.columns.join(",")} is ` +
697
721
  `${describeKey(existing)}, schema says ${describeKey(declared)}`);
698
722
  }
699
723
  }
700
724
  for (const name of existingKeys.keys()) {
701
725
  if (!declaredKeys.has(name)) {
726
+ if (existingKeys.get(name)?.enforced === false) {
727
+ informationalChanged = true;
728
+ continue;
729
+ }
702
730
  throw new TypeError(`FOREIGN KEY cannot be dropped: ${definition.name} still has ${name}. ` +
703
731
  `Declare the relation, or recreate the table without it.`);
704
732
  }
705
733
  }
734
+ if (informationalChanged) {
735
+ steps.push({
736
+ kind: "alter-foreign-keys",
737
+ tableName: definition.name,
738
+ foreignKeys: declaredList,
739
+ });
740
+ }
706
741
  const existingChecks = new Map(record.checks.map((check) => [check.name, check.sql]));
707
742
  const declaredChecks = new Map(definition.checks.map((check) => [check.name, check.sql]));
708
743
  for (const [name, sql] of declaredChecks) {
@@ -889,7 +924,7 @@ export function planMigration(catalog, definition, options = {}) {
889
924
  desiredKeyIds.some((id, index) => id !== existingKeyIds[index])) {
890
925
  throw new TypeError(`Primary key order cannot change: ${tableDefinition.name}`);
891
926
  }
892
- assertConstraintsUnchanged(record, tableDefinition);
927
+ planConstraintChanges(record, tableDefinition, steps);
893
928
  }
894
929
  if (options.schemaOwnsDatabase === true) {
895
930
  const declaredTables = new Set(definition.tables.map(({ name }) => name));
@@ -19,6 +19,9 @@ export declare function exactNumericCompare(left: unknown, right: unknown): numb
19
19
  export declare function boundedJsonText(value: unknown, canonical: boolean, label?: string): string;
20
20
  export declare function jsonDomainValue(value: unknown, binary: boolean): string | null;
21
21
  export declare function uuidDomainValue(value: unknown): string | null;
22
+ /** Canonical, zoneless SQL DATE value. No JavaScript time zone participates in validation. */
23
+ export declare function dateDomainValue(value: unknown): string | null;
24
+ export declare function isDateDomainValue(value: unknown): value is string;
22
25
  export declare function timeDomainValue(value: unknown): string | null;
23
26
  export declare function intervalDomainValue(value: unknown): string | null;
24
27
  export declare function arrayDomainValue(values: readonly unknown[]): string;
@@ -8,6 +8,7 @@ const NUMERIC = `${PREFIX}numeric:`;
8
8
  const JSON_VALUE = `${PREFIX}json:`;
9
9
  const JSONB_VALUE = `${PREFIX}jsonb:`;
10
10
  const UUID_VALUE = `${PREFIX}uuid:`;
11
+ const DATE_VALUE = `${PREFIX}date:`;
11
12
  const TIME_VALUE = `${PREFIX}time:`;
12
13
  const INTERVAL_VALUE = `${PREFIX}interval:`;
13
14
  const ARRAY_VALUE = `${PREFIX}array:`;
@@ -324,6 +325,42 @@ export function uuidDomainValue(value) {
324
325
  }
325
326
  return UUID_VALUE + source;
326
327
  }
328
+ /** Canonical, zoneless SQL DATE value. No JavaScript time zone participates in validation. */
329
+ export function dateDomainValue(value) {
330
+ if (value === null || value === undefined)
331
+ return null;
332
+ let source;
333
+ if (value instanceof Date) {
334
+ if (!Number.isFinite(value.getTime()))
335
+ throw new TypeError("DATE accepts a valid Date value");
336
+ source = dateIsoString(value).slice(0, 10);
337
+ }
338
+ else if (typeof value === "string") {
339
+ assertBoundedDomainString(value, "DATE value");
340
+ source = value.startsWith(DATE_VALUE) ? value.slice(DATE_VALUE.length) : value;
341
+ }
342
+ else {
343
+ throw new TypeError("DATE accepts a YYYY-MM-DD string value");
344
+ }
345
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(source);
346
+ const year = Number(match?.[1]);
347
+ const month = Number(match?.[2]);
348
+ const day = Number(match?.[3]);
349
+ const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
350
+ const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
351
+ if (match === null ||
352
+ year < 1 ||
353
+ month < 1 ||
354
+ month > 12 ||
355
+ day < 1 ||
356
+ day > (days[month - 1] ?? 0)) {
357
+ throw new TypeError(`Invalid DATE value: ${String(value)}`);
358
+ }
359
+ return DATE_VALUE + source;
360
+ }
361
+ export function isDateDomainValue(value) {
362
+ return typeof value === "string" && value.startsWith(DATE_VALUE);
363
+ }
327
364
  export function timeDomainValue(value) {
328
365
  if (value === null || value === undefined)
329
366
  return null;
@@ -464,6 +501,8 @@ export function normalizeSqlDomainValue(domain, value) {
464
501
  return jsonDomainValue(value, true);
465
502
  if (domain.kind === "uuid")
466
503
  return uuidDomainValue(value);
504
+ if (domain.kind === "date")
505
+ return dateDomainValue(value);
467
506
  if (domain.kind === "time")
468
507
  return timeDomainValue(value);
469
508
  if (domain.kind === "interval")
@@ -562,7 +601,7 @@ export function externalSqlDomainValue(value) {
562
601
  return value;
563
602
  if (value.startsWith(TEXT_VALUE))
564
603
  return value.slice(TEXT_VALUE.length);
565
- for (const prefix of [NUMERIC, JSON_VALUE, JSONB_VALUE, UUID_VALUE, TIME_VALUE]) {
604
+ for (const prefix of [NUMERIC, JSON_VALUE, JSONB_VALUE, UUID_VALUE, DATE_VALUE, TIME_VALUE]) {
566
605
  if (value.startsWith(prefix))
567
606
  return value.slice(prefix.length);
568
607
  }
@@ -1,7 +1,7 @@
1
1
  import { dateMilliseconds } from "../date-value.js";
2
2
  import { assertWellFormedString } from "../block-format/unicode.js";
3
3
  import { MAX_SQL_NESTING_DEPTH, MAX_SQL_PATTERN_CHARACTERS, MAX_SQL_PATTERN_MATCH_STEPS, } from "./cache-limits.js";
4
- import { collatedDomainCompare, enumDomainCompare, exactNumericCompare, externalSqlDomainValue, externalSqlTextValue, } from "./sql-domains.js";
4
+ import { collatedDomainCompare, enumDomainCompare, exactNumericCompare, externalSqlDomainValue, externalSqlTextValue, isDateDomainValue, } from "./sql-domains.js";
5
5
  /**
6
6
  * SQL value semantics shared by the row and vector executors.
7
7
  *
@@ -31,8 +31,26 @@ export function compareSqlValues(left, right) {
31
31
  const exact = exactNumericCompare(left, right);
32
32
  if (exact !== undefined)
33
33
  return exact;
34
- const a = left instanceof Date ? dateMilliseconds(left) : left;
35
- const b = right instanceof Date ? dateMilliseconds(right) : right;
34
+ const temporal = (value) => {
35
+ if (value instanceof Date)
36
+ return dateMilliseconds(value);
37
+ if (!isDateDomainValue(value))
38
+ return undefined;
39
+ const external = externalSqlDomainValue(value);
40
+ return typeof external === "string" ? Date.parse(`${external}T00:00:00.000Z`) : undefined;
41
+ };
42
+ const temporalLeft = temporal(left);
43
+ const temporalRight = temporal(right);
44
+ if (temporalLeft !== undefined || temporalRight !== undefined) {
45
+ const comparableLeft = temporalLeft ?? (typeof left === "number" ? left : undefined);
46
+ const comparableRight = temporalRight ?? (typeof right === "number" ? right : undefined);
47
+ if (comparableLeft === undefined || comparableRight === undefined) {
48
+ throw new TypeError("Values must have comparable SQL types");
49
+ }
50
+ return comparableLeft - comparableRight;
51
+ }
52
+ const a = left;
53
+ const b = right;
36
54
  if (a === b)
37
55
  return 0;
38
56
  if (a === null || a === undefined)
@@ -1,13 +1,14 @@
1
1
  import { dateMilliseconds } from "../date-value.js";
2
2
  import { MAX_TEMP_RUN_BATCH_BYTES, MAX_TEMP_RUN_PAGE_BYTES, MAX_TEMP_RUN_PAGES_PER_BATCH, } from "../storage/types.js";
3
- import { cachedListMembership, childExpressions, distinctFromComparison, nullOrder, isScalarFunctionName, likeMatches, orderOutputName, parseQuantified, quantifiedComparison, scalarFunctionValue, } from "./query.js";
3
+ import { cachedListMembership, childExpressions, distinctFromComparison, nullOrder, isScalarFunctionName, likeMatches, orderOutputName, parseQuantified, quantifiedComparison, scalarFunctionValue, unknownColumnDomains, } from "./query.js";
4
4
  import { jsonValueOf } from "./sql-json.js";
5
5
  import { bm25DocumentScore, cachedQueryTerms, FtsStatsAccumulator, fullTermsMask, renderDocumentValue, termFrequencies, termsMask, tokenize, } from "./fts.js";
6
6
  import { ByteGroupIndex } from "./group-index.js";
7
7
  import { ByteJoinIndex } from "./join-index.js";
8
+ import { UnknownTableError } from "./errors.js";
8
9
  import { QueryMemoryBudgetError, QueryMemoryContext, } from "./memory.js";
9
- import { compareSqlStrings, compileSimilarPattern, defineSqlResultProperty, } from "./sql-semantics.js";
10
- import { exactNumericBinary, exactNumericCompare, collatedDomainCompare, enumDomainCompare, externalSqlDomainValue, isExactNumeric, protectedSqlTextValue, } from "./sql-domains.js";
10
+ import { compareSqlValues, compileSimilarPattern, defineSqlResultProperty, } from "./sql-semantics.js";
11
+ import { exactNumericBinary, externalSqlDomainValue, isDateDomainValue, isExactNumeric, protectedSqlTextValue, } from "./sql-domains.js";
11
12
  import { buildSortKeyColumn, sortKeyIndexes } from "./sort-keys.js";
12
13
  const DEFAULT_BATCH_ROWS = 2_048;
13
14
  /** Above this, locating each IN member separately costs more than scanning between them. */
@@ -559,7 +560,7 @@ function bindPlan(plan, tables, memory, ftsStats) {
559
560
  const sourceTables = sources.map((source) => {
560
561
  const table = tables.get(source.table);
561
562
  if (table === undefined)
562
- throw new TypeError(`Unknown table: ${source.table}`);
563
+ throw new UnknownTableError(source.table);
563
564
  return table;
564
565
  });
565
566
  const sourceAliases = sources.map((source) => source.alias);
@@ -1304,7 +1305,11 @@ async function executeBoundPlanBatches(plan, memory, options, consume) {
1304
1305
  }
1305
1306
  if (rows.length > 0) {
1306
1307
  emitted += rows.length;
1307
- await consume({ columns: [...columns], rows });
1308
+ await consume({
1309
+ columns: [...columns],
1310
+ columnDomains: unknownColumnDomains(columns),
1311
+ rows,
1312
+ });
1308
1313
  }
1309
1314
  }
1310
1315
  finally {
@@ -1374,7 +1379,7 @@ async function executeBoundPlanWithSortSpill(plan, memory, options) {
1374
1379
  start += length;
1375
1380
  }
1376
1381
  if (runs.length === 0)
1377
- return { columns, rows: [] };
1382
+ return { columns, columnDomains: unknownColumnDomains(columns), rows: [] };
1378
1383
  let active = runs;
1379
1384
  while (active.length > 1) {
1380
1385
  const merged = [];
@@ -1408,7 +1413,7 @@ async function executeBoundPlanWithSortSpill(plan, memory, options) {
1408
1413
  }
1409
1414
  if (offset > 0)
1410
1415
  rows.splice(0, Math.min(offset, rows.length));
1411
- return { columns, rows };
1416
+ return { columns, columnDomains: unknownColumnDomains(columns), rows };
1412
1417
  }
1413
1418
  finally {
1414
1419
  await store.removeOwner(ownerId);
@@ -1563,7 +1568,7 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
1563
1568
  }
1564
1569
  }
1565
1570
  if (runs.length === 0)
1566
- return { columns, rows: [] };
1571
+ return { columns, columnDomains: unknownColumnDomains(columns), rows: [] };
1567
1572
  const finalRun = await mergeAllSpillRuns(store, ownerId, runs, columns, plan.orderBy, pageRows, () => `merge-${String(runSequence++)}`, memory);
1568
1573
  const spillOffset = plan.offset ?? 0;
1569
1574
  const result = await readFinalSpillRun(store, ownerId, finalRun, columns, plan.limit === undefined ? undefined : plan.limit + spillOffset);
@@ -1609,7 +1614,7 @@ async function readFinalSpillRun(store, ownerId, run, columns, requestedLimit) {
1609
1614
  rows.push(row);
1610
1615
  }
1611
1616
  }
1612
- return { columns: [...columns], rows };
1617
+ return { columns: [...columns], columnDomains: unknownColumnDomains(columns), rows };
1613
1618
  }
1614
1619
  const hashScratch = new DataView(new ArrayBuffer(8));
1615
1620
  /**
@@ -1909,7 +1914,7 @@ function finishResult(plan, inputRows, memory) {
1909
1914
  rows.length = Math.min(plan.limit, rows.length);
1910
1915
  }
1911
1916
  const columns = plan.wildcard ? wildcardColumnNames(plan) : plan.select.map((item) => item.alias);
1912
- return { columns, rows };
1917
+ return { columns, columnDomains: unknownColumnDomains(columns), rows };
1913
1918
  }
1914
1919
  /** Source indexes a bound expression reads, for pre-join predicate placement. */
1915
1920
  function prefilterSources(expression, into) {
@@ -3883,6 +3888,8 @@ function projectBatchRow(plan, batch, row) {
3883
3888
  const rowIndex = batch.rowsBySource[source]?.[row] ?? -1;
3884
3889
  const prefix = multiple ? `${plan.sourceAliases[source] ?? ""}.` : "";
3885
3890
  for (const [name, vector] of table.columns) {
3891
+ if (name.startsWith("\0"))
3892
+ continue;
3886
3893
  const outputName = multiple ? prefix + name : name;
3887
3894
  const value = vectorValue(vector, rowIndex);
3888
3895
  if (outputName === "__proto__")
@@ -3895,7 +3902,9 @@ function projectBatchRow(plan, batch, row) {
3895
3902
  }
3896
3903
  function wildcardColumnNames(plan) {
3897
3904
  const multiple = plan.sourceTables.length > 1;
3898
- return plan.sourceTables.flatMap((table, source) => [...table.columns.keys()].map((name) => multiple ? `${plan.sourceAliases[source] ?? ""}.${name}` : name));
3905
+ return plan.sourceTables.flatMap((table, source) => [...table.columns.keys()]
3906
+ .filter((name) => !name.startsWith("\0"))
3907
+ .map((name) => (multiple ? `${plan.sourceAliases[source] ?? ""}.${name}` : name)));
3899
3908
  }
3900
3909
  /** Detects `stringColumn = 'literal'` (or !=) so batches compare dictionary codes per row. */
3901
3910
  function detectDictionaryEquality(predicate) {
@@ -4506,7 +4515,13 @@ function comparisonValue(operator, leftValue, rightValue) {
4506
4515
  return comparison <= 0;
4507
4516
  }
4508
4517
  function comparable(value) {
4509
- return value instanceof Date ? dateMilliseconds(value) : value;
4518
+ if (value instanceof Date)
4519
+ return dateMilliseconds(value);
4520
+ if (isDateDomainValue(value)) {
4521
+ const external = externalSqlDomainValue(value);
4522
+ return typeof external === "string" ? Date.parse(`${external}T00:00:00.000Z`) : value;
4523
+ }
4524
+ return value;
4510
4525
  }
4511
4526
  function groupKey(value) {
4512
4527
  const comparableValue = comparable(value);
@@ -4547,35 +4562,7 @@ function stableSortRows(rows, orderBy) {
4547
4562
  }
4548
4563
  }
4549
4564
  function compareValues(left, right) {
4550
- const collated = collatedDomainCompare(left, right);
4551
- if (collated !== undefined)
4552
- return collated;
4553
- const enumOrder = enumDomainCompare(left, right);
4554
- if (enumOrder !== undefined)
4555
- return enumOrder;
4556
- const exact = exactNumericCompare(left, right);
4557
- if (exact !== undefined)
4558
- return exact;
4559
- const a = left instanceof Date ? dateMilliseconds(left) : left;
4560
- const b = right instanceof Date ? dateMilliseconds(right) : right;
4561
- if (a === b)
4562
- return 0;
4563
- if (a === null || a === undefined)
4564
- return -1;
4565
- if (b === null || b === undefined)
4566
- return 1;
4567
- if (typeof a === "number" && typeof b === "number") {
4568
- if (Number.isNaN(a))
4569
- return Number.isNaN(b) ? 0 : 1;
4570
- if (Number.isNaN(b))
4571
- return -1;
4572
- return a - b;
4573
- }
4574
- if (typeof a === "string" && typeof b === "string")
4575
- return compareSqlStrings(a, b);
4576
- if (typeof a === "boolean" && typeof b === "boolean")
4577
- return Number(a) - Number(b);
4578
- throw new TypeError("Values must have comparable SQL types");
4565
+ return compareSqlValues(left, right);
4579
4566
  }
4580
4567
  function numeric(value) {
4581
4568
  if (typeof value !== "number")
@@ -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, 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, validateFtsOrderedReadLimits, 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";
@@ -9678,7 +9678,7 @@ function asTableRecord(value, location = "catalog/table") {
9678
9678
  }
9679
9679
  for (const [index, foreignKey] of rawForeignKeys.entries()) {
9680
9680
  if (isRecord(foreignKey)) {
9681
- assertKnownFields(foreignKey, ["name", "columns", "parentTable", "parentColumns", "onDelete"], `${location}/foreignKeys/${String(index)}`);
9681
+ assertKnownFields(foreignKey, ["name", "columns", "parentTable", "parentColumns", "onDelete", "enforced"], `${location}/foreignKeys/${String(index)}`);
9682
9682
  }
9683
9683
  if (!isRecord(foreignKey) ||
9684
9684
  !isCatalogName(foreignKey.name) ||
@@ -9690,6 +9690,7 @@ function asTableRecord(value, location = "catalog/table") {
9690
9690
  foreignKey.parentColumns.length === 0 ||
9691
9691
  foreignKey.columns.length !== foreignKey.parentColumns.length ||
9692
9692
  !["restrict", "cascade", "set null"].includes(String(foreignKey.onDelete)) ||
9693
+ (foreignKey.enforced !== undefined && typeof foreignKey.enforced !== "boolean") ||
9693
9694
  foreignKey.parentColumns.some((columnName) => !isCatalogName(columnName))) {
9694
9695
  throw corruption(`${location}/foreignKeys/${String(index)}`, "foreign-key metadata is invalid");
9695
9696
  }
@@ -11965,16 +11966,7 @@ async function assertTableForeignKeysInTransaction(catalog, record) {
11965
11966
  if (parent === undefined) {
11966
11967
  throw new TypeError(`FOREIGN KEY ${key.name} references a missing table: ${key.parentTable}`);
11967
11968
  }
11968
- const addressIds = parent.primaryKeyColumnIds?.length
11969
- ? parent.primaryKeyColumnIds
11970
- : parent.uniqueKeyColumnId === undefined
11971
- ? []
11972
- : [parent.uniqueKeyColumnId];
11973
- const addressNames = addressIds.map((id) => parent.columns.find((column) => column.id === id)?.name ?? "");
11974
- if (addressNames.length !== key.parentColumns.length ||
11975
- addressNames.some((name, index) => name !== key.parentColumns[index])) {
11976
- throw new TypeError(`FOREIGN KEY ${key.name} must reference the parent primary or unique key`);
11977
- }
11969
+ validateTableForeignKey(record, key, parent);
11978
11970
  }
11979
11971
  }
11980
11972
  async function updateRetiredHistoryLedger(store, deltaBytes) {