@minnowdb/core 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +5 -5
  2. package/dist/engine/cancellation.d.ts +2 -0
  3. package/dist/engine/cancellation.js +4 -0
  4. package/dist/engine/catalog.d.ts +5 -1
  5. package/dist/engine/catalog.js +5 -1
  6. package/dist/engine/client.d.ts +34 -6
  7. package/dist/engine/client.js +87 -19
  8. package/dist/engine/database.d.ts +43 -20
  9. package/dist/engine/database.js +823 -164
  10. package/dist/engine/defaults.js +11 -0
  11. package/dist/engine/errors.d.ts +19 -0
  12. package/dist/engine/errors.js +31 -0
  13. package/dist/engine/fts.d.ts +2 -15
  14. package/dist/engine/live.d.ts +1 -7
  15. package/dist/engine/live.js +12 -13
  16. package/dist/engine/optimizer.js +546 -39
  17. package/dist/engine/query-cache.js +1 -0
  18. package/dist/engine/query.d.ts +16 -278
  19. package/dist/engine/query.js +260 -74
  20. package/dist/engine/result-wire.d.ts +2 -0
  21. package/dist/engine/result-wire.js +21 -5
  22. package/dist/engine/schema-wire.d.ts +14 -1
  23. package/dist/engine/schema-wire.js +7 -1
  24. package/dist/engine/schema.d.ts +83 -32
  25. package/dist/engine/schema.js +180 -14
  26. package/dist/engine/sql-domains.d.ts +11 -0
  27. package/dist/engine/sql-domains.js +65 -1
  28. package/dist/engine/sql-json.js +22 -3
  29. package/dist/engine/sql-semantics.js +21 -3
  30. package/dist/engine/vector.d.ts +2 -2
  31. package/dist/engine/vector.js +328 -79
  32. package/dist/engine/worker-host.js +119 -44
  33. package/dist/plan/index.d.ts +5 -4
  34. package/dist/plan/index.js +3 -3
  35. package/dist/plan/model.d.ts +218 -0
  36. package/dist/plan/model.js +1 -0
  37. package/dist/storage/indexeddb.js +4 -12
  38. package/dist/storage/toolkit/record-core.js +7 -22
  39. package/dist/storage/types.d.ts +26 -8
  40. package/dist/storage/types.js +85 -0
  41. package/dist/transactions/index.d.ts +5 -3
  42. package/dist/transactions/index.js +58 -8
  43. package/dist/worker-protocol/index.d.ts +6 -1
  44. package/dist/worker-protocol/index.js +5 -2
  45. package/package.json +1 -1
  46. package/postgres-feature-profile.json +5 -0
  47. package/sql-feature-matrix.json +75 -19
@@ -1,7 +1,52 @@
1
1
  import { copyDate, dateMilliseconds } from "../date-value.js";
2
2
  import { validateColumnDefault, validateEnumValues, validateSqlDomain, } from "../storage/types.js";
3
- import { compileCheckExpression, expressionColumns, validateDefaultExpression, } from "./query.js";
3
+ import { childExpressions, compileCheckExpression, expressionColumns, hasAggregate, validateDefaultExpression, } from "./query.js";
4
4
  import { externalSqlDomainValue, normalizeSqlDomainValue } from "./sql-domains.js";
