@minnowdb/core 0.6.9 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/dist/block-format/block.js +254 -325
  2. package/dist/block-format/checksum.js +30 -52
  3. package/dist/block-format/codecs.js +112 -125
  4. package/dist/block-format/column.js +187 -205
  5. package/dist/block-format/index.js +5 -1
  6. package/dist/block-format/physical.js +371 -387
  7. package/dist/block-format/types.js +4 -1
  8. package/dist/block-format/unicode.js +30 -39
  9. package/dist/date-value.d.ts +5 -0
  10. package/dist/date-value.js +71 -41
  11. package/dist/engine/artifact-cache.js +106 -116
  12. package/dist/engine/batch.js +36 -44
  13. package/dist/engine/buffered-writer.js +156 -158
  14. package/dist/engine/byte-estimates.js +20 -22
  15. package/dist/engine/cache-limits.js +24 -25
  16. package/dist/engine/cancellation.js +5 -3
  17. package/dist/engine/catalog.js +64 -73
  18. package/dist/engine/client.js +715 -780
  19. package/dist/engine/database.js +15952 -18465
  20. package/dist/engine/defaults.js +104 -111
  21. package/dist/engine/errors.js +113 -128
  22. package/dist/engine/fts.js +233 -280
  23. package/dist/engine/group-index.js +287 -321
  24. package/dist/engine/index.js +23 -12
  25. package/dist/engine/join-index.js +177 -186
  26. package/dist/engine/keyed-live.js +168 -173
  27. package/dist/engine/live-api.js +0 -1
  28. package/dist/engine/live-equal.js +32 -37
  29. package/dist/engine/live.js +603 -647
  30. package/dist/engine/memory.js +109 -111
  31. package/dist/engine/optimizer.d.ts +19 -1
  32. package/dist/engine/optimizer.js +2642 -2515
  33. package/dist/engine/point-read.d.ts +4 -2
  34. package/dist/engine/point-read.js +121 -171
  35. package/dist/engine/query-api.js +12 -3
  36. package/dist/engine/query-cache.js +61 -69
  37. package/dist/engine/query.d.ts +45 -0
  38. package/dist/engine/query.js +8506 -9375
  39. package/dist/engine/result-wire.js +221 -252
  40. package/dist/engine/schema-wire.js +97 -112
  41. package/dist/engine/schema.js +1099 -1254
  42. package/dist/engine/sort-keys.js +315 -389
  43. package/dist/engine/sql-domains.d.ts +7 -1
  44. package/dist/engine/sql-domains.js +648 -734
  45. package/dist/engine/sql-driver.js +0 -1
  46. package/dist/engine/sql-functions.d.ts +11 -0
  47. package/dist/engine/sql-functions.js +1092 -0
  48. package/dist/engine/sql-json.js +199 -218
  49. package/dist/engine/sql-semantics.d.ts +25 -4
  50. package/dist/engine/sql-semantics.js +579 -513
  51. package/dist/engine/typed-live.js +271 -293
  52. package/dist/engine/vector.d.ts +7 -1
  53. package/dist/engine/vector.js +4513 -4580
  54. package/dist/engine/worker-host.d.ts +8 -63
  55. package/dist/engine/worker-host.js +28 -1130
  56. package/dist/engine/worker-indexeddb.d.ts +1 -0
  57. package/dist/engine/worker-indexeddb.js +3 -0
  58. package/dist/engine/worker-memory.d.ts +1 -0
  59. package/dist/engine/worker-memory.js +3 -0
  60. package/dist/engine/worker-opfs.d.ts +1 -0
  61. package/dist/engine/worker-opfs.js +3 -0
  62. package/dist/engine/worker-server.d.ts +93 -0
  63. package/dist/engine/worker-server.js +999 -0
  64. package/dist/engine/worker-store-indexeddb.d.ts +2 -0
  65. package/dist/engine/worker-store-indexeddb.js +11 -0
  66. package/dist/engine/worker-store-memory.d.ts +2 -0
  67. package/dist/engine/worker-store-memory.js +6 -0
  68. package/dist/engine/worker-store-opfs.d.ts +2 -0
  69. package/dist/engine/worker-store-opfs.js +9 -0
  70. package/dist/engine/worker.js +0 -12
  71. package/dist/engine/write-block-planner.js +93 -100
  72. package/dist/plan/index.js +15 -16
  73. package/dist/plan/model.d.ts +9 -1
  74. package/dist/plan/model.js +20 -27
  75. package/dist/storage/index.js +0 -12
  76. package/dist/storage/indexeddb.js +11776 -13171
  77. package/dist/storage/memory.js +1198 -1241
  78. package/dist/storage/opfs/files.js +238 -295
  79. package/dist/storage/opfs/index.js +9 -3
  80. package/dist/storage/opfs/leader.js +4386 -4856
  81. package/dist/storage/opfs/rpc.js +223 -259
  82. package/dist/storage/opfs/snapshot-ledger.js +219 -260
  83. package/dist/storage/opfs/store.js +884 -1003
  84. package/dist/storage/persistence.js +59 -74
  85. package/dist/storage/snapshot-stream.js +788 -826
  86. package/dist/storage/snapshot.js +36 -37
  87. package/dist/storage/toolkit/extents.js +444 -520
  88. package/dist/storage/toolkit/index.js +28 -29
  89. package/dist/storage/toolkit/record-core.js +5792 -6377
  90. package/dist/storage/toolkit/sync-file.js +34 -42
  91. package/dist/storage/toolkit/wal.js +92 -127
  92. package/dist/storage/toolkit/wire.js +232 -256
  93. package/dist/storage/types.js +2952 -3228
  94. package/dist/testing/block-store-conformance.js +1556 -1629
  95. package/dist/testing/index.js +310 -291
  96. package/dist/testing/opfs-shim.js +280 -312
  97. package/dist/testing/simulator.js +496 -534
  98. package/dist/testing/sqllogictest.js +425 -365
  99. package/dist/transactions/index.js +1464 -1712
  100. package/dist/worker-protocol/index.js +70 -69
  101. package/package.json +20 -2
  102. package/postgres-feature-profile.json +15 -5
  103. package/sql-feature-matrix.json +252 -2
@@ -1,1329 +1,1174 @@
1
1
  import { copyDate, dateMilliseconds } from "../date-value.js";
