@minnowdb/core 0.2.1 → 0.4.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 +23 -32
- package/dist/block-format/block.d.ts +32 -13
- package/dist/block-format/block.js +179 -55
- package/dist/block-format/checksum.d.ts +2 -1
- package/dist/block-format/checksum.js +5 -2
- package/dist/block-format/codecs.d.ts +7 -2
- package/dist/block-format/codecs.js +53 -12
- package/dist/block-format/column.d.ts +3 -2
- package/dist/block-format/column.js +96 -30
- package/dist/block-format/index.d.ts +2 -2
- package/dist/block-format/index.js +2 -2
- package/dist/block-format/physical.d.ts +8 -2
- package/dist/block-format/physical.js +14 -6
- package/dist/block-format/types.d.ts +7 -3
- package/dist/block-format/types.js +0 -1
- package/dist/block-format/unicode.d.ts +11 -0
- package/dist/block-format/unicode.js +46 -0
- package/dist/date-value.d.ts +15 -0
- package/dist/date-value.js +64 -0
- package/dist/engine/artifact-cache.d.ts +2 -1
- package/dist/engine/artifact-cache.js +5 -1
- package/dist/engine/batch.d.ts +11 -3
- package/dist/engine/batch.js +17 -7
- package/dist/engine/buffered-writer.d.ts +2 -1
- package/dist/engine/buffered-writer.js +34 -9
- package/dist/engine/cache-limits.d.ts +25 -0
- package/dist/engine/cache-limits.js +25 -0
- package/dist/engine/catalog.d.ts +24 -5
- package/dist/engine/catalog.js +34 -7
- package/dist/engine/client.d.ts +29 -7
- package/dist/engine/client.js +216 -40
- package/dist/engine/database.d.ts +224 -39
- package/dist/engine/database.js +7475 -1539
- package/dist/engine/defaults.d.ts +4 -8
- package/dist/engine/defaults.js +46 -22
- package/dist/engine/errors.d.ts +19 -1
- package/dist/engine/errors.js +32 -2
- package/dist/engine/fts.d.ts +0 -6
- package/dist/engine/fts.js +41 -14
- package/dist/engine/group-index.d.ts +0 -1
- package/dist/engine/group-index.js +6 -11
- package/dist/engine/index.d.ts +16 -10
- package/dist/engine/index.js +12 -9
- package/dist/engine/join-index.d.ts +0 -1
- package/dist/engine/join-index.js +2 -2
- package/dist/engine/keyed-live.d.ts +57 -0
- package/dist/engine/keyed-live.js +226 -0
- package/dist/engine/live-api.d.ts +4 -0
- package/dist/engine/live-api.js +4 -0
- package/dist/engine/live.d.ts +43 -26
- package/dist/engine/live.js +504 -126
- package/dist/engine/memory.d.ts +2 -1
- package/dist/engine/memory.js +3 -2
- package/dist/engine/optimizer.d.ts +0 -1
- package/dist/engine/optimizer.js +262 -27
- package/dist/engine/query-api.d.ts +3 -0
- package/dist/engine/query-api.js +3 -0
- package/dist/engine/query-cache.d.ts +1 -2
- package/dist/engine/query-cache.js +7 -5
- package/dist/engine/query.d.ts +134 -29
- package/dist/engine/query.js +1668 -372
- package/dist/engine/result-wire.d.ts +0 -1
- package/dist/engine/result-wire.js +2 -2
- package/dist/engine/schema-wire.d.ts +6 -3
- package/dist/engine/schema-wire.js +40 -34
- package/dist/engine/schema.d.ts +156 -80
- package/dist/engine/schema.js +570 -108
- package/dist/engine/sort-keys.d.ts +4 -4
- package/dist/engine/sort-keys.js +44 -29
- package/dist/engine/sql-domains.d.ts +31 -0
- package/dist/engine/sql-domains.js +585 -0
- package/dist/engine/sql-driver.d.ts +19 -0
- package/dist/engine/sql-driver.js +1 -0
- package/dist/engine/sql-json.d.ts +7 -9
- package/dist/engine/sql-json.js +75 -15
- package/dist/engine/sql-semantics.d.ts +8 -3
- package/dist/engine/sql-semantics.js +458 -30
- package/dist/engine/typed-live.d.ts +67 -0
- package/dist/engine/typed-live.js +349 -0
- package/dist/engine/vector.d.ts +12 -2
- package/dist/engine/vector.js +635 -119
- package/dist/engine/worker-host.d.ts +9 -2
- package/dist/engine/worker-host.js +620 -152
- package/dist/engine/worker.d.ts +0 -1
- package/dist/engine/worker.js +3 -3
- package/dist/engine/write-block-planner.d.ts +18 -0
- package/dist/engine/write-block-planner.js +112 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/plan/index.d.ts +3 -4
- package/dist/plan/index.js +3 -4
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.js +1 -1
- package/dist/storage/indexeddb.d.ts +99 -51
- package/dist/storage/indexeddb.js +13448 -2660
- package/dist/storage/memory.d.ts +64 -57
- package/dist/storage/memory.js +1042 -128
- package/dist/storage/opfs/files.d.ts +18 -7
- package/dist/storage/opfs/files.js +115 -28
- package/dist/storage/opfs/index.d.ts +1 -1
- package/dist/storage/opfs/index.js +1 -1
- package/dist/storage/opfs/leader.d.ts +409 -75
- package/dist/storage/opfs/leader.js +4621 -650
- package/dist/storage/opfs/rpc.d.ts +15 -3
- package/dist/storage/opfs/rpc.js +224 -2
- package/dist/storage/opfs/snapshot-ledger.d.ts +40 -0
- package/dist/storage/opfs/snapshot-ledger.js +281 -0
- package/dist/storage/opfs/store.d.ts +33 -99
- package/dist/storage/opfs/store.js +453 -364
- package/dist/storage/persistence.d.ts +37 -0
- package/dist/storage/persistence.js +78 -0
- package/dist/storage/snapshot-stream.d.ts +24 -0
- package/dist/storage/snapshot-stream.js +897 -0
- package/dist/storage/snapshot.d.ts +9 -85
- package/dist/storage/snapshot.js +33 -265
- package/dist/storage/toolkit/extents.d.ts +41 -7
- package/dist/storage/toolkit/extents.js +402 -39
- package/dist/storage/toolkit/index.d.ts +4 -5
- package/dist/storage/toolkit/index.js +28 -4
- package/dist/storage/toolkit/record-core.d.ts +182 -55
- package/dist/storage/toolkit/record-core.js +6158 -1331
- package/dist/storage/toolkit/sync-file.d.ts +10 -1
- package/dist/storage/toolkit/sync-file.js +50 -2
- package/dist/storage/toolkit/wal.d.ts +23 -5
- package/dist/storage/toolkit/wal.js +89 -27
- package/dist/storage/toolkit/wire.d.ts +15 -2
- package/dist/storage/toolkit/wire.js +199 -10
- package/dist/storage/types.d.ts +1439 -204
- package/dist/storage/types.js +1745 -141
- package/dist/testing/block-store-conformance.d.ts +0 -1
- package/dist/testing/block-store-conformance.js +1009 -119
- package/dist/testing/index.d.ts +46 -26
- package/dist/testing/index.js +112 -57
- package/dist/testing/opfs-shim.d.ts +13 -3
- package/dist/testing/opfs-shim.js +40 -6
- package/dist/testing/simulator.d.ts +97 -0
- package/dist/testing/simulator.js +591 -0
- package/dist/testing/sqllogictest.d.ts +89 -0
- package/dist/testing/sqllogictest.js +434 -0
- package/dist/transactions/index.d.ts +92 -16
- package/dist/transactions/index.js +1074 -236
- package/dist/worker-protocol/index.d.ts +3 -2
- package/dist/worker-protocol/index.js +2 -5
- package/package.json +53 -3
- package/postgres-feature-profile.json +223 -0
- package/sql-feature-matrix.json +172 -252
- package/dist/block-format/block.d.ts.map +0 -1
- package/dist/block-format/block.js.map +0 -1
- package/dist/block-format/checksum.d.ts.map +0 -1
- package/dist/block-format/checksum.js.map +0 -1
- package/dist/block-format/codecs.d.ts.map +0 -1
- package/dist/block-format/codecs.js.map +0 -1
- package/dist/block-format/column.d.ts.map +0 -1
- package/dist/block-format/column.js.map +0 -1
- package/dist/block-format/index.d.ts.map +0 -1
- package/dist/block-format/index.js.map +0 -1
- package/dist/block-format/physical.d.ts.map +0 -1
- package/dist/block-format/physical.js.map +0 -1
- package/dist/block-format/types.d.ts.map +0 -1
- package/dist/block-format/types.js.map +0 -1
- package/dist/engine/artifact-cache.d.ts.map +0 -1
- package/dist/engine/artifact-cache.js.map +0 -1
- package/dist/engine/batch.d.ts.map +0 -1
- package/dist/engine/batch.js.map +0 -1
- package/dist/engine/buffered-writer.d.ts.map +0 -1
- package/dist/engine/buffered-writer.js.map +0 -1
- package/dist/engine/catalog.d.ts.map +0 -1
- package/dist/engine/catalog.js.map +0 -1
- package/dist/engine/client.d.ts.map +0 -1
- package/dist/engine/client.js.map +0 -1
- package/dist/engine/coordinator.d.ts +0 -17
- package/dist/engine/coordinator.d.ts.map +0 -1
- package/dist/engine/coordinator.js +0 -60
- package/dist/engine/coordinator.js.map +0 -1
- package/dist/engine/database.d.ts.map +0 -1
- package/dist/engine/database.js.map +0 -1
- package/dist/engine/defaults.d.ts.map +0 -1
- package/dist/engine/defaults.js.map +0 -1
- package/dist/engine/errors.d.ts.map +0 -1
- package/dist/engine/errors.js.map +0 -1
- package/dist/engine/fts.d.ts.map +0 -1
- package/dist/engine/fts.js.map +0 -1
- package/dist/engine/group-index.d.ts.map +0 -1
- package/dist/engine/group-index.js.map +0 -1
- package/dist/engine/index.d.ts.map +0 -1
- package/dist/engine/index.js.map +0 -1
- package/dist/engine/join-index.d.ts.map +0 -1
- package/dist/engine/join-index.js.map +0 -1
- package/dist/engine/live.d.ts.map +0 -1
- package/dist/engine/live.js.map +0 -1
- package/dist/engine/memory.d.ts.map +0 -1
- package/dist/engine/memory.js.map +0 -1
- package/dist/engine/optimizer.d.ts.map +0 -1
- package/dist/engine/optimizer.js.map +0 -1
- package/dist/engine/query-cache.d.ts.map +0 -1
- package/dist/engine/query-cache.js.map +0 -1
- package/dist/engine/query.d.ts.map +0 -1
- package/dist/engine/query.js.map +0 -1
- package/dist/engine/result-wire.d.ts.map +0 -1
- package/dist/engine/result-wire.js.map +0 -1
- package/dist/engine/schema-wire.d.ts.map +0 -1
- package/dist/engine/schema-wire.js.map +0 -1
- package/dist/engine/schema.d.ts.map +0 -1
- package/dist/engine/schema.js.map +0 -1
- package/dist/engine/sort-keys.d.ts.map +0 -1
- package/dist/engine/sort-keys.js.map +0 -1
- package/dist/engine/sql-json.d.ts.map +0 -1
- package/dist/engine/sql-json.js.map +0 -1
- package/dist/engine/sql-semantics.d.ts.map +0 -1
- package/dist/engine/sql-semantics.js.map +0 -1
- package/dist/engine/vector.d.ts.map +0 -1
- package/dist/engine/vector.js.map +0 -1
- package/dist/engine/worker-host.d.ts.map +0 -1
- package/dist/engine/worker-host.js.map +0 -1
- package/dist/engine/worker.d.ts.map +0 -1
- package/dist/engine/worker.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/plan/index.d.ts.map +0 -1
- package/dist/plan/index.js.map +0 -1
- package/dist/storage/fixture-shape.d.ts +0 -42
- package/dist/storage/fixture-shape.d.ts.map +0 -1
- package/dist/storage/fixture-shape.js +0 -146
- package/dist/storage/fixture-shape.js.map +0 -1
- package/dist/storage/index.d.ts.map +0 -1
- package/dist/storage/index.js.map +0 -1
- package/dist/storage/indexeddb.d.ts.map +0 -1
- package/dist/storage/indexeddb.js.map +0 -1
- package/dist/storage/memory.d.ts.map +0 -1
- package/dist/storage/memory.js.map +0 -1
- package/dist/storage/opfs/files.d.ts.map +0 -1
- package/dist/storage/opfs/files.js.map +0 -1
- package/dist/storage/opfs/index.d.ts.map +0 -1
- package/dist/storage/opfs/index.js.map +0 -1
- package/dist/storage/opfs/leader.d.ts.map +0 -1
- package/dist/storage/opfs/leader.js.map +0 -1
- package/dist/storage/opfs/rpc.d.ts.map +0 -1
- package/dist/storage/opfs/rpc.js.map +0 -1
- package/dist/storage/opfs/store.d.ts.map +0 -1
- package/dist/storage/opfs/store.js.map +0 -1
- package/dist/storage/snapshot.d.ts.map +0 -1
- package/dist/storage/snapshot.js.map +0 -1
- package/dist/storage/toolkit/extents.d.ts.map +0 -1
- package/dist/storage/toolkit/extents.js.map +0 -1
- package/dist/storage/toolkit/index.d.ts.map +0 -1
- package/dist/storage/toolkit/index.js.map +0 -1
- package/dist/storage/toolkit/record-core.d.ts.map +0 -1
- package/dist/storage/toolkit/record-core.js.map +0 -1
- package/dist/storage/toolkit/sync-file.d.ts.map +0 -1
- package/dist/storage/toolkit/sync-file.js.map +0 -1
- package/dist/storage/toolkit/wal.d.ts.map +0 -1
- package/dist/storage/toolkit/wal.js.map +0 -1
- package/dist/storage/toolkit/wire.d.ts.map +0 -1
- package/dist/storage/toolkit/wire.js.map +0 -1
- package/dist/storage/types.d.ts.map +0 -1
- package/dist/storage/types.js.map +0 -1
- package/dist/testing/block-store-conformance.d.ts.map +0 -1
- package/dist/testing/block-store-conformance.js.map +0 -1
- package/dist/testing/index.d.ts.map +0 -1
- package/dist/testing/index.js.map +0 -1
- package/dist/testing/opfs-shim.d.ts.map +0 -1
- package/dist/testing/opfs-shim.js.map +0 -1
- package/dist/testing/seeds.d.ts +0 -11
- package/dist/testing/seeds.d.ts.map +0 -1
- package/dist/testing/seeds.js +0 -50
- package/dist/testing/seeds.js.map +0 -1
- package/dist/transactions/index.d.ts.map +0 -1
- package/dist/transactions/index.js.map +0 -1
- package/dist/worker-protocol/index.d.ts.map +0 -1
- package/dist/worker-protocol/index.js.map +0 -1
package/dist/engine/schema.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { copyDate, dateMilliseconds } from "../date-value.js";
|
|
2
|
+
import { validateColumnDefault, validateEnumValues, validateSqlDomain, } from "../storage/types.js";
|
|
3
|
+
import { compileCheckExpression, expressionColumns, validateDefaultExpression, } from "./query.js";
|
|
4
|
+
import { externalSqlDomainValue, normalizeSqlDomainValue } from "./sql-domains.js";
|
|
3
5
|
/**
|
|
4
6
|
* The constraint name `migrate()` gives a declared relation. It matches the name the SQL parser
|
|
5
7
|
* derives for an unnamed inline REFERENCES, so a table built either way has the same catalog.
|
|
@@ -7,12 +9,20 @@ import { compileCheckExpression, expressionColumns } from "./query.js";
|
|
|
7
9
|
export function foreignKeyName(tableName, columnName) {
|
|
8
10
|
return `${tableName}_${columnName}_fkey`;
|
|
9
11
|
}
|
|
10
|
-
function defaultSpecFromArg(type, value) {
|
|
12
|
+
function defaultSpecFromArg(type, value, sqlDomain) {
|
|
13
|
+
if (sqlDomain?.kind === "numeric") {
|
|
14
|
+
if ((typeof value !== "number" || !Number.isFinite(value)) && typeof value !== "string") {
|
|
15
|
+
throw new TypeError("NUMERIC default must be a finite number or decimal string");
|
|
16
|
+
}
|
|
17
|
+
normalizeSqlDomainValue(sqlDomain, value);
|
|
18
|
+
return { kind: "literal", value };
|
|
19
|
+
}
|
|
11
20
|
switch (type) {
|
|
12
21
|
case "datetime":
|
|
13
|
-
if (value
|
|
14
|
-
throw new TypeError(
|
|
15
|
-
|
|
22
|
+
if (!(value instanceof Date) || !Number.isFinite(dateMilliseconds(value))) {
|
|
23
|
+
throw new TypeError("Default literal must be a valid Date");
|
|
24
|
+
}
|
|
25
|
+
return { kind: "literal", value: copyDate(value) };
|
|
16
26
|
case "string":
|
|
17
27
|
if (typeof value !== "string")
|
|
18
28
|
throw new TypeError("Default literal must be a string");
|
|
@@ -34,70 +44,135 @@ function createColumn(type, state = {}) {
|
|
|
34
44
|
type,
|
|
35
45
|
isNullable: (state.isNullable ?? false),
|
|
36
46
|
isUnique: (state.isUnique ?? false),
|
|
37
|
-
hasDefault: (state.defaultSpec !== undefined
|
|
47
|
+
hasDefault: (state.defaultSpec !== undefined),
|
|
48
|
+
integer: state.integer ?? false,
|
|
49
|
+
...(state.sqlDomain === undefined ? {} : { sqlDomain: state.sqlDomain }),
|
|
38
50
|
...(state.defaultSpec === undefined ? {} : { defaultSpec: state.defaultSpec }),
|
|
39
|
-
...(state.defaultFn === undefined
|
|
40
|
-
? {}
|
|
41
|
-
: { defaultFn: state.defaultFn }),
|
|
42
51
|
...(state.renamedFromName === undefined ? {} : { renamedFromName: state.renamedFromName }),
|
|
43
52
|
...(state.reference === undefined ? {} : { reference: state.reference }),
|
|
44
53
|
...(state.enumValues === undefined ? {} : { enumValues: state.enumValues }),
|
|
45
|
-
...(state.backfillValue === undefined
|
|
46
|
-
? {}
|
|
47
|
-
: { backfillValue: state.backfillValue }),
|
|
54
|
+
...(state.backfillValue === undefined ? {} : { backfillValue: state.backfillValue }),
|
|
48
55
|
};
|
|
49
56
|
return {
|
|
50
57
|
...base,
|
|
51
|
-
nullable: () => createColumn(type, {
|
|
52
|
-
unique: () => createColumn(type, { ...state, isUnique: true }),
|
|
53
|
-
renamedFrom: (name) => createColumn(type, { ...state, renamedFromName: name }),
|
|
54
|
-
backfill: (value) => createColumn(type, { ...state, backfillValue: value }),
|
|
55
|
-
references: (table, referencedColumn, options = {}) => createColumn(type, {
|
|
58
|
+
nullable: () => createColumn(type, {
|
|
56
59
|
...state,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
isNullable: true,
|
|
61
|
+
}),
|
|
62
|
+
unique: () => createColumn(type, {
|
|
63
|
+
...state,
|
|
64
|
+
isUnique: true,
|
|
62
65
|
}),
|
|
66
|
+
renamedFrom: (name) => {
|
|
67
|
+
validateSchemaName(name, "Rename source");
|
|
68
|
+
return createColumn(type, { ...state, renamedFromName: name });
|
|
69
|
+
},
|
|
70
|
+
backfill: (value) => createColumn(type, { ...state, backfillValue: value }),
|
|
71
|
+
references: (table, referencedColumn, options = {}) => {
|
|
72
|
+
validateSchemaName(table, "Referenced table");
|
|
73
|
+
validateSchemaName(referencedColumn, "Referenced column");
|
|
74
|
+
return createColumn(type, {
|
|
75
|
+
...state,
|
|
76
|
+
reference: {
|
|
77
|
+
table,
|
|
78
|
+
column: referencedColumn,
|
|
79
|
+
onDelete: options.onDelete ?? "restrict",
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
},
|
|
63
83
|
default: ((value) => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
84
|
+
return createColumn(type, {
|
|
85
|
+
...state,
|
|
86
|
+
defaultSpec: defaultSpecFromArg(type, value, state.sqlDomain),
|
|
87
|
+
});
|
|
88
|
+
}),
|
|
89
|
+
defaultSql: ((sql) => {
|
|
90
|
+
const expression = sql.trim();
|
|
91
|
+
if (expression.length === 0 || expression !== sql) {
|
|
92
|
+
throw new TypeError("Default SQL must be a trimmed non-empty expression");
|
|
93
|
+
}
|
|
94
|
+
return createColumn(type, {
|
|
95
|
+
...state,
|
|
96
|
+
defaultSpec: { kind: "expression", sql: expression },
|
|
97
|
+
});
|
|
71
98
|
}),
|
|
72
99
|
autoIncrement: (() => {
|
|
73
100
|
if (type !== "number") {
|
|
74
101
|
throw new TypeError("Auto-increment requires a number column");
|
|
75
102
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
103
|
+
return createColumn(type, {
|
|
104
|
+
...state,
|
|
105
|
+
defaultSpec: { kind: "autoincrement" },
|
|
106
|
+
});
|
|
79
107
|
}),
|
|
80
108
|
};
|
|
81
109
|
}
|
|
110
|
+
function validateSchemaName(name, kind) {
|
|
111
|
+
if (name.length === 0)
|
|
112
|
+
throw new TypeError(`${kind} name cannot be empty`);
|
|
113
|
+
if (name.trim() !== name) {
|
|
114
|
+
throw new TypeError(`${kind} name cannot start or end with whitespace: ${JSON.stringify(name)}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
82
117
|
/**
|
|
83
118
|
* Rebuilds a column carrying an exact default spec — the wire layer's escape hatch, since the
|
|
84
|
-
* public `.default()`
|
|
119
|
+
* public `.default()` accepts literals and cannot express auto-increment catalog metadata.
|
|
85
120
|
*/
|
|
86
121
|
export function columnWithDefaultSpec(base, spec) {
|
|
87
|
-
return
|
|
122
|
+
return columnFromState({
|
|
123
|
+
type: base.type,
|
|
88
124
|
isNullable: base.isNullable,
|
|
89
125
|
isUnique: base.isUnique,
|
|
126
|
+
integer: base.integer ?? false,
|
|
127
|
+
...(base.sqlDomain === undefined ? {} : { sqlDomain: base.sqlDomain }),
|
|
90
128
|
...(base.renamedFromName === undefined ? {} : { renamedFromName: base.renamedFromName }),
|
|
91
129
|
...(base.reference === undefined ? {} : { reference: base.reference }),
|
|
92
130
|
...(base.enumValues === undefined ? {} : { enumValues: base.enumValues }),
|
|
93
131
|
defaultSpec: spec,
|
|
94
132
|
});
|
|
95
133
|
}
|
|
134
|
+
/** Rebuilds a fluent column from structured-clone-safe metadata. */
|
|
135
|
+
export function columnFromState(state) {
|
|
136
|
+
return createColumn(state.type, {
|
|
137
|
+
isNullable: state.isNullable,
|
|
138
|
+
isUnique: state.isUnique,
|
|
139
|
+
integer: state.integer ?? false,
|
|
140
|
+
...(state.sqlDomain === undefined ? {} : { sqlDomain: state.sqlDomain }),
|
|
141
|
+
...(state.renamedFromName === undefined ? {} : { renamedFromName: state.renamedFromName }),
|
|
142
|
+
...(state.reference === undefined ? {} : { reference: state.reference }),
|
|
143
|
+
...(state.defaultSpec === undefined ? {} : { defaultSpec: state.defaultSpec }),
|
|
144
|
+
...(state.enumValues === undefined ? {} : { enumValues: state.enumValues }),
|
|
145
|
+
...(state.backfillValue === undefined ? {} : { backfillValue: state.backfillValue }),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
96
148
|
export const column = {
|
|
97
149
|
boolean: () => createColumn("boolean"),
|
|
98
150
|
number: () => createColumn("number"),
|
|
151
|
+
/** Exact safe-integer semantics, matching SQL INTEGER/SMALLINT/BIGINT. */
|
|
152
|
+
integer: () => createColumn("number", { integer: true }),
|
|
99
153
|
string: () => createColumn("string"),
|
|
100
154
|
datetime: () => createColumn("datetime"),
|
|
155
|
+
/** Exact decimal SQL NUMERIC. Selects return strings; writes accept strings or numbers. */
|
|
156
|
+
numeric: (options = {}) => {
|
|
157
|
+
const sqlDomain = validateSqlDomain({ kind: "numeric", ...options }, "numeric column");
|
|
158
|
+
return createColumn("string", { sqlDomain });
|
|
159
|
+
},
|
|
160
|
+
json: () => createColumn("string", { sqlDomain: { kind: "json" } }),
|
|
161
|
+
jsonb: () => createColumn("string", { sqlDomain: { kind: "jsonb" } }),
|
|
162
|
+
uuid: () => createColumn("string", { sqlDomain: { kind: "uuid" } }),
|
|
163
|
+
time: () => createColumn("string", { sqlDomain: { kind: "time" } }),
|
|
164
|
+
interval: () => createColumn("string", { sqlDomain: { kind: "interval" } }),
|
|
165
|
+
/** JSON array text at the JavaScript boundary, with the SQL element type retained in metadata. */
|
|
166
|
+
array: (element) => createColumn("string", {
|
|
167
|
+
sqlDomain: validateSqlDomain({ kind: "array", element }, "array column"),
|
|
168
|
+
}),
|
|
169
|
+
/** A named SQL enum domain, distinct from the lightweight `column.enum()` restriction. */
|
|
170
|
+
sqlEnum: (name, values) => {
|
|
171
|
+
validateSchemaName(name, "Enum type");
|
|
172
|
+
return createColumn("string", {
|
|
173
|
+
sqlDomain: validateSqlDomain({ kind: "enum", name, values: validateEnumValues(values, name) }, name),
|
|
174
|
+
});
|
|
175
|
+
},
|
|
101
176
|
/**
|
|
102
177
|
* A string column restricted to a closed set of values, typed as their literal union:
|
|
103
178
|
*
|
|
@@ -112,7 +187,35 @@ export const column = {
|
|
|
112
187
|
*/
|
|
113
188
|
enum: (values) => createColumn("string", { enumValues: validateEnumValues(values, "enum column") }),
|
|
114
189
|
};
|
|
190
|
+
function validateDeclaredColumnValue(definition, value, context, label) {
|
|
191
|
+
if (definition.sqlDomain !== undefined) {
|
|
192
|
+
try {
|
|
193
|
+
normalizeSqlDomainValue(definition.sqlDomain, value);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
throw new TypeError(`${label} does not fit ${context}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const matches = definition.type === "datetime"
|
|
201
|
+
? value instanceof Date && Number.isFinite(dateMilliseconds(value))
|
|
202
|
+
: definition.type === "number"
|
|
203
|
+
? typeof value === "number" &&
|
|
204
|
+
Number.isFinite(value) &&
|
|
205
|
+
(!definition.integer || Number.isSafeInteger(value))
|
|
206
|
+
: typeof value === definition.type;
|
|
207
|
+
if (!matches) {
|
|
208
|
+
const expected = definition.integer ? "safe integer" : definition.type;
|
|
209
|
+
throw new TypeError(`${label} value must be a ${expected}: ${context}`);
|
|
210
|
+
}
|
|
211
|
+
if (definition.enumValues !== undefined &&
|
|
212
|
+
typeof value === "string" &&
|
|
213
|
+
!definition.enumValues.includes(value)) {
|
|
214
|
+
throw new TypeError(`${label} value must be one of: ${definition.enumValues.join(", ")} (${context})`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
115
217
|
export function table(name, columns, options = {}) {
|
|
218
|
+
validateSchemaName(name, "Table");
|
|
116
219
|
const entries = Object.entries(columns);
|
|
117
220
|
if (entries.length === 0)
|
|
118
221
|
throw new TypeError(`Table ${name} needs at least one column`);
|
|
@@ -121,26 +224,48 @@ export function table(name, columns, options = {}) {
|
|
|
121
224
|
throw new TypeError(`Table ${name} may name at most one unique column`);
|
|
122
225
|
}
|
|
123
226
|
const uniqueEntry = uniqueColumns[0];
|
|
227
|
+
const primaryKey = options.primaryKey ?? [];
|
|
228
|
+
if (uniqueEntry !== undefined && primaryKey.length > 0) {
|
|
229
|
+
throw new TypeError(`Table ${name} cannot declare both .unique() and a table primary key`);
|
|
230
|
+
}
|
|
231
|
+
if (primaryKey.length === 0 && options.primaryKey !== undefined) {
|
|
232
|
+
throw new TypeError(`Table ${name} primary key needs at least one column`);
|
|
233
|
+
}
|
|
234
|
+
if (new Set(primaryKey).size !== primaryKey.length) {
|
|
235
|
+
throw new TypeError(`Table ${name} primary key columns must be distinct`);
|
|
236
|
+
}
|
|
237
|
+
for (const columnName of primaryKey) {
|
|
238
|
+
const definition = columns[columnName];
|
|
239
|
+
if (definition === undefined) {
|
|
240
|
+
throw new TypeError(`PRIMARY KEY column not found: ${name}.${columnName}`);
|
|
241
|
+
}
|
|
242
|
+
if (definition.isNullable) {
|
|
243
|
+
throw new TypeError(`PRIMARY KEY cannot be nullable: ${name}.${columnName}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
124
246
|
if (uniqueEntry?.[1].isNullable === true) {
|
|
125
247
|
throw new TypeError(`Table ${name} unique column must not be nullable: ${uniqueEntry[0]}`);
|
|
126
248
|
}
|
|
127
249
|
for (const [columnName, definition] of entries) {
|
|
128
|
-
|
|
129
|
-
|
|
250
|
+
validateSchemaName(columnName, "Column");
|
|
251
|
+
if (definition.integer && definition.type !== "number") {
|
|
252
|
+
throw new TypeError(`Integer domain requires a number column: ${name}.${columnName}`);
|
|
253
|
+
}
|
|
254
|
+
if (definition.sqlDomain !== undefined) {
|
|
255
|
+
if (definition.type !== "string") {
|
|
256
|
+
throw new TypeError(`SQL domains require string storage: ${name}.${columnName}`);
|
|
257
|
+
}
|
|
258
|
+
validateSqlDomain(definition.sqlDomain, `${name}.${columnName}`);
|
|
259
|
+
}
|
|
260
|
+
if (definition.integer && definition.sqlDomain !== undefined) {
|
|
261
|
+
throw new TypeError(`A column cannot be both integer and a SQL domain: ${name}.${columnName}`);
|
|
262
|
+
}
|
|
263
|
+
if (definition.enumValues !== undefined && definition.sqlDomain !== undefined) {
|
|
264
|
+
throw new TypeError(`A column cannot have both enum restrictions and a SQL domain: ${name}.${columnName}`);
|
|
130
265
|
}
|
|
131
266
|
const backfill = definition.backfillValue;
|
|
132
267
|
if (backfill !== undefined && typeof backfill !== "function") {
|
|
133
|
-
|
|
134
|
-
? backfill instanceof Date
|
|
135
|
-
: typeof backfill === definition.type;
|
|
136
|
-
if (!matches) {
|
|
137
|
-
throw new TypeError(`Backfill value must be a ${definition.type}: ${name}.${columnName}`);
|
|
138
|
-
}
|
|
139
|
-
if (definition.enumValues !== undefined &&
|
|
140
|
-
typeof backfill === "string" &&
|
|
141
|
-
!definition.enumValues.includes(backfill)) {
|
|
142
|
-
throw new TypeError(`Backfill value must be one of: ${definition.enumValues.join(", ")} (${name}.${columnName})`);
|
|
143
|
-
}
|
|
268
|
+
validateDeclaredColumnValue(definition, backfill, `${name}.${columnName}`, "Backfill");
|
|
144
269
|
}
|
|
145
270
|
if (backfill !== undefined && definition.isNullable) {
|
|
146
271
|
throw new TypeError(`A nullable column needs no backfill: ${name}.${columnName}. Rows without it already read NULL.`);
|
|
@@ -154,28 +279,91 @@ export function table(name, columns, options = {}) {
|
|
|
154
279
|
validateColumnDefault({
|
|
155
280
|
name: columnName,
|
|
156
281
|
type: definition.type,
|
|
282
|
+
...(definition.integer ? { integer: true } : {}),
|
|
283
|
+
...(definition.sqlDomain === undefined ? {} : { sqlDomain: definition.sqlDomain }),
|
|
157
284
|
nullable: definition.isNullable,
|
|
158
|
-
isUniqueKey: definition.isUnique,
|
|
285
|
+
isUniqueKey: definition.isUnique || (primaryKey.length === 1 && primaryKey[0] === columnName),
|
|
159
286
|
...(definition.enumValues === undefined ? {} : { enumValues: definition.enumValues }),
|
|
160
287
|
}, spec);
|
|
288
|
+
if (spec.kind === "expression") {
|
|
289
|
+
validateDefaultExpression(spec.sql, {
|
|
290
|
+
name: `${name}.${columnName}`,
|
|
291
|
+
type: definition.type,
|
|
292
|
+
...(definition.sqlDomain === undefined ? {} : { sqlDomain: definition.sqlDomain }),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
if (spec.kind === "literal" && definition.sqlDomain !== undefined) {
|
|
296
|
+
normalizeSqlDomainValue(definition.sqlDomain, spec.value);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const foreignKeys = options.foreignKeys ?? [];
|
|
300
|
+
const constraintNames = new Set();
|
|
301
|
+
const foreignKeyNames = new Set();
|
|
302
|
+
for (const key of foreignKeys) {
|
|
303
|
+
validateSchemaName(key.name, "FOREIGN KEY");
|
|
304
|
+
if (foreignKeyNames.has(key.name)) {
|
|
305
|
+
throw new TypeError(`Duplicate FOREIGN KEY in table ${name}: ${key.name}`);
|
|
306
|
+
}
|
|
307
|
+
foreignKeyNames.add(key.name);
|
|
308
|
+
if (constraintNames.has(key.name)) {
|
|
309
|
+
throw new TypeError(`Duplicate constraint in table ${name}: ${key.name}`);
|
|
310
|
+
}
|
|
311
|
+
constraintNames.add(key.name);
|
|
312
|
+
if (key.columns.length === 0 || new Set(key.columns).size !== key.columns.length) {
|
|
313
|
+
throw new TypeError(`FOREIGN KEY ${key.name} needs distinct child columns`);
|
|
314
|
+
}
|
|
315
|
+
if (key.references.columns.length === 0 ||
|
|
316
|
+
new Set(key.references.columns).size !== key.references.columns.length) {
|
|
317
|
+
throw new TypeError(`FOREIGN KEY ${key.name} needs distinct parent columns`);
|
|
318
|
+
}
|
|
319
|
+
if (key.columns.length !== key.references.columns.length) {
|
|
320
|
+
throw new TypeError(`FOREIGN KEY ${key.name} has different child and parent arity`);
|
|
321
|
+
}
|
|
322
|
+
validateSchemaName(key.references.table, "Referenced table");
|
|
323
|
+
key.references.columns.forEach((columnName) => validateSchemaName(columnName, "Referenced column"));
|
|
324
|
+
for (const columnName of key.columns) {
|
|
325
|
+
const definition = columns[columnName];
|
|
326
|
+
if (definition === undefined) {
|
|
327
|
+
throw new TypeError(`FOREIGN KEY ${key.name} names an unknown column: ${name}.${columnName}`);
|
|
328
|
+
}
|
|
329
|
+
if ((key.onDelete ?? "restrict") === "set null" && !definition.isNullable) {
|
|
330
|
+
throw new TypeError(`FOREIGN KEY ${key.name} cannot SET NULL a NOT NULL column`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
161
333
|
}
|
|
162
334
|
const checks = options.checks ?? [];
|
|
163
335
|
const checkNames = new Set();
|
|
164
336
|
for (const check of checks) {
|
|
165
|
-
|
|
166
|
-
throw new TypeError(`Table ${name} has an unnamed CHECK`);
|
|
337
|
+
validateSchemaName(check.name, "CHECK");
|
|
167
338
|
if (checkNames.has(check.name)) {
|
|
168
339
|
throw new TypeError(`Duplicate CHECK in table ${name}: ${check.name}`);
|
|
169
340
|
}
|
|
170
341
|
checkNames.add(check.name);
|
|
342
|
+
if (constraintNames.has(check.name)) {
|
|
343
|
+
throw new TypeError(`Duplicate constraint in table ${name}: ${check.name}`);
|
|
344
|
+
}
|
|
345
|
+
constraintNames.add(check.name);
|
|
171
346
|
if (check.sql.trim().length === 0) {
|
|
172
347
|
throw new TypeError(`CHECK ${check.name} in table ${name} has no expression`);
|
|
173
348
|
}
|
|
349
|
+
for (const reference of expressionColumns(compileCheckExpression(check.sql, check.name))) {
|
|
350
|
+
const pieces = reference.split(".");
|
|
351
|
+
const columnName = pieces.at(-1) ?? reference;
|
|
352
|
+
const qualifier = pieces.length > 1 ? pieces.slice(0, -1).join(".") : undefined;
|
|
353
|
+
if (qualifier !== undefined && qualifier !== name) {
|
|
354
|
+
throw new TypeError(`CHECK ${check.name} references another table: ${reference}`);
|
|
355
|
+
}
|
|
356
|
+
if (columns[columnName] === undefined) {
|
|
357
|
+
throw new TypeError(`CHECK ${check.name} names an unknown column: ${columnName}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
174
360
|
}
|
|
175
361
|
return {
|
|
176
362
|
kind: "table",
|
|
177
363
|
name,
|
|
178
364
|
columns,
|
|
365
|
+
primaryKey,
|
|
366
|
+
foreignKeys,
|
|
179
367
|
checks,
|
|
180
368
|
"~standard": {
|
|
181
369
|
version: 1,
|
|
@@ -188,27 +376,31 @@ export function table(name, columns, options = {}) {
|
|
|
188
376
|
const issues = [];
|
|
189
377
|
for (const [columnName, definition] of entries) {
|
|
190
378
|
const columnValue = row[columnName];
|
|
191
|
-
if (columnValue === undefined
|
|
192
|
-
|
|
193
|
-
// function defaults) fills it at insert time.
|
|
194
|
-
if (!definition.isNullable &&
|
|
195
|
-
definition.defaultSpec === undefined &&
|
|
196
|
-
definition.defaultFn === undefined) {
|
|
379
|
+
if (columnValue === undefined) {
|
|
380
|
+
if (!definition.isNullable && definition.defaultSpec === undefined) {
|
|
197
381
|
issues.push({ message: `Missing non-nullable column`, path: [columnName] });
|
|
198
382
|
}
|
|
199
383
|
continue;
|
|
200
384
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
385
|
+
if (columnValue === null) {
|
|
386
|
+
if (!definition.isNullable) {
|
|
387
|
+
issues.push({ message: `Expected non-null value`, path: [columnName] });
|
|
388
|
+
}
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
validateDeclaredColumnValue(definition, columnValue, `${name}.${columnName}`, "Value");
|
|
206
393
|
}
|
|
207
|
-
|
|
208
|
-
typeof columnValue === "string"
|
|
209
|
-
|
|
394
|
+
catch (error) {
|
|
395
|
+
const message = definition.enumValues !== undefined && typeof columnValue === "string"
|
|
396
|
+
? `Expected one of: ${definition.enumValues.join(", ")}`
|
|
397
|
+
: definition.sqlDomain === undefined
|
|
398
|
+
? `Expected ${definition.integer ? "safe integer" : definition.type}`
|
|
399
|
+
: error instanceof Error
|
|
400
|
+
? error.message
|
|
401
|
+
: `Invalid value`;
|
|
210
402
|
issues.push({
|
|
211
|
-
message
|
|
403
|
+
message,
|
|
212
404
|
path: [columnName],
|
|
213
405
|
});
|
|
214
406
|
}
|
|
@@ -235,12 +427,23 @@ export function declaredForeignKeys(definition) {
|
|
|
235
427
|
continue;
|
|
236
428
|
keys.push({
|
|
237
429
|
name: foreignKeyName(definition.name, columnName),
|
|
238
|
-
|
|
430
|
+
columns: [columnName],
|
|
239
431
|
parentTable: reference.table,
|
|
240
|
-
|
|
432
|
+
parentColumns: [reference.column],
|
|
241
433
|
onDelete: reference.onDelete,
|
|
242
434
|
});
|
|
243
435
|
}
|
|
436
|
+
for (const key of definition.foreignKeys) {
|
|
437
|
+
const childColumns = [...key.columns];
|
|
438
|
+
const parentColumns = [...key.references.columns];
|
|
439
|
+
keys.push({
|
|
440
|
+
name: key.name,
|
|
441
|
+
columns: childColumns,
|
|
442
|
+
parentTable: key.references.table,
|
|
443
|
+
parentColumns,
|
|
444
|
+
onDelete: key.onDelete ?? "restrict",
|
|
445
|
+
});
|
|
446
|
+
}
|
|
244
447
|
return keys;
|
|
245
448
|
}
|
|
246
449
|
/**
|
|
@@ -255,16 +458,18 @@ export function declaredForeignKeys(definition) {
|
|
|
255
458
|
* ```
|
|
256
459
|
*/
|
|
257
460
|
export function view(name, definition) {
|
|
461
|
+
validateSchemaName(name, "View");
|
|
258
462
|
const entries = Object.entries(definition.columns);
|
|
259
463
|
if (entries.length === 0)
|
|
260
464
|
throw new TypeError(`View ${name} needs at least one column`);
|
|
261
465
|
if (definition.sql.trim().length === 0)
|
|
262
466
|
throw new TypeError(`View ${name} has no query`);
|
|
263
467
|
for (const [columnName, columnDefinition] of entries) {
|
|
468
|
+
validateSchemaName(columnName, "Column");
|
|
264
469
|
if (columnDefinition.isUnique) {
|
|
265
470
|
throw new TypeError(`A view column cannot be a unique key: ${name}.${columnName}`);
|
|
266
471
|
}
|
|
267
|
-
if (columnDefinition.defaultSpec !== undefined
|
|
472
|
+
if (columnDefinition.defaultSpec !== undefined) {
|
|
268
473
|
throw new TypeError(`A view column cannot have a default: ${name}.${columnName}`);
|
|
269
474
|
}
|
|
270
475
|
}
|
|
@@ -286,14 +491,38 @@ export function schema(tables, options = {}) {
|
|
|
286
491
|
names.add(definition.name);
|
|
287
492
|
}
|
|
288
493
|
for (const definition of tables) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
494
|
+
const declaredNames = new Set();
|
|
495
|
+
for (const key of declaredForeignKeys(definition)) {
|
|
496
|
+
if (declaredNames.has(key.name)) {
|
|
497
|
+
throw new TypeError(`Duplicate FOREIGN KEY in table ${definition.name}: ${key.name}`);
|
|
498
|
+
}
|
|
499
|
+
declaredNames.add(key.name);
|
|
500
|
+
const childNames = key.columns;
|
|
501
|
+
const parentNames = key.parentColumns;
|
|
502
|
+
const target = tables.find(({ name }) => name === key.parentTable);
|
|
503
|
+
if (target === undefined ||
|
|
504
|
+
childNames.some((columnName) => !(columnName in definition.columns)) ||
|
|
505
|
+
parentNames.some((columnName) => !(columnName in target.columns))) {
|
|
506
|
+
throw new TypeError(`Relation target does not exist: ${definition.name}.${childNames.join(",")} -> ${key.parentTable}.${parentNames.join(",")}`);
|
|
296
507
|
}
|
|
508
|
+
const targetPrimary = target.primaryKey.length > 0
|
|
509
|
+
? [...target.primaryKey]
|
|
510
|
+
: Object.entries(target.columns).flatMap(([name, columnDefinition]) => columnDefinition.isUnique ? [name] : []);
|
|
511
|
+
if (parentNames.length !== targetPrimary.length ||
|
|
512
|
+
parentNames.some((columnName, index) => columnName !== targetPrimary[index])) {
|
|
513
|
+
throw new TypeError(`Relation target must be the unique key: ${definition.name}.${childNames.join(",")} -> ${key.parentTable}(${targetPrimary.join(", ")})`);
|
|
514
|
+
}
|
|
515
|
+
childNames.forEach((childName, index) => {
|
|
516
|
+
const child = definition.columns[childName];
|
|
517
|
+
const parentName = parentNames[index];
|
|
518
|
+
const parent = parentName === undefined ? undefined : target.columns[parentName];
|
|
519
|
+
if (child === undefined ||
|
|
520
|
+
child.type !== parent?.type ||
|
|
521
|
+
child.integer !== parent.integer ||
|
|
522
|
+
JSON.stringify(child.sqlDomain ?? null) !== JSON.stringify(parent.sqlDomain ?? null)) {
|
|
523
|
+
throw new TypeError(`Relation types must match: ${definition.name}.${childName} and ${key.parentTable}.${String(parentName)}`);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
297
526
|
}
|
|
298
527
|
}
|
|
299
528
|
return { kind: "schema", tables, views };
|
|
@@ -367,16 +596,22 @@ export function isDestructiveStep(step) {
|
|
|
367
596
|
* the catalog still points at it. Each of these would leave a constraint or key referring to a
|
|
368
597
|
* column no longer there, so the drop is refused rather than silently invalidating them.
|
|
369
598
|
*/
|
|
370
|
-
function assertColumnDroppable(record, column) {
|
|
599
|
+
export function assertColumnDroppable(record, column) {
|
|
371
600
|
const where = `${record.name}.${column.name}`;
|
|
372
|
-
if (record.uniqueKeyColumnId === column.id
|
|
601
|
+
if (record.uniqueKeyColumnId === column.id ||
|
|
602
|
+
(record.primaryKeyColumnIds ?? []).includes(column.id)) {
|
|
373
603
|
throw new TypeError(`The unique key cannot be dropped: ${where}. Unique-key changes need the table recreated.`);
|
|
374
604
|
}
|
|
375
605
|
for (const key of record.foreignKeys) {
|
|
376
|
-
if (key.column
|
|
606
|
+
if (key.columns.includes(column.name)) {
|
|
377
607
|
throw new TypeError(`FOREIGN KEY ${key.name} still uses this column: ${where}`);
|
|
378
608
|
}
|
|
379
609
|
}
|
|
610
|
+
for (const index of record.indexes ?? []) {
|
|
611
|
+
if (index.columns.some(({ name }) => name === column.name)) {
|
|
612
|
+
throw new TypeError(`Index ${index.name} still uses this column: ${where}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
380
615
|
for (const check of record.checks) {
|
|
381
616
|
let referenced;
|
|
382
617
|
try {
|
|
@@ -391,6 +626,24 @@ function assertColumnDroppable(record, column) {
|
|
|
391
626
|
}
|
|
392
627
|
}
|
|
393
628
|
}
|
|
629
|
+
function assertColumnRenamable(catalog, record, column) {
|
|
630
|
+
const where = `${record.name}.${column.name}`;
|
|
631
|
+
for (const owner of catalog.tables) {
|
|
632
|
+
for (const key of owner.foreignKeys) {
|
|
633
|
+
const usesChild = owner.name === record.name && key.columns.includes(column.name);
|
|
634
|
+
const usesParent = key.parentTable === record.name && key.parentColumns.includes(column.name);
|
|
635
|
+
if (usesChild || usesParent) {
|
|
636
|
+
throw new TypeError(`FOREIGN KEY ${key.name} prevents renaming ${where}`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
for (const check of record.checks) {
|
|
641
|
+
const referenced = expressionColumns(compileCheckExpression(check.sql, check.name));
|
|
642
|
+
if (referenced.includes(column.name)) {
|
|
643
|
+
throw new TypeError(`CHECK ${check.name} prevents renaming ${where}`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
394
647
|
/**
|
|
395
648
|
* Freezes a column's backfill. A generator runs exactly once — here, while the migration is being
|
|
396
649
|
* planned — so the catalog stores a value rather than a function and no two readers can disagree.
|
|
@@ -399,7 +652,27 @@ function resolveBackfill(definition) {
|
|
|
399
652
|
const declared = definition.backfillValue;
|
|
400
653
|
if (declared === undefined)
|
|
401
654
|
return undefined;
|
|
402
|
-
|
|
655
|
+
const value = typeof declared === "function" ? declared() : declared;
|
|
656
|
+
validateDeclaredColumnValue(definition, value, "added column", "Backfill");
|
|
657
|
+
return value;
|
|
658
|
+
}
|
|
659
|
+
function storedBackfill(definition, value) {
|
|
660
|
+
if (definition.sqlDomain === undefined)
|
|
661
|
+
return value;
|
|
662
|
+
const normalized = normalizeSqlDomainValue(definition.sqlDomain, value);
|
|
663
|
+
if (normalized === null)
|
|
664
|
+
throw new TypeError("A backfill cannot be NULL");
|
|
665
|
+
return normalized;
|
|
666
|
+
}
|
|
667
|
+
function backfillsEqual(left, right) {
|
|
668
|
+
const externalLeft = externalSqlDomainValue(left);
|
|
669
|
+
const externalRight = externalSqlDomainValue(right);
|
|
670
|
+
if (externalLeft instanceof Date || externalRight instanceof Date) {
|
|
671
|
+
return (externalLeft instanceof Date &&
|
|
672
|
+
externalRight instanceof Date &&
|
|
673
|
+
dateMilliseconds(externalLeft) === dateMilliseconds(externalRight));
|
|
674
|
+
}
|
|
675
|
+
return externalLeft === externalRight;
|
|
403
676
|
}
|
|
404
677
|
/**
|
|
405
678
|
* Constraints on an existing table cannot change through a metadata-only step. Attaching a
|
|
@@ -409,17 +682,18 @@ function resolveBackfill(definition) {
|
|
|
409
682
|
* discipline every other unprovable change gets: an explicit error naming the fix.
|
|
410
683
|
*/
|
|
411
684
|
function assertConstraintsUnchanged(record, definition) {
|
|
412
|
-
const describeKey = (key) => `${key.
|
|
685
|
+
const describeKey = (key) => `${key.columns.join(",")} -> ${key.parentTable}.` +
|
|
686
|
+
`${key.parentColumns.join(",")} ON DELETE ${key.onDelete}`;
|
|
413
687
|
const existingKeys = new Map(record.foreignKeys.map((key) => [key.name, key]));
|
|
414
688
|
const declaredKeys = new Map(declaredForeignKeys(definition).map((key) => [key.name, key]));
|
|
415
689
|
for (const [name, declared] of declaredKeys) {
|
|
416
690
|
const existing = existingKeys.get(name);
|
|
417
691
|
if (existing === undefined) {
|
|
418
|
-
throw new TypeError(`FOREIGN KEY cannot be added after creation: ${definition.name}.${declared.
|
|
692
|
+
throw new TypeError(`FOREIGN KEY cannot be added after creation: ${definition.name}.${declared.columns.join(",")}. ` +
|
|
419
693
|
`Existing rows are not known to satisfy it; recreate the table to add a relation.`);
|
|
420
694
|
}
|
|
421
695
|
if (describeKey(existing) !== describeKey(declared)) {
|
|
422
|
-
throw new TypeError(`FOREIGN KEY cannot change: ${definition.name}.${declared.
|
|
696
|
+
throw new TypeError(`FOREIGN KEY cannot change: ${definition.name}.${declared.columns.join(",")} is ` +
|
|
423
697
|
`${describeKey(existing)}, schema says ${describeKey(declared)}`);
|
|
424
698
|
}
|
|
425
699
|
}
|
|
@@ -466,9 +740,13 @@ export function planMigration(catalog, definition, options = {}) {
|
|
|
466
740
|
if (existing === undefined && from !== undefined) {
|
|
467
741
|
const source = recordColumnsByName.get(from);
|
|
468
742
|
if (source !== undefined) {
|
|
743
|
+
if (renameSources.has(from)) {
|
|
744
|
+
throw new TypeError(`Rename source is used more than once: ${tableDefinition.name}.${from}`);
|
|
745
|
+
}
|
|
469
746
|
if (definedNames.has(from)) {
|
|
470
747
|
throw new TypeError(`Rename source is still defined: ${tableDefinition.name}.${from}`);
|
|
471
748
|
}
|
|
749
|
+
assertColumnRenamable(catalog, record, source);
|
|
472
750
|
steps.push({
|
|
473
751
|
kind: "rename-column",
|
|
474
752
|
tableName: tableDefinition.name,
|
|
@@ -484,7 +762,7 @@ export function planMigration(catalog, definition, options = {}) {
|
|
|
484
762
|
if (!columnDefinition.isNullable && backfill === undefined) {
|
|
485
763
|
throw new TypeError(`Added columns must be nullable, or carry a backfill: ${tableDefinition.name}.${columnName}`);
|
|
486
764
|
}
|
|
487
|
-
if (columnDefinition.isUnique) {
|
|
765
|
+
if (columnDefinition.isUnique || tableDefinition.primaryKey.includes(columnName)) {
|
|
488
766
|
throw new TypeError(`Unique keys cannot be added after creation: ${tableDefinition.name}.${columnName}`);
|
|
489
767
|
}
|
|
490
768
|
steps.push({
|
|
@@ -499,6 +777,23 @@ export function planMigration(catalog, definition, options = {}) {
|
|
|
499
777
|
if (existing.type !== columnDefinition.type) {
|
|
500
778
|
throw new TypeError(`Column types cannot change: ${tableDefinition.name}.${columnName} is ${existing.type}, schema says ${columnDefinition.type}`);
|
|
501
779
|
}
|
|
780
|
+
if ((existing.integer === true) !== columnDefinition.integer ||
|
|
781
|
+
JSON.stringify(existing.sqlDomain ?? null) !==
|
|
782
|
+
JSON.stringify(columnDefinition.sqlDomain ?? null)) {
|
|
783
|
+
throw new TypeError(`Column domains cannot change: ${tableDefinition.name}.${columnName}`);
|
|
784
|
+
}
|
|
785
|
+
if (existing.backfill === undefined && columnDefinition.backfillValue !== undefined) {
|
|
786
|
+
throw new TypeError(`Backfills cannot be added after a column exists: ${tableDefinition.name}.${columnName}`);
|
|
787
|
+
}
|
|
788
|
+
if (existing.backfill !== undefined && columnDefinition.backfillValue === undefined) {
|
|
789
|
+
throw new TypeError(`Backfills cannot be removed: ${tableDefinition.name}.${columnName}`);
|
|
790
|
+
}
|
|
791
|
+
if (existing.backfill !== undefined &&
|
|
792
|
+
columnDefinition.backfillValue !== undefined &&
|
|
793
|
+
typeof columnDefinition.backfillValue !== "function" &&
|
|
794
|
+
!backfillsEqual(existing.backfill, columnDefinition.backfillValue)) {
|
|
795
|
+
throw new TypeError(`Backfills cannot change: ${tableDefinition.name}.${columnName}`);
|
|
796
|
+
}
|
|
502
797
|
const existingEnum = existing.enumValues;
|
|
503
798
|
const definedEnum = columnDefinition.enumValues;
|
|
504
799
|
if (definedEnum !== undefined && existingEnum === undefined) {
|
|
@@ -528,8 +823,10 @@ export function planMigration(catalog, definition, options = {}) {
|
|
|
528
823
|
}
|
|
529
824
|
}
|
|
530
825
|
}
|
|
531
|
-
const existingIsKey = record.uniqueKeyColumnId === existing.id
|
|
532
|
-
|
|
826
|
+
const existingIsKey = record.uniqueKeyColumnId === existing.id ||
|
|
827
|
+
(record.primaryKeyColumnIds ?? []).includes(existing.id);
|
|
828
|
+
const definedIsKey = columnDefinition.isUnique || tableDefinition.primaryKey.includes(columnName);
|
|
829
|
+
if (existingIsKey !== definedIsKey) {
|
|
533
830
|
throw new TypeError(`Unique keys cannot change: ${tableDefinition.name}.${columnName}`);
|
|
534
831
|
}
|
|
535
832
|
if (existing.nullable && !columnDefinition.isNullable) {
|
|
@@ -578,6 +875,20 @@ export function planMigration(catalog, definition, options = {}) {
|
|
|
578
875
|
columnName: columnRecord.name,
|
|
579
876
|
});
|
|
580
877
|
}
|
|
878
|
+
const existingKeyIds = record.primaryKeyColumnIds ??
|
|
879
|
+
(record.uniqueKeyColumnId === undefined ? [] : [record.uniqueKeyColumnId]);
|
|
880
|
+
const desiredKeyNames = tableDefinition.primaryKey.length > 0
|
|
881
|
+
? tableDefinition.primaryKey
|
|
882
|
+
: Object.entries(tableDefinition.columns).flatMap(([name, definition]) => definition.isUnique ? [name] : []);
|
|
883
|
+
const desiredKeyIds = desiredKeyNames.map((name) => {
|
|
884
|
+
const definition = tableDefinition.columns[name];
|
|
885
|
+
const currentName = definition?.renamedFromName ?? name;
|
|
886
|
+
return recordColumnsByName.get(name)?.id ?? recordColumnsByName.get(currentName)?.id;
|
|
887
|
+
});
|
|
888
|
+
if (desiredKeyIds.length !== existingKeyIds.length ||
|
|
889
|
+
desiredKeyIds.some((id, index) => id !== existingKeyIds[index])) {
|
|
890
|
+
throw new TypeError(`Primary key order cannot change: ${tableDefinition.name}`);
|
|
891
|
+
}
|
|
581
892
|
assertConstraintsUnchanged(record, tableDefinition);
|
|
582
893
|
}
|
|
583
894
|
if (options.schemaOwnsDatabase === true) {
|
|
@@ -591,32 +902,164 @@ export function planMigration(catalog, definition, options = {}) {
|
|
|
591
902
|
}
|
|
592
903
|
}
|
|
593
904
|
planViewSteps(catalog, definition, steps);
|
|
594
|
-
|
|
905
|
+
// A managed view can depend on a managed table the same authoritative schema removes. Drop
|
|
906
|
+
// dependents first; the SQL engine correctly refuses the reverse order. Replacements stay
|
|
907
|
+
// after table-creation steps so a newly declared body can resolve its sources.
|
|
908
|
+
const droppedViews = steps.filter((step) => step.kind === "drop-view");
|
|
909
|
+
const droppedTables = steps.filter((step) => step.kind === "drop-table");
|
|
910
|
+
return {
|
|
911
|
+
steps: [
|
|
912
|
+
...steps.filter((step) => step.kind !== "drop-view" && step.kind !== "drop-table"),
|
|
913
|
+
...droppedViews,
|
|
914
|
+
...droppedTables,
|
|
915
|
+
],
|
|
916
|
+
};
|
|
595
917
|
}
|
|
596
918
|
/**
|
|
597
|
-
* A thin, fully typed handle
|
|
598
|
-
*
|
|
919
|
+
* A thin, fully typed SQL handle: inserts require every non-nullable column, updates exclude the
|
|
920
|
+
* unique key, and reads return the complete inferred row shape. Every operation is rendered as
|
|
921
|
+
* parameterized SQL and goes through the parser/planner/executor; this helper has no private
|
|
922
|
+
* batch or table-read semantics that can drift from SQL.
|
|
599
923
|
*/
|
|
600
924
|
export function typedTable(database, definition) {
|
|
601
925
|
const columnNames = Object.keys(definition.columns);
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
926
|
+
const quote = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
927
|
+
const scalarUniqueKey = Object.entries(definition.columns).find(([, columnDefinition]) => columnDefinition.isUnique)?.[0];
|
|
928
|
+
const keyColumns = definition.primaryKey.length > 0
|
|
929
|
+
? [...definition.primaryKey]
|
|
930
|
+
: scalarUniqueKey === undefined
|
|
931
|
+
? []
|
|
932
|
+
: [scalarUniqueKey];
|
|
933
|
+
const normalizedRows = (rows) => {
|
|
934
|
+
if (rows.length === 0)
|
|
935
|
+
throw new TypeError("A batch needs at least one row");
|
|
936
|
+
// Keep extra runtime keys in the SQL column list. TypeScript normally excludes them, but a
|
|
937
|
+
// cast or JavaScript caller must still get the engine's "column does not exist" error rather
|
|
938
|
+
// than having data silently discarded by this facade.
|
|
939
|
+
const names = new Set(columnNames);
|
|
940
|
+
for (const row of rows)
|
|
941
|
+
for (const name of Object.keys(row))
|
|
942
|
+
names.add(name);
|
|
943
|
+
const ordered = [...names];
|
|
944
|
+
return { names: ordered, rows };
|
|
945
|
+
};
|
|
946
|
+
const insert = (rows, replace) => {
|
|
947
|
+
const normalized = normalizedRows(rows);
|
|
948
|
+
const params = [];
|
|
949
|
+
const values = normalized.rows
|
|
950
|
+
.map((row) => {
|
|
951
|
+
const placeholders = normalized.names.map((name) => {
|
|
952
|
+
const value = row[name];
|
|
953
|
+
if (!Object.hasOwn(row, name) || value === undefined)
|
|
954
|
+
return "DEFAULT";
|
|
955
|
+
params.push(value);
|
|
956
|
+
return `$${String(params.length)}`;
|
|
957
|
+
});
|
|
958
|
+
return `(${placeholders.join(", ")})`;
|
|
959
|
+
})
|
|
960
|
+
.join(", ");
|
|
961
|
+
let sql = `INSERT INTO ${quote(definition.name)} (${normalized.names.map(quote).join(", ")}) VALUES ${values}`;
|
|
962
|
+
if (replace) {
|
|
963
|
+
if (keyColumns.length === 0) {
|
|
964
|
+
throw new TypeError(`Upsert requires a unique key: ${definition.name}`);
|
|
965
|
+
}
|
|
966
|
+
sql += ` ON CONFLICT (${keyColumns.map(quote).join(", ")}) DO REPLACE`;
|
|
967
|
+
}
|
|
968
|
+
return database.execute(sql, params);
|
|
969
|
+
};
|
|
970
|
+
const requireUniqueKey = (operation) => {
|
|
971
|
+
if (keyColumns.length === 0) {
|
|
972
|
+
throw new TypeError(`${operation} requires a table with a unique key: ${definition.name}`);
|
|
973
|
+
}
|
|
974
|
+
return keyColumns;
|
|
975
|
+
};
|
|
976
|
+
const keyParts = (key) => {
|
|
977
|
+
if (keyColumns.length === 1)
|
|
978
|
+
return [key];
|
|
979
|
+
if (typeof key !== "object" || key === null) {
|
|
980
|
+
throw new TypeError(`Composite key for ${definition.name} must be an object`);
|
|
981
|
+
}
|
|
982
|
+
return keyColumns.map((name) => key[name]);
|
|
983
|
+
};
|
|
984
|
+
const keyToken = (value) => {
|
|
985
|
+
const parts = keyParts(value);
|
|
986
|
+
return JSON.stringify(parts.map((part) => part instanceof Date ? ["date", dateMilliseconds(part)] : [typeof part, String(part)]));
|
|
987
|
+
};
|
|
988
|
+
const assertKeys = (keys, operation) => {
|
|
989
|
+
if (keys.length === 0)
|
|
990
|
+
throw new TypeError(`A ${operation} batch needs at least one key`);
|
|
991
|
+
const seen = new Set();
|
|
992
|
+
for (const key of keys) {
|
|
993
|
+
if (keyParts(key).some((part) => part === undefined || part === null)) {
|
|
994
|
+
throw new TypeError(`Missing ${operation} key value for ${definition.name}`);
|
|
995
|
+
}
|
|
996
|
+
const token = keyToken(key);
|
|
997
|
+
if (seen.has(token))
|
|
998
|
+
throw new TypeError(`Duplicate key in ${operation} batch: ${String(key)}`);
|
|
999
|
+
seen.add(token);
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
610
1002
|
return {
|
|
611
1003
|
definition,
|
|
612
|
-
insert: (rows) =>
|
|
613
|
-
upsert: (rows) =>
|
|
614
|
-
update: (input) =>
|
|
615
|
-
keys
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
1004
|
+
insert: (rows) => insert(rows, false),
|
|
1005
|
+
upsert: (rows) => insert(rows, true),
|
|
1006
|
+
update: (input) => {
|
|
1007
|
+
const keys = requireUniqueKey("UPDATE");
|
|
1008
|
+
assertKeys(input.keys, "update");
|
|
1009
|
+
const assignments = [];
|
|
1010
|
+
const params = [];
|
|
1011
|
+
for (const [name, rawValues] of Object.entries(input.changes)) {
|
|
1012
|
+
if (!(name in definition.columns)) {
|
|
1013
|
+
// Render it anyway so SQL owns the public validation error.
|
|
1014
|
+
}
|
|
1015
|
+
const values = rawValues;
|
|
1016
|
+
if (values.length !== input.keys.length) {
|
|
1017
|
+
throw new TypeError(`Update column ${name} has ${String(values.length)} values for ${String(input.keys.length)} keys`);
|
|
1018
|
+
}
|
|
1019
|
+
const branches = [];
|
|
1020
|
+
values.forEach((value, index) => {
|
|
1021
|
+
if (value === undefined)
|
|
1022
|
+
return;
|
|
1023
|
+
const keyValue = input.keys[index];
|
|
1024
|
+
if (keyValue === undefined) {
|
|
1025
|
+
throw new TypeError(`Missing update key at position ${String(index)}`);
|
|
1026
|
+
}
|
|
1027
|
+
const conditions = keyParts(keyValue).map((part, keyIndex) => {
|
|
1028
|
+
params.push(part);
|
|
1029
|
+
return `${quote(keys[keyIndex] ?? "")} = $${String(params.length)}`;
|
|
1030
|
+
});
|
|
1031
|
+
params.push(value);
|
|
1032
|
+
branches.push(`WHEN ${conditions.join(" AND ")} THEN $${String(params.length)}`);
|
|
1033
|
+
});
|
|
1034
|
+
if (branches.length > 0) {
|
|
1035
|
+
assignments.push(`${quote(name)} = CASE ${branches.join(" ")} ELSE ${quote(name)} END`);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
if (assignments.length === 0)
|
|
1039
|
+
throw new TypeError("An update batch needs at least one change");
|
|
1040
|
+
const predicates = input.keys.map((keyValue) => {
|
|
1041
|
+
const conditions = keyParts(keyValue).map((part, keyIndex) => {
|
|
1042
|
+
params.push(part);
|
|
1043
|
+
return `${quote(keys[keyIndex] ?? "")} = $${String(params.length)}`;
|
|
1044
|
+
});
|
|
1045
|
+
return `(${conditions.join(" AND ")})`;
|
|
1046
|
+
});
|
|
1047
|
+
return database.execute(`UPDATE ${quote(definition.name)} SET ${assignments.join(", ")} WHERE ${predicates.join(" OR ")}`, params);
|
|
1048
|
+
},
|
|
1049
|
+
delete: (input) => {
|
|
1050
|
+
const keys = requireUniqueKey("DELETE");
|
|
1051
|
+
assertKeys(input.keys, "delete");
|
|
1052
|
+
const params = [];
|
|
1053
|
+
const predicates = input.keys.map((keyValue) => {
|
|
1054
|
+
const conditions = keyParts(keyValue).map((part, keyIndex) => {
|
|
1055
|
+
params.push(part);
|
|
1056
|
+
return `${quote(keys[keyIndex] ?? "")} = $${String(params.length)}`;
|
|
1057
|
+
});
|
|
1058
|
+
return `(${conditions.join(" AND ")})`;
|
|
1059
|
+
});
|
|
1060
|
+
return database.execute(`DELETE FROM ${quote(definition.name)} WHERE ${predicates.join(" OR ")}`, params);
|
|
1061
|
+
},
|
|
1062
|
+
rows: async () => (await database.query(`SELECT ${columnNames.map(quote).join(", ")} FROM ${quote(definition.name)}`)).rows,
|
|
620
1063
|
};
|
|
621
1064
|
}
|
|
622
1065
|
/** Applies one table's alteration steps onto its current column records. */
|
|
@@ -635,10 +1078,21 @@ export function applyColumnSteps(record, steps, createId) {
|
|
|
635
1078
|
id: createId(),
|
|
636
1079
|
name: step.columnName,
|
|
637
1080
|
type: step.definition.type,
|
|
1081
|
+
...(step.definition.integer ? { integer: true } : {}),
|
|
1082
|
+
...(step.definition.sqlDomain === undefined
|
|
1083
|
+
? {}
|
|
1084
|
+
: { sqlDomain: structuredClone(step.definition.sqlDomain) }),
|
|
638
1085
|
// A backfill is what lets the column be non-nullable: every row has a value, either
|
|
639
1086
|
// written or substituted at read time.
|
|
640
1087
|
nullable: step.backfill === undefined ? true : step.definition.isNullable,
|
|
641
|
-
...(step.backfill === undefined
|
|
1088
|
+
...(step.backfill === undefined
|
|
1089
|
+
? {}
|
|
1090
|
+
: {
|
|
1091
|
+
backfill: storedBackfill(step.definition, step.backfill),
|
|
1092
|
+
}),
|
|
1093
|
+
...(step.definition.defaultSpec === undefined
|
|
1094
|
+
? {}
|
|
1095
|
+
: { defaultValue: step.definition.defaultSpec }),
|
|
642
1096
|
...(step.definition.enumValues === undefined
|
|
643
1097
|
? {}
|
|
644
1098
|
: { enumValues: [...step.definition.enumValues] }),
|
|
@@ -704,6 +1158,14 @@ function columnDefaultsEqual(left, right) {
|
|
|
704
1158
|
return left === right;
|
|
705
1159
|
if (left.kind !== right.kind)
|
|
706
1160
|
return false;
|
|
707
|
-
|
|
1161
|
+
if (left.kind === "expression" && right.kind === "expression")
|
|
1162
|
+
return left.sql === right.sql;
|
|
1163
|
+
if (left.kind !== "literal" || right.kind !== "literal")
|
|
1164
|
+
return true;
|
|
1165
|
+
if (left.value instanceof Date || right.value instanceof Date) {
|
|
1166
|
+
return (left.value instanceof Date &&
|
|
1167
|
+
right.value instanceof Date &&
|
|
1168
|
+
dateMilliseconds(left.value) === dateMilliseconds(right.value));
|
|
1169
|
+
}
|
|
1170
|
+
return left.value === right.value;
|
|
708
1171
|
}
|
|
709
|
-
//# sourceMappingURL=schema.js.map
|