5
+ const volatileGeneratedFunctions = new Set([
6
+ "CURRENT_DATE",
7
+ "CURRENT_TIMESTAMP",
8
+ "LOCALTIME",
9
+ "RANDOM",
10
+ "GEN_RANDOM_UUID",
11
+ "NEXTVAL",
12
+ "CURRVAL",
13
+ ]);
14
+ /** Compiles and validates one immutable, row-local generated-column expression. */
15
+ export function compileGeneratedColumnExpression(tableName, columnName, sql, columns) {
16
+ if (sql.length === 0 || sql.trim() !== sql) {
17
+ throw new TypeError(`Generated SQL must be a trimmed non-empty expression: ${tableName}.${columnName}`);
18
+ }
19
+ const expression = compileCheckExpression(sql, `generated ${tableName}.${columnName}`);
20
+ if (hasAggregate(expression)) {
21
+ throw new TypeError(`Generated column ${tableName}.${columnName} must be an immutable expression over sibling columns`);
22
+ }
23
+ const inspect = (node) => {
24
+ if (node.kind === "subquery" || node.kind === "exists") {
25
+ throw new TypeError(`Generated column ${tableName}.${columnName} must be an immutable expression over sibling columns`);
26
+ }
27
+ if (node.kind === "call" && volatileGeneratedFunctions.has(node.name)) {
28
+ throw new TypeError(`Generated column ${tableName}.${columnName} cannot call volatile function ${node.name}`);
29
+ }
30
+ childExpressions(node).forEach(inspect);
31
+ };
32
+ inspect(expression);
33
+ for (const reference of expressionColumns(expression)) {
34
+ const pieces = reference.split(".");
35
+ const referencedName = pieces.at(-1) ?? reference;
36
+ const qualifier = pieces.length > 1 ? pieces.slice(0, -1).join(".") : undefined;
37
+ if (qualifier !== undefined && qualifier !== tableName) {
38
+ throw new TypeError(`Generated column ${tableName}.${columnName} references another table: ${reference}`);
39
+ }
40
+ const referenced = columns.find(({ name }) => name === referencedName);
41
+ if (referenced === undefined) {
42
+ throw new TypeError(`Generated column ${tableName}.${columnName} names an unknown column: ${referencedName}`);
43
+ }
44
+ if (referencedName === columnName || referenced.generatedValue !== undefined) {
45
+ throw new TypeError(`Generated column ${tableName}.${columnName} cannot reference a generated column: ${referencedName}`);
46
+ }
47
+ }
48
+ return expression;
49
+ }
5
50
  /**
6
51
  * The constraint name `migrate()` gives a declared relation. It matches the name the SQL parser
7
52
  * derives for an unnamed inline REFERENCES, so a table built either way has the same catalog.
@@ -45,9 +90,11 @@ function createColumn(type, state = {}) {
45
90
  isNullable: (state.isNullable ?? false),
46
91
  isUnique: (state.isUnique ?? false),
47
92
  hasDefault: (state.defaultSpec !== undefined),
93
+ isGenerated: (state.generatedSpec !== undefined),
48
94
  integer: state.integer ?? false,
49
95
  ...(state.sqlDomain === undefined ? {} : { sqlDomain: state.sqlDomain }),
50
96
  ...(state.defaultSpec === undefined ? {} : { defaultSpec: state.defaultSpec }),
97
+ ...(state.generatedSpec === undefined ? {} : { generatedSpec: state.generatedSpec }),
51
98
  ...(state.renamedFromName === undefined ? {} : { renamedFromName: state.renamedFromName }),
52
99
  ...(state.reference === undefined ? {} : { reference: state.reference }),
53
100
  ...(state.enumValues === undefined ? {} : { enumValues: state.enumValues }),
@@ -65,18 +112,28 @@ function createColumn(type, state = {}) {
65
112
  }),
66
113
  renamedFrom: (name) => {
67
114
  validateSchemaName(name, "Rename source");
68
- return createColumn(type, { ...state, renamedFromName: name });
115
+ return createColumn(type, {
116
+ ...state,
117
+ renamedFromName: name,
118
+ });
69
119
  },
70
- backfill: (value) => createColumn(type, { ...state, backfillValue: value }),
120
+ backfill: (value) => createColumn(type, {
121
+ ...state,
122
+ backfillValue: value,
123
+ }),
71
124
  references: (table, referencedColumn, options = {}) => {
72
125
  validateSchemaName(table, "Referenced table");
73
126
  validateSchemaName(referencedColumn, "Referenced column");
127
+ if (options.enforced === false && options.onDelete !== undefined) {
128
+ throw new TypeError("An informational FOREIGN KEY cannot declare ON DELETE behavior");
129
+ }
74
130
  return createColumn(type, {
75
131
  ...state,
76
132
  reference: {
77
133
  table,
78
134
  column: referencedColumn,
79
135
  onDelete: options.onDelete ?? "restrict",
136
+ enforced: options.enforced !== false,
80
137
  },
81
138
  });
82
139
  },
@@ -96,6 +153,19 @@ function createColumn(type, state = {}) {
96
153
  defaultSpec: { kind: "expression", sql: expression },
97
154
  });
98
155
  }),
156
+ generatedSql: (sql) => {
157
+ const expression = sql.trim();
158
+ if (expression.length === 0 || expression !== sql) {
159
+ throw new TypeError("Generated SQL must be a trimmed non-empty expression");
160
+ }
161
+ if (state.defaultSpec !== undefined) {
162
+ throw new TypeError("A generated column cannot also have a default");
163
+ }
164
+ return createColumn(type, {
165
+ ...state,
166
+ generatedSpec: { kind: "stored", sql: expression },
167
+ });
168
+ },
99
169
  autoIncrement: (() => {
100
170
  if (type !== "number") {
101
171
  throw new TypeError("Auto-increment requires a number column");
@@ -141,6 +211,7 @@ export function columnFromState(state) {
141
211
  ...(state.renamedFromName === undefined ? {} : { renamedFromName: state.renamedFromName }),
142
212
  ...(state.reference === undefined ? {} : { reference: state.reference }),
143
213
  ...(state.defaultSpec === undefined ? {} : { defaultSpec: state.defaultSpec }),
214
+ ...(state.generatedSpec === undefined ? {} : { generatedSpec: state.generatedSpec }),
144
215
  ...(state.enumValues === undefined ? {} : { enumValues: state.enumValues }),
145
216
  ...(state.backfillValue === undefined ? {} : { backfillValue: state.backfillValue }),
146
217
  });
@@ -160,8 +231,12 @@ export const column = {
160
231
  json: () => createColumn("string", { sqlDomain: { kind: "json" } }),
161
232
  jsonb: () => createColumn("string", { sqlDomain: { kind: "jsonb" } }),
162
233
  uuid: () => createColumn("string", { sqlDomain: { kind: "uuid" } }),
234
+ /** A zoneless calendar date, represented publicly as canonical `YYYY-MM-DD` text. */
235
+ date: () => createColumn("string", { sqlDomain: { kind: "date" } }),
163
236
  time: () => createColumn("string", { sqlDomain: { kind: "time" } }),
