@minnowdb/core 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.md +21 -32
  2. package/dist/engine/artifact-cache.d.ts +2 -0
  3. package/dist/engine/artifact-cache.d.ts.map +1 -1
  4. package/dist/engine/artifact-cache.js +5 -0
  5. package/dist/engine/artifact-cache.js.map +1 -1
  6. package/dist/engine/batch.d.ts +6 -1
  7. package/dist/engine/batch.d.ts.map +1 -1
  8. package/dist/engine/batch.js +17 -6
  9. package/dist/engine/batch.js.map +1 -1
  10. package/dist/engine/catalog.d.ts +22 -2
  11. package/dist/engine/catalog.d.ts.map +1 -1
  12. package/dist/engine/catalog.js +30 -3
  13. package/dist/engine/catalog.js.map +1 -1
  14. package/dist/engine/client.d.ts +2 -0
  15. package/dist/engine/client.d.ts.map +1 -1
  16. package/dist/engine/client.js +20 -10
  17. package/dist/engine/client.js.map +1 -1
  18. package/dist/engine/database.d.ts +67 -5
  19. package/dist/engine/database.d.ts.map +1 -1
  20. package/dist/engine/database.js +3508 -369
  21. package/dist/engine/database.js.map +1 -1
  22. package/dist/engine/defaults.d.ts +4 -7
  23. package/dist/engine/defaults.d.ts.map +1 -1
  24. package/dist/engine/defaults.js +47 -21
  25. package/dist/engine/defaults.js.map +1 -1
  26. package/dist/engine/fts.d.ts.map +1 -1
  27. package/dist/engine/fts.js +2 -0
  28. package/dist/engine/fts.js.map +1 -1
  29. package/dist/engine/index.d.ts +1 -0
  30. package/dist/engine/index.d.ts.map +1 -1
  31. package/dist/engine/index.js +1 -0
  32. package/dist/engine/index.js.map +1 -1
  33. package/dist/engine/live.d.ts.map +1 -1
  34. package/dist/engine/live.js +10 -2
  35. package/dist/engine/live.js.map +1 -1
  36. package/dist/engine/optimizer.d.ts.map +1 -1
  37. package/dist/engine/optimizer.js +258 -23
  38. package/dist/engine/optimizer.js.map +1 -1
  39. package/dist/engine/query-cache.d.ts +1 -1
  40. package/dist/engine/query-cache.d.ts.map +1 -1
  41. package/dist/engine/query-cache.js +3 -1
  42. package/dist/engine/query-cache.js.map +1 -1
  43. package/dist/engine/query.d.ts +130 -22
  44. package/dist/engine/query.d.ts.map +1 -1
  45. package/dist/engine/query.js +1344 -241
  46. package/dist/engine/query.js.map +1 -1
  47. package/dist/engine/schema-wire.d.ts +6 -2
  48. package/dist/engine/schema-wire.d.ts.map +1 -1
  49. package/dist/engine/schema-wire.js +40 -33
  50. package/dist/engine/schema-wire.js.map +1 -1
  51. package/dist/engine/schema.d.ts +157 -76
  52. package/dist/engine/schema.d.ts.map +1 -1
  53. package/dist/engine/schema.js +548 -103
  54. package/dist/engine/schema.js.map +1 -1
  55. package/dist/engine/sort-keys.d.ts +4 -3
  56. package/dist/engine/sort-keys.d.ts.map +1 -1
  57. package/dist/engine/sort-keys.js +42 -27
  58. package/dist/engine/sort-keys.js.map +1 -1
  59. package/dist/engine/sql-domains.d.ts +26 -0
  60. package/dist/engine/sql-domains.d.ts.map +1 -0
  61. package/dist/engine/sql-domains.js +421 -0
  62. package/dist/engine/sql-domains.js.map +1 -0
  63. package/dist/engine/sql-driver.d.ts +19 -0
  64. package/dist/engine/sql-driver.d.ts.map +1 -0
  65. package/dist/engine/sql-driver.js +2 -0
  66. package/dist/engine/sql-driver.js.map +1 -0
  67. package/dist/engine/sql-json.d.ts +7 -8
  68. package/dist/engine/sql-json.d.ts.map +1 -1
  69. package/dist/engine/sql-json.js +28 -14
  70. package/dist/engine/sql-json.js.map +1 -1
  71. package/dist/engine/sql-semantics.d.ts +2 -0
  72. package/dist/engine/sql-semantics.d.ts.map +1 -1
  73. package/dist/engine/sql-semantics.js +69 -3
  74. package/dist/engine/sql-semantics.js.map +1 -1
  75. package/dist/engine/vector.d.ts +5 -1
  76. package/dist/engine/vector.d.ts.map +1 -1
  77. package/dist/engine/vector.js +433 -61
  78. package/dist/engine/vector.js.map +1 -1
  79. package/dist/engine/worker-host.d.ts +1 -0
  80. package/dist/engine/worker-host.d.ts.map +1 -1
  81. package/dist/engine/worker-host.js +4 -0
  82. package/dist/engine/worker-host.js.map +1 -1
  83. package/dist/plan/index.d.ts +3 -3
  84. package/dist/plan/index.js +3 -3
  85. package/dist/storage/indexeddb.d.ts +44 -5
  86. package/dist/storage/indexeddb.d.ts.map +1 -1
  87. package/dist/storage/indexeddb.js +738 -175
  88. package/dist/storage/indexeddb.js.map +1 -1
  89. package/dist/storage/memory.d.ts +27 -10
  90. package/dist/storage/memory.d.ts.map +1 -1
  91. package/dist/storage/memory.js +67 -18
  92. package/dist/storage/memory.js.map +1 -1
  93. package/dist/storage/opfs/leader.d.ts +28 -6
  94. package/dist/storage/opfs/leader.d.ts.map +1 -1
  95. package/dist/storage/opfs/leader.js +301 -34
  96. package/dist/storage/opfs/leader.js.map +1 -1
  97. package/dist/storage/opfs/rpc.d.ts.map +1 -1
  98. package/dist/storage/opfs/rpc.js +2 -1
  99. package/dist/storage/opfs/rpc.js.map +1 -1
  100. package/dist/storage/opfs/store.d.ts +34 -5
  101. package/dist/storage/opfs/store.d.ts.map +1 -1
  102. package/dist/storage/opfs/store.js +41 -0
  103. package/dist/storage/opfs/store.js.map +1 -1
  104. package/dist/storage/snapshot.d.ts +14 -0
  105. package/dist/storage/snapshot.d.ts.map +1 -1
  106. package/dist/storage/snapshot.js +44 -0
  107. package/dist/storage/snapshot.js.map +1 -1
  108. package/dist/storage/toolkit/index.d.ts +1 -1
  109. package/dist/storage/toolkit/index.d.ts.map +1 -1
  110. package/dist/storage/toolkit/index.js +1 -1
  111. package/dist/storage/toolkit/index.js.map +1 -1
  112. package/dist/storage/toolkit/record-core.d.ts +19 -5
  113. package/dist/storage/toolkit/record-core.d.ts.map +1 -1
  114. package/dist/storage/toolkit/record-core.js +353 -78
  115. package/dist/storage/toolkit/record-core.js.map +1 -1
  116. package/dist/storage/toolkit/wire.d.ts +15 -0
  117. package/dist/storage/toolkit/wire.d.ts.map +1 -1
  118. package/dist/storage/toolkit/wire.js +134 -0
  119. package/dist/storage/toolkit/wire.js.map +1 -1
  120. package/dist/storage/types.d.ts +213 -31
  121. package/dist/storage/types.d.ts.map +1 -1
  122. package/dist/storage/types.js +291 -20
  123. package/dist/storage/types.js.map +1 -1
  124. package/dist/testing/block-store-conformance.d.ts.map +1 -1
  125. package/dist/testing/block-store-conformance.js +153 -2
  126. package/dist/testing/block-store-conformance.js.map +1 -1
  127. package/dist/testing/index.d.ts +9 -0
  128. package/dist/testing/index.d.ts.map +1 -1
  129. package/dist/testing/index.js +23 -0
  130. package/dist/testing/index.js.map +1 -1
  131. package/dist/testing/simulator.d.ts +98 -0
  132. package/dist/testing/simulator.d.ts.map +1 -0
  133. package/dist/testing/simulator.js +560 -0
  134. package/dist/testing/simulator.js.map +1 -0
  135. package/dist/testing/sqllogictest.d.ts +90 -0
  136. package/dist/testing/sqllogictest.d.ts.map +1 -0
  137. package/dist/testing/sqllogictest.js +435 -0
  138. package/dist/testing/sqllogictest.js.map +1 -0
  139. package/dist/transactions/index.d.ts +18 -3
  140. package/dist/transactions/index.d.ts.map +1 -1
  141. package/dist/transactions/index.js +64 -8
  142. package/dist/transactions/index.js.map +1 -1
  143. package/package.json +5 -3
  144. package/postgres-feature-profile.json +223 -0
  145. package/sql-feature-matrix.json +172 -252
