@minnowdb/core 0.2.1 → 0.4.0

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