@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.
- package/README.md +5 -5
- package/dist/engine/cancellation.d.ts +2 -0
- package/dist/engine/cancellation.js +4 -0
- package/dist/engine/catalog.d.ts +5 -1
- package/dist/engine/catalog.js +5 -1
- package/dist/engine/client.d.ts +34 -6
- package/dist/engine/client.js +87 -19
- package/dist/engine/database.d.ts +43 -20
- package/dist/engine/database.js +823 -164
- package/dist/engine/defaults.js +11 -0
- package/dist/engine/errors.d.ts +19 -0
- package/dist/engine/errors.js +31 -0
- package/dist/engine/fts.d.ts +2 -15
- package/dist/engine/live.d.ts +1 -7
- package/dist/engine/live.js +12 -13
- package/dist/engine/optimizer.js +546 -39
- package/dist/engine/query-cache.js +1 -0
- package/dist/engine/query.d.ts +16 -278
- package/dist/engine/query.js +260 -74
- package/dist/engine/result-wire.d.ts +2 -0
- package/dist/engine/result-wire.js +21 -5
- package/dist/engine/schema-wire.d.ts +14 -1
- package/dist/engine/schema-wire.js +7 -1
- package/dist/engine/schema.d.ts +83 -32
- package/dist/engine/schema.js +180 -14
- package/dist/engine/sql-domains.d.ts +11 -0
- package/dist/engine/sql-domains.js +65 -1
- package/dist/engine/sql-json.js +22 -3
- package/dist/engine/sql-semantics.js +21 -3
- package/dist/engine/vector.d.ts +2 -2
- package/dist/engine/vector.js +328 -79
- package/dist/engine/worker-host.js +119 -44
- package/dist/plan/index.d.ts +5 -4
- package/dist/plan/index.js +3 -3
- package/dist/plan/model.d.ts +218 -0
- package/dist/plan/model.js +1 -0
- package/dist/storage/indexeddb.js +4 -12
- package/dist/storage/toolkit/record-core.js +7 -22
- package/dist/storage/types.d.ts +26 -8
- package/dist/storage/types.js +85 -0
- package/dist/transactions/index.d.ts +5 -3
- package/dist/transactions/index.js +58 -8
- package/dist/worker-protocol/index.d.ts +6 -1
- package/dist/worker-protocol/index.js +5 -2
- package/package.json +1 -1
- package/postgres-feature-profile.json +5 -0
- package/sql-feature-matrix.json +75 -19
package/dist/engine/schema.js
CHANGED
|
@@ -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, {
|
|
115
|
+
return createColumn(type, {
|
|
116
|
+
...state,
|
|
117
|
+
renamedFromName: name,
|
|
118
|
+
});
|
|
69
119
|
},
|
|
70
|
-
backfill: (value) => createColumn(type, {
|
|
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", {
|
|
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
|
-
*
|
|
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.
|
|
682
|
-
*
|
|
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
|
|
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(
|
|
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-
|
|
1017
|
+
kind: "alter-generated",
|
|
862
1018
|
tableName: tableDefinition.name,
|
|
863
1019
|
columnName,
|
|
864
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/engine/sql-json.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
35
|
-
|
|
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)
|
package/dist/engine/vector.d.ts
CHANGED
|
@@ -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;
|