@@ -1,5 +1,6 @@
1
- import { validateColumnDefault, validateEnumValues, } from "../storage/index.js";
2
- import { compileCheckExpression, expressionColumns } from "./query.js";
1
+ import { validateColumnDefault, validateEnumValues, validateSqlDomain, } from "../storage/index.js";
2
+ import { compileCheckExpression, expressionColumns, validateDefaultExpression, } from "./query.js";
3
+ import { externalSqlDomainValue, normalizeSqlDomainValue } from "./sql-domains.js";
3
4
  /**
4
5
  * The constraint name `migrate()` gives a declared relation. It matches the name the SQL parser
5
6
  * derives for an unnamed inline REFERENCES, so a table built either way has the same catalog.
@@ -7,12 +8,20 @@ import { compileCheckExpression, expressionColumns } from "./query.js";
7
8
  export function foreignKeyName(tableName, columnName) {
8
9
  return `${tableName}_${columnName}_fkey`;
9
10
  }
10
- function defaultSpecFromArg(type, value) {
11
+ function defaultSpecFromArg(type, value, sqlDomain) {
12
+ if (sqlDomain?.kind === "numeric") {
13
+ if ((typeof value !== "number" || !Number.isFinite(value)) && typeof value !== "string") {
14
+ throw new TypeError("NUMERIC default must be a finite number or decimal string");
15
+ }
16
+ normalizeSqlDomainValue(sqlDomain, value);
17
+ return { kind: "literal", value };
18
+ }
11
19
  switch (type) {
12
20
  case "datetime":
13
- if (value !== "now")
14
- throw new TypeError('Datetime columns default with "now"');
15
- return { kind: "now" };
21
+ if (!(value instanceof Date) || !Number.isFinite(value.getTime())) {
22
+ throw new TypeError("Default literal must be a valid Date");
23
+ }
24
+ return { kind: "literal", value: new Date(value.getTime()) };
16
25
  case "string":
17
26
  if (typeof value !== "string")
18
27
  throw new TypeError("Default literal must be a string");
@@ -34,70 +43,135 @@ function createColumn(type, state = {}) {
34
43
  type,
35
44
  isNullable: (state.isNullable ?? false),
36
45
  isUnique: (state.isUnique ?? false),
37
- hasDefault: (state.defaultSpec !== undefined || state.defaultFn !== undefined),
46
+ hasDefault: (state.defaultSpec !== undefined),
47
+ integer: state.integer ?? false,
48
+ ...(state.sqlDomain === undefined ? {} : { sqlDomain: state.sqlDomain }),
38
49
  ...(state.defaultSpec === undefined ? {} : { defaultSpec: state.defaultSpec }),
39
- ...(state.defaultFn === undefined
40
- ? {}
41
- : { defaultFn: state.defaultFn }),
42
50
  ...(state.renamedFromName === undefined ? {} : { renamedFromName: state.renamedFromName }),
43
51
  ...(state.reference === undefined ? {} : { reference: state.reference }),
44
52
  ...(state.enumValues === undefined ? {} : { enumValues: state.enumValues }),
45
- ...(state.backfillValue === undefined
46
- ? {}
47
- : { backfillValue: state.backfillValue }),
53
+ ...(state.backfillValue === undefined ? {} : { backfillValue: state.backfillValue }),
48
54
  };
49
55
  return {
50
56
  ...base,
51
- nullable: () => createColumn(type, { ...state, isNullable: true }),
52
- unique: () => createColumn(type, { ...state, isUnique: true }),
53
- renamedFrom: (name) => createColumn(type, { ...state, renamedFromName: name }),
54
- backfill: (value) => createColumn(type, { ...state, backfillValue: value }),
55
- references: (table, referencedColumn, options = {}) => createColumn(type, {
57
+ nullable: () => createColumn(type, {
56
58
  ...state,
57
- reference: {
58
- table,
59
- column: referencedColumn,
60
- onDelete: options.onDelete ?? "restrict",
61
- },
59
+ isNullable: true,
60
+ }),
61
+ unique: () => createColumn(type, {
62
+ ...state,
63
+ isUnique: true,
62
64
  }),
65
+ renamedFrom: (name) => {
66
+ validateSchemaName(name, "Rename source");
67
+ return createColumn(type, { ...state, renamedFromName: name });
68
+ },
69
+ backfill: (value) => createColumn(type, { ...state, backfillValue: value }),
70
+ references: (table, referencedColumn, options = {}) => {
71
+ validateSchemaName(table, "Referenced table");
72
+ validateSchemaName(referencedColumn, "Referenced column");
73
+ return createColumn(type, {
74
+ ...state,
75
+ reference: {
76
+ table,
77
+ column: referencedColumn,
78
+ onDelete: options.onDelete ?? "restrict",
79
+ },
80
+ });
81
+ },
63
82
  default: ((value) => {
64
- // A declared default is one thing: a function replaces any spec and vice versa.
65
- const cleared = { ...state };
66
- delete cleared.defaultSpec;
67
- delete cleared.defaultFn;
68
- return typeof value === "function"
69
- ? createColumn(type, { ...cleared, defaultFn: value })
70
- : createColumn(type, { ...cleared, defaultSpec: defaultSpecFromArg(type, value) });
83
+ return createColumn(type, {
84
+ ...state,
85
+ defaultSpec: defaultSpecFromArg(type, value, state.sqlDomain),
86
+ });
87
+ }),
88
+ defaultSql: ((sql) => {
89
+ const expression = sql.trim();
90
+ if (expression.length === 0 || expression !== sql) {
91
+ throw new TypeError("Default SQL must be a trimmed non-empty expression");
92
+ }
93
+ return createColumn(type, {
94
+ ...state,
95
+ defaultSpec: { kind: "expression", sql: expression },
96
+ });
71
97
  }),
72
98
  autoIncrement: (() => {
73
99
  if (type !== "number") {
74
100
  throw new TypeError("Auto-increment requires a number column");
75
101
  }
76
- const cleared = { ...state };
77
- delete cleared.defaultFn;
78
- return createColumn(type, { ...cleared, defaultSpec: { kind: "autoincrement" } });
102
+ return createColumn(type, {
103
+ ...state,
104
+ defaultSpec: { kind: "autoincrement" },
105
+ });
79
106
  }),
80
107
  };
81
108
  }
109
+ function validateSchemaName(name, kind) {
110
+ if (name.length === 0)
111
+ throw new TypeError(`${kind} name cannot be empty`);
112
+ if (name.trim() !== name) {
113
+ throw new TypeError(`${kind} name cannot start or end with whitespace: ${JSON.stringify(name)}`);
114
+ }
115
+ }
82
116
  /**
83
117
  * Rebuilds a column carrying an exact default spec — the wire layer's escape hatch, since the
84
- * public `.default()` interprets "now" on datetime columns and cannot express auto-increment.
118
+ * public `.default()` accepts literals and cannot express auto-increment catalog metadata.
85
119
  */