2
- import { validateColumnDefault, validateEnumValues, validateSqlDomain, } from "../storage/types.js";
3
- import { childExpressions, compileCheckExpression, expressionColumns, hasAggregate, validateDefaultExpression, } from "./query.js";
2
+ import { validateColumnDefault, validateEnumValues, validateSqlDomain } from "../storage/types.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",
5
+ const volatileGeneratedFunctions = /* @__PURE__ */ new Set([
6
+ "CURRENT_DATE",
7
+ "CURRENT_TIMESTAMP",
8
+ "LOCALTIME",
9
+ "RANDOM",
10
+ "GEN_RANDOM_UUID",
11
+ "NEXTVAL",
12
+ "CURRVAL"
13
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
- }
14
+ function compileGeneratedColumnExpression(tableName, columnName, sql, columns) {
15
+ if (sql.length === 0 || sql.trim() !== sql) {
16
+ throw new TypeError(`Generated SQL must be a trimmed non-empty expression: ${tableName}.${columnName}`);
17
+ }
18
+ const expression = compileCheckExpression(sql, `generated ${tableName}.${columnName}`);
19
+ if (hasAggregate(expression)) {
20
+ throw new TypeError(`Generated column ${tableName}.${columnName} must be an immutable expression over sibling columns`);
21
+ }
22
+ const inspect = (node) => {
23
+ if (node.kind === "subquery" || node.kind === "exists") {
24
+ throw new TypeError(`Generated column ${tableName}.${columnName} must be an immutable expression over sibling columns`);
47
25
  }
48
- return expression;
49
- }
50
- /**
51
- * The constraint name `migrate()` gives a declared relation. It matches the name the SQL parser
52
- * derives for an unnamed inline REFERENCES, so a table built either way has the same catalog.
53
- */
54
- export function foreignKeyName(tableName, columnName) {
55
- return `${tableName}_${columnName}_fkey`;
56
- }
57
- function defaultSpecFromArg(type, value, sqlDomain) {
58
- if (sqlDomain?.kind === "numeric") {
59
- if ((typeof value !== "number" || !Number.isFinite(value)) && typeof value !== "string") {
60
- throw new TypeError("NUMERIC default must be a finite number or decimal string");
61
- }
62
- normalizeSqlDomainValue(sqlDomain, value);
63
- return { kind: "literal", value };
26
+ if (node.kind === "call" && volatileGeneratedFunctions.has(node.name)) {
27
+ throw new TypeError(`Generated column ${tableName}.${columnName} cannot call volatile function ${node.name}`);
64
28
  }
65
- switch (type) {
66
- case "datetime":
67
- if (!(value instanceof Date) || !Number.isFinite(dateMilliseconds(value))) {
68
- throw new TypeError("Default literal must be a valid Date");
69
- }
70
- return { kind: "literal", value: copyDate(value) };
71
- case "string":
72
- if (typeof value !== "string")
73
- throw new TypeError("Default literal must be a string");
74
- return { kind: "literal", value };
75
- case "number":
76
- if (typeof value !== "number" || !Number.isFinite(value)) {
77
- throw new TypeError("Default literal must be a finite number");
78
- }
79
- return { kind: "literal", value };
80
- case "boolean":
81
- if (typeof value !== "boolean")
82
- throw new TypeError("Default literal must be a boolean");
83
- return { kind: "literal", value };
29
+ childExpressions(node).forEach(inspect);
30
+ };
31
+ inspect(expression);
32
+ for (const reference of expressionColumns(expression)) {
33
+ const pieces = reference.split(".");
34
+ const referencedName = pieces.at(-1) ?? reference;
35
+ const qualifier = pieces.length > 1 ? pieces.slice(0, -1).join(".") : void 0;
36
+ if (qualifier !== void 0 && qualifier !== tableName) {
37
+ throw new TypeError(`Generated column ${tableName}.${columnName} references another table: ${reference}`);
38
+ }
39
+ const referenced = columns.find(({ name }) => name === referencedName);
40
+ if (referenced === void 0) {
41
+ throw new TypeError(`Generated column ${tableName}.${columnName} names an unknown column: ${referencedName}`);
84
42
  }
43
+ if (referencedName === columnName || referenced.generatedValue !== void 0) {
44
+ throw new TypeError(`Generated column ${tableName}.${columnName} cannot reference a generated column: ${referencedName}`);
45
+ }
46
+ }
47
+ return expression;
85
48
  }
86
- function createColumn(type, state = {}) {
87
- const base = {
88
- kind: "column",
89
- type,
90
- isNullable: (state.isNullable ?? false),
91
- isUnique: (state.isUnique ?? false),
92
- hasDefault: (state.defaultSpec !== undefined),
93
- isGenerated: (state.generatedSpec !== undefined),
94
- integer: state.integer ?? false,
95
- ...(state.sqlDomain === undefined ? {} : { sqlDomain: state.sqlDomain }),
96
- ...(state.defaultSpec === undefined ? {} : { defaultSpec: state.defaultSpec }),
97
- ...(state.generatedSpec === undefined ? {} : { generatedSpec: state.generatedSpec }),
98
- ...(state.renamedFromName === undefined ? {} : { renamedFromName: state.renamedFromName }),
99
- ...(state.reference === undefined ? {} : { reference: state.reference }),
100
- ...(state.enumValues === undefined ? {} : { enumValues: state.enumValues }),
101
- ...(state.backfillValue === undefined ? {} : { backfillValue: state.backfillValue }),
102
- };
103
- return {
104
- ...base,
105
- nullable: () => createColumn(type, {
106
- ...state,
107
- isNullable: true,
108
- }),
109
- unique: () => createColumn(type, {
110
- ...state,
111
- isUnique: true,
112
- }),
113
- renamedFrom: (name) => {
114
- validateSchemaName(name, "Rename source");
115
- return createColumn(type, {
116
- ...state,
117
- renamedFromName: name,
118
- });
119
- },
120
- backfill: (value) => createColumn(type, {
121
- ...state,
122
- backfillValue: value,
123
- }),
124
- references: (table, referencedColumn, options = {}) => {
125
- validateSchemaName(table, "Referenced table");
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
- }
130
- return createColumn(type, {
131
- ...state,
132
- reference: {
133
- table,
134
- column: referencedColumn,
135
- onDelete: options.onDelete ?? "restrict",
136
- enforced: options.enforced !== false,
137
- },
138
- });
139
- },
140
- default: ((value) => {
141
- return createColumn(type, {
142
- ...state,
143
- defaultSpec: defaultSpecFromArg(type, value, state.sqlDomain),
144
- });
145
- }),
146
- defaultSql: ((sql) => {
147
- const expression = sql.trim();
148
- if (expression.length === 0 || expression !== sql) {
149
- throw new TypeError("Default SQL must be a trimmed non-empty expression");
150
- }
151
- return createColumn(type, {
152
- ...state,
153
- defaultSpec: { kind: "expression", sql: expression },
154
- });
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
- },
169
- autoIncrement: (() => {
170
- if (type !== "number") {
171
- throw new TypeError("Auto-increment requires a number column");
172
- }
173
- return createColumn(type, {
174
- ...state,
175
- defaultSpec: { kind: "autoincrement" },
176
- });
177
- }),
178
- };
49
+ function foreignKeyName(tableName, columnName) {
50
+ return `${tableName}_${columnName}_fkey`;
179
51
  }
180
- function validateSchemaName(name, kind) {
181
- if (name.length === 0)
182
- throw new TypeError(`${kind} name cannot be empty`);
183
- if (name.trim() !== name) {
184
- throw new TypeError(`${kind} name cannot start or end with whitespace: ${JSON.stringify(name)}`);
52
+ function defaultSpecFromArg(type, value, sqlDomain) {
53
+ if (sqlDomain?.kind === "numeric") {
54
+ if ((typeof value !== "number" || !Number.isFinite(value)) && typeof value !== "string") {
55
+ throw new TypeError("NUMERIC default must be a finite number or decimal string");
185
56
  }
57
+ normalizeSqlDomainValue(sqlDomain, value);
58
+ return { kind: "literal", value };
59
+ }
60
+ switch (type) {
61
+ case "datetime":
62
+ if (!(value instanceof Date) || !Number.isFinite(dateMilliseconds(value))) {
63
+ throw new TypeError("Default literal must be a valid Date");
64
+ }
65
+ return { kind: "literal", value: copyDate(value) };
66
+ case "string":
67
+ if (typeof value !== "string")
68
+ throw new TypeError("Default literal must be a string");
69
+ return { kind: "literal", value };
70
+ case "number":
71
+ if (typeof value !== "number" || !Number.isFinite(value)) {
72
+ throw new TypeError("Default literal must be a finite number");
73
+ }
74
+ return { kind: "literal", value };
75
+ case "boolean":
76
+ if (typeof value !== "boolean")
77
+ throw new TypeError("Default literal must be a boolean");
78
+ return { kind: "literal", value };
79
+ }
186
80
  }
187
- /** Rebuilds a fluent column from structured-clone-safe metadata. */
188
- export function columnFromState(state) {
189
- return createColumn(state.type, {
190
- isNullable: state.isNullable,
191
- isUnique: state.isUnique,
192
- integer: state.integer ?? false,
193
- ...(state.sqlDomain === undefined ? {} : { sqlDomain: state.sqlDomain }),
194
- ...(state.renamedFromName === undefined ? {} : { renamedFromName: state.renamedFromName }),
195
- ...(state.reference === undefined ? {} : { reference: state.reference }),
196
- ...(state.defaultSpec === undefined ? {} : { defaultSpec: state.defaultSpec }),
197
- ...(state.generatedSpec === undefined ? {} : { generatedSpec: state.generatedSpec }),
198
- ...(state.enumValues === undefined ? {} : { enumValues: state.enumValues }),
199
- ...(state.backfillValue === undefined ? {} : { backfillValue: state.backfillValue }),
200
- });
201
- }
202
- export const column = {
203
- boolean: () => createColumn("boolean"),
204
- number: () => createColumn("number"),
205
- /** Exact safe-integer semantics, matching SQL INTEGER/SMALLINT/BIGINT. */
206
- integer: () => createColumn("number", { integer: true }),
207
- string: () => createColumn("string"),
208
- datetime: () => createColumn("datetime"),
209
- /** Exact decimal SQL NUMERIC. Selects return strings; writes accept strings or numbers. */
210
- numeric: (options = {}) => {
211
- const sqlDomain = validateSqlDomain({ kind: "numeric", ...options }, "numeric column");
212
- return createColumn("string", { sqlDomain });
213
- },
214
- /**
215
- * JSON text at the JavaScript boundary. Optionally declare the document's shape —
216
- * `column.json<{ name: string }>()` — for adapters to type `->`/`->>` traversal; the runtime
217
- * value stays JSON text either way.
218
- */
219
- json: () => createColumn("string", {
220
- sqlDomain: { kind: "json" },
81
+ function createColumn(type, state = {}) {
82
+ const base = {
83
+ kind: "column",
84
+ type,
85
+ isNullable: state.isNullable ?? false,
86
+ isUnique: state.isUnique ?? false,
87
+ hasDefault: state.defaultSpec !== void 0,
88
+ isGenerated: state.generatedSpec !== void 0,
89
+ integer: state.integer ?? false,
90
+ ...state.sqlDomain === void 0 ? {} : { sqlDomain: state.sqlDomain },
91
+ ...state.defaultSpec === void 0 ? {} : { defaultSpec: state.defaultSpec },
92
+ ...state.generatedSpec === void 0 ? {} : { generatedSpec: state.generatedSpec },
93
+ ...state.renamedFromName === void 0 ? {} : { renamedFromName: state.renamedFromName },
94
+ ...state.reference === void 0 ? {} : { reference: state.reference },
95
+ ...state.enumValues === void 0 ? {} : { enumValues: state.enumValues },
96
+ ...state.backfillValue === void 0 ? {} : { backfillValue: state.backfillValue }
97
+ };
98
+ return {
99
+ ...base,
100
+ nullable: () => createColumn(type, {
101
+ ...state,
102
+ isNullable: true
221
103
  }),
222
- jsonb: () => createColumn("string", {
223
- sqlDomain: { kind: "jsonb" },
104
+ unique: () => createColumn(type, {
105
+ ...state,
106
+ isUnique: true
107
+ }),
108
+ renamedFrom: (name) => {
109
+ validateSchemaName(name, "Rename source");
110
+ return createColumn(type, {
111
+ ...state,
112
+ renamedFromName: name
113
+ });
114
+ },
115
+ backfill: (value) => createColumn(type, {
116
+ ...state,
117
+ backfillValue: value
224
118
  }),
225
- uuid: () => createColumn("string", { sqlDomain: { kind: "uuid" } }),
226
- /** A zoneless calendar date, represented publicly as canonical `YYYY-MM-DD` text. */
227
- date: () => createColumn("string", { sqlDomain: { kind: "date" } }),
228
- time: () => createColumn("string", { sqlDomain: { kind: "time" } }),
229
- interval: () => createColumn("string", {
230
- sqlDomain: { kind: "interval" },
119
+ references: (table2, referencedColumn, options = {}) => {
120
+ validateSchemaName(table2, "Referenced table");
121
+ validateSchemaName(referencedColumn, "Referenced column");
122
+ if (options.enforced === false && options.onDelete !== void 0) {
123
+ throw new TypeError("An informational FOREIGN KEY cannot declare ON DELETE behavior");
124
+ }
125
+ return createColumn(type, {
126
+ ...state,
127
+ reference: {
128
+ table: table2,
129
+ column: referencedColumn,
130
+ onDelete: options.onDelete ?? "restrict",
131
+ enforced: options.enforced !== false
132
+ }
133
+ });
134
+ },
135
+ default: ((value) => {
136
+ return createColumn(type, {
137
+ ...state,
138
+ defaultSpec: defaultSpecFromArg(type, value, state.sqlDomain)
139
+ });
231
140
  }),
232
- /** JSON array text at the JavaScript boundary, with the SQL element type retained in metadata. */
233
- array: (element) => createColumn("string", {
234
- sqlDomain: validateSqlDomain({ kind: "array", element }, "array column"),
141
+ defaultSql: ((sql) => {
142
+ const expression = sql.trim();
143
+ if (expression.length === 0 || expression !== sql) {
144
+ throw new TypeError("Default SQL must be a trimmed non-empty expression");
145
+ }
146
+ return createColumn(type, {
147
+ ...state,
148
+ defaultSpec: { kind: "expression", sql: expression }
149
+ });
235
150
  }),
236
- /** A named SQL enum domain, distinct from the lightweight `column.enum()` restriction. */
237
- sqlEnum: (name, values) => {
238
- validateSchemaName(name, "Enum type");
239
- return createColumn("string", {
240
- sqlDomain: validateSqlDomain({ kind: "enum", name, values: validateEnumValues(values, name) }, name),
241
- });
151
+ generatedSql: (sql) => {
152
+ const expression = sql.trim();
153
+ if (expression.length === 0 || expression !== sql) {
154
+ throw new TypeError("Generated SQL must be a trimmed non-empty expression");
155
+ }
156
+ if (state.defaultSpec !== void 0) {
157
+ throw new TypeError("A generated column cannot also have a default");
158
+ }
159
+ return createColumn(type, {
160
+ ...state,
161
+ generatedSpec: { kind: "stored", sql: expression }
162
+ });
242
163
  },
243
- /**
244
- * A string column restricted to a closed set of values, typed as their literal union:
245
- *
246
- * ```ts
247
- * status: column.enum(["draft", "published", "archived"]).default("draft")
248
- * ```
249
- *
250
- * Selects return `"draft" | "published" | "archived"` and inserts accept nothing else; every
251
- * write path also validates membership at runtime. Physically the column is a plain string
252
- * column, so migrations may add values or relax the column to `column.string()`, but never
253
- * remove values — existing rows could already hold them.
254
- */
255
- enum: (values) => createColumn("string", { enumValues: validateEnumValues(values, "enum column") }),
164
+ autoIncrement: (() => {
165
+ if (type !== "number") {
166
+ throw new TypeError("Auto-increment requires a number column");
167
+ }
168
+ return createColumn(type, {
169
+ ...state,
170
+ defaultSpec: { kind: "autoincrement" }
171
+ });
172
+ })
173
+ };
174
+ }
175
+ function validateSchemaName(name, kind) {
176
+ if (name.length === 0)
177
+ throw new TypeError(`${kind} name cannot be empty`);
178
+ if (name.trim() !== name) {
179
+ throw new TypeError(`${kind} name cannot start or end with whitespace: ${JSON.stringify(name)}`);
180
+ }
181
+ }
182
+ function columnFromState(state) {
183
+ return createColumn(state.type, {
184
+ isNullable: state.isNullable,
185
+ isUnique: state.isUnique,
186
+ integer: state.integer ?? false,
187
+ ...state.sqlDomain === void 0 ? {} : { sqlDomain: state.sqlDomain },
188
+ ...state.renamedFromName === void 0 ? {} : { renamedFromName: state.renamedFromName },
189
+ ...state.reference === void 0 ? {} : { reference: state.reference },
190
+ ...state.defaultSpec === void 0 ? {} : { defaultSpec: state.defaultSpec },
191
+ ...state.generatedSpec === void 0 ? {} : { generatedSpec: state.generatedSpec },
192
+ ...state.enumValues === void 0 ? {} : { enumValues: state.enumValues },
193
+ ...state.backfillValue === void 0 ? {} : { backfillValue: state.backfillValue }
194
+ });
195
+ }
196
+ const column = {
197
+ boolean: () => createColumn("boolean"),
198
+ number: () => createColumn("number"),
199
+ integer: () => createColumn("number", { integer: true }),
200
+ string: () => createColumn("string"),
201
+ datetime: () => createColumn("datetime"),
202
+ numeric: (options = {}) => {
203
+ const sqlDomain = validateSqlDomain({ kind: "numeric", ...options }, "numeric column");
204
+ return createColumn("string", { sqlDomain });
205
+ },
206
+ json: () => createColumn("string", {
207
+ sqlDomain: { kind: "json" }
208
+ }),
209
+ jsonb: () => createColumn("string", {
210
+ sqlDomain: { kind: "jsonb" }
211
+ }),
212
+ uuid: () => createColumn("string", { sqlDomain: { kind: "uuid" } }),
213
+ date: () => createColumn("string", { sqlDomain: { kind: "date" } }),
214
+ time: () => createColumn("string", { sqlDomain: { kind: "time" } }),
215
+ interval: () => createColumn("string", {
216
+ sqlDomain: { kind: "interval" }
217
+ }),
218
+ array: (element) => createColumn("string", {
219
+ sqlDomain: validateSqlDomain({ kind: "array", element }, "array column")
220
+ }),
221
+ sqlEnum: (name, values) => {
222
+ validateSchemaName(name, "Enum type");
223
+ return createColumn("string", {
224
+ sqlDomain: validateSqlDomain({ kind: "enum", name, values: validateEnumValues(values, name) }, name)
225
+ });
226
+ },
227
+ enum: (values) => createColumn("string", { enumValues: validateEnumValues(values, "enum column") })
256
228
  };
257
229
  function validateDeclaredColumnValue(definition, value, context, label) {
258
- if (definition.sqlDomain !== undefined) {
259
- try {
260
- normalizeSqlDomainValue(definition.sqlDomain, value);
261
- }
262
- catch (error) {
263
- throw new TypeError(`${label} does not fit ${context}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
264
- }
265
- return;
266
- }
267
- const matches = definition.type === "datetime"
268
- ? value instanceof Date && Number.isFinite(dateMilliseconds(value))
269
- : definition.type === "number"
270
- ? typeof value === "number" &&
271
- Number.isFinite(value) &&
272
- (!definition.integer || Number.isSafeInteger(value))
273
- : typeof value === definition.type;
274
- if (!matches) {
275
- const expected = definition.integer ? "safe integer" : definition.type;
276
- throw new TypeError(`${label} value must be a ${expected}: ${context}`);
277
- }
278
- if (definition.enumValues !== undefined &&
279
- typeof value === "string" &&
280
- !definition.enumValues.includes(value)) {
281
- throw new TypeError(`${label} value must be one of: ${definition.enumValues.join(", ")} (${context})`);
230
+ if (definition.sqlDomain !== void 0) {
231
+ try {
232
+ normalizeSqlDomainValue(definition.sqlDomain, value);
233
+ } catch (error) {
234
+ throw new TypeError(`${label} does not fit ${context}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
282
235
  }
236
+ return;
237
+ }
238
+ const matches = definition.type === "datetime" ? value instanceof Date && Number.isFinite(dateMilliseconds(value)) : definition.type === "number" ? typeof value === "number" && Number.isFinite(value) && (!definition.integer || Number.isSafeInteger(value)) : typeof value === definition.type;
239
+ if (!matches) {
240
+ const expected = definition.integer ? "safe integer" : definition.type;
241
+ throw new TypeError(`${label} value must be a ${expected}: ${context}`);
242
+ }
243
+ if (definition.enumValues !== void 0 && typeof value === "string" && !definition.enumValues.includes(value)) {
244
+ throw new TypeError(`${label} value must be one of: ${definition.enumValues.join(", ")} (${context})`);
245
+ }
283
246
  }
284
- export function table(name, columns, options = {}) {
285
- validateSchemaName(name, "Table");
286
- const entries = Object.entries(columns);
287
- if (entries.length === 0)
288
- throw new TypeError(`Table ${name} needs at least one column`);
289
- const uniqueColumns = entries.filter(([, definition]) => definition.isUnique);
290
- if (uniqueColumns.length > 1) {
291
- throw new TypeError(`Table ${name} may name at most one unique column`);
292
- }
293
- const uniqueEntry = uniqueColumns[0];
294
- const primaryKey = options.primaryKey ?? [];
295
- if (uniqueEntry !== undefined && primaryKey.length > 0) {
296
- throw new TypeError(`Table ${name} cannot declare both .unique() and a table primary key`);
297
- }
298
- if (primaryKey.length === 0 && options.primaryKey !== undefined) {
299
- throw new TypeError(`Table ${name} primary key needs at least one column`);
300
- }
301
- if (new Set(primaryKey).size !== primaryKey.length) {
302
- throw new TypeError(`Table ${name} primary key columns must be distinct`);
303
- }
304
- for (const columnName of primaryKey) {
305
- const definition = columns[columnName];
306
- if (definition === undefined) {
307
- throw new TypeError(`PRIMARY KEY column not found: ${name}.${columnName}`);
308
- }
309
- if (definition.isNullable) {
310
- throw new TypeError(`PRIMARY KEY cannot be nullable: ${name}.${columnName}`);
311
- }
247
+ function table(name, columns, options = {}) {
248
+ validateSchemaName(name, "Table");
249
+ const entries = Object.entries(columns);
250
+ if (entries.length === 0)
251
+ throw new TypeError(`Table ${name} needs at least one column`);
252
+ const uniqueColumns = entries.filter(([, definition]) => definition.isUnique);
253
+ if (uniqueColumns.length > 1) {
254
+ throw new TypeError(`Table ${name} may name at most one unique column`);
255
+ }
256
+ const uniqueEntry = uniqueColumns[0];
257
+ const primaryKey = options.primaryKey ?? [];
258
+ if (uniqueEntry !== void 0 && primaryKey.length > 0) {
259
+ throw new TypeError(`Table ${name} cannot declare both .unique() and a table primary key`);
260
+ }
261
+ if (primaryKey.length === 0 && options.primaryKey !== void 0) {
262
+ throw new TypeError(`Table ${name} primary key needs at least one column`);
263
+ }
264
+ if (new Set(primaryKey).size !== primaryKey.length) {
265
+ throw new TypeError(`Table ${name} primary key columns must be distinct`);
266
+ }
267
+ for (const columnName of primaryKey) {
268
+ const definition = columns[columnName];
269
+ if (definition === void 0) {
270
+ throw new TypeError(`PRIMARY KEY column not found: ${name}.${columnName}`);
312
271
  }
313
- if (uniqueEntry?.[1].isNullable === true) {
314
- throw new TypeError(`Table ${name} unique column must not be nullable: ${uniqueEntry[0]}`);
315
- }
316
- const generatedColumns = entries.map(([columnName, definition]) => ({
317
- name: columnName,
318
- ...(definition.generatedSpec === undefined ? {} : { generatedValue: definition.generatedSpec }),
319
- }));
320
- for (const [columnName, definition] of entries) {
321
- validateSchemaName(columnName, "Column");
322
- if (definition.integer && definition.type !== "number") {
323
- throw new TypeError(`Integer domain requires a number column: ${name}.${columnName}`);
324
- }
325
- if (definition.sqlDomain !== undefined) {
326
- if (definition.type !== "string") {
327
- throw new TypeError(`SQL domains require string storage: ${name}.${columnName}`);
328
- }
329
- validateSqlDomain(definition.sqlDomain, `${name}.${columnName}`);
330
- }
331
- if (definition.integer && definition.sqlDomain !== undefined) {
332
- throw new TypeError(`A column cannot be both integer and a SQL domain: ${name}.${columnName}`);
333
- }
334
- if (definition.enumValues !== undefined && definition.sqlDomain !== undefined) {
335
- throw new TypeError(`A column cannot have both enum restrictions and a SQL domain: ${name}.${columnName}`);
336
- }
337
- const backfill = definition.backfillValue;
338
- if (backfill !== undefined && typeof backfill !== "function") {
339
- validateDeclaredColumnValue(definition, backfill, `${name}.${columnName}`, "Backfill");
340
- }
341
- if (backfill !== undefined && definition.isNullable) {
342
- throw new TypeError(`A nullable column needs no backfill: ${name}.${columnName}. Rows without it already read NULL.`);
343
- }
344
- if (definition.generatedSpec !== undefined) {
345
- if (definition.defaultSpec !== undefined || backfill !== undefined) {
346
- throw new TypeError(`A generated column cannot also have a default or backfill: ${name}.${columnName}`);
272
+ if (definition.isNullable) {
273
+ throw new TypeError(`PRIMARY KEY cannot be nullable: ${name}.${columnName}`);
274
+ }
275
+ }
276
+ if (uniqueEntry?.[1].isNullable === true) {
277
+ throw new TypeError(`Table ${name} unique column must not be nullable: ${uniqueEntry[0]}`);
278
+ }
279
+ const generatedColumns = entries.map(([columnName, definition]) => ({
280
+ name: columnName,
281
+ ...definition.generatedSpec === void 0 ? {} : { generatedValue: definition.generatedSpec }
282
+ }));
283
+ for (const [columnName, definition] of entries) {
284
+ validateSchemaName(columnName, "Column");
285
+ if (definition.integer && definition.type !== "number") {
286
+ throw new TypeError(`Integer domain requires a number column: ${name}.${columnName}`);
287
+ }
288
+ if (definition.sqlDomain !== void 0) {
289
+ if (definition.type !== "string") {
290
+ throw new TypeError(`SQL domains require string storage: ${name}.${columnName}`);
291
+ }
292
+ validateSqlDomain(definition.sqlDomain, `${name}.${columnName}`);
293
+ }
294
+ if (definition.integer && definition.sqlDomain !== void 0) {
295
+ throw new TypeError(`A column cannot be both integer and a SQL domain: ${name}.${columnName}`);
296
+ }
297
+ if (definition.enumValues !== void 0 && definition.sqlDomain !== void 0) {
298
+ throw new TypeError(`A column cannot have both enum restrictions and a SQL domain: ${name}.${columnName}`);
299
+ }
300
+ const backfill = definition.backfillValue;
301
+ if (backfill !== void 0 && typeof backfill !== "function") {
302
+ validateDeclaredColumnValue(definition, backfill, `${name}.${columnName}`, "Backfill");
303
+ }
304
+ if (backfill !== void 0 && definition.isNullable) {
305
+ throw new TypeError(`A nullable column needs no backfill: ${name}.${columnName}. Rows without it already read NULL.`);
306
+ }
307
+ if (definition.generatedSpec !== void 0) {
308
+ if (definition.defaultSpec !== void 0 || backfill !== void 0) {
309
+ throw new TypeError(`A generated column cannot also have a default or backfill: ${name}.${columnName}`);
310
+ }
311
+ compileGeneratedColumnExpression(name, columnName, definition.generatedSpec.sql, generatedColumns);
312
+ if (definition.isUnique || primaryKey.includes(columnName)) {
313
+ throw new TypeError(`Generated columns cannot be row-addressing keys: ${name}.${columnName}`);
314
+ }
315
+ }
316
+ if (definition.reference?.onDelete === "set null" && !definition.isNullable) {
317
+ throw new TypeError(`ON DELETE SET NULL requires a nullable column: ${name}.${columnName}`);
318
+ }
319
+ const spec = definition.defaultSpec;
320
+ if (spec === void 0)
321
+ continue;
322
+ validateColumnDefault({
323
+ name: columnName,
324
+ type: definition.type,
325
+ ...definition.integer ? { integer: true } : {},
326
+ ...definition.sqlDomain === void 0 ? {} : { sqlDomain: definition.sqlDomain },
327
+ nullable: definition.isNullable,
328
+ isUniqueKey: definition.isUnique || primaryKey.length === 1 && primaryKey[0] === columnName,
329
+ ...definition.enumValues === void 0 ? {} : { enumValues: definition.enumValues }
330
+ }, spec);
331
+ if (spec.kind === "expression") {
332
+ validateDefaultExpression(spec.sql, {
333
+ name: `${name}.${columnName}`,
334
+ type: definition.type,
335
+ ...definition.sqlDomain === void 0 ? {} : { sqlDomain: definition.sqlDomain }
336
+ });
337
+ }
338
+ if (spec.kind === "literal" && definition.sqlDomain !== void 0) {
339
+ normalizeSqlDomainValue(definition.sqlDomain, spec.value);
340
+ }
341
+ }
342
+ const foreignKeys = options.foreignKeys ?? [];
343
+ const constraintNames = /* @__PURE__ */ new Set();
344
+ const foreignKeyNames = /* @__PURE__ */ new Set();
345
+ for (const key of foreignKeys) {
346
+ validateSchemaName(key.name, "FOREIGN KEY");
347
+ if (key.enforced === false && key.onDelete !== void 0) {
348
+ throw new TypeError(`Informational FOREIGN KEY ${key.name} cannot declare ON DELETE behavior`);
349
+ }
350
+ if (foreignKeyNames.has(key.name)) {
351
+ throw new TypeError(`Duplicate FOREIGN KEY in table ${name}: ${key.name}`);
352
+ }
353
+ foreignKeyNames.add(key.name);
354
+ if (constraintNames.has(key.name)) {
355
+ throw new TypeError(`Duplicate constraint in table ${name}: ${key.name}`);
356
+ }
357
+ constraintNames.add(key.name);
358
+ if (key.columns.length === 0 || new Set(key.columns).size !== key.columns.length) {
359
+ throw new TypeError(`FOREIGN KEY ${key.name} needs distinct child columns`);
360
+ }
361
+ if (key.references.columns.length === 0 || new Set(key.references.columns).size !== key.references.columns.length) {
362
+ throw new TypeError(`FOREIGN KEY ${key.name} needs distinct parent columns`);
363
+ }
364
+ if (key.columns.length !== key.references.columns.length) {
365
+ throw new TypeError(`FOREIGN KEY ${key.name} has different child and parent arity`);
366
+ }
367
+ validateSchemaName(key.references.table, "Referenced table");
368
+ key.references.columns.forEach((columnName) => validateSchemaName(columnName, "Referenced column"));
369
+ for (const columnName of key.columns) {
370
+ const definition = columns[columnName];
371
+ if (definition === void 0) {
372
+ throw new TypeError(`FOREIGN KEY ${key.name} names an unknown column: ${name}.${columnName}`);
373
+ }
374
+ if ((key.onDelete ?? "restrict") === "set null" && !definition.isNullable) {
375
+ throw new TypeError(`FOREIGN KEY ${key.name} cannot SET NULL a NOT NULL column`);
376
+ }
377
+ }
378
+ }
379
+ const checks = options.checks ?? [];
380
+ const checkNames = /* @__PURE__ */ new Set();
381
+ for (const check of checks) {
382
+ validateSchemaName(check.name, "CHECK");
383
+ if (checkNames.has(check.name)) {
384
+ throw new TypeError(`Duplicate CHECK in table ${name}: ${check.name}`);
385
+ }
386
+ checkNames.add(check.name);
387
+ if (constraintNames.has(check.name)) {
388
+ throw new TypeError(`Duplicate constraint in table ${name}: ${check.name}`);
389
+ }
390
+ constraintNames.add(check.name);
391
+ if (check.sql.trim().length === 0) {
392
+ throw new TypeError(`CHECK ${check.name} in table ${name} has no expression`);
393
+ }
394
+ for (const reference of expressionColumns(compileCheckExpression(check.sql, check.name))) {
395
+ const pieces = reference.split(".");
396
+ const columnName = pieces.at(-1) ?? reference;
397
+ const qualifier = pieces.length > 1 ? pieces.slice(0, -1).join(".") : void 0;
398
+ if (qualifier !== void 0 && qualifier !== name) {
399
+ throw new TypeError(`CHECK ${check.name} references another table: ${reference}`);
400
+ }
401
+ if (columns[columnName] === void 0) {
402
+ throw new TypeError(`CHECK ${check.name} names an unknown column: ${columnName}`);
403
+ }
404
+ }
405
+ }
406
+ return {
407
+ kind: "table",
408
+ name,
409
+ columns,
410
+ primaryKey,
411
+ foreignKeys,
412
+ checks,
413
+ "~standard": {
414
+ version: 1,
415
+ vendor: "minnow",
416
+ validate(value) {
417
+ if (typeof value !== "object" || value === null) {
418
+ return { issues: [{ message: "A row must be an object", path: [] }] };
419
+ }
420
+ const row = value;
421
+ const issues = [];
422
+ for (const [columnName, definition] of entries) {
423
+ const columnValue = row[columnName];
424
+ if (columnValue === void 0) {
425
+ if (!definition.isNullable && definition.defaultSpec === void 0) {
426
+ issues.push({ message: `Missing non-nullable column`, path: [columnName] });
347
427
  }
348
- compileGeneratedColumnExpression(name, columnName, definition.generatedSpec.sql, generatedColumns);
349
- if (definition.isUnique || primaryKey.includes(columnName)) {
350
- throw new TypeError(`Generated columns cannot be row-addressing keys: ${name}.${columnName}`);
428
+ continue;
429
+ }
430
+ if (columnValue === null) {
431
+ if (!definition.isNullable) {
432
+ issues.push({ message: `Expected non-null value`, path: [columnName] });
351
433
  }
352
- }
353
- if (definition.reference?.onDelete === "set null" && !definition.isNullable) {
354
- throw new TypeError(`ON DELETE SET NULL requires a nullable column: ${name}.${columnName}`);
355
- }
356
- const spec = definition.defaultSpec;
357
- if (spec === undefined)
358
434
  continue;
359
- validateColumnDefault({
360
- name: columnName,
361
- type: definition.type,
362
- ...(definition.integer ? { integer: true } : {}),
363
- ...(definition.sqlDomain === undefined ? {} : { sqlDomain: definition.sqlDomain }),
364
- nullable: definition.isNullable,
365
- isUniqueKey: definition.isUnique || (primaryKey.length === 1 && primaryKey[0] === columnName),
366
- ...(definition.enumValues === undefined ? {} : { enumValues: definition.enumValues }),
367
- }, spec);
368
- if (spec.kind === "expression") {
369
- validateDefaultExpression(spec.sql, {
370
- name: `${name}.${columnName}`,
371
- type: definition.type,
372
- ...(definition.sqlDomain === undefined ? {} : { sqlDomain: definition.sqlDomain }),
435
+ }
436
+ try {
437
+ validateDeclaredColumnValue(definition, columnValue, `${name}.${columnName}`, "Value");
438
+ } catch (error) {
439
+ const message = definition.enumValues !== void 0 && typeof columnValue === "string" ? `Expected one of: ${definition.enumValues.join(", ")}` : definition.sqlDomain === void 0 ? `Expected ${definition.integer ? "safe integer" : definition.type}` : error instanceof Error ? error.message : `Invalid value`;
440
+ issues.push({
441
+ message,
442
+ path: [columnName]
373
443
  });
444
+ }
374
445
  }
375
- if (spec.kind === "literal" && definition.sqlDomain !== undefined) {
376
- normalizeSqlDomainValue(definition.sqlDomain, spec.value);
377
- }
378
- }
379
- const foreignKeys = options.foreignKeys ?? [];
380
- const constraintNames = new Set();
381
- const foreignKeyNames = new Set();
382
- for (const key of foreignKeys) {
383
- validateSchemaName(key.name, "FOREIGN KEY");
384
- if (key.enforced === false && key.onDelete !== undefined) {
385
- throw new TypeError(`Informational FOREIGN KEY ${key.name} cannot declare ON DELETE behavior`);
386
- }
387
- if (foreignKeyNames.has(key.name)) {
388
- throw new TypeError(`Duplicate FOREIGN KEY in table ${name}: ${key.name}`);
389
- }
390
- foreignKeyNames.add(key.name);
391
- if (constraintNames.has(key.name)) {
392
- throw new TypeError(`Duplicate constraint in table ${name}: ${key.name}`);
393
- }
394
- constraintNames.add(key.name);
395
- if (key.columns.length === 0 || new Set(key.columns).size !== key.columns.length) {
396
- throw new TypeError(`FOREIGN KEY ${key.name} needs distinct child columns`);
397
- }
398
- if (key.references.columns.length === 0 ||
399
- new Set(key.references.columns).size !== key.references.columns.length) {
400
- throw new TypeError(`FOREIGN KEY ${key.name} needs distinct parent columns`);
401
- }
402
- if (key.columns.length !== key.references.columns.length) {
403
- throw new TypeError(`FOREIGN KEY ${key.name} has different child and parent arity`);
404
- }
405
- validateSchemaName(key.references.table, "Referenced table");
406
- key.references.columns.forEach((columnName) => validateSchemaName(columnName, "Referenced column"));
407
- for (const columnName of key.columns) {
408
- const definition = columns[columnName];
409
- if (definition === undefined) {
410
- throw new TypeError(`FOREIGN KEY ${key.name} names an unknown column: ${name}.${columnName}`);
411
- }
412
- if ((key.onDelete ?? "restrict") === "set null" && !definition.isNullable) {
413
- throw new TypeError(`FOREIGN KEY ${key.name} cannot SET NULL a NOT NULL column`);
414
- }
415
- }
416
- }
417
- const checks = options.checks ?? [];
418
- const checkNames = new Set();
419
- for (const check of checks) {
420
- validateSchemaName(check.name, "CHECK");
421
- if (checkNames.has(check.name)) {
422
- throw new TypeError(`Duplicate CHECK in table ${name}: ${check.name}`);
423
- }
424
- checkNames.add(check.name);
425
- if (constraintNames.has(check.name)) {
426
- throw new TypeError(`Duplicate constraint in table ${name}: ${check.name}`);
427
- }
428
- constraintNames.add(check.name);
429
- if (check.sql.trim().length === 0) {
430
- throw new TypeError(`CHECK ${check.name} in table ${name} has no expression`);
431
- }
432
- for (const reference of expressionColumns(compileCheckExpression(check.sql, check.name))) {
433
- const pieces = reference.split(".");
434
- const columnName = pieces.at(-1) ?? reference;
435
- const qualifier = pieces.length > 1 ? pieces.slice(0, -1).join(".") : undefined;
436
- if (qualifier !== undefined && qualifier !== name) {
437
- throw new TypeError(`CHECK ${check.name} references another table: ${reference}`);
438
- }
439
- if (columns[columnName] === undefined) {
440
- throw new TypeError(`CHECK ${check.name} names an unknown column: ${columnName}`);
441
- }
446
+ for (const key of Object.keys(row)) {
447
+ if (!(key in columns))
448
+ issues.push({ message: "Unknown column", path: [key] });
442
449
  }
450
+ return issues.length > 0 ? { issues } : { value: row };
451
+ }
443
452
  }
444
- return {
445
- kind: "table",
446
- name,
447
- columns,
448
- primaryKey,
449
- foreignKeys,
450
- checks,
451
- "~standard": {
452
- version: 1,
453
- vendor: "minnow",
454
- validate(value) {
455
- if (typeof value !== "object" || value === null) {
456
- return { issues: [{ message: "A row must be an object", path: [] }] };
457
- }
458
- const row = value;
459
- const issues = [];
460
- for (const [columnName, definition] of entries) {
461
- const columnValue = row[columnName];
462
- if (columnValue === undefined) {
463
- if (!definition.isNullable && definition.defaultSpec === undefined) {
464
- issues.push({ message: `Missing non-nullable column`, path: [columnName] });
465
- }
466
- continue;
467
- }
468
- if (columnValue === null) {
469
- if (!definition.isNullable) {
470
- issues.push({ message: `Expected non-null value`, path: [columnName] });
471
- }
472
- continue;
473
- }
474
- try {
475
- validateDeclaredColumnValue(definition, columnValue, `${name}.${columnName}`, "Value");
476
- }
477
- catch (error) {
478
- const message = definition.enumValues !== undefined && typeof columnValue === "string"
479
- ? `Expected one of: ${definition.enumValues.join(", ")}`
480
- : definition.sqlDomain === undefined
481
- ? `Expected ${definition.integer ? "safe integer" : definition.type}`
482
- : error instanceof Error
483
- ? error.message
484
- : `Invalid value`;
485
- issues.push({
486
- message,
487
- path: [columnName],
488
- });
489
- }
490
- }
491
- for (const key of Object.keys(row)) {
492
- if (!(key in columns))
493
- issues.push({ message: "Unknown column", path: [key] });
494
- }
495
- return issues.length > 0 ? { issues } : { value: row };
496
- },
497
- },
498
- };
453
+ };
499
454
  }
500
- /**
501
- * The FOREIGN KEY constraints a table declares, in column order. This is the single derivation
502
- * of "what relations does this table have" — `planMigration` diffs against it and `migrate()`
503
- * creates from it, so the two can never disagree.
504
- */
505
- export function declaredForeignKeys(definition) {
506
- const keys = [];
507
- for (const [columnName, columnDefinition] of Object.entries(definition.columns)) {
508
- const reference = columnDefinition.reference;
509
- if (reference === undefined)
510
- continue;
511
- keys.push({
512
- name: foreignKeyName(definition.name, columnName),
513
- columns: [columnName],
514
- parentTable: reference.table,
515
- parentColumns: [reference.column],
516
- onDelete: reference.onDelete,
517
- enforced: reference.enforced,
518
- });
455
+ function declaredForeignKeys(definition) {
456
+ const keys = [];
457
+ for (const [columnName, columnDefinition] of Object.entries(definition.columns)) {
458
+ const reference = columnDefinition.reference;
459
+ if (reference === void 0)
460
+ continue;
461
+ keys.push({
462
+ name: foreignKeyName(definition.name, columnName),
463
+ columns: [columnName],
464
+ parentTable: reference.table,
465
+ parentColumns: [reference.column],
466
+ onDelete: reference.onDelete,
467
+ enforced: reference.enforced
468
+ });
469
+ }
470
+ for (const key of definition.foreignKeys) {
471
+ const childColumns = [...key.columns];
472
+ const parentColumns = [...key.references.columns];
473
+ keys.push({
474
+ name: key.name,
475
+ columns: childColumns,
476
+ parentTable: key.references.table,
477
+ parentColumns,
478
+ onDelete: key.onDelete ?? "restrict",
479
+ enforced: key.enforced !== false
480
+ });
481
+ }
482
+ return keys;
483
+ }
484
+ function view(name, definition) {
485
+ validateSchemaName(name, "View");
486
+ const entries = Object.entries(definition.columns);
487
+ if (entries.length === 0)
488
+ throw new TypeError(`View ${name} needs at least one column`);
489
+ if (definition.sql.trim().length === 0)
490
+ throw new TypeError(`View ${name} has no query`);
491
+ for (const [columnName, columnDefinition] of entries) {
492
+ validateSchemaName(columnName, "Column");
493
+ if (columnDefinition.isUnique) {
494
+ throw new TypeError(`A view column cannot be a unique key: ${name}.${columnName}`);
519
495
  }
520
- for (const key of definition.foreignKeys) {
521
- const childColumns = [...key.columns];
522
- const parentColumns = [...key.references.columns];
523
- keys.push({
524
- name: key.name,
525
- columns: childColumns,
526
- parentTable: key.references.table,
527
- parentColumns,
528
- onDelete: key.onDelete ?? "restrict",
529
- enforced: key.enforced !== false,
530
- });
496
+ if (columnDefinition.defaultSpec !== void 0) {
497
+ throw new TypeError(`A view column cannot have a default: ${name}.${columnName}`);
531
498
  }
532
- return keys;
533
- }
534
- /**
535
- * Declares a view. Its columns use the same builders tables use, so a view row types exactly like
536
- * a table row — but a view is read-only, and the typed facade will not let a write name one.
537
- *
538
- * ```ts
539
- * const activeCustomers = view("active_customers", {
540
- * sql: `SELECT customer_id, name FROM customers WHERE status = 'active'`,
541
- * columns: { customer_id: column.number(), name: column.string() },
542
- * });
543
- * ```
544
- */
545
- export function view(name, definition) {
546
- validateSchemaName(name, "View");
547
- const entries = Object.entries(definition.columns);
548
- if (entries.length === 0)
549
- throw new TypeError(`View ${name} needs at least one column`);
550
- if (definition.sql.trim().length === 0)
551
- throw new TypeError(`View ${name} has no query`);
552
- for (const [columnName, columnDefinition] of entries) {
553
- validateSchemaName(columnName, "Column");
554
- if (columnDefinition.isUnique) {
555
- throw new TypeError(`A view column cannot be a unique key: ${name}.${columnName}`);
556
- }
557
- if (columnDefinition.defaultSpec !== undefined) {
558
- throw new TypeError(`A view column cannot have a default: ${name}.${columnName}`);
559
- }
560
- if (columnDefinition.generatedSpec !== undefined) {
561
- throw new TypeError(`A view column cannot be generated: ${name}.${columnName}`);
562
- }
499
+ if (columnDefinition.generatedSpec !== void 0) {
500
+ throw new TypeError(`A view column cannot be generated: ${name}.${columnName}`);
563
501
  }
564
- return { kind: "view", name, sql: definition.sql, columns: definition.columns };
502
+ }
503
+ return { kind: "view", name, sql: definition.sql, columns: definition.columns };
565
504
  }
566
- export function schema(tables, options = {}) {
567
- const views = (options.views ?? []);
568
- const names = new Set();
569
- for (const definition of tables) {
570
- if (names.has(definition.name)) {
571
- throw new TypeError(`Duplicate table in schema: ${definition.name}`);
572
- }
573
- names.add(definition.name);
505
+ function schema(tables, options = {}) {
506
+ const views = options.views ?? [];
507
+ const names = /* @__PURE__ */ new Set();
508
+ for (const definition of tables) {
509
+ if (names.has(definition.name)) {
510
+ throw new TypeError(`Duplicate table in schema: ${definition.name}`);
574
511
  }
575
- for (const definition of views) {
576
- if (names.has(definition.name)) {
577
- throw new TypeError(`Duplicate name in schema: ${definition.name}`);
578
- }
579
- names.add(definition.name);
512
+ names.add(definition.name);
513
+ }
514
+ for (const definition of views) {
515
+ if (names.has(definition.name)) {
516
+ throw new TypeError(`Duplicate name in schema: ${definition.name}`);
580
517
  }
581
- for (const definition of tables) {
582
- const declaredNames = new Set();
583
- for (const key of declaredForeignKeys(definition)) {
584
- if (declaredNames.has(key.name)) {
585
- throw new TypeError(`Duplicate FOREIGN KEY in table ${definition.name}: ${key.name}`);
586
- }
587
- declaredNames.add(key.name);
588
- const childNames = key.columns;
589
- const parentNames = key.parentColumns;
590
- const target = tables.find(({ name }) => name === key.parentTable);
591
- if (target === undefined ||
592
- childNames.some((columnName) => !(columnName in definition.columns)) ||
593
- parentNames.some((columnName) => !(columnName in target.columns))) {
594
- throw new TypeError(`Relation target does not exist: ${definition.name}.${childNames.join(",")} -> ${key.parentTable}.${parentNames.join(",")}`);
595
- }
596
- const targetPrimary = target.primaryKey.length > 0
597
- ? [...target.primaryKey]
598
- : Object.entries(target.columns).flatMap(([name, columnDefinition]) => columnDefinition.isUnique ? [name] : []);
599
- if (parentNames.length !== targetPrimary.length ||
600
- parentNames.some((columnName, index) => columnName !== targetPrimary[index])) {
601
- throw new TypeError(`Relation target must be the unique key: ${definition.name}.${childNames.join(",")} -> ${key.parentTable}(${targetPrimary.join(", ")})`);
602
- }
603
- childNames.forEach((childName, index) => {
604
- const child = definition.columns[childName];
605
- const parentName = parentNames[index];
606
- const parent = parentName === undefined ? undefined : target.columns[parentName];
607
- if (child === undefined ||
608
- child.type !== parent?.type ||
609
- child.integer !== parent.integer ||
610
- JSON.stringify(child.sqlDomain ?? null) !== JSON.stringify(parent.sqlDomain ?? null)) {
611
- throw new TypeError(`Relation types must match: ${definition.name}.${childName} and ${key.parentTable}.${String(parentName)}`);
612
- }
613
- });
614
- }
518
+ names.add(definition.name);
519
+ }
520
+ for (const definition of tables) {
521
+ const declaredNames = /* @__PURE__ */ new Set();
522
+ for (const key of declaredForeignKeys(definition)) {
523
+ if (declaredNames.has(key.name)) {
524
+ throw new TypeError(`Duplicate FOREIGN KEY in table ${definition.name}: ${key.name}`);
525
+ }
526
+ declaredNames.add(key.name);
527
+ const childNames = key.columns;
528
+ const parentNames = key.parentColumns;
529
+ const target = tables.find(({ name }) => name === key.parentTable);
530
+ if (target === void 0 || childNames.some((columnName) => !(columnName in definition.columns)) || parentNames.some((columnName) => !(columnName in target.columns))) {
531
+ throw new TypeError(`Relation target does not exist: ${definition.name}.${childNames.join(",")} -> ${key.parentTable}.${parentNames.join(",")}`);
532
+ }
533
+ const targetPrimary = target.primaryKey.length > 0 ? [...target.primaryKey] : Object.entries(target.columns).flatMap(([name, columnDefinition]) => columnDefinition.isUnique ? [name] : []);
534
+ if (parentNames.length !== targetPrimary.length || parentNames.some((columnName, index) => columnName !== targetPrimary[index])) {
535
+ throw new TypeError(`Relation target must be the unique key: ${definition.name}.${childNames.join(",")} -> ${key.parentTable}(${targetPrimary.join(", ")})`);
536
+ }
537
+ childNames.forEach((childName, index) => {
538
+ const child = definition.columns[childName];
539
+ const parentName = parentNames[index];
540
+ const parent = parentName === void 0 ? void 0 : target.columns[parentName];
541
+ if (child === void 0 || child.type !== parent?.type || child.integer !== parent.integer || JSON.stringify(child.sqlDomain ?? null) !== JSON.stringify(parent.sqlDomain ?? null)) {
542
+ throw new TypeError(`Relation types must match: ${definition.name}.${childName} and ${key.parentTable}.${String(parentName)}`);
543
+ }
544
+ });
615
545
  }
616
- return { kind: "schema", tables, views };
546
+ }
547
+ return { kind: "schema", tables, views };
617
548
  }
618
- /**
619
- * Views are derived: nothing is stored under one, so a body change is a replace rather than a
620
- * rewrite and needs none of the proofs a table alteration needs.
621
- *
622
- * Within the set of views a migration created, the schema is authoritative — removing a
623
- * declaration drops the view. Outside it nothing is touched: a view created with `CREATE VIEW`,
624
- * or one written before ownership was recorded, is not any schema's to remove. That is the
625
- * difference between a declaration being the source of truth and a declaration being allowed to
626
- * destroy things it never made.
627
- */
628
549
  function planViewSteps(catalog, definition, steps) {
629
- const tableNames = new Set(catalog.tables.map(({ name }) => name));
630
- const existing = new Map(catalog.views.map((record) => [record.name, record]));
631
- const declared = new Set(definition.views.map(({ name }) => name));
632
- for (const viewDefinition of definition.views) {
633
- if (tableNames.has(viewDefinition.name)) {
634
- throw new TypeError(`A table already exists with this name: ${viewDefinition.name}`);
635
- }
636
- if (existing.get(viewDefinition.name)?.sql === viewDefinition.sql)
637
- continue;
638
- steps.push({ kind: "replace-view", view: viewDefinition });
639
- }
640
- for (const record of catalog.views) {
641
- if (declared.has(record.name) || !record.managed)
642
- continue;
643
- steps.push({ kind: "drop-view", viewName: record.name });
550
+ const tableNames = new Set(catalog.tables.map(({ name }) => name));
551
+ const existing = new Map(catalog.views.map((record) => [record.name, record]));
552
+ const declared = new Set(definition.views.map(({ name }) => name));
553
+ for (const viewDefinition of definition.views) {
554
+ if (tableNames.has(viewDefinition.name)) {
555
+ throw new TypeError(`A table already exists with this name: ${viewDefinition.name}`);
644
556
  }
557
+ if (existing.get(viewDefinition.name)?.sql === viewDefinition.sql)
558
+ continue;
559
+ steps.push({ kind: "replace-view", view: viewDefinition });
560
+ }
561
+ for (const record of catalog.views) {
562
+ if (declared.has(record.name) || !record.managed)
563
+ continue;
564
+ steps.push({ kind: "drop-view", viewName: record.name });
565
+ }
645
566
  }
646
- /**
647
- * Creation order, so a table is created after the tables it references. A FOREIGN KEY names a
648
- * parent that must already exist, which would otherwise make a schema fail purely because its
649
- * tables were listed child-first. Declaration order is preserved among tables that do not
650
- * constrain each other, and a reference cycle (or a self-reference, which the engine allows)
651
- * falls back to declaration order rather than failing here — the engine still has the final say.
652
- */
653
567
  function orderedForCreation(tables) {
654
- const byName = new Map(tables.map((definition) => [definition.name, definition]));
655
- const ordered = [];
656
- const state = new Map();
657
- const visit = (definition) => {
658
- const status = state.get(definition.name);
659
- if (status !== undefined)
660
- return; // done, or a cycle we decline to reorder
661
- state.set(definition.name, "visiting");
662
- for (const key of declaredForeignKeys(definition)) {
663
- const parent = byName.get(key.parentTable);
664
- if (parent !== undefined && parent !== definition)
665
- visit(parent);
666
- }
667
- state.set(definition.name, "done");
668
- ordered.push(definition);
669
- };
670
- for (const definition of tables)
671
- visit(definition);
672
- return ordered;
568
+ const byName = new Map(tables.map((definition) => [definition.name, definition]));
569
+ const ordered = [];
570
+ const state = /* @__PURE__ */ new Map();
571
+ const visit = (definition) => {
572
+ const status = state.get(definition.name);
573
+ if (status !== void 0)
574
+ return;
575
+ state.set(definition.name, "visiting");
576
+ for (const key of declaredForeignKeys(definition)) {
577
+ const parent = byName.get(key.parentTable);
578
+ if (parent !== void 0 && parent !== definition)
579
+ visit(parent);
580
+ }
581
+ state.set(definition.name, "done");
582
+ ordered.push(definition);
583
+ };
584
+ for (const definition of tables)
585
+ visit(definition);
586
+ return ordered;
673
587
  }
674
- /**
675
- * Whether a step destroys data a reader could still see. These are the steps `migrate()` refuses
676
- * to apply unless the caller opts in, so that a schema file drifting out of sync cannot delete
677
- * rows the next time an application opens.
678
- */
679
- export function isDestructiveStep(step) {
680
- return step.kind === "drop-column" || step.kind === "drop-table";
588
+ function isDestructiveStep(step) {
589
+ return step.kind === "drop-column" || step.kind === "drop-table";
681
590
  }
682
- /**
683
- * A column can stop being projected without rewriting anything, but not while something else in
684
- * the catalog still points at it. Each of these would leave a constraint or key referring to a
685
- * column no longer there, so the drop is refused rather than silently invalidating them.
686
- */
687
- export function assertColumnDroppable(record, column) {
688
- const where = `${record.name}.${column.name}`;
689
- if (record.uniqueKeyColumnId === column.id ||
690
- (record.primaryKeyColumnIds ?? []).includes(column.id)) {
691
- throw new TypeError(`The unique key cannot be dropped: ${where}. Unique-key changes need the table recreated.`);
692
- }
693
- for (const key of record.foreignKeys) {
694
- if (key.columns.includes(column.name)) {
695
- throw new TypeError(`FOREIGN KEY ${key.name} still uses this column: ${where}`);
696
- }
591
+ function assertColumnDroppable(record, column2) {
592
+ const where = `${record.name}.${column2.name}`;
593
+ if (record.uniqueKeyColumnId === column2.id || (record.primaryKeyColumnIds ?? []).includes(column2.id)) {
594
+ throw new TypeError(`The unique key cannot be dropped: ${where}. Unique-key changes need the table recreated.`);
595
+ }
596
+ for (const key of record.foreignKeys) {
597
+ if (key.columns.includes(column2.name)) {
598
+ throw new TypeError(`FOREIGN KEY ${key.name} still uses this column: ${where}`);
697
599
  }
698
- for (const index of record.indexes ?? []) {
699
- if (index.columns.some(({ name }) => name === column.name)) {
700
- throw new TypeError(`Index ${index.name} still uses this column: ${where}`);
701
- }
600
+ }
601
+ for (const index of record.indexes ?? []) {
602
+ if (index.columns.some(({ name }) => name === column2.name)) {
603
+ throw new TypeError(`Index ${index.name} still uses this column: ${where}`);
702
604
  }
703
- for (const check of record.checks) {
704
- let referenced;
705
- try {
706
- referenced = expressionColumns(compileCheckExpression(check.sql, check.name));
707
- }
708
- catch {
709
- // A check the engine can no longer compile is not a licence to drop what it names.
710
- throw new TypeError(`CHECK ${check.name} cannot be re-read, so ${where} is not droppable`);
711
- }
712
- if (referenced.includes(column.name)) {
713
- throw new TypeError(`CHECK ${check.name} still uses this column: ${where}`);
714
- }
605
+ }
606
+ for (const check of record.checks) {
607
+ let referenced;
608
+ try {
609
+ referenced = expressionColumns(compileCheckExpression(check.sql, check.name));
610
+ } catch {
611
+ throw new TypeError(`CHECK ${check.name} cannot be re-read, so ${where} is not droppable`);
715
612
  }
716
- for (const dependent of record.columns) {
717
- if (dependent.generatedValue === undefined)
718
- continue;
719
- let referenced;
720
- try {
721
- referenced = expressionColumns(compileCheckExpression(dependent.generatedValue.sql, `generated ${record.name}.${dependent.name}`));
722
- }
723
- catch {
724
- throw new TypeError(`Generated column ${dependent.name} cannot be re-read, so ${where} is not droppable`);
725
- }
726
- if (referenced.includes(column.name)) {
727
- throw new TypeError(`Generated column ${dependent.name} still uses this column: ${where}`);
728
- }
613
+ if (referenced.includes(column2.name)) {
614
+ throw new TypeError(`CHECK ${check.name} still uses this column: ${where}`);
615
+ }
616
+ }
617
+ for (const dependent of record.columns) {
618
+ if (dependent.generatedValue === void 0)
619
+ continue;
620
+ let referenced;
621
+ try {
622
+ referenced = expressionColumns(compileCheckExpression(dependent.generatedValue.sql, `generated ${record.name}.${dependent.name}`));
623
+ } catch {
624
+ throw new TypeError(`Generated column ${dependent.name} cannot be re-read, so ${where} is not droppable`);
729
625
  }
626
+ if (referenced.includes(column2.name)) {
627
+ throw new TypeError(`Generated column ${dependent.name} still uses this column: ${where}`);
628
+ }
629
+ }
730
630
  }
731
- function assertColumnRenamable(catalog, record, column) {
732
- const where = `${record.name}.${column.name}`;
733
- for (const owner of catalog.tables) {
734
- for (const key of owner.foreignKeys) {
735
- const usesChild = owner.name === record.name && key.columns.includes(column.name);
736
- const usesParent = key.parentTable === record.name && key.parentColumns.includes(column.name);
737
- if (usesChild || usesParent) {
738
- throw new TypeError(`FOREIGN KEY ${key.name} prevents renaming ${where}`);
739
- }
740
- }
631
+ function assertColumnRenamable(catalog, record, column2) {
632
+ const where = `${record.name}.${column2.name}`;
633
+ for (const owner of catalog.tables) {
634
+ for (const key of owner.foreignKeys) {
635
+ const usesChild = owner.name === record.name && key.columns.includes(column2.name);
636
+ const usesParent = key.parentTable === record.name && key.parentColumns.includes(column2.name);
637
+ if (usesChild || usesParent) {
638
+ throw new TypeError(`FOREIGN KEY ${key.name} prevents renaming ${where}`);
639
+ }
741
640
  }
742
- for (const check of record.checks) {
743
- const referenced = expressionColumns(compileCheckExpression(check.sql, check.name));
744
- if (referenced.includes(column.name)) {
745
- throw new TypeError(`CHECK ${check.name} prevents renaming ${where}`);
746
- }
641
+ }
642
+ for (const check of record.checks) {
643
+ const referenced = expressionColumns(compileCheckExpression(check.sql, check.name));
644
+ if (referenced.includes(column2.name)) {
645
+ throw new TypeError(`CHECK ${check.name} prevents renaming ${where}`);
747
646
  }
748
- for (const dependent of record.columns) {
749
- if (dependent.generatedValue === undefined)
750
- continue;
751
- const referenced = expressionColumns(compileCheckExpression(dependent.generatedValue.sql, `generated ${record.name}.${dependent.name}`));
752
- if (referenced.includes(column.name)) {
753
- throw new TypeError(`Generated column ${dependent.name} prevents renaming ${where}`);
754
- }
647
+ }
648
+ for (const dependent of record.columns) {
649
+ if (dependent.generatedValue === void 0)
650
+ continue;
651
+ const referenced = expressionColumns(compileCheckExpression(dependent.generatedValue.sql, `generated ${record.name}.${dependent.name}`));
652
+ if (referenced.includes(column2.name)) {
653
+ throw new TypeError(`Generated column ${dependent.name} prevents renaming ${where}`);
755
654
  }
655
+ }
756
656
  }
757
- /**
758
- * Freezes a column's backfill. A generator runs exactly once — here, while the migration is being
759
- * planned — so the catalog stores a value rather than a function and no two readers can disagree.
760
- */
761
657
  function resolveBackfill(definition) {
762
- const declared = definition.backfillValue;
763
- if (declared === undefined)
764
- return undefined;
765
- const value = typeof declared === "function" ? declared() : declared;
766
- validateDeclaredColumnValue(definition, value, "added column", "Backfill");
767
- return value;
658
+ const declared = definition.backfillValue;
659
+ if (declared === void 0)
660
+ return void 0;
661
+ const value = typeof declared === "function" ? declared() : declared;
662
+ validateDeclaredColumnValue(definition, value, "added column", "Backfill");
663
+ return value;
768
664
  }
769
665
  function storedBackfill(definition, value) {
770
- if (definition.sqlDomain === undefined)
771
- return value;
772
- const normalized = normalizeSqlDomainValue(definition.sqlDomain, value);
773
- if (normalized === null)
774
- throw new TypeError("A backfill cannot be NULL");
775
- return normalized;
666
+ if (definition.sqlDomain === void 0)
667
+ return value;
668
+ const normalized = normalizeSqlDomainValue(definition.sqlDomain, value);
669
+ if (normalized === null)
670
+ throw new TypeError("A backfill cannot be NULL");
671
+ return normalized;
776
672
  }
777
673
  function backfillsEqual(left, right) {
778
- const externalLeft = externalSqlDomainValue(left);
779
- const externalRight = externalSqlDomainValue(right);
780
- if (externalLeft instanceof Date || externalRight instanceof Date) {
781
- return (externalLeft instanceof Date &&
782
- externalRight instanceof Date &&
783
- dateMilliseconds(externalLeft) === dateMilliseconds(externalRight));
784
- }
785
- return externalLeft === externalRight;
674
+ const externalLeft = externalSqlDomainValue(left);
675
+ const externalRight = externalSqlDomainValue(right);
676
+ if (externalLeft instanceof Date || externalRight instanceof Date) {
677
+ return externalLeft instanceof Date && externalRight instanceof Date && dateMilliseconds(externalLeft) === dateMilliseconds(externalRight);
678
+ }
679
+ return externalLeft === externalRight;
786
680
  }
787
- /**
788
- * Enforced constraints on an existing table cannot change through a metadata-only step. Attaching
789
- * a FOREIGN KEY or CHECK to a table that already holds rows would claim something about those rows
790
- * that nobody has verified, and no validation scan exists; dropping one is refused for the mirror
791
- * reason, so that a constraint never disappears because a schema file drifted. Informational
792
- * foreign keys are catalog metadata only, so adding, dropping, or remapping one is safe.
793
- */
794
681
  function planConstraintChanges(record, definition, steps) {
795
- const describeKey = (key) => `${key.columns.join(",")} -> ${key.parentTable}.` +
796
- `${key.parentColumns.join(",")} ON DELETE ${key.onDelete}`;
797
- const declaredList = declaredForeignKeys(definition);
798
- const existingKeys = new Map(record.foreignKeys.map((key) => [key.name, key]));
799
- const declaredKeys = new Map(declaredList.map((key) => [key.name, key]));
800
- let informationalChanged = false;
801
- for (const [name, declared] of declaredKeys) {
802
- const existing = existingKeys.get(name);
803
- if (existing === undefined) {
804
- if (!declared.enforced) {
805
- informationalChanged = true;
806
- continue;
807
- }
808
- throw new TypeError(`FOREIGN KEY cannot be added after creation: ${definition.name}.${declared.columns.join(",")}. ` +
809
- `Existing rows are not known to satisfy it; recreate the table to add a relation.`);
810
- }
811
- if (existing.enforced !== declared.enforced) {
812
- throw new TypeError(`FOREIGN KEY enforcement cannot change: ${definition.name}.${declared.columns.join(",")}`);
813
- }
814
- if (describeKey(existing) !== describeKey(declared)) {
815
- if (!declared.enforced) {
816
- informationalChanged = true;
817
- continue;
818
- }
819
- throw new TypeError(`FOREIGN KEY cannot change: ${definition.name}.${declared.columns.join(",")} is ` +
820
- `${describeKey(existing)}, schema says ${describeKey(declared)}`);
821
- }
682
+ const describeKey = (key) => `${key.columns.join(",")} -> ${key.parentTable}.${key.parentColumns.join(",")} ON DELETE ${key.onDelete}`;
683
+ const declaredList = declaredForeignKeys(definition);
684
+ const existingKeys = new Map(record.foreignKeys.map((key) => [key.name, key]));
685
+ const declaredKeys = new Map(declaredList.map((key) => [key.name, key]));
686
+ let informationalChanged = false;
687
+ for (const [name, declared] of declaredKeys) {
688
+ const existing = existingKeys.get(name);
689
+ if (existing === void 0) {
690
+ if (!declared.enforced) {
691
+ informationalChanged = true;
692
+ continue;
693
+ }
694
+ throw new TypeError(`FOREIGN KEY cannot be added after creation: ${definition.name}.${declared.columns.join(",")}. Existing rows are not known to satisfy it; recreate the table to add a relation.`);
822
695
  }
823
- for (const name of existingKeys.keys()) {
824
- if (!declaredKeys.has(name)) {
825
- if (existingKeys.get(name)?.enforced === false) {
826
- informationalChanged = true;
827
- continue;
828
- }
829
- throw new TypeError(`FOREIGN KEY cannot be dropped: ${definition.name} still has ${name}. ` +
830
- `Declare the relation, or recreate the table without it.`);
831
- }
696
+ if (existing.enforced !== declared.enforced) {
697
+ throw new TypeError(`FOREIGN KEY enforcement cannot change: ${definition.name}.${declared.columns.join(",")}`);
832
698
  }
833
- if (informationalChanged) {
834
- steps.push({
835
- kind: "alter-foreign-keys",
836
- tableName: definition.name,
837
- foreignKeys: declaredList,
838
- });
699
+ if (describeKey(existing) !== describeKey(declared)) {
700
+ if (!declared.enforced) {
701
+ informationalChanged = true;
702
+ continue;
703
+ }
704
+ throw new TypeError(`FOREIGN KEY cannot change: ${definition.name}.${declared.columns.join(",")} is ${describeKey(existing)}, schema says ${describeKey(declared)}`);
839
705
  }
840
- const existingChecks = new Map(record.checks.map((check) => [check.name, check.sql]));
841
- const declaredChecks = new Map(definition.checks.map((check) => [check.name, check.sql]));
842
- for (const [name, sql] of declaredChecks) {
843
- const existing = existingChecks.get(name);
844
- if (existing === undefined) {
845
- throw new TypeError(`CHECK cannot be added after creation: ${definition.name}.${name}. ` +
846
- `Existing rows are not known to satisfy it; recreate the table to add a constraint.`);
847
- }
848
- if (existing !== sql) {
849
- throw new TypeError(`CHECK cannot change: ${definition.name}.${name} is (${existing}), schema says (${sql})`);
850
- }
706
+ }
707
+ for (const name of existingKeys.keys()) {
708
+ if (!declaredKeys.has(name)) {
709
+ if (existingKeys.get(name)?.enforced === false) {
710
+ informationalChanged = true;
711
+ continue;
712
+ }
713
+ throw new TypeError(`FOREIGN KEY cannot be dropped: ${definition.name} still has ${name}. Declare the relation, or recreate the table without it.`);
851
714
  }
852
- for (const name of existingChecks.keys()) {
853
- if (!declaredChecks.has(name)) {
854
- throw new TypeError(`CHECK cannot be dropped: ${definition.name} still has ${name}. ` +
855
- `Declare the constraint, or recreate the table without it.`);
856
- }
715
+ }
716
+ if (informationalChanged) {
717
+ steps.push({
718
+ kind: "alter-foreign-keys",
719
+ tableName: definition.name,
720
+ foreignKeys: declaredList
721
+ });
722
+ }
723
+ const existingChecks = new Map(record.checks.map((check) => [check.name, check.sql]));
724
+ const declaredChecks = new Map(definition.checks.map((check) => [check.name, check.sql]));
725
+ for (const [name, sql] of declaredChecks) {
726
+ const existing = existingChecks.get(name);
727
+ if (existing === void 0) {
728
+ throw new TypeError(`CHECK cannot be added after creation: ${definition.name}.${name}. Existing rows are not known to satisfy it; recreate the table to add a constraint.`);
729
+ }
730
+ if (existing !== sql) {
731
+ throw new TypeError(`CHECK cannot change: ${definition.name}.${name} is (${existing}), schema says (${sql})`);
732
+ }
733
+ }
734
+ for (const name of existingChecks.keys()) {
735
+ if (!declaredChecks.has(name)) {
736
+ throw new TypeError(`CHECK cannot be dropped: ${definition.name} still has ${name}. Declare the constraint, or recreate the table without it.`);
857
737
  }
738
+ }
858
739
  }
859
- export function planMigration(catalog, definition, options = {}) {
860
- const steps = [];
861
- const currentByName = new Map(catalog.tables.map((record) => [record.name, record]));
862
- for (const tableDefinition of orderedForCreation(definition.tables)) {
863
- const record = currentByName.get(tableDefinition.name);
864
- if (record === undefined) {
865
- steps.push({ kind: "create-table", table: tableDefinition });
866
- continue;
867
- }
868
- const recordColumnsByName = new Map(record.columns.map((columnRecord) => [columnRecord.name, columnRecord]));
869
- const definedNames = new Set(Object.keys(tableDefinition.columns));
870
- const renameSources = new Set();
871
- for (const [columnName, columnDefinition] of Object.entries(tableDefinition.columns)) {
872
- const from = columnDefinition.renamedFromName;
873
- let existing = recordColumnsByName.get(columnName);
874
- if (existing === undefined && from !== undefined) {
875
- const source = recordColumnsByName.get(from);
876
- if (source !== undefined) {
877
- if (renameSources.has(from)) {
878
- throw new TypeError(`Rename source is used more than once: ${tableDefinition.name}.${from}`);
879
- }
880
- if (definedNames.has(from)) {
881
- throw new TypeError(`Rename source is still defined: ${tableDefinition.name}.${from}`);
882
- }
883
- assertColumnRenamable(catalog, record, source);
884
- steps.push({
885
- kind: "rename-column",
886
- tableName: tableDefinition.name,
887
- from,
888
- to: columnName,
889
- });
890
- renameSources.add(from);
891
- existing = source;
892
- }
893
- }
894
- if (existing === undefined) {
895
- if (columnDefinition.generatedSpec !== undefined) {
896
- throw new TypeError(`Generated columns cannot be added to an existing table without rewriting its rows: ${tableDefinition.name}.${columnName}`);
897
- }
898
- const backfill = resolveBackfill(columnDefinition);
899
- if (!columnDefinition.isNullable && backfill === undefined) {
900
- throw new TypeError(`Added columns must be nullable, or carry a backfill: ${tableDefinition.name}.${columnName}`);
901
- }
902
- if (columnDefinition.isUnique || tableDefinition.primaryKey.includes(columnName)) {
903
- throw new TypeError(`Unique keys cannot be added after creation: ${tableDefinition.name}.${columnName}`);
904
- }
905
- steps.push({
906
- kind: "add-column",
907
- tableName: tableDefinition.name,
908
- columnName,
909
- definition: columnDefinition,
910
- ...(backfill === undefined ? {} : { backfill }),
911
- });
912
- continue;
913
- }
914
- if (existing.type !== columnDefinition.type) {
915
- throw new TypeError(`Column types cannot change: ${tableDefinition.name}.${columnName} is ${existing.type}, schema says ${columnDefinition.type}`);
916
- }
917
- if ((existing.integer === true) !== columnDefinition.integer ||
918
- JSON.stringify(existing.sqlDomain ?? null) !==
919
- JSON.stringify(columnDefinition.sqlDomain ?? null)) {
920
- throw new TypeError(`Column domains cannot change: ${tableDefinition.name}.${columnName}`);
921
- }
922
- if (existing.backfill === undefined && columnDefinition.backfillValue !== undefined) {
923
- throw new TypeError(`Backfills cannot be added after a column exists: ${tableDefinition.name}.${columnName}`);
924
- }
925
- if (existing.backfill !== undefined && columnDefinition.backfillValue === undefined) {
926
- throw new TypeError(`Backfills cannot be removed: ${tableDefinition.name}.${columnName}`);
927
- }
928
- if (existing.backfill !== undefined &&
929
- columnDefinition.backfillValue !== undefined &&
930
- typeof columnDefinition.backfillValue !== "function" &&
931
- !backfillsEqual(existing.backfill, columnDefinition.backfillValue)) {
932
- throw new TypeError(`Backfills cannot change: ${tableDefinition.name}.${columnName}`);
933
- }
934
- const existingEnum = existing.enumValues;
935
- const definedEnum = columnDefinition.enumValues;
936
- if (definedEnum !== undefined && existingEnum === undefined) {
937
- throw new TypeError(`Plain string columns cannot tighten to an enum: ${tableDefinition.name}.${columnName}. Existing rows may hold values outside the set.`);
938
- }
939
- if (existingEnum !== undefined) {
940
- if (definedEnum === undefined) {
941
- steps.push({
942
- kind: "widen-enum",
943
- tableName: tableDefinition.name,
944
- columnName,
945
- enumValues: null,
946
- });
947
- }
948
- else {
949
- const removed = existingEnum.filter((value) => !definedEnum.includes(value));
950
- if (removed.length > 0) {
951
- throw new TypeError(`Enum values cannot be removed: ${tableDefinition.name}.${columnName} drops ${removed.join(", ")}`);
952
- }
953
- if (definedEnum.some((value) => !existingEnum.includes(value))) {
954
- steps.push({
955
- kind: "widen-enum",
956
- tableName: tableDefinition.name,
957
- columnName,
958
- enumValues: [...definedEnum],
959
- });
960
- }
961
- }
962
- }
963
- const existingIsKey = record.uniqueKeyColumnId === existing.id ||
964
- (record.primaryKeyColumnIds ?? []).includes(existing.id);
965
- const definedIsKey = columnDefinition.isUnique || tableDefinition.primaryKey.includes(columnName);
966
- if (existingIsKey !== definedIsKey) {
967
- throw new TypeError(`Unique keys cannot change: ${tableDefinition.name}.${columnName}`);
968
- }
969
- if (existing.nullable && !columnDefinition.isNullable) {
970
- steps.push({
971
- kind: "tighten-nullable",
972
- tableName: tableDefinition.name,
973
- columnName,
974
- });
975
- }
976
- if (!existing.nullable && columnDefinition.isNullable) {
977
- steps.push({
978
- kind: "widen-nullable",
979
- tableName: tableDefinition.name,
980
- columnName,
981
- });
982
- }
983
- if (!columnDefaultsEqual(existing.defaultValue, columnDefinition.defaultSpec)) {
984
- const wasAuto = existing.defaultValue?.kind === "autoincrement";
985
- const isAuto = columnDefinition.defaultSpec?.kind === "autoincrement";
986
- if (wasAuto !== isAuto) {
987
- // Adopting one seeds the counter past the largest key already stored; dropping one
988
- // simply stops generating. Neither rewrites a row.
989
- steps.push({
990
- kind: "set-auto-increment",
991
- tableName: tableDefinition.name,
992
- columnName,
993
- enabled: isAuto,
994
- });
995
- }
996
- else {
997
- steps.push({
998
- kind: "alter-default",
999
- tableName: tableDefinition.name,
1000
- columnName,
1001
- defaultValue: columnDefinition.defaultSpec ?? null,
1002
- });
1003
- }
1004
- }
1005
- const existingGenerated = existing.generatedValue;
1006
- const definedGenerated = columnDefinition.generatedSpec;
1007
- if (JSON.stringify(existingGenerated ?? null) !== JSON.stringify(definedGenerated ?? null)) {
1008
- steps.push({
1009
- kind: "alter-generated",
1010
- tableName: tableDefinition.name,
1011
- columnName,
1012
- generatedValue: definedGenerated ?? null,
1013
- });
1014
- }
740
+ function planMigration(catalog, definition, options = {}) {
741
+ const steps = [];
742
+ const currentByName = new Map(catalog.tables.map((record) => [record.name, record]));
743
+ for (const tableDefinition of orderedForCreation(definition.tables)) {
744
+ const record = currentByName.get(tableDefinition.name);
745
+ if (record === void 0) {
746
+ steps.push({ kind: "create-table", table: tableDefinition });
747
+ continue;
748
+ }
749
+ const recordColumnsByName = new Map(record.columns.map((columnRecord) => [columnRecord.name, columnRecord]));
750
+ const definedNames = new Set(Object.keys(tableDefinition.columns));
751
+ const renameSources = /* @__PURE__ */ new Set();
752
+ for (const [columnName, columnDefinition] of Object.entries(tableDefinition.columns)) {
753
+ const from = columnDefinition.renamedFromName;
754
+ let existing = recordColumnsByName.get(columnName);
755
+ if (existing === void 0 && from !== void 0) {
756
+ const source = recordColumnsByName.get(from);
757
+ if (source !== void 0) {
758
+ if (renameSources.has(from)) {
759
+ throw new TypeError(`Rename source is used more than once: ${tableDefinition.name}.${from}`);
760
+ }
761
+ if (definedNames.has(from)) {
762
+ throw new TypeError(`Rename source is still defined: ${tableDefinition.name}.${from}`);
763
+ }
764
+ assertColumnRenamable(catalog, record, source);
765
+ steps.push({
766
+ kind: "rename-column",
767
+ tableName: tableDefinition.name,
768
+ from,
769
+ to: columnName
770
+ });
771
+ renameSources.add(from);
772
+ existing = source;
773
+ }
774
+ }
775
+ if (existing === void 0) {
776
+ if (columnDefinition.generatedSpec !== void 0) {
777
+ throw new TypeError(`Generated columns cannot be added to an existing table without rewriting its rows: ${tableDefinition.name}.${columnName}`);
778
+ }
779
+ const backfill = resolveBackfill(columnDefinition);
780
+ if (!columnDefinition.isNullable && backfill === void 0) {
781
+ throw new TypeError(`Added columns must be nullable, or carry a backfill: ${tableDefinition.name}.${columnName}`);
782
+ }
783
+ if (columnDefinition.isUnique || tableDefinition.primaryKey.includes(columnName)) {
784
+ throw new TypeError(`Unique keys cannot be added after creation: ${tableDefinition.name}.${columnName}`);
1015
785
  }
1016
- for (const columnRecord of record.columns) {
1017
- if (definedNames.has(columnRecord.name) || renameSources.has(columnRecord.name))
1018
- continue;
1019
- assertColumnDroppable(record, columnRecord);
786
+ steps.push({
787
+ kind: "add-column",
788
+ tableName: tableDefinition.name,
789
+ columnName,
790
+ definition: columnDefinition,
791
+ ...backfill === void 0 ? {} : { backfill }
792
+ });
793
+ continue;
794
+ }
795
+ if (existing.type !== columnDefinition.type) {
796
+ throw new TypeError(`Column types cannot change: ${tableDefinition.name}.${columnName} is ${existing.type}, schema says ${columnDefinition.type}`);
797
+ }
798
+ if (existing.integer === true !== columnDefinition.integer || JSON.stringify(existing.sqlDomain ?? null) !== JSON.stringify(columnDefinition.sqlDomain ?? null)) {
799
+ throw new TypeError(`Column domains cannot change: ${tableDefinition.name}.${columnName}`);
800
+ }
801
+ if (existing.backfill === void 0 && columnDefinition.backfillValue !== void 0) {
802
+ throw new TypeError(`Backfills cannot be added after a column exists: ${tableDefinition.name}.${columnName}`);
803
+ }
804
+ if (existing.backfill !== void 0 && columnDefinition.backfillValue === void 0) {
805
+ throw new TypeError(`Backfills cannot be removed: ${tableDefinition.name}.${columnName}`);
806
+ }
807
+ if (existing.backfill !== void 0 && columnDefinition.backfillValue !== void 0 && typeof columnDefinition.backfillValue !== "function" && !backfillsEqual(existing.backfill, columnDefinition.backfillValue)) {
808
+ throw new TypeError(`Backfills cannot change: ${tableDefinition.name}.${columnName}`);
809
+ }
810
+ const existingEnum = existing.enumValues;
811
+ const definedEnum = columnDefinition.enumValues;
812
+ if (definedEnum !== void 0 && existingEnum === void 0) {
813
+ throw new TypeError(`Plain string columns cannot tighten to an enum: ${tableDefinition.name}.${columnName}. Existing rows may hold values outside the set.`);
814
+ }
815
+ if (existingEnum !== void 0) {
816
+ if (definedEnum === void 0) {
817
+ steps.push({
818
+ kind: "widen-enum",
819
+ tableName: tableDefinition.name,
820
+ columnName,
821
+ enumValues: null
822
+ });
823
+ } else {
824
+ const removed = existingEnum.filter((value) => !definedEnum.includes(value));
825
+ if (removed.length > 0) {
826
+ throw new TypeError(`Enum values cannot be removed: ${tableDefinition.name}.${columnName} drops ${removed.join(", ")}`);
827
+ }
828
+ if (definedEnum.some((value) => !existingEnum.includes(value))) {
1020
829
  steps.push({
1021
- kind: "drop-column",
1022
- tableName: tableDefinition.name,
1023
- columnName: columnRecord.name,
830
+ kind: "widen-enum",
831
+ tableName: tableDefinition.name,
832
+ columnName,
833
+ enumValues: [...definedEnum]
1024
834
  });
1025
- }
1026
- const existingKeyIds = record.primaryKeyColumnIds ??
1027
- (record.uniqueKeyColumnId === undefined ? [] : [record.uniqueKeyColumnId]);
1028
- const desiredKeyNames = tableDefinition.primaryKey.length > 0
1029
- ? tableDefinition.primaryKey
1030
- : Object.entries(tableDefinition.columns).flatMap(([name, definition]) => definition.isUnique ? [name] : []);
1031
- const desiredKeyIds = desiredKeyNames.map((name) => {
1032
- const definition = tableDefinition.columns[name];
1033
- const currentName = definition?.renamedFromName ?? name;
1034
- return recordColumnsByName.get(name)?.id ?? recordColumnsByName.get(currentName)?.id;
835
+ }
836
+ }
837
+ }
838
+ const existingIsKey = record.uniqueKeyColumnId === existing.id || (record.primaryKeyColumnIds ?? []).includes(existing.id);
839
+ const definedIsKey = columnDefinition.isUnique || tableDefinition.primaryKey.includes(columnName);
840
+ if (existingIsKey !== definedIsKey) {
841
+ throw new TypeError(`Unique keys cannot change: ${tableDefinition.name}.${columnName}`);
842
+ }
843
+ if (existing.nullable && !columnDefinition.isNullable) {
844
+ steps.push({
845
+ kind: "tighten-nullable",
846
+ tableName: tableDefinition.name,
847
+ columnName
1035
848
  });
1036
- if (desiredKeyIds.length !== existingKeyIds.length ||
1037
- desiredKeyIds.some((id, index) => id !== existingKeyIds[index])) {
1038
- throw new TypeError(`Primary key order cannot change: ${tableDefinition.name}`);
1039
- }
1040
- planConstraintChanges(record, tableDefinition, steps);
1041
- }
1042
- if (options.schemaOwnsDatabase === true) {
1043
- const declaredTables = new Set(definition.tables.map(({ name }) => name));
1044
- for (const record of catalog.tables) {
1045
- // Only tables a migration created; see `managed`. Rows are at stake, so an undeclared
1046
- // table someone else made is never a migration's to remove.
1047
- if (declaredTables.has(record.name) || !record.managed)
1048
- continue;
1049
- steps.push({ kind: "drop-table", tableName: record.name });
1050
- }
849
+ }
850
+ if (!existing.nullable && columnDefinition.isNullable) {
851
+ steps.push({
852
+ kind: "widen-nullable",
853
+ tableName: tableDefinition.name,
854
+ columnName
855
+ });
856
+ }
857
+ if (!columnDefaultsEqual(existing.defaultValue, columnDefinition.defaultSpec)) {
858
+ const wasAuto = existing.defaultValue?.kind === "autoincrement";
859
+ const isAuto = columnDefinition.defaultSpec?.kind === "autoincrement";
860
+ if (wasAuto !== isAuto) {
861
+ steps.push({
862
+ kind: "set-auto-increment",
863
+ tableName: tableDefinition.name,
864
+ columnName,
865
+ enabled: isAuto
866
+ });
867
+ } else {
868
+ steps.push({
869
+ kind: "alter-default",
870
+ tableName: tableDefinition.name,
871
+ columnName,
872
+ defaultValue: columnDefinition.defaultSpec ?? null
873
+ });
874
+ }
875
+ }
876
+ const existingGenerated = existing.generatedValue;
877
+ const definedGenerated = columnDefinition.generatedSpec;
878
+ if (JSON.stringify(existingGenerated ?? null) !== JSON.stringify(definedGenerated ?? null)) {
879
+ steps.push({
880
+ kind: "alter-generated",
881
+ tableName: tableDefinition.name,
882
+ columnName,
883
+ generatedValue: definedGenerated ?? null
884
+ });
885
+ }
886
+ }
887
+ for (const columnRecord of record.columns) {
888
+ if (definedNames.has(columnRecord.name) || renameSources.has(columnRecord.name))
889
+ continue;
890
+ assertColumnDroppable(record, columnRecord);
891
+ steps.push({
892
+ kind: "drop-column",
893
+ tableName: tableDefinition.name,
894
+ columnName: columnRecord.name
895
+ });
896
+ }
897
+ const existingKeyIds = record.primaryKeyColumnIds ?? (record.uniqueKeyColumnId === void 0 ? [] : [record.uniqueKeyColumnId]);
898
+ const desiredKeyNames = tableDefinition.primaryKey.length > 0 ? tableDefinition.primaryKey : Object.entries(tableDefinition.columns).flatMap(([name, definition2]) => definition2.isUnique ? [name] : []);
899
+ const desiredKeyIds = desiredKeyNames.map((name) => {
900
+ const definition2 = tableDefinition.columns[name];
901
+ const currentName = definition2?.renamedFromName ?? name;
902
+ return recordColumnsByName.get(name)?.id ?? recordColumnsByName.get(currentName)?.id;
903
+ });
904
+ if (desiredKeyIds.length !== existingKeyIds.length || desiredKeyIds.some((id, index) => id !== existingKeyIds[index])) {
905
+ throw new TypeError(`Primary key order cannot change: ${tableDefinition.name}`);
906
+ }
907
+ planConstraintChanges(record, tableDefinition, steps);
908
+ }
909
+ if (options.schemaOwnsDatabase === true) {
910
+ const declaredTables = new Set(definition.tables.map(({ name }) => name));
911
+ for (const record of catalog.tables) {
912
+ if (declaredTables.has(record.name) || !record.managed)
913
+ continue;
914
+ steps.push({ kind: "drop-table", tableName: record.name });
1051
915
  }
1052
- planViewSteps(catalog, definition, steps);
1053
- // A managed view can depend on a managed table the same authoritative schema removes. Drop
1054
- // dependents first; the SQL engine correctly refuses the reverse order. Replacements stay
1055
- // after table-creation steps so a newly declared body can resolve its sources.
1056
- const droppedViews = steps.filter((step) => step.kind === "drop-view");
1057
- const droppedTables = steps.filter((step) => step.kind === "drop-table");
1058
- return {
1059
- steps: [
1060
- ...steps.filter((step) => step.kind !== "drop-view" && step.kind !== "drop-table"),
1061
- ...droppedViews,
1062
- ...droppedTables,
1063
- ],
1064
- };
916
+ }
917
+ planViewSteps(catalog, definition, steps);
918
+ const droppedViews = steps.filter((step) => step.kind === "drop-view");
919
+ const droppedTables = steps.filter((step) => step.kind === "drop-table");
920
+ return {
921
+ steps: [
922
+ ...steps.filter((step) => step.kind !== "drop-view" && step.kind !== "drop-table"),
923
+ ...droppedViews,
924
+ ...droppedTables
925
+ ]
926
+ };
1065
927
  }
1066
- /**
1067
- * A thin, fully typed SQL handle: inserts require every non-nullable column, updates exclude the
1068
- * unique key, and reads return the complete inferred row shape. Every operation is rendered as
1069
- * parameterized SQL and goes through the parser/planner/executor; this helper has no private
1070
- * batch or table-read semantics that can drift from SQL.
1071
- */
1072
- export function typedTable(database, definition) {
1073
- const columnNames = Object.keys(definition.columns);
1074
- const quote = (name) => `"${name.replaceAll('"', '""')}"`;
1075
- const scalarUniqueKey = Object.entries(definition.columns).find(([, columnDefinition]) => columnDefinition.isUnique)?.[0];
1076
- const keyColumns = definition.primaryKey.length > 0
1077
- ? [...definition.primaryKey]
1078
- : scalarUniqueKey === undefined
1079
- ? []
1080
- : [scalarUniqueKey];
1081
- const normalizedRows = (rows) => {
1082
- if (rows.length === 0)
1083
- throw new TypeError("A batch needs at least one row");
1084
- // Keep extra runtime keys in the SQL column list. TypeScript normally excludes them, but a
1085
- // cast or JavaScript caller must still get the engine's "column does not exist" error rather
1086
- // than having data silently discarded by this facade.
1087
- const names = new Set(columnNames);
1088
- for (const row of rows)
1089
- for (const name of Object.keys(row))
1090
- names.add(name);
1091
- const ordered = [...names];
1092
- return { names: ordered, rows };
1093
- };
1094
- const insert = (rows, replace) => {
1095
- const normalized = normalizedRows(rows);
1096
- const params = [];
1097
- const values = normalized.rows
1098
- .map((row) => {
1099
- const placeholders = normalized.names.map((name) => {
1100
- const value = row[name];
1101
- if (!Object.hasOwn(row, name) || value === undefined)
1102
- return "DEFAULT";
1103
- params.push(value);
1104
- return `$${String(params.length)}`;
1105
- });
1106
- return `(${placeholders.join(", ")})`;
1107
- })
1108
- .join(", ");
1109
- let sql = `INSERT INTO ${quote(definition.name)} (${normalized.names.map(quote).join(", ")}) VALUES ${values}`;
1110
- if (replace) {
1111
- if (keyColumns.length === 0) {
1112
- throw new TypeError(`Upsert requires a unique key: ${definition.name}`);
1113
- }
1114
- sql += ` ON CONFLICT (${keyColumns.map(quote).join(", ")}) DO REPLACE`;
1115
- }
1116
- return database.execute(sql, params);
1117
- };
1118
- const requireUniqueKey = (operation) => {
1119
- if (keyColumns.length === 0) {
1120
- throw new TypeError(`${operation} requires a table with a unique key: ${definition.name}`);
1121
- }
1122
- return keyColumns;
1123
- };
1124
- const keyParts = (key) => {
1125
- if (keyColumns.length === 1)
1126
- return [key];
1127
- if (typeof key !== "object" || key === null) {
1128
- throw new TypeError(`Composite key for ${definition.name} must be an object`);
1129
- }
1130
- return keyColumns.map((name) => key[name]);
1131
- };
1132
- const keyToken = (value) => {
1133
- const parts = keyParts(value);
1134
- return JSON.stringify(parts.map((part) => part instanceof Date ? ["date", dateMilliseconds(part)] : [typeof part, String(part)]));
1135
- };
1136
- const assertKeys = (keys, operation) => {
1137
- if (keys.length === 0)
1138
- throw new TypeError(`A ${operation} batch needs at least one key`);
1139
- const seen = new Set();
1140
- for (const key of keys) {
1141
- if (keyParts(key).some((part) => part === undefined || part === null)) {
1142
- throw new TypeError(`Missing ${operation} key value for ${definition.name}`);
1143
- }
1144
- const token = keyToken(key);
1145
- if (seen.has(token))
1146
- throw new TypeError(`Duplicate key in ${operation} batch: ${String(key)}`);
1147
- seen.add(token);
1148
- }
1149
- };
1150
- return {
1151
- definition,
1152
- insert: (rows) => insert(rows, false),
1153
- upsert: (rows) => insert(rows, true),
1154
- update: (input) => {
1155
- const keys = requireUniqueKey("UPDATE");
1156
- assertKeys(input.keys, "update");
1157
- const assignments = [];
1158
- const params = [];
1159
- for (const [name, rawValues] of Object.entries(input.changes)) {
1160
- if (!(name in definition.columns)) {
1161
- // Render it anyway so SQL owns the public validation error.
1162
- }
1163
- const values = rawValues;
1164
- if (values.length !== input.keys.length) {
1165
- throw new TypeError(`Update column ${name} has ${String(values.length)} values for ${String(input.keys.length)} keys`);
1166
- }
1167
- const branches = [];
1168
- values.forEach((value, index) => {
1169
- if (value === undefined)
1170
- return;
1171
- const keyValue = input.keys[index];
1172
- if (keyValue === undefined) {
1173
- throw new TypeError(`Missing update key at position ${String(index)}`);
1174
- }
1175
- const conditions = keyParts(keyValue).map((part, keyIndex) => {
1176
- params.push(part);
1177
- return `${quote(keys[keyIndex] ?? "")} = $${String(params.length)}`;
1178
- });
1179
- params.push(value);
1180
- branches.push(`WHEN ${conditions.join(" AND ")} THEN $${String(params.length)}`);
1181
- });
1182
- if (branches.length > 0) {
1183
- assignments.push(`${quote(name)} = CASE ${branches.join(" ")} ELSE ${quote(name)} END`);
1184
- }
1185
- }
1186
- if (assignments.length === 0)
1187
- throw new TypeError("An update batch needs at least one change");
1188
- const predicates = input.keys.map((keyValue) => {
1189
- const conditions = keyParts(keyValue).map((part, keyIndex) => {
1190
- params.push(part);
1191
- return `${quote(keys[keyIndex] ?? "")} = $${String(params.length)}`;
1192
- });
1193
- return `(${conditions.join(" AND ")})`;
1194
- });
1195
- return database.execute(`UPDATE ${quote(definition.name)} SET ${assignments.join(", ")} WHERE ${predicates.join(" OR ")}`, params);
1196
- },
1197
- delete: (input) => {
1198
- const keys = requireUniqueKey("DELETE");
1199
- assertKeys(input.keys, "delete");
1200
- const params = [];
1201
- const predicates = input.keys.map((keyValue) => {
1202
- const conditions = keyParts(keyValue).map((part, keyIndex) => {
1203
- params.push(part);
1204
- return `${quote(keys[keyIndex] ?? "")} = $${String(params.length)}`;
1205
- });
1206
- return `(${conditions.join(" AND ")})`;
1207
- });
1208
- return database.execute(`DELETE FROM ${quote(definition.name)} WHERE ${predicates.join(" OR ")}`, params);
1209
- },
1210
- rows: async () => (await database.query(`SELECT ${columnNames.map(quote).join(", ")} FROM ${quote(definition.name)}`)).rows,
1211
- };
928
+ function typedTable(database, definition) {
929
+ const columnNames = Object.keys(definition.columns);
930
+ const quote = (name) => `"${name.replaceAll('"', '""')}"`;
931
+ const scalarUniqueKey = Object.entries(definition.columns).find(([, columnDefinition]) => columnDefinition.isUnique)?.[0];
932
+ const keyColumns = definition.primaryKey.length > 0 ? [...definition.primaryKey] : scalarUniqueKey === void 0 ? [] : [scalarUniqueKey];
933
+ const normalizedRows = (rows) => {
934
+ if (rows.length === 0)
935
+ throw new TypeError("A batch needs at least one row");
936
+ const names = new Set(columnNames);
937
+ for (const row of rows)
938
+ for (const name of Object.keys(row))
939
+ names.add(name);
940
+ const ordered = [...names];
941
+ return { names: ordered, rows };
942
+ };
943
+ const insert = (rows, replace) => {
944
+ const normalized = normalizedRows(rows);
945
+ const params = [];
946
+ const values = normalized.rows.map((row) => {
947
+ const placeholders = normalized.names.map((name) => {
948
+ const value = row[name];
949
+ if (!Object.hasOwn(row, name) || value === void 0)
950
+ return "DEFAULT";
951
+ params.push(value);
952
+ return `$${String(params.length)}`;
953
+ });
954
+ return `(${placeholders.join(", ")})`;
955
+ }).join(", ");
956
+ let sql = `INSERT INTO ${quote(definition.name)} (${normalized.names.map(quote).join(", ")}) VALUES ${values}`;
957
+ if (replace) {
958
+ if (keyColumns.length === 0) {
959
+ throw new TypeError(`Upsert requires a unique key: ${definition.name}`);
960
+ }
961
+ sql += ` ON CONFLICT (${keyColumns.map(quote).join(", ")}) DO REPLACE`;
962
+ }
963
+ return database.execute(sql, params);
964
+ };
965
+ const requireUniqueKey = (operation) => {
966
+ if (keyColumns.length === 0) {
967
+ throw new TypeError(`${operation} requires a table with a unique key: ${definition.name}`);
968
+ }
969
+ return keyColumns;
970
+ };
971
+ const keyParts = (key) => {
972
+ if (keyColumns.length === 1)
973
+ return [key];
974
+ if (typeof key !== "object" || key === null) {
975
+ throw new TypeError(`Composite key for ${definition.name} must be an object`);
976
+ }
977
+ return keyColumns.map((name) => key[name]);
978
+ };
979
+ const keyToken = (value) => {
980
+ const parts = keyParts(value);
981
+ return JSON.stringify(parts.map((part) => part instanceof Date ? ["date", dateMilliseconds(part)] : [typeof part, String(part)]));
982
+ };
983
+ const assertKeys = (keys, operation) => {
984
+ if (keys.length === 0)
985
+ throw new TypeError(`A ${operation} batch needs at least one key`);
986
+ const seen = /* @__PURE__ */ new Set();
987
+ for (const key of keys) {
988
+ if (keyParts(key).some((part) => part === void 0 || part === null)) {
989
+ throw new TypeError(`Missing ${operation} key value for ${definition.name}`);
990
+ }
991
+ const token = keyToken(key);
992
+ if (seen.has(token))
993
+ throw new TypeError(`Duplicate key in ${operation} batch: ${String(key)}`);
994
+ seen.add(token);
995
+ }
996
+ };
997
+ return {
998
+ definition,
999
+ insert: (rows) => insert(rows, false),
1000
+ upsert: (rows) => insert(rows, true),
1001
+ update: (input) => {
1002
+ const keys = requireUniqueKey("UPDATE");
1003
+ assertKeys(input.keys, "update");
1004
+ const assignments = [];
1005
+ const params = [];
1006
+ for (const [name, rawValues] of Object.entries(input.changes)) {
1007
+ if (!(name in definition.columns)) {
1008
+ }
1009
+ const values = rawValues;
1010
+ if (values.length !== input.keys.length) {
1011
+ throw new TypeError(`Update column ${name} has ${String(values.length)} values for ${String(input.keys.length)} keys`);
1012
+ }
1013
+ const branches = [];
1014
+ values.forEach((value, index) => {
1015
+ if (value === void 0)
1016
+ return;
1017
+ const keyValue = input.keys[index];
1018
+ if (keyValue === void 0) {
1019
+ throw new TypeError(`Missing update key at position ${String(index)}`);
1020
+ }
1021
+ const conditions = keyParts(keyValue).map((part, keyIndex) => {
1022
+ params.push(part);
1023
+ return `${quote(keys[keyIndex] ?? "")} = $${String(params.length)}`;
1024
+ });
1025
+ params.push(value);
1026
+ branches.push(`WHEN ${conditions.join(" AND ")} THEN $${String(params.length)}`);
1027
+ });
1028
+ if (branches.length > 0) {
1029
+ assignments.push(`${quote(name)} = CASE ${branches.join(" ")} ELSE ${quote(name)} END`);
1030
+ }
1031
+ }
1032
+ if (assignments.length === 0)
1033
+ throw new TypeError("An update batch needs at least one change");
1034
+ const predicates = input.keys.map((keyValue) => {
1035
+ const conditions = keyParts(keyValue).map((part, keyIndex) => {
1036
+ params.push(part);
1037
+ return `${quote(keys[keyIndex] ?? "")} = $${String(params.length)}`;
1038
+ });
1039
+ return `(${conditions.join(" AND ")})`;
1040
+ });
1041
+ return database.execute(`UPDATE ${quote(definition.name)} SET ${assignments.join(", ")} WHERE ${predicates.join(" OR ")}`, params);
1042
+ },
1043
+ delete: (input) => {
1044
+ const keys = requireUniqueKey("DELETE");
1045
+ assertKeys(input.keys, "delete");
1046
+ const params = [];
1047
+ const predicates = input.keys.map((keyValue) => {
1048
+ const conditions = keyParts(keyValue).map((part, keyIndex) => {
1049
+ params.push(part);
1050
+ return `${quote(keys[keyIndex] ?? "")} = $${String(params.length)}`;
1051
+ });
1052
+ return `(${conditions.join(" AND ")})`;
1053
+ });
1054
+ return database.execute(`DELETE FROM ${quote(definition.name)} WHERE ${predicates.join(" OR ")}`, params);
1055
+ },
1056
+ rows: async () => (await database.query(`SELECT ${columnNames.map(quote).join(", ")} FROM ${quote(definition.name)}`)).rows
1057
+ };
1212
1058
  }
1213
- /** Applies one table's alteration steps onto its current column records. */
1214
- export function applyColumnSteps(record, steps, createId) {
1215
- const columns = structuredClone(record.columns);
1216
- for (const step of steps) {
1217
- if (step.kind === "rename-column") {
1218
- const target = columns.find(({ name }) => name === step.from);
1219
- if (target !== undefined)
1220
- target.name = step.to;
1221
- continue;
1222
- }
1223
- if (step.kind === "add-column") {
1224
- if (!columns.some(({ name }) => name === step.columnName)) {
1225
- columns.push({
1226
- id: createId(),
1227
- name: step.columnName,
1228
- type: step.definition.type,
1229
- ...(step.definition.integer ? { integer: true } : {}),
1230
- ...(step.definition.sqlDomain === undefined
1231
- ? {}
1232
- : { sqlDomain: structuredClone(step.definition.sqlDomain) }),
1233
- // A backfill is what lets the column be non-nullable: every row has a value, either
1234
- // written or substituted at read time.
1235
- nullable: step.backfill === undefined ? true : step.definition.isNullable,
1236
- ...(step.backfill === undefined
1237
- ? {}
1238
- : {
1239
- backfill: storedBackfill(step.definition, step.backfill),
1240
- }),
1241
- ...(step.definition.defaultSpec === undefined
1242
- ? {}
1243
- : { defaultValue: step.definition.defaultSpec }),
1244
- ...(step.definition.enumValues === undefined
1245
- ? {}
1246
- : { enumValues: [...step.definition.enumValues] }),
1247
- });
1248
- }
1249
- continue;
1250
- }
1251
- if (step.kind === "widen-enum") {
1252
- const target = columns.find(({ name }) => name === step.columnName);
1253
- if (target !== undefined) {
1254
- if (step.enumValues === null)
1255
- delete target.enumValues;
1256
- else
1257
- target.enumValues = [...step.enumValues];
1258
- }
1259
- continue;
1260
- }
1261
- if (step.kind === "widen-nullable") {
1262
- const target = columns.find(({ name }) => name === step.columnName);
1263
- if (target !== undefined)
1264
- target.nullable = true;
1265
- continue;
1266
- }
1267
- if (step.kind === "drop-column") {
1268
- // The column record goes; the stored blocks stay until compaction rewrites the segments
1269
- // that hold them, which is what keeps the drop a metadata step.
1270
- const index = columns.findIndex(({ name }) => name === step.columnName);
1271
- if (index !== -1)
1272
- columns.splice(index, 1);
1273
- continue;
1274
- }
1275
- if (step.kind === "tighten-nullable") {
1276
- // The proof happens before this runs; see MinnowDatabase#columnHoldsNull.
1277
- const target = columns.find(({ name }) => name === step.columnName);
1278
- if (target !== undefined)
1279
- target.nullable = false;
1280
- continue;
1281
- }
1282
- if (step.kind === "set-auto-increment") {
1283
- const target = columns.find(({ name }) => name === step.columnName);
1284
- if (target !== undefined) {
1285
- if (step.enabled)
1286
- target.defaultValue = { kind: "autoincrement" };
1287
- else
1288
- delete target.defaultValue;
1289
- }
1290
- continue;
1291
- }
1292
- if (step.kind === "alter-default") {
1293
- const target = columns.find(({ name }) => name === step.columnName);
1294
- if (target !== undefined) {
1295
- if (step.defaultValue === null)
1296
- delete target.defaultValue;
1297
- else
1298
- target.defaultValue = step.defaultValue;
1299
- }
1300
- continue;
1301
- }
1302
- if (step.kind === "alter-generated") {
1303
- const target = columns.find(({ name }) => name === step.columnName);
1304
- if (target !== undefined) {
1305
- if (step.generatedValue === null)
1306
- delete target.generatedValue;
1307
- else
1308
- target.generatedValue = step.generatedValue;
1309
- }
1310
- }
1059
+ function applyColumnSteps(record, steps, createId) {
1060
+ const columns = structuredClone(record.columns);
1061
+ for (const step of steps) {
1062
+ if (step.kind === "rename-column") {
1063
+ const target = columns.find(({ name }) => name === step.from);
1064
+ if (target !== void 0)
1065
+ target.name = step.to;
1066
+ continue;
1067
+ }
1068
+ if (step.kind === "add-column") {
1069
+ if (!columns.some(({ name }) => name === step.columnName)) {
1070
+ columns.push({
1071
+ id: createId(),
1072
+ name: step.columnName,
1073
+ type: step.definition.type,
1074
+ ...step.definition.integer ? { integer: true } : {},
1075
+ ...step.definition.sqlDomain === void 0 ? {} : { sqlDomain: structuredClone(step.definition.sqlDomain) },
1076
+ nullable: step.backfill === void 0 ? true : step.definition.isNullable,
1077
+ ...step.backfill === void 0 ? {} : {
1078
+ backfill: storedBackfill(step.definition, step.backfill)
1079
+ },
1080
+ ...step.definition.defaultSpec === void 0 ? {} : { defaultValue: step.definition.defaultSpec },
1081
+ ...step.definition.enumValues === void 0 ? {} : { enumValues: [...step.definition.enumValues] }
1082
+ });
1083
+ }
1084
+ continue;
1085
+ }
1086
+ if (step.kind === "widen-enum") {
1087
+ const target = columns.find(({ name }) => name === step.columnName);
1088
+ if (target !== void 0) {
1089
+ if (step.enumValues === null)
1090
+ delete target.enumValues;
1091
+ else
1092
+ target.enumValues = [...step.enumValues];
1093
+ }
1094
+ continue;
1311
1095
  }
1312
- return columns;
1096
+ if (step.kind === "widen-nullable") {
1097
+ const target = columns.find(({ name }) => name === step.columnName);
1098
+ if (target !== void 0)
1099
+ target.nullable = true;
1100
+ continue;
1101
+ }
1102
+ if (step.kind === "drop-column") {
1103
+ const index = columns.findIndex(({ name }) => name === step.columnName);
1104
+ if (index !== -1)
1105
+ columns.splice(index, 1);
1106
+ continue;
1107
+ }
1108
+ if (step.kind === "tighten-nullable") {
1109
+ const target = columns.find(({ name }) => name === step.columnName);
1110
+ if (target !== void 0)
1111
+ target.nullable = false;
1112
+ continue;
1113
+ }
1114
+ if (step.kind === "set-auto-increment") {
1115
+ const target = columns.find(({ name }) => name === step.columnName);
1116
+ if (target !== void 0) {
1117
+ if (step.enabled)
1118
+ target.defaultValue = { kind: "autoincrement" };
1119
+ else
1120
+ delete target.defaultValue;
1121
+ }
1122
+ continue;
1123
+ }
1124
+ if (step.kind === "alter-default") {
1125
+ const target = columns.find(({ name }) => name === step.columnName);
1126
+ if (target !== void 0) {
1127
+ if (step.defaultValue === null)
1128
+ delete target.defaultValue;
1129
+ else
1130
+ target.defaultValue = step.defaultValue;
1131
+ }
1132
+ continue;
1133
+ }
1134
+ if (step.kind === "alter-generated") {
1135
+ const target = columns.find(({ name }) => name === step.columnName);
1136
+ if (target !== void 0) {
1137
+ if (step.generatedValue === null)
1138
+ delete target.generatedValue;
1139
+ else
1140
+ target.generatedValue = step.generatedValue;
1141
+ }
1142
+ }
1143
+ }
1144
+ return columns;
1313
1145
  }
1314
1146
  function columnDefaultsEqual(left, right) {
1315
- if (left === undefined || right === undefined)
1316
- return left === right;
1317
- if (left.kind !== right.kind)
1318
- return false;
1319
- if (left.kind === "expression" && right.kind === "expression")
1320
- return left.sql === right.sql;
1321
- if (left.kind !== "literal" || right.kind !== "literal")
1322
- return true;
1323
- if (left.value instanceof Date || right.value instanceof Date) {
1324
- return (left.value instanceof Date &&
1325
- right.value instanceof Date &&
1326
- dateMilliseconds(left.value) === dateMilliseconds(right.value));
1327
- }
1328
- return left.value === right.value;
1147
+ if (left === void 0 || right === void 0)
1148
+ return left === right;
1149
+ if (left.kind !== right.kind)
1150
+ return false;
1151
+ if (left.kind === "expression" && right.kind === "expression")
1152
+ return left.sql === right.sql;
1153
+ if (left.kind !== "literal" || right.kind !== "literal")
1154
+ return true;
1155
+ if (left.value instanceof Date || right.value instanceof Date) {
1156
+ return left.value instanceof Date && right.value instanceof Date && dateMilliseconds(left.value) === dateMilliseconds(right.value);
1157
+ }
1158
+ return left.value === right.value;
1329
1159
  }
1160
+ export {
1161
+ applyColumnSteps,
1162
+ assertColumnDroppable,
1163
+ column,
1164
+ columnFromState,
1165
+ compileGeneratedColumnExpression,
1166
+ declaredForeignKeys,
1167
+ foreignKeyName,
1168
+ isDestructiveStep,
1169
+ planMigration,
1170
+ schema,
1171
+ table,
1172
+ typedTable,
1173
+ view
1174
+ };