164
- interval: () => createColumn("string", { sqlDomain: { kind: "interval" } }),
237
+ interval: () => createColumn("string", {
238
+ sqlDomain: { kind: "interval" },
239
+ }),
165
240
  /** JSON array text at the JavaScript boundary, with the SQL element type retained in metadata. */
166
241
  array: (element) => createColumn("string", {
167
242
  sqlDomain: validateSqlDomain({ kind: "array", element }, "array column"),
@@ -246,6 +321,10 @@ export function table(name, columns, options = {}) {
246
321
  if (uniqueEntry?.[1].isNullable === true) {
247
322
  throw new TypeError(`Table ${name} unique column must not be nullable: ${uniqueEntry[0]}`);
248
323
  }
324
+ const generatedColumns = entries.map(([columnName, definition]) => ({
325
+ name: columnName,
326
+ ...(definition.generatedSpec === undefined ? {} : { generatedValue: definition.generatedSpec }),
327
+ }));
249
328
  for (const [columnName, definition] of entries) {
250
329
  validateSchemaName(columnName, "Column");
251
330
  if (definition.integer && definition.type !== "number") {
@@ -270,6 +349,15 @@ export function table(name, columns, options = {}) {
270
349
  if (backfill !== undefined && definition.isNullable) {
271
350
  throw new TypeError(`A nullable column needs no backfill: ${name}.${columnName}. Rows without it already read NULL.`);
272
351
  }
352
+ if (definition.generatedSpec !== undefined) {
353
+ if (definition.defaultSpec !== undefined || backfill !== undefined) {
354
+ throw new TypeError(`A generated column cannot also have a default or backfill: ${name}.${columnName}`);
355
+ }
356
+ compileGeneratedColumnExpression(name, columnName, definition.generatedSpec.sql, generatedColumns);
357
+ if (definition.isUnique || primaryKey.includes(columnName)) {
358
+ throw new TypeError(`Generated columns cannot be row-addressing keys: ${name}.${columnName}`);
359
+ }
360
+ }
273
361
  if (definition.reference?.onDelete === "set null" && !definition.isNullable) {
274
362
  throw new TypeError(`ON DELETE SET NULL requires a nullable column: ${name}.${columnName}`);
275
363
  }
@@ -301,6 +389,9 @@ export function table(name, columns, options = {}) {
301
389
  const foreignKeyNames = new Set();
302
390
  for (const key of foreignKeys) {
303
391
  validateSchemaName(key.name, "FOREIGN KEY");
392
+ if (key.enforced === false && key.onDelete !== undefined) {
393
+ throw new TypeError(`Informational FOREIGN KEY ${key.name} cannot declare ON DELETE behavior`);
394
+ }
304
395
  if (foreignKeyNames.has(key.name)) {
305
396
  throw new TypeError(`Duplicate FOREIGN KEY in table ${name}: ${key.name}`);
306
397
  }
@@ -431,6 +522,7 @@ export function declaredForeignKeys(definition) {
431
522
  parentTable: reference.table,
432
523
  parentColumns: [reference.column],
433
524
  onDelete: reference.onDelete,
525
+ enforced: reference.enforced,
434
526
  });
435
527
  }
436
528
  for (const key of definition.foreignKeys) {
@@ -442,6 +534,7 @@ export function declaredForeignKeys(definition) {
442
534
  parentTable: key.references.table,
443
535
  parentColumns,
444
536
  onDelete: key.onDelete ?? "restrict",
537
+ enforced: key.enforced !== false,
445
538
  });
446
539
  }
447
540
  return keys;
@@ -472,6 +565,9 @@ export function view(name, definition) {
472
565
  if (columnDefinition.defaultSpec !== undefined) {
473
566
  throw new TypeError(`A view column cannot have a default: ${name}.${columnName}`);
474
567
  }
568
+ if (columnDefinition.generatedSpec !== undefined) {
569
+ throw new TypeError(`A view column cannot be generated: ${name}.${columnName}`);
570
+ }
475
571
  }
476
572
  return { kind: "view", name, sql: definition.sql, columns: definition.columns };
477
573
  }
@@ -625,6 +721,20 @@ export function assertColumnDroppable(record, column) {
625
721
  throw new TypeError(`CHECK ${check.name} still uses this column: ${where}`);
626
722
  }
627
723
  }
724
+ for (const dependent of record.columns) {
725
+ if (dependent.generatedValue === undefined)
726
+ continue;
727
+ let referenced;
728
+ try {
729
+ referenced = expressionColumns(compileCheckExpression(dependent.generatedValue.sql, `generated ${record.name}.${dependent.name}`));
730
+ }
731
+ catch {
732
+ throw new TypeError(`Generated column ${dependent.name} cannot be re-read, so ${where} is not droppable`);
733
+ }
734
+ if (referenced.includes(column.name)) {
735
+ throw new TypeError(`Generated column ${dependent.name} still uses this column: ${where}`);
736
+ }
737
+ }
628
738
  }
629
739
  function assertColumnRenamable(catalog, record, column) {
630
740
  const where = `${record.name}.${column.name}`;
@@ -643,6 +753,14 @@ function assertColumnRenamable(catalog, record, column) {
643
753
  throw new TypeError(`CHECK ${check.name} prevents renaming ${where}`);
644
754
  }
645
755
  }
756
+ for (const dependent of record.columns) {
757
+ if (dependent.generatedValue === undefined)
758
+ continue;
759
+ const referenced = expressionColumns(compileCheckExpression(dependent.generatedValue.sql, `generated ${record.name}.${dependent.name}`));
760
+ if (referenced.includes(column.name)) {
761
+ throw new TypeError(`Generated column ${dependent.name} prevents renaming ${where}`);
762
+ }
763
+ }
646
764
  }
647
765
  /**
648
766
  * Freezes a column's backfill. A generator runs exactly once — here, while the migration is being
@@ -675,34 +793,58 @@ function backfillsEqual(left, right) {
675
793
  return externalLeft === externalRight;
676
794
  }
677
795
  /**
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
796
+ * Enforced constraints on an existing table cannot change through a metadata-only step. Attaching
797
+ * a FOREIGN KEY or CHECK to a table that already holds rows would claim something about those rows
680
798
  * 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.
799
+ * reason, so that a constraint never disappears because a schema file drifted. Informational
800
+ * foreign keys are catalog metadata only, so adding, dropping, or remapping one is safe.
683
801
  */
684
- function assertConstraintsUnchanged(record, definition) {
802
+ function planConstraintChanges(record, definition, steps) {
685
803
  const describeKey = (key) => `${key.columns.join(",")} -> ${key.parentTable}.` +
686
804
  `${key.parentColumns.join(",")} ON DELETE ${key.onDelete}`;
805
+ const declaredList = declaredForeignKeys(definition);
687
806
  const existingKeys = new Map(record.foreignKeys.map((key) => [key.name, key]));
688
- const declaredKeys = new Map(declaredForeignKeys(definition).map((key) => [key.name, key]));
807
+ const declaredKeys = new Map(declaredList.map((key) => [key.name, key]));
808
+ let informationalChanged = false;
689
809
  for (const [name, declared] of declaredKeys) {
690
810
  const existing = existingKeys.get(name);
691
811
  if (existing === undefined) {
812
+ if (!declared.enforced) {
813
+ informationalChanged = true;
814
+ continue;
815
+ }
692
816
  throw new TypeError(`FOREIGN KEY cannot be added after creation: ${definition.name}.${declared.columns.join(",")}. ` +
693
817
  `Existing rows are not known to satisfy it; recreate the table to add a relation.`);
694
818
  }
819
+ if (existing.enforced !== declared.enforced) {
820
+ throw new TypeError(`FOREIGN KEY enforcement cannot change: ${definition.name}.${declared.columns.join(",")}`);
821
+ }
695
822
  if (describeKey(existing) !== describeKey(declared)) {
823
+ if (!declared.enforced) {
824
+ informationalChanged = true;
825
+ continue;
826
+ }
696
827
  throw new TypeError(`FOREIGN KEY cannot change: ${definition.name}.${declared.columns.join(",")} is ` +
697
828
  `${describeKey(existing)}, schema says ${describeKey(declared)}`);
698
829
  }
699
830
  }
700
831
  for (const name of existingKeys.keys()) {
701
832
  if (!declaredKeys.has(name)) {
833
+ if (existingKeys.get(name)?.enforced === false) {
834
+ informationalChanged = true;
835
+ continue;
836
+ }
702
837
  throw new TypeError(`FOREIGN KEY cannot be dropped: ${definition.name} still has ${name}. ` +
703
838
  `Declare the relation, or recreate the table without it.`);
704
839
  }
705
840
  }
841
+ if (informationalChanged) {
842
+ steps.push({
843
+ kind: "alter-foreign-keys",
844
+ tableName: definition.name,
845
+ foreignKeys: declaredList,
846
+ });
847
+ }
706
848
  const existingChecks = new Map(record.checks.map((check) => [check.name, check.sql]));
707
849
  const declaredChecks = new Map(definition.checks.map((check) => [check.name, check.sql]));
708
850
  for (const [name, sql] of declaredChecks) {
@@ -758,6 +900,9 @@ export function planMigration(catalog, definition, options = {}) {
758
900
  }
759
901
  }
760
902
  if (existing === undefined) {
903
+ if (columnDefinition.generatedSpec !== undefined) {
904
+ throw new TypeError(`Generated columns cannot be added to an existing table without rewriting its rows: ${tableDefinition.name}.${columnName}`);
905
+ }
761
906
  const backfill = resolveBackfill(columnDefinition);
762
907
  if (!columnDefinition.isNullable && backfill === undefined) {
763
908
  throw new TypeError(`Added columns must be nullable, or carry a backfill: ${tableDefinition.name}.${columnName}`);
@@ -855,13 +1000,24 @@ export function planMigration(catalog, definition, options = {}) {
855
1000
  columnName,
856
1001
  enabled: isAuto,
857
1002
  });
858
- continue;
859
1003
  }
1004
+ else {
1005
+ steps.push({
1006
+ kind: "alter-default",
1007
+ tableName: tableDefinition.name,
1008
+ columnName,
1009
+ defaultValue: columnDefinition.defaultSpec ?? null,
1010
+ });
1011
+ }
1012
+ }
1013
+ const existingGenerated = existing.generatedValue;
1014
+ const definedGenerated = columnDefinition.generatedSpec;
1015
+ if (JSON.stringify(existingGenerated ?? null) !== JSON.stringify(definedGenerated ?? null)) {
860
1016
  steps.push({
861
- kind: "alter-default",
1017
+ kind: "alter-generated",
862
1018
  tableName: tableDefinition.name,
863
1019
  columnName,
864
- defaultValue: columnDefinition.defaultSpec ?? null,
1020
+ generatedValue: definedGenerated ?? null,
865
1021
  });
866
1022
  }
867
1023
  }
@@ -889,7 +1045,7 @@ export function planMigration(catalog, definition, options = {}) {
889
1045
  desiredKeyIds.some((id, index) => id !== existingKeyIds[index])) {
890
1046
  throw new TypeError(`Primary key order cannot change: ${tableDefinition.name}`);
891
1047
  }
892
- assertConstraintsUnchanged(record, tableDefinition);
1048
+ planConstraintChanges(record, tableDefinition, steps);
893
1049
  }