86
120
  export function columnWithDefaultSpec(base, spec) {
87
- return createColumn(base.type, {
121
+ return columnFromState({
122
+ type: base.type,
88
123
  isNullable: base.isNullable,
89
124
  isUnique: base.isUnique,
125
+ integer: base.integer ?? false,
126
+ ...(base.sqlDomain === undefined ? {} : { sqlDomain: base.sqlDomain }),
90
127
  ...(base.renamedFromName === undefined ? {} : { renamedFromName: base.renamedFromName }),
91
128
  ...(base.reference === undefined ? {} : { reference: base.reference }),
92
129
  ...(base.enumValues === undefined ? {} : { enumValues: base.enumValues }),
93
130
  defaultSpec: spec,
94
131
  });
95
132
  }
133
+ /** Rebuilds a fluent column from structured-clone-safe metadata. */
134
+ export function columnFromState(state) {
135
+ return createColumn(state.type, {
136
+ isNullable: state.isNullable,
137
+ isUnique: state.isUnique,
138
+ integer: state.integer ?? false,
139
+ ...(state.sqlDomain === undefined ? {} : { sqlDomain: state.sqlDomain }),
140
+ ...(state.renamedFromName === undefined ? {} : { renamedFromName: state.renamedFromName }),
141
+ ...(state.reference === undefined ? {} : { reference: state.reference }),
142
+ ...(state.defaultSpec === undefined ? {} : { defaultSpec: state.defaultSpec }),
143
+ ...(state.enumValues === undefined ? {} : { enumValues: state.enumValues }),
144
+ ...(state.backfillValue === undefined ? {} : { backfillValue: state.backfillValue }),
145
+ });
146
+ }
96
147
  export const column = {
97
148
  boolean: () => createColumn("boolean"),
98
149
  number: () => createColumn("number"),
150
+ /** Exact safe-integer semantics, matching SQL INTEGER/SMALLINT/BIGINT. */
151
+ integer: () => createColumn("number", { integer: true }),
99
152
  string: () => createColumn("string"),
100
153
  datetime: () => createColumn("datetime"),
154
+ /** Exact decimal SQL NUMERIC. Selects return strings; writes accept strings or numbers. */
155
+ numeric: (options = {}) => {
156
+ const sqlDomain = validateSqlDomain({ kind: "numeric", ...options }, "numeric column");
157
+ return createColumn("string", { sqlDomain });
158
+ },
159
+ json: () => createColumn("string", { sqlDomain: { kind: "json" } }),
160
+ jsonb: () => createColumn("string", { sqlDomain: { kind: "jsonb" } }),
161
+ uuid: () => createColumn("string", { sqlDomain: { kind: "uuid" } }),
162
+ time: () => createColumn("string", { sqlDomain: { kind: "time" } }),
163
+ interval: () => createColumn("string", { sqlDomain: { kind: "interval" } }),
164
+ /** JSON array text at the JavaScript boundary, with the SQL element type retained in metadata. */
165
+ array: (element) => createColumn("string", {
166
+ sqlDomain: validateSqlDomain({ kind: "array", element }, "array column"),
167
+ }),
168
+ /** A named SQL enum domain, distinct from the lightweight `column.enum()` restriction. */
169
+ sqlEnum: (name, values) => {
170
+ validateSchemaName(name, "Enum type");
171
+ return createColumn("string", {
172
+ sqlDomain: validateSqlDomain({ kind: "enum", name, values: validateEnumValues(values, name) }, name),
173
+ });
174
+ },
101
175
  /**
102
176
  * A string column restricted to a closed set of values, typed as their literal union:
103
177
  *
@@ -112,7 +186,35 @@ export const column = {
112
186
  */
113
187
  enum: (values) => createColumn("string", { enumValues: validateEnumValues(values, "enum column") }),
114
188
  };
189
+ function validateDeclaredColumnValue(definition, value, context, label) {
190
+ if (definition.sqlDomain !== undefined) {
191
+ try {
192
+ normalizeSqlDomainValue(definition.sqlDomain, value);
193
+ }
194
+ catch (error) {
195
+ throw new TypeError(`${label} does not fit ${context}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
196
+ }
197
+ return;
198
+ }
199
+ const matches = definition.type === "datetime"
200
+ ? value instanceof Date && Number.isFinite(value.getTime())
201
+ : definition.type === "number"
202
+ ? typeof value === "number" &&
203
+ Number.isFinite(value) &&
204
+ (!definition.integer || Number.isSafeInteger(value))
205
+ : typeof value === definition.type;
206
+ if (!matches) {
207
+ const expected = definition.integer ? "safe integer" : definition.type;
208
+ throw new TypeError(`${label} value must be a ${expected}: ${context}`);
209
+ }
210
+ if (definition.enumValues !== undefined &&
211
+ typeof value === "string" &&
212
+ !definition.enumValues.includes(value)) {
213
+ throw new TypeError(`${label} value must be one of: ${definition.enumValues.join(", ")} (${context})`);
214
+ }
215
+ }
115
216
  export function table(name, columns, options = {}) {
217
+ validateSchemaName(name, "Table");
116
218
  const entries = Object.entries(columns);
117
219
  if (entries.length === 0)
118
220
  throw new TypeError(`Table ${name} needs at least one column`);
@@ -121,26 +223,48 @@ export function table(name, columns, options = {}) {
121
223
  throw new TypeError(`Table ${name} may name at most one unique column`);
122
224
  }
123
225
  const uniqueEntry = uniqueColumns[0];
226
+ const primaryKey = options.primaryKey ?? [];
227
+ if (uniqueEntry !== undefined && primaryKey.length > 0) {
228
+ throw new TypeError(`Table ${name} cannot declare both .unique() and a table primary key`);
229
+ }
230
+ if (primaryKey.length === 0 && options.primaryKey !== undefined) {
231
+ throw new TypeError(`Table ${name} primary key needs at least one column`);
232
+ }
233
+ if (new Set(primaryKey).size !== primaryKey.length) {
234
+ throw new TypeError(`Table ${name} primary key columns must be distinct`);
235
+ }
236
+ for (const columnName of primaryKey) {
237
+ const definition = columns[columnName];
238
+ if (definition === undefined) {
239
+ throw new TypeError(`PRIMARY KEY column not found: ${name}.${columnName}`);
240
+ }
241
+ if (definition.isNullable) {
242
+ throw new TypeError(`PRIMARY KEY cannot be nullable: ${name}.${columnName}`);
243
+ }
244
+ }
124
245
  if (uniqueEntry?.[1].isNullable === true) {
125
246
  throw new TypeError(`Table ${name} unique column must not be nullable: ${uniqueEntry[0]}`);
126
247
  }
127
248
  for (const [columnName, definition] of entries) {
128
- if (definition.defaultFn !== undefined && definition.isNullable) {
129
- throw new TypeError(`Defaults require a non-nullable column: ${columnName}`);
249
+ validateSchemaName(columnName, "Column");
250
+ if (definition.integer && definition.type !== "number") {
251
+ throw new TypeError(`Integer domain requires a number column: ${name}.${columnName}`);
252
+ }
253
+ if (definition.sqlDomain !== undefined) {
254
+ if (definition.type !== "string") {
255
+ throw new TypeError(`SQL domains require string storage: ${name}.${columnName}`);
256
+ }
257
+ validateSqlDomain(definition.sqlDomain, `${name}.${columnName}`);
258
+ }
259
+ if (definition.integer && definition.sqlDomain !== undefined) {
260
+ throw new TypeError(`A column cannot be both integer and a SQL domain: ${name}.${columnName}`);
261
+ }
262
+ if (definition.enumValues !== undefined && definition.sqlDomain !== undefined) {
263
+ throw new TypeError(`A column cannot have both enum restrictions and a SQL domain: ${name}.${columnName}`);
130
264
  }
131
265
  const backfill = definition.backfillValue;
132
266
  if (backfill !== undefined && typeof backfill !== "function") {
133
- const matches = definition.type === "datetime"
134
- ? backfill instanceof Date
135
- : typeof backfill === definition.type;
136
- if (!matches) {
137
- throw new TypeError(`Backfill value must be a ${definition.type}: ${name}.${columnName}`);
138
- }
139
- if (definition.enumValues !== undefined &&
140
- typeof backfill === "string" &&
141
- !definition.enumValues.includes(backfill)) {
142
- throw new TypeError(`Backfill value must be one of: ${definition.enumValues.join(", ")} (${name}.${columnName})`);
143
- }
267
+ validateDeclaredColumnValue(definition, backfill, `${name}.${columnName}`, "Backfill");
144
268
  }
145
269
  if (backfill !== undefined && definition.isNullable) {
146
270
  throw new TypeError(`A nullable column needs no backfill: ${name}.${columnName}. Rows without it already read NULL.`);
@@ -154,16 +278,57 @@ export function table(name, columns, options = {}) {
154
278
  validateColumnDefault({
155
279
  name: columnName,
156
280
  type: definition.type,
281
+ ...(definition.integer ? { integer: true } : {}),
282
+ ...(definition.sqlDomain === undefined ? {} : { sqlDomain: definition.sqlDomain }),
157
283
  nullable: definition.isNullable,
158
- isUniqueKey: definition.isUnique,
284
+ isUniqueKey: definition.isUnique || (primaryKey.length === 1 && primaryKey[0] === columnName),
159
285
  ...(definition.enumValues === undefined ? {} : { enumValues: definition.enumValues }),
160
286
  }, spec);
287
+ if (spec.kind === "expression") {
288
+ validateDefaultExpression(spec.sql, {
289
+ name: `${name}.${columnName}`,
290
+ type: definition.type,
291
+ ...(definition.sqlDomain === undefined ? {} : { sqlDomain: definition.sqlDomain }),
292
+ });
293
+ }
294
+ if (spec.kind === "literal" && definition.sqlDomain !== undefined) {
295
+ normalizeSqlDomainValue(definition.sqlDomain, spec.value);
296
+ }
297
+ }
298
+ const foreignKeys = options.foreignKeys ?? [];
299
+ const foreignKeyNames = new Set();
300
+ for (const key of foreignKeys) {
301
+ validateSchemaName(key.name, "FOREIGN KEY");
302
+ if (foreignKeyNames.has(key.name)) {
303
+ throw new TypeError(`Duplicate FOREIGN KEY in table ${name}: ${key.name}`);
304
+ }
305
+ foreignKeyNames.add(key.name);
306
+ if (key.columns.length === 0 || new Set(key.columns).size !== key.columns.length) {
307
+ throw new TypeError(`FOREIGN KEY ${key.name} needs distinct child columns`);
308
+ }
309
+ if (key.references.columns.length === 0 ||
310
+ new Set(key.references.columns).size !== key.references.columns.length) {
311
+ throw new TypeError(`FOREIGN KEY ${key.name} needs distinct parent columns`);
312
+ }
313
+ if (key.columns.length !== key.references.columns.length) {
314
+ throw new TypeError(`FOREIGN KEY ${key.name} has different child and parent arity`);
315
+ }
316
+ validateSchemaName(key.references.table, "Referenced table");
317
+ key.references.columns.forEach((columnName) => validateSchemaName(columnName, "Referenced column"));
318
+ for (const columnName of key.columns) {
319
+ const definition = columns[columnName];
320
+ if (definition === undefined) {
321
+ throw new TypeError(`FOREIGN KEY ${key.name} names an unknown column: ${name}.${columnName}`);
322
+ }
323
+ if ((key.onDelete ?? "restrict") === "set null" && !definition.isNullable) {
324
+ throw new TypeError(`FOREIGN KEY ${key.name} cannot SET NULL a NOT NULL column`);
325
+ }
326
+ }
161
327
  }
162
328
  const checks = options.checks ?? [];
163
329
  const checkNames = new Set();
164
330
  for (const check of checks) {
165
- if (check.name.length === 0)
166
- throw new TypeError(`Table ${name} has an unnamed CHECK`);
331
+ validateSchemaName(check.name, "CHECK");
167
332
  if (checkNames.has(check.name)) {
168
333
  throw new TypeError(`Duplicate CHECK in table ${name}: ${check.name}`);
169
334
  }
@@ -176,6 +341,8 @@ export function table(name, columns, options = {}) {
176
341
  kind: "table",
177
342
  name,
178
343
  columns,
344
+ primaryKey,
345
+ foreignKeys,
179
346
  checks,
180
347
  "~standard": {
181
348
  version: 1,
@@ -188,27 +355,31 @@ export function table(name, columns, options = {}) {
188
355
  const issues = [];
189
356
  for (const [columnName, definition] of entries) {
190
357
  const columnValue = row[columnName];
191
- if (columnValue === undefined || columnValue === null) {
192
- // A default-bearing column may be omitted — the engine (or the facade, for
193
- // function defaults) fills it at insert time.
194
- if (!definition.isNullable &&
195
- definition.defaultSpec === undefined &&
196
- definition.defaultFn === undefined) {
358
+ if (columnValue === undefined) {
359
+ if (!definition.isNullable && definition.defaultSpec === undefined) {
197
360
  issues.push({ message: `Missing non-nullable column`, path: [columnName] });
198
361
  }
199
362
  continue;
200
363
  }
201
- const matches = definition.type === "datetime"
202
- ? columnValue instanceof Date
203
- : typeof columnValue === definition.type;
204
- if (!matches) {
205
- issues.push({ message: `Expected ${definition.type}`, path: [columnName] });
364
+ if (columnValue === null) {
365
+ if (!definition.isNullable) {
366
+ issues.push({ message: `Expected non-null value`, path: [columnName] });
367
+ }
368
+ continue;
369
+ }
370
+ try {
371
+ validateDeclaredColumnValue(definition, columnValue, `${name}.${columnName}`, "Value");
206
372
  }
207
- else if (definition.enumValues !== undefined &&
208
- typeof columnValue === "string" &&
209
- !definition.enumValues.includes(columnValue)) {
373
+ catch (error) {
374
+ const message = definition.enumValues !== undefined && typeof columnValue === "string"
375
+ ? `Expected one of: ${definition.enumValues.join(", ")}`
376
+ : definition.sqlDomain === undefined
377
+ ? `Expected ${definition.integer ? "safe integer" : definition.type}`
378
+ : error instanceof Error
379
+ ? error.message
380
+ : `Invalid value`;
210
381
  issues.push({
211
- message: `Expected one of: ${definition.enumValues.join(", ")}`,
382
+ message,
212
383
  path: [columnName],
213
384
  });
214
385
  }
@@ -241,6 +412,19 @@ export function declaredForeignKeys(definition) {
241
412
  onDelete: reference.onDelete,
242
413
  });
243
414
  }
415
+ for (const key of definition.foreignKeys) {
416
+ const childColumns = [...key.columns];
417
+ const parentColumns = [...key.references.columns];
418
+ keys.push({
419
+ name: key.name,
420
+ column: childColumns[0] ?? "",
421
+ ...(childColumns.length === 1 ? {} : { columns: childColumns }),
422
+ parentTable: key.references.table,
423
+ parentColumn: parentColumns[0] ?? "",
424
+ ...(parentColumns.length === 1 ? {} : { parentColumns }),
425
+ onDelete: key.onDelete ?? "restrict",
426
+ });
427
+ }
244
428
  return keys;
245
429
  }
246
430
  /**
@@ -255,16 +439,18 @@ export function declaredForeignKeys(definition) {
255
439
  * ```
256
440
  */
257
441
  export function view(name, definition) {
442
+ validateSchemaName(name, "View");
258
443
  const entries = Object.entries(definition.columns);
259
444
  if (entries.length === 0)
260
445
  throw new TypeError(`View ${name} needs at least one column`);
261
446
  if (definition.sql.trim().length === 0)
262
447
  throw new TypeError(`View ${name} has no query`);
263
448
  for (const [columnName, columnDefinition] of entries) {
449
+ validateSchemaName(columnName, "Column");
264
450
  if (columnDefinition.isUnique) {
265
451
  throw new TypeError(`A view column cannot be a unique key: ${name}.${columnName}`);
266
452
  }
267
- if (columnDefinition.defaultSpec !== undefined || columnDefinition.defaultFn !== undefined) {
453
+ if (columnDefinition.defaultSpec !== undefined) {
268
454
  throw new TypeError(`A view column cannot have a default: ${name}.${columnName}`);
269
455
  }
270
456
  }
@@ -286,14 +472,38 @@ export function schema(tables, options = {}) {
286
472
  names.add(definition.name);
287
473
  }
288
474
  for (const definition of tables) {
289
- for (const [columnName, columnDefinition] of Object.entries(definition.columns)) {
290
- const reference = columnDefinition.reference;
291
- if (reference === undefined)
292
- continue;
293
- const target = tables.find(({ name }) => name === reference.table);
294
- if (target === undefined || !(reference.column in target.columns)) {
295
- throw new TypeError(`Relation target does not exist: ${definition.name}.${columnName} -> ${reference.table}.${reference.column}`);
475
+ const declaredNames = new Set();
476
+ for (const key of declaredForeignKeys(definition)) {
477
+ if (declaredNames.has(key.name)) {
478
+ throw new TypeError(`Duplicate FOREIGN KEY in table ${definition.name}: ${key.name}`);
479
+ }
480
+ declaredNames.add(key.name);
481
+ const childNames = key.columns ?? [key.column];
482
+ const parentNames = key.parentColumns ?? [key.parentColumn];
483
+ const target = tables.find(({ name }) => name === key.parentTable);
484
+ if (target === undefined ||
485
+ childNames.some((columnName) => !(columnName in definition.columns)) ||
486
+ parentNames.some((columnName) => !(columnName in target.columns))) {
487
+ throw new TypeError(`Relation target does not exist: ${definition.name}.${childNames.join(",")} -> ${key.parentTable}.${parentNames.join(",")}`);
488
+ }
489
+ const targetPrimary = target.primaryKey.length > 0
490
+ ? [...target.primaryKey]
491
+ : Object.entries(target.columns).flatMap(([name, columnDefinition]) => columnDefinition.isUnique ? [name] : []);
492
+ if (parentNames.length !== targetPrimary.length ||
493
+ parentNames.some((columnName, index) => columnName !== targetPrimary[index])) {
494
+ throw new TypeError(`Relation target must be the unique key: ${definition.name}.${childNames.join(",")} -> ${key.parentTable}(${targetPrimary.join(", ")})`);
296
495
  }
496
+ childNames.forEach((childName, index) => {
497
+ const child = definition.columns[childName];
498
+ const parentName = parentNames[index];
499
+ const parent = parentName === undefined ? undefined : target.columns[parentName];
500
+ if (child === undefined ||
501
+ child.type !== parent?.type ||
502
+ child.integer !== parent.integer ||
503
+ JSON.stringify(child.sqlDomain ?? null) !== JSON.stringify(parent.sqlDomain ?? null)) {
504
+ throw new TypeError(`Relation types must match: ${definition.name}.${childName} and ${key.parentTable}.${String(parentName)}`);
505
+ }
506
+ });
297
507
  }
298
508
  }
299
509
  return { kind: "schema", tables, views };
@@ -367,16 +577,22 @@ export function isDestructiveStep(step) {
367
577
  * the catalog still points at it. Each of these would leave a constraint or key referring to a
368
578
  * column no longer there, so the drop is refused rather than silently invalidating them.
369
579
  */
370
- function assertColumnDroppable(record, column) {
580
+ export function assertColumnDroppable(record, column) {
371
581
  const where = `${record.name}.${column.name}`;
372
- if (record.uniqueKeyColumnId === column.id) {
582
+ if (record.uniqueKeyColumnId === column.id ||
583
+ (record.primaryKeyColumnIds ?? []).includes(column.id)) {
373
584
  throw new TypeError(`The unique key cannot be dropped: ${where}. Unique-key changes need the table recreated.`);
374
585
  }
375
586
  for (const key of record.foreignKeys) {
376
- if (key.column === column.name) {
587
+ if ((key.columns ?? [key.column]).includes(column.name)) {
377
588
  throw new TypeError(`FOREIGN KEY ${key.name} still uses this column: ${where}`);
378
589
  }
379
590
  }
591
+ for (const index of record.indexes ?? []) {
592
+ if (index.columns.some(({ name }) => name === column.name)) {
593
+ throw new TypeError(`Index ${index.name} still uses this column: ${where}`);
594
+ }
595
+ }
380
596
  for (const check of record.checks) {
381
597
  let referenced;
382
598
  try {
@@ -391,6 +607,25 @@ function assertColumnDroppable(record, column) {
391
607
  }
392
608
  }
393
609
  }
610
+ function assertColumnRenamable(catalog, record, column) {
611
+ const where = `${record.name}.${column.name}`;
612
+ for (const owner of catalog.tables) {
613
+ for (const key of owner.foreignKeys) {
614
+ const usesChild = owner.name === record.name && (key.columns ?? [key.column]).includes(column.name);
615
+ const usesParent = key.parentTable === record.name &&
616
+ (key.parentColumns ?? [key.parentColumn]).includes(column.name);
617
+ if (usesChild || usesParent) {
618
+ throw new TypeError(`FOREIGN KEY ${key.name} prevents renaming ${where}`);
619
+ }
620
+ }
621
+ }
622
+ for (const check of record.checks) {
623
+ const referenced = expressionColumns(compileCheckExpression(check.sql, check.name));
624
+ if (referenced.includes(column.name)) {
625
+ throw new TypeError(`CHECK ${check.name} prevents renaming ${where}`);
626
+ }
627
+ }
628
+ }
394
629
  /**
395
630
  * Freezes a column's backfill. A generator runs exactly once — here, while the migration is being
396
631
  * planned — so the catalog stores a value rather than a function and no two readers can disagree.
@@ -399,7 +634,27 @@ function resolveBackfill(definition) {
399
634
  const declared = definition.backfillValue;
400
635
  if (declared === undefined)
401
636
  return undefined;
402
- return typeof declared === "function" ? declared() : declared;
637
+ const value = typeof declared === "function" ? declared() : declared;
638
+ validateDeclaredColumnValue(definition, value, "added column", "Backfill");
639
+ return value;
640
+ }
641
+ function storedBackfill(definition, value) {
642
+ if (definition.sqlDomain === undefined)
643
+ return value;
644
+ const normalized = normalizeSqlDomainValue(definition.sqlDomain, value);
645
+ if (normalized === null)
646
+ throw new TypeError("A backfill cannot be NULL");
647
+ return normalized;
648
+ }
649
+ function backfillsEqual(left, right) {
650
+ const externalLeft = externalSqlDomainValue(left);
651
+ const externalRight = externalSqlDomainValue(right);
652
+ if (externalLeft instanceof Date || externalRight instanceof Date) {
653
+ return (externalLeft instanceof Date &&
654
+ externalRight instanceof Date &&
655
+ externalLeft.getTime() === externalRight.getTime());
656
+ }
657
+ return externalLeft === externalRight;
403
658
  }
404
659
  /**
405
660
  * Constraints on an existing table cannot change through a metadata-only step. Attaching a
@@ -409,7 +664,8 @@ function resolveBackfill(definition) {
409
664
  * discipline every other unprovable change gets: an explicit error naming the fix.
410
665
  */
411
666
  function assertConstraintsUnchanged(record, definition) {
412
- const describeKey = (key) => `${key.column} -> ${key.parentTable}.${key.parentColumn} ON DELETE ${key.onDelete}`;
667
+ const describeKey = (key) => `${(key.columns ?? [key.column]).join(",")} -> ${key.parentTable}.` +
668
+ `${(key.parentColumns ?? [key.parentColumn]).join(",")} ON DELETE ${key.onDelete}`;
413
669
  const existingKeys = new Map(record.foreignKeys.map((key) => [key.name, key]));
414
670
  const declaredKeys = new Map(declaredForeignKeys(definition).map((key) => [key.name, key]));
415
671
  for (const [name, declared] of declaredKeys) {
@@ -466,9 +722,13 @@ export function planMigration(catalog, definition, options = {}) {
466
722
  if (existing === undefined && from !== undefined) {
467
723
  const source = recordColumnsByName.get(from);
468
724
  if (source !== undefined) {
725
+ if (renameSources.has(from)) {
726
+ throw new TypeError(`Rename source is used more than once: ${tableDefinition.name}.${from}`);
727
+ }
469
728
  if (definedNames.has(from)) {
470
729
  throw new TypeError(`Rename source is still defined: ${tableDefinition.name}.${from}`);
471
730
  }
731
+ assertColumnRenamable(catalog, record, source);
472
732
  steps.push({
473
733
  kind: "rename-column",
474
734
  tableName: tableDefinition.name,
@@ -484,7 +744,7 @@ export function planMigration(catalog, definition, options = {}) {
484
744
  if (!columnDefinition.isNullable && backfill === undefined) {
485
745
  throw new TypeError(`Added columns must be nullable, or carry a backfill: ${tableDefinition.name}.${columnName}`);
486
746
  }
487
- if (columnDefinition.isUnique) {
747
+ if (columnDefinition.isUnique || tableDefinition.primaryKey.includes(columnName)) {
488
748
  throw new TypeError(`Unique keys cannot be added after creation: ${tableDefinition.name}.${columnName}`);
489
749
  }
490
750
  steps.push({
@@ -499,6 +759,23 @@ export function planMigration(catalog, definition, options = {}) {
499
759
  if (existing.type !== columnDefinition.type) {
500
760
  throw new TypeError(`Column types cannot change: ${tableDefinition.name}.${columnName} is ${existing.type}, schema says ${columnDefinition.type}`);
501
761
  }
762
+ if ((existing.integer === true) !== columnDefinition.integer ||
763
+ JSON.stringify(existing.sqlDomain ?? null) !==
764
+ JSON.stringify(columnDefinition.sqlDomain ?? null)) {
765
+ throw new TypeError(`Column domains cannot change: ${tableDefinition.name}.${columnName}`);
766
+ }
767
+ if (existing.backfill === undefined && columnDefinition.backfillValue !== undefined) {
768
+ throw new TypeError(`Backfills cannot be added after a column exists: ${tableDefinition.name}.${columnName}`);
769
+ }
770
+ if (existing.backfill !== undefined && columnDefinition.backfillValue === undefined) {
771
+ throw new TypeError(`Backfills cannot be removed: ${tableDefinition.name}.${columnName}`);
772
+ }
773
+ if (existing.backfill !== undefined &&
774
+ columnDefinition.backfillValue !== undefined &&
775
+ typeof columnDefinition.backfillValue !== "function" &&
776
+ !backfillsEqual(existing.backfill, columnDefinition.backfillValue)) {
777
+ throw new TypeError(`Backfills cannot change: ${tableDefinition.name}.${columnName}`);
778
+ }
502
779
  const existingEnum = existing.enumValues;
503
780
  const definedEnum = columnDefinition.enumValues;
504
781
  if (definedEnum !== undefined && existingEnum === undefined) {
@@ -528,8 +805,10 @@ export function planMigration(catalog, definition, options = {}) {
528
805
  }
529
806
  }
530
807
  }
531
- const existingIsKey = record.uniqueKeyColumnId === existing.id;
532
- if (existingIsKey !== columnDefinition.isUnique) {
808
+ const existingIsKey = record.uniqueKeyColumnId === existing.id ||
809
+ (record.primaryKeyColumnIds ?? []).includes(existing.id);
810
+ const definedIsKey = columnDefinition.isUnique || tableDefinition.primaryKey.includes(columnName);
811
+ if (existingIsKey !== definedIsKey) {
533
812
  throw new TypeError(`Unique keys cannot change: ${tableDefinition.name}.${columnName}`);
534
813
  }
535
814
  if (existing.nullable && !columnDefinition.isNullable) {
@@ -578,6 +857,20 @@ export function planMigration(catalog, definition, options = {}) {
578
857
  columnName: columnRecord.name,
579
858
  });
580
859
  }
860
+ const existingKeyIds = record.primaryKeyColumnIds ??
861
+ (record.uniqueKeyColumnId === undefined ? [] : [record.uniqueKeyColumnId]);
862
+ const desiredKeyNames = tableDefinition.primaryKey.length > 0
863
+ ? tableDefinition.primaryKey
864
+ : Object.entries(tableDefinition.columns).flatMap(([name, definition]) => definition.isUnique ? [name] : []);
865
+ const desiredKeyIds = desiredKeyNames.map((name) => {
866
+ const definition = tableDefinition.columns[name];
867
+ const currentName = definition?.renamedFromName ?? name;
868
+ return recordColumnsByName.get(name)?.id ?? recordColumnsByName.get(currentName)?.id;
869
+ });
870
+ if (desiredKeyIds.length !== existingKeyIds.length ||
871
+ desiredKeyIds.some((id, index) => id !== existingKeyIds[index])) {
872
+ throw new TypeError(`Primary key order cannot change: ${tableDefinition.name}`);
873
+ }
581
874
  assertConstraintsUnchanged(record, tableDefinition);
582
875
  }
583
876
  if (options.schemaOwnsDatabase === true) {
@@ -591,32 +884,164 @@ export function planMigration(catalog, definition, options = {}) {
591
884
  }
592
885
  }
593
886
  planViewSteps(catalog, definition, steps);
594
- return { steps };
887
+ // A managed view can depend on a managed table the same authoritative schema removes. Drop
888
+ // dependents first; the SQL engine correctly refuses the reverse order. Replacements stay
889
+ // after table-creation steps so a newly declared body can resolve its sources.
890
+ const droppedViews = steps.filter((step) => step.kind === "drop-view");
891
+ const droppedTables = steps.filter((step) => step.kind === "drop-table");
892
+ return {
893
+ steps: [
894
+ ...steps.filter((step) => step.kind !== "drop-view" && step.kind !== "drop-table"),
895
+ ...droppedViews,
896
+ ...droppedTables,
897
+ ],
898
+ };
595
899
  }
596
900
  /**
597
- * A thin, fully typed handle over the existing batch APIs: inserts require every non-nullable
598
- * column, updates exclude the unique key, and reads return the complete inferred row shape.
901
+ * A thin, fully typed SQL handle: inserts require every non-nullable column, updates exclude the
902
+ * unique key, and reads return the complete inferred row shape. Every operation is rendered as
903
+ * parameterized SQL and goes through the parser/planner/executor; this helper has no private
904
+ * batch or table-read semantics that can drift from SQL.
599
905
  */
600
906
  export function typedTable(database, definition) {
601
907
  const columnNames = Object.keys(definition.columns);
602
- // Pads every schema column, so omitting a nullable one is an explicit null rather than a
603
- // "Missing column" error from the engine; function defaults fill their omitted slots here,
604
- // before the batch reaches the engine.
605
- const pad = (rows) => rows.map((row) => Object.fromEntries(columnNames.map((name) => {
606
- const value = row[name] ?? null;
607
- const fill = definition.columns[name]?.defaultFn;
608
- return [name, value === null && fill !== undefined ? fill() : value];
609
- })));
908
+ const quote = (name) => `"${name.replaceAll('"', '""')}"`;
909
+ const scalarUniqueKey = Object.entries(definition.columns).find(([, columnDefinition]) => columnDefinition.isUnique)?.[0];
910
+ const keyColumns = definition.primaryKey.length > 0
911
+ ? [...definition.primaryKey]
912
+ : scalarUniqueKey === undefined
913
+ ? []
914
+ : [scalarUniqueKey];
915
+ const normalizedRows = (rows) => {
916
+ if (rows.length === 0)
917
+ throw new TypeError("A batch needs at least one row");
918
+ // Keep extra runtime keys in the SQL column list. TypeScript normally excludes them, but a
919
+ // cast or JavaScript caller must still get the engine's "column does not exist" error rather
920
+ // than having data silently discarded by this facade.
921
+ const names = new Set(columnNames);
922
+ for (const row of rows)
923
+ for (const name of Object.keys(row))
924
+ names.add(name);
925
+ const ordered = [...names];
926
+ return { names: ordered, rows };
927
+ };
928
+ const insert = (rows, replace) => {
929
+ const normalized = normalizedRows(rows);
930
+ const params = [];
931
+ const values = normalized.rows
932
+ .map((row) => {
933
+ const placeholders = normalized.names.map((name) => {
934
+ const value = row[name];
935
+ if (!Object.hasOwn(row, name) || value === undefined)
936
+ return "DEFAULT";
937
+ params.push(value);
938
+ return `$${String(params.length)}`;
939
+ });
940
+ return `(${placeholders.join(", ")})`;
941
+ })
942
+ .join(", ");
943
+ let sql = `INSERT INTO ${quote(definition.name)} (${normalized.names.map(quote).join(", ")}) VALUES ${values}`;
944
+ if (replace) {
945
+ if (keyColumns.length === 0) {
946
+ throw new TypeError(`Upsert requires a unique key: ${definition.name}`);
947
+ }
948
+ sql += ` ON CONFLICT (${keyColumns.map(quote).join(", ")}) DO REPLACE`;
949
+ }
950
+ return database.execute(sql, params);
951
+ };
952
+ const requireUniqueKey = (operation) => {
953
+ if (keyColumns.length === 0) {
954
+ throw new TypeError(`${operation} requires a table with a unique key: ${definition.name}`);
955
+ }
956
+ return keyColumns;
957
+ };
958
+ const keyParts = (key) => {
959
+ if (keyColumns.length === 1)
960
+ return [key];
961
+ if (typeof key !== "object" || key === null) {
962
+ throw new TypeError(`Composite key for ${definition.name} must be an object`);
963
+ }
964
+ return keyColumns.map((name) => key[name]);
965
+ };
966
+ const keyToken = (value) => {
967
+ const parts = keyParts(value);
968
+ return JSON.stringify(parts.map((part) => part instanceof Date ? ["date", part.getTime()] : [typeof part, String(part)]));
969
+ };
970
+ const assertKeys = (keys, operation) => {
971
+ if (keys.length === 0)
972
+ throw new TypeError(`A ${operation} batch needs at least one key`);
973
+ const seen = new Set();
974
+ for (const key of keys) {
975
+ if (keyParts(key).some((part) => part === undefined || part === null)) {
976
+ throw new TypeError(`Missing ${operation} key value for ${definition.name}`);
977
+ }
978
+ const token = keyToken(key);
979
+ if (seen.has(token))
980
+ throw new TypeError(`Duplicate key in ${operation} batch: ${String(key)}`);
981
+ seen.add(token);
982
+ }
983
+ };
610
984
  return {
611
985
  definition,
612
- insert: (rows) => database.insertBatch(definition.name, pad(rows)),
613
- upsert: (rows) => database.upsertBatch(definition.name, pad(rows)),
614
- update: (input) => database.updateBatch(definition.name, {
615
- keys: input.keys,
616
- changes: input.changes,
617
- }),
618
- delete: (input) => database.deleteBatch(definition.name, { keys: input.keys }),
619
- rows: async () => (await database.readTable(definition.name)),
986
+ insert: (rows) => insert(rows, false),
987
+ upsert: (rows) => insert(rows, true),
988
+ update: (input) => {
989
+ const keys = requireUniqueKey("UPDATE");
990
+ assertKeys(input.keys, "update");
991
+ const assignments = [];
992
+ const params = [];
993
+ for (const [name, rawValues] of Object.entries(input.changes)) {
994
+ if (!(name in definition.columns)) {
995
+ // Render it anyway so SQL owns the public validation error.
996
+ }
997
+ const values = rawValues;
998
+ if (values.length !== input.keys.length) {
999
+ throw new TypeError(`Update column ${name} has ${String(values.length)} values for ${String(input.keys.length)} keys`);
1000
+ }
1001
+ const branches = [];
1002
+ values.forEach((value, index) => {
1003
+ if (value === undefined)
1004
+ return;
1005
+ const keyValue = input.keys[index];
1006
+ if (keyValue === undefined) {
1007
+ throw new TypeError(`Missing update key at position ${String(index)}`);
1008
+ }
1009
+ const conditions = keyParts(keyValue).map((part, keyIndex) => {
1010
+ params.push(part);
1011
+ return `${quote(keys[keyIndex] ?? "")} = $${String(params.length)}`;
1012
+ });
1013
+ params.push(value);
1014
+ branches.push(`WHEN ${conditions.join(" AND ")} THEN $${String(params.length)}`);
1015
+ });
1016
+ if (branches.length > 0) {
1017
+ assignments.push(`${quote(name)} = CASE ${branches.join(" ")} ELSE ${quote(name)} END`);
1018
+ }
1019
+ }
1020
+ if (assignments.length === 0)
1021
+ throw new TypeError("An update batch needs at least one change");
1022
+ const predicates = input.keys.map((keyValue) => {
1023
+ const conditions = keyParts(keyValue).map((part, keyIndex) => {
1024
+ params.push(part);
1025
+ return `${quote(keys[keyIndex] ?? "")} = $${String(params.length)}`;
1026
+ });
1027
+ return `(${conditions.join(" AND ")})`;
1028
+ });
1029
+ return database.execute(`UPDATE ${quote(definition.name)} SET ${assignments.join(", ")} WHERE ${predicates.join(" OR ")}`, params);
1030
+ },
1031
+ delete: (input) => {
1032
+ const keys = requireUniqueKey("DELETE");
1033
+ assertKeys(input.keys, "delete");
1034
+ const params = [];
1035
+ const predicates = input.keys.map((keyValue) => {
1036
+ const conditions = keyParts(keyValue).map((part, keyIndex) => {
1037
+ params.push(part);
1038
+ return `${quote(keys[keyIndex] ?? "")} = $${String(params.length)}`;
1039
+ });
1040
+ return `(${conditions.join(" AND ")})`;
1041
+ });
1042
+ return database.execute(`DELETE FROM ${quote(definition.name)} WHERE ${predicates.join(" OR ")}`, params);
1043
+ },
1044
+ rows: async () => (await database.query(`SELECT ${columnNames.map(quote).join(", ")} FROM ${quote(definition.name)}`)).rows,
620
1045
  };
621
1046
  }
622
1047
  /** Applies one table's alteration steps onto its current column records. */
@@ -635,10 +1060,21 @@ export function applyColumnSteps(record, steps, createId) {
635
1060
  id: createId(),
636
1061
  name: step.columnName,
637
1062
  type: step.definition.type,
1063
+ ...(step.definition.integer ? { integer: true } : {}),
1064
+ ...(step.definition.sqlDomain === undefined
1065
+ ? {}
1066
+ : { sqlDomain: structuredClone(step.definition.sqlDomain) }),
638
1067
  // A backfill is what lets the column be non-nullable: every row has a value, either
639
1068
  // written or substituted at read time.
640
1069
  nullable: step.backfill === undefined ? true : step.definition.isNullable,
641
- ...(step.backfill === undefined ? {} : { backfill: step.backfill }),
1070
+ ...(step.backfill === undefined
1071
+ ? {}
1072
+ : {
1073
+ backfill: storedBackfill(step.definition, step.backfill),
1074
+ }),
1075
+ ...(step.definition.defaultSpec === undefined
1076
+ ? {}
1077
+ : { defaultValue: step.definition.defaultSpec }),
642
1078
  ...(step.definition.enumValues === undefined
643
1079
  ? {}
644
1080
  : { enumValues: [...step.definition.enumValues] }),
@@ -704,6 +1140,15 @@ function columnDefaultsEqual(left, right) {
704
1140
  return left === right;
705
1141
  if (left.kind !== right.kind)
706
1142
  return false;
707
- return left.kind !== "literal" || right.kind !== "literal" || left.value === right.value;
1143
+ if (left.kind === "expression" && right.kind === "expression")
1144
+ return left.sql === right.sql;
1145
+ if (left.kind !== "literal" || right.kind !== "literal")
1146
+ return true;
1147
+ if (left.value instanceof Date || right.value instanceof Date) {
1148
+ return (left.value instanceof Date &&
1149
+ right.value instanceof Date &&
1150
+ left.value.getTime() === right.value.getTime());
1151
+ }
1152
+ return left.value === right.value;
708
1153
  }
709
1154
  //# sourceMappingURL=schema.js.map