894
1050
  if (options.schemaOwnsDatabase === true) {
895
1051
  const declaredTables = new Set(definition.tables.map(({ name }) => name));
@@ -1149,6 +1305,16 @@ export function applyColumnSteps(record, steps, createId) {
1149
1305
  else
1150
1306
  target.defaultValue = step.defaultValue;
1151
1307
  }
1308
+ continue;
1309
+ }
1310
+ if (step.kind === "alter-generated") {
1311
+ const target = columns.find(({ name }) => name === step.columnName);
1312
+ if (target !== undefined) {
1313
+ if (step.generatedValue === null)
1314
+ delete target.generatedValue;
1315
+ else
1316
+ target.generatedValue = step.generatedValue;
1317
+ }
1152
1318
  }
1153
1319
  }
1154
1320
  return columns;
@@ -18,7 +18,18 @@ export declare function exactNumericCompare(left: unknown, right: unknown): numb
18
18
  */
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
+ /** Returns the JSON document carried by an internal JSON/JSONB scalar, if any. */
22
+ export declare function jsonDomainDocument(value: unknown): string | undefined;
23
+ /**
24
+ * Tags already-constructed JSON without parsing and re-stringifying it. The validation parse
25
+ * rejects malformed documents, while retaining duplicate object names and the constructor's
26
+ * exact member order for embedding in an outer JSON value.
27
+ */
28
+ export declare function preservedJsonDomainValue(document: string, binary?: boolean): string;
21
29
  export declare function uuidDomainValue(value: unknown): string | null;
30
+ /** Canonical, zoneless SQL DATE value. No JavaScript time zone participates in validation. */
31
+ export declare function dateDomainValue(value: unknown): string | null;
32
+ export declare function isDateDomainValue(value: unknown): value is string;
22
33
  export declare function timeDomainValue(value: unknown): string | null;
23
34
  export declare function intervalDomainValue(value: unknown): string | null;
24
35
  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:`;
@@ -312,6 +313,31 @@ export function jsonDomainValue(value, binary) {
312
313
  const text = boundedJsonText(parsed, binary, binary ? "JSONB value" : "JSON value");
313
314
  return boundedTaggedDomainValue(binary ? JSONB_VALUE : JSON_VALUE, text, binary ? "JSONB value" : "JSON value");
314
315
  }
316
+ /** Returns the JSON document carried by an internal JSON/JSONB scalar, if any. */
317
+ export function jsonDomainDocument(value) {
318
+ if (typeof value !== "string")
319
+ return undefined;
320
+ if (value.startsWith(JSONB_VALUE))
321
+ return value.slice(JSONB_VALUE.length);
322
+ if (value.startsWith(JSON_VALUE))
323
+ return value.slice(JSON_VALUE.length);
324
+ return undefined;
325
+ }
326
+ /**
327
+ * Tags already-constructed JSON without parsing and re-stringifying it. The validation parse
328
+ * rejects malformed documents, while retaining duplicate object names and the constructor's
329
+ * exact member order for embedding in an outer JSON value.
330
+ */
331
+ export function preservedJsonDomainValue(document, binary = false) {
332
+ assertBoundedDomainString(document, binary ? "JSONB value" : "JSON value");
333
+ try {
334
+ JSON.parse(document);
335
+ }
336
+ catch {
337
+ throw new TypeError("Invalid JSON value");
338
+ }
339
+ return boundedTaggedDomainValue(binary ? JSONB_VALUE : JSON_VALUE, document, binary ? "JSONB value" : "JSON value");
340
+ }
315
341
  export function uuidDomainValue(value) {
316
342
  if (value === null || value === undefined)
317
343
  return null;
@@ -324,6 +350,42 @@ export function uuidDomainValue(value) {
324
350
  }
325
351
  return UUID_VALUE + source;
326
352
  }
353
+ /** Canonical, zoneless SQL DATE value. No JavaScript time zone participates in validation. */
354
+ export function dateDomainValue(value) {
355
+ if (value === null || value === undefined)
356
+ return null;
357
+ let source;
358
+ if (value instanceof Date) {
359
+ if (!Number.isFinite(value.getTime()))
360
+ throw new TypeError("DATE accepts a valid Date value");
361
+ source = dateIsoString(value).slice(0, 10);
362
+ }
363
+ else if (typeof value === "string") {
364
+ assertBoundedDomainString(value, "DATE value");
365
+ source = value.startsWith(DATE_VALUE) ? value.slice(DATE_VALUE.length) : value;
366
+ }
367
+ else {
368
+ throw new TypeError("DATE accepts a YYYY-MM-DD string value");
369
+ }
370
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(source);
371
+ const year = Number(match?.[1]);
372
+ const month = Number(match?.[2]);
373
+ const day = Number(match?.[3]);
374
+ const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
375
+ const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
376
+ if (match === null ||
377
+ year < 1 ||
378
+ month < 1 ||
379
+ month > 12 ||
380
+ day < 1 ||
381
+ day > (days[month - 1] ?? 0)) {
382
+ throw new TypeError(`Invalid DATE value: ${String(value)}`);
383
+ }
384
+ return DATE_VALUE + source;
385
+ }
386
+ export function isDateDomainValue(value) {
387
+ return typeof value === "string" && value.startsWith(DATE_VALUE);
388
+ }
327
389
  export function timeDomainValue(value) {
328
390
  if (value === null || value === undefined)
329
391
  return null;
@@ -464,6 +526,8 @@ export function normalizeSqlDomainValue(domain, value) {
464
526
  return jsonDomainValue(value, true);
465
527
  if (domain.kind === "uuid")
466
528
  return uuidDomainValue(value);
529
+ if (domain.kind === "date")
530
+ return dateDomainValue(value);
467
531
  if (domain.kind === "time")
468
532
  return timeDomainValue(value);
469
533
  if (domain.kind === "interval")
@@ -562,7 +626,7 @@ export function externalSqlDomainValue(value) {
562
626
  return value;
563
627
  if (value.startsWith(TEXT_VALUE))
564
628
  return value.slice(TEXT_VALUE.length);
565
- for (const prefix of [NUMERIC, JSON_VALUE, JSONB_VALUE, UUID_VALUE, TIME_VALUE]) {
629
+ for (const prefix of [NUMERIC, JSON_VALUE, JSONB_VALUE, UUID_VALUE, DATE_VALUE, TIME_VALUE]) {
566
630
  if (value.startsWith(prefix))
567
631
  return value.slice(prefix.length);
568
632
  }
@@ -1,5 +1,5 @@
1
1
  import { stringArgument } from "./sql-semantics.js";
2
- import { externalSqlDomainValue } from "./sql-domains.js";
2
+ import { externalSqlDomainValue, jsonDomainDocument } from "./sql-domains.js";
3
3
  import { MAX_CACHEABLE_TEXT_CHARACTERS, MAX_SQL_SCALAR_RESULT_CHARACTERS } from "./cache-limits.js";
4
4
  export function parseJsonPath(path, caller) {
5
5
  const text = stringArgument(caller, path).trim();
@@ -119,7 +119,7 @@ export function jsonConstructor(name, values) {
119
119
  }
120
120
  const members = [];
121
121
  for (let index = 0; index + 1 < values.length; index += 2) {
122
- const rawKey = values[index];
122
+ const rawKey = externalSqlDomainValue(values[index]);
123
123
  if (rawKey === null || rawKey === undefined) {
124
124
  throw new TypeError("JSON_OBJECT keys cannot be NULL");
125
125
  }
@@ -136,12 +136,24 @@ export function jsonConstructor(name, values) {
136
136
  // Build JSON text directly. WITHOUT UNIQUE KEYS is the default, so duplicate names must be
137
137
  // preserved; assigning through a JavaScript object would collapse them and mishandle
138
138
  // special names such as "__proto__".
139
- const encodedKey = boundedJsonValue(key, "JSON_OBJECT key");
139
+ // Keys are always JSON strings. In particular, a JSON-domain expression used as a key is
140
+ // its document text, not a raw object member token as JSON-domain values are below.
141
+ const encodedKey = boundedJsonString(key, "JSON_OBJECT key");
140
142
  const encodedValue = boundedJsonValue(member ?? null, "JSON_OBJECT value");
141
143
  members.push(`${encodedKey}:${encodedValue}`);
142
144
  }
143
145
  return joinBoundedJson("{", members, "}", "JSON_OBJECT result");
144
146
  }
147
+ function boundedJsonString(value, label) {
148
+ if (value.length > MAX_SQL_SCALAR_RESULT_CHARACTERS) {
149
+ throw new RangeError(`${label} exceeds ${String(MAX_SQL_SCALAR_RESULT_CHARACTERS)} characters`);
150
+ }
151
+ const encoded = JSON.stringify(value);
152
+ if (encoded.length > MAX_SQL_SCALAR_RESULT_CHARACTERS) {
153
+ throw new RangeError(`${label} exceeds ${String(MAX_SQL_SCALAR_RESULT_CHARACTERS)} characters`);
154
+ }
155
+ return encoded;
156
+ }
145
157
  function boundedJsonDocument(value, caller) {
146
158
  const document = stringArgument(caller, externalSqlDomainValue(value));
147
159
  if (document.length > MAX_SQL_SCALAR_RESULT_CHARACTERS) {
@@ -150,6 +162,13 @@ function boundedJsonDocument(value, caller) {
150
162
  return document;
151
163
  }
152
164
  function boundedJsonValue(value, label) {
165
+ const domainDocument = jsonDomainDocument(value);
166
+ if (domainDocument !== undefined) {
167
+ if (domainDocument.length > MAX_SQL_SCALAR_RESULT_CHARACTERS) {
168
+ throw new RangeError(`${label} exceeds ${String(MAX_SQL_SCALAR_RESULT_CHARACTERS)} characters`);
169
+ }
170
+ return domainDocument;
171
+ }
153
172
  const normalized = jsonValueOf(value);
154
173
  if (typeof normalized === "string" && normalized.length > MAX_SQL_SCALAR_RESULT_CHARACTERS) {
155
174
  throw new RangeError(`${label} exceeds ${String(MAX_SQL_SCALAR_RESULT_CHARACTERS)} characters`);
@@ -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)
@@ -75,6 +75,8 @@ export interface QuerySpillStore {
75
75
  export interface AsyncQueryExecutionOptions {
76
76
  readonly spillStore?: QuerySpillStore;
77
77
  readonly spillPageRows?: number;
78
+ /** Stops before the next execution or spill-storage batch. */
79
+ readonly signal?: AbortSignal;
78
80
  /**
79
81
  * Makes the scan-source window [start, start + length) resident before each batch. Supplied by
80
82
  * a streaming preparation whose scan-source vectors hold only a sliding window; the executor
@@ -88,8 +90,6 @@ export interface AsyncQueryExecutionOptions {
88
90
  export interface QueryBatchExecutionOptions extends AsyncQueryExecutionOptions {
89
91
  /** Maximum result rows handed to the consumer at once. */
90
92
  readonly batchRows: number;
91
- /** Stops before the next scan batch and is checked after every awaited storage read. */
92
- readonly signal?: AbortSignal;
93
93
  }
94
94
  export interface PrepareVectorQueryOptions {
95
95
  readonly memoryContext?: QueryMemoryContext;