@minnowdb/core 0.0.1 → 0.1.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 (218) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +27 -1
  3. package/dist/block-format/block.d.ts +46 -0
  4. package/dist/block-format/block.d.ts.map +1 -0
  5. package/dist/block-format/block.js +244 -0
  6. package/dist/block-format/block.js.map +1 -0
  7. package/dist/block-format/checksum.d.ts +7 -0
  8. package/dist/block-format/checksum.d.ts.map +1 -0
  9. package/dist/block-format/checksum.js +56 -0
  10. package/dist/block-format/checksum.js.map +1 -0
  11. package/dist/block-format/codecs.d.ts +24 -0
  12. package/dist/block-format/codecs.d.ts.map +1 -0
  13. package/dist/block-format/codecs.js +102 -0
  14. package/dist/block-format/codecs.js.map +1 -0
  15. package/dist/block-format/column.d.ts +8 -0
  16. package/dist/block-format/column.d.ts.map +1 -0
  17. package/dist/block-format/column.js +166 -0
  18. package/dist/block-format/column.js.map +1 -0
  19. package/dist/block-format/index.d.ts +7 -0
  20. package/dist/block-format/index.d.ts.map +1 -0
  21. package/dist/block-format/index.js +7 -0
  22. package/dist/block-format/index.js.map +1 -0
  23. package/dist/block-format/physical.d.ts +20 -0
  24. package/dist/block-format/physical.d.ts.map +1 -0
  25. package/dist/block-format/physical.js +439 -0
  26. package/dist/block-format/physical.js.map +1 -0
  27. package/dist/block-format/types.d.ts +93 -0
  28. package/dist/block-format/types.d.ts.map +1 -0
  29. package/dist/block-format/types.js +2 -0
  30. package/dist/block-format/types.js.map +1 -0
  31. package/dist/engine/artifact-cache.d.ts +22 -0
  32. package/dist/engine/artifact-cache.d.ts.map +1 -0
  33. package/dist/engine/artifact-cache.js +68 -0
  34. package/dist/engine/artifact-cache.js.map +1 -0
  35. package/dist/engine/batch.d.ts +28 -0
  36. package/dist/engine/batch.d.ts.map +1 -0
  37. package/dist/engine/batch.js +38 -0
  38. package/dist/engine/batch.js.map +1 -0
  39. package/dist/engine/buffered-writer.d.ts +43 -0
  40. package/dist/engine/buffered-writer.d.ts.map +1 -0
  41. package/dist/engine/buffered-writer.js +158 -0
  42. package/dist/engine/buffered-writer.js.map +1 -0
  43. package/dist/engine/catalog.d.ts +82 -0
  44. package/dist/engine/catalog.d.ts.map +1 -0
  45. package/dist/engine/catalog.js +47 -0
  46. package/dist/engine/catalog.js.map +1 -0
  47. package/dist/engine/client.d.ts +211 -0
  48. package/dist/engine/client.d.ts.map +1 -0
  49. package/dist/engine/client.js +538 -0
  50. package/dist/engine/client.js.map +1 -0
  51. package/dist/engine/coordinator.d.ts +17 -0
  52. package/dist/engine/coordinator.d.ts.map +1 -0
  53. package/dist/engine/coordinator.js +60 -0
  54. package/dist/engine/coordinator.js.map +1 -0
  55. package/dist/engine/database.d.ts +661 -0
  56. package/dist/engine/database.d.ts.map +1 -0
  57. package/dist/engine/database.js +10313 -0
  58. package/dist/engine/database.js.map +1 -0
  59. package/dist/engine/defaults.d.ts +33 -0
  60. package/dist/engine/defaults.d.ts.map +1 -0
  61. package/dist/engine/defaults.js +87 -0
  62. package/dist/engine/defaults.js.map +1 -0
  63. package/dist/engine/dsl/db.d.ts +137 -0
  64. package/dist/engine/dsl/db.d.ts.map +1 -0
  65. package/dist/engine/dsl/db.js +161 -0
  66. package/dist/engine/dsl/db.js.map +1 -0
  67. package/dist/engine/dsl/expression.d.ts +172 -0
  68. package/dist/engine/dsl/expression.d.ts.map +1 -0
  69. package/dist/engine/dsl/expression.js +397 -0
  70. package/dist/engine/dsl/expression.js.map +1 -0
  71. package/dist/engine/dsl/index.d.ts +8 -0
  72. package/dist/engine/dsl/index.d.ts.map +1 -0
  73. package/dist/engine/dsl/index.js +7 -0
  74. package/dist/engine/dsl/index.js.map +1 -0
  75. package/dist/engine/dsl/live-query.d.ts +45 -0
  76. package/dist/engine/dsl/live-query.d.ts.map +1 -0
  77. package/dist/engine/dsl/live-query.js +116 -0
  78. package/dist/engine/dsl/live-query.js.map +1 -0
  79. package/dist/engine/dsl/mutations.d.ts +123 -0
  80. package/dist/engine/dsl/mutations.d.ts.map +1 -0
  81. package/dist/engine/dsl/mutations.js +241 -0
  82. package/dist/engine/dsl/mutations.js.map +1 -0
  83. package/dist/engine/dsl/select-query-builder.d.ts +148 -0
  84. package/dist/engine/dsl/select-query-builder.d.ts.map +1 -0
  85. package/dist/engine/dsl/select-query-builder.js +377 -0
  86. package/dist/engine/dsl/select-query-builder.js.map +1 -0
  87. package/dist/engine/dsl/sql-tag.d.ts +46 -0
  88. package/dist/engine/dsl/sql-tag.d.ts.map +1 -0
  89. package/dist/engine/dsl/sql-tag.js +85 -0
  90. package/dist/engine/dsl/sql-tag.js.map +1 -0
  91. package/dist/engine/dsl/types.d.ts +133 -0
  92. package/dist/engine/dsl/types.d.ts.map +1 -0
  93. package/dist/engine/dsl/types.js +9 -0
  94. package/dist/engine/dsl/types.js.map +1 -0
  95. package/dist/engine/errors.d.ts +55 -0
  96. package/dist/engine/errors.d.ts.map +1 -0
  97. package/dist/engine/errors.js +81 -0
  98. package/dist/engine/errors.js.map +1 -0
  99. package/dist/engine/fts.d.ts +93 -0
  100. package/dist/engine/fts.d.ts.map +1 -0
  101. package/dist/engine/fts.js +273 -0
  102. package/dist/engine/fts.js.map +1 -0
  103. package/dist/engine/group-index.d.ts +32 -0
  104. package/dist/engine/group-index.d.ts.map +1 -0
  105. package/dist/engine/group-index.js +366 -0
  106. package/dist/engine/group-index.js.map +1 -0
  107. package/dist/engine/index.d.ts +13 -0
  108. package/dist/engine/index.d.ts.map +1 -0
  109. package/dist/engine/index.js +12 -0
  110. package/dist/engine/index.js.map +1 -0
  111. package/dist/engine/join-index.d.ts +12 -0
  112. package/dist/engine/join-index.d.ts.map +1 -0
  113. package/dist/engine/join-index.js +217 -0
  114. package/dist/engine/join-index.js.map +1 -0
  115. package/dist/engine/live.d.ts +86 -0
  116. package/dist/engine/live.d.ts.map +1 -0
  117. package/dist/engine/live.js +289 -0
  118. package/dist/engine/live.js.map +1 -0
  119. package/dist/engine/memory.d.ts +37 -0
  120. package/dist/engine/memory.d.ts.map +1 -0
  121. package/dist/engine/memory.js +119 -0
  122. package/dist/engine/memory.js.map +1 -0
  123. package/dist/engine/optimizer.d.ts +20 -0
  124. package/dist/engine/optimizer.d.ts.map +1 -0
  125. package/dist/engine/optimizer.js +1170 -0
  126. package/dist/engine/optimizer.js.map +1 -0
  127. package/dist/engine/query-cache.d.ts +10 -0
  128. package/dist/engine/query-cache.d.ts.map +1 -0
  129. package/dist/engine/query-cache.js +58 -0
  130. package/dist/engine/query-cache.js.map +1 -0
  131. package/dist/engine/query.d.ts +844 -0
  132. package/dist/engine/query.d.ts.map +1 -0
  133. package/dist/engine/query.js +7450 -0
  134. package/dist/engine/query.js.map +1 -0
  135. package/dist/engine/schema-wire.d.ts +92 -0
  136. package/dist/engine/schema-wire.d.ts.map +1 -0
  137. package/dist/engine/schema-wire.js +115 -0
  138. package/dist/engine/schema-wire.js.map +1 -0
  139. package/dist/engine/schema.d.ts +394 -0
  140. package/dist/engine/schema.d.ts.map +1 -0
  141. package/dist/engine/schema.js +709 -0
  142. package/dist/engine/schema.js.map +1 -0
  143. package/dist/engine/sort-keys.d.ts +36 -0
  144. package/dist/engine/sort-keys.d.ts.map +1 -0
  145. package/dist/engine/sort-keys.js +60 -0
  146. package/dist/engine/sort-keys.js.map +1 -0
  147. package/dist/engine/sql-json.d.ts +30 -0
  148. package/dist/engine/sql-json.d.ts.map +1 -0
  149. package/dist/engine/sql-json.js +120 -0
  150. package/dist/engine/sql-json.js.map +1 -0
  151. package/dist/engine/sql-semantics.d.ts +31 -0
  152. package/dist/engine/sql-semantics.d.ts.map +1 -0
  153. package/dist/engine/sql-semantics.js +121 -0
  154. package/dist/engine/sql-semantics.js.map +1 -0
  155. package/dist/engine/vector.d.ts +87 -0
  156. package/dist/engine/vector.d.ts.map +1 -0
  157. package/dist/engine/vector.js +4227 -0
  158. package/dist/engine/vector.js.map +1 -0
  159. package/dist/engine/worker-host.d.ts +48 -0
  160. package/dist/engine/worker-host.d.ts.map +1 -0
  161. package/dist/engine/worker-host.js +501 -0
  162. package/dist/engine/worker-host.js.map +1 -0
  163. package/dist/engine/worker.d.ts +2 -0
  164. package/dist/engine/worker.d.ts.map +1 -0
  165. package/dist/engine/worker.js +14 -0
  166. package/dist/engine/worker.js.map +1 -0
  167. package/dist/index.d.ts +2 -0
  168. package/dist/index.d.ts.map +1 -0
  169. package/dist/index.js +2 -0
  170. package/dist/index.js.map +1 -0
  171. package/dist/plan/index.d.ts +17 -0
  172. package/dist/plan/index.d.ts.map +1 -0
  173. package/dist/plan/index.js +17 -0
  174. package/dist/plan/index.js.map +1 -0
  175. package/dist/storage/fixture-shape.d.ts +42 -0
  176. package/dist/storage/fixture-shape.d.ts.map +1 -0
  177. package/dist/storage/fixture-shape.js +146 -0
  178. package/dist/storage/fixture-shape.js.map +1 -0
  179. package/dist/storage/index.d.ts +5 -0
  180. package/dist/storage/index.d.ts.map +1 -0
  181. package/dist/storage/index.js +5 -0
  182. package/dist/storage/index.js.map +1 -0
  183. package/dist/storage/indexeddb.d.ts +139 -0
  184. package/dist/storage/indexeddb.d.ts.map +1 -0
  185. package/dist/storage/indexeddb.js +3161 -0
  186. package/dist/storage/indexeddb.js.map +1 -0
  187. package/dist/storage/memory.d.ts +121 -0
  188. package/dist/storage/memory.d.ts.map +1 -0
  189. package/dist/storage/memory.js +1455 -0
  190. package/dist/storage/memory.js.map +1 -0
  191. package/dist/storage/snapshot.d.ts +108 -0
  192. package/dist/storage/snapshot.d.ts.map +1 -0
  193. package/dist/storage/snapshot.js +276 -0
  194. package/dist/storage/snapshot.js.map +1 -0
  195. package/dist/storage/types.d.ts +895 -0
  196. package/dist/storage/types.d.ts.map +1 -0
  197. package/dist/storage/types.js +1549 -0
  198. package/dist/storage/types.js.map +1 -0
  199. package/dist/testing/index.d.ts +76 -0
  200. package/dist/testing/index.d.ts.map +1 -0
  201. package/dist/testing/index.js +233 -0
  202. package/dist/testing/index.js.map +1 -0
  203. package/dist/testing/seeds.d.ts +11 -0
  204. package/dist/testing/seeds.d.ts.map +1 -0
  205. package/dist/testing/seeds.js +50 -0
  206. package/dist/testing/seeds.js.map +1 -0
  207. package/dist/transactions/index.d.ts +148 -0
  208. package/dist/transactions/index.d.ts.map +1 -0
  209. package/dist/transactions/index.js +662 -0
  210. package/dist/transactions/index.js.map +1 -0
  211. package/dist/worker-protocol/index.d.ts +84 -0
  212. package/dist/worker-protocol/index.d.ts.map +1 -0
  213. package/dist/worker-protocol/index.js +117 -0
  214. package/dist/worker-protocol/index.js.map +1 -0
  215. package/package.json +66 -16
  216. package/sql-feature-matrix.json +1275 -0
  217. package/index.d.ts +0 -1
  218. package/index.js +0 -1
@@ -0,0 +1,709 @@
1
+ import { validateColumnDefault, validateEnumValues, } from "../storage/index.js";
2
+ import { compileCheckExpression, expressionColumns } from "./query.js";
3
+ /**
4
+ * The constraint name `migrate()` gives a declared relation. It matches the name the SQL parser
5
+ * derives for an unnamed inline REFERENCES, so a table built either way has the same catalog.
6
+ */
7
+ export function foreignKeyName(tableName, columnName) {
8
+ return `${tableName}_${columnName}_fkey`;
9
+ }
10
+ function defaultSpecFromArg(type, value) {
11
+ switch (type) {
12
+ case "datetime":
13
+ if (value !== "now")
14
+ throw new TypeError('Datetime columns default with "now"');
15
+ return { kind: "now" };
16
+ case "string":
17
+ if (typeof value !== "string")
18
+ throw new TypeError("Default literal must be a string");
19
+ return { kind: "literal", value };
20
+ case "number":
21
+ if (typeof value !== "number" || !Number.isFinite(value)) {
22
+ throw new TypeError("Default literal must be a finite number");
23
+ }
24
+ return { kind: "literal", value };
25
+ case "boolean":
26
+ if (typeof value !== "boolean")
27
+ throw new TypeError("Default literal must be a boolean");
28
+ return { kind: "literal", value };
29
+ }
30
+ }
31
+ function createColumn(type, state = {}) {
32
+ const base = {
33
+ kind: "column",
34
+ type,
35
+ isNullable: (state.isNullable ?? false),
36
+ isUnique: (state.isUnique ?? false),
37
+ hasDefault: (state.defaultSpec !== undefined || state.defaultFn !== undefined),
38
+ ...(state.defaultSpec === undefined ? {} : { defaultSpec: state.defaultSpec }),
39
+ ...(state.defaultFn === undefined
40
+ ? {}
41
+ : { defaultFn: state.defaultFn }),
42
+ ...(state.renamedFromName === undefined ? {} : { renamedFromName: state.renamedFromName }),
43
+ ...(state.reference === undefined ? {} : { reference: state.reference }),
44
+ ...(state.enumValues === undefined ? {} : { enumValues: state.enumValues }),
45
+ ...(state.backfillValue === undefined
46
+ ? {}
47
+ : { backfillValue: state.backfillValue }),
48
+ };
49
+ return {
50
+ ...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, {
56
+ ...state,
57
+ reference: {
58
+ table,
59
+ column: referencedColumn,
60
+ onDelete: options.onDelete ?? "restrict",
61
+ },
62
+ }),
63
+ 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) });
71
+ }),
72
+ autoIncrement: (() => {
73
+ if (type !== "number") {
74
+ throw new TypeError("Auto-increment requires a number column");
75
+ }
76
+ const cleared = { ...state };
77
+ delete cleared.defaultFn;
78
+ return createColumn(type, { ...cleared, defaultSpec: { kind: "autoincrement" } });
79
+ }),
80
+ };
81
+ }
82
+ /**
83
+ * 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.
85
+ */
86
+ export function columnWithDefaultSpec(base, spec) {
87
+ return createColumn(base.type, {
88
+ isNullable: base.isNullable,
89
+ isUnique: base.isUnique,
90
+ ...(base.renamedFromName === undefined ? {} : { renamedFromName: base.renamedFromName }),
91
+ ...(base.reference === undefined ? {} : { reference: base.reference }),
92
+ ...(base.enumValues === undefined ? {} : { enumValues: base.enumValues }),
93
+ defaultSpec: spec,
94
+ });
95
+ }
96
+ export const column = {
97
+ boolean: () => createColumn("boolean"),
98
+ number: () => createColumn("number"),
99
+ string: () => createColumn("string"),
100
+ datetime: () => createColumn("datetime"),
101
+ /**
102
+ * A string column restricted to a closed set of values, typed as their literal union:
103
+ *
104
+ * ```ts
105
+ * status: column.enum(["draft", "published", "archived"]).default("draft")
106
+ * ```
107
+ *
108
+ * Selects return `"draft" | "published" | "archived"` and inserts accept nothing else; every
109
+ * write path also validates membership at runtime. Physically the column is a plain string
110
+ * column, so migrations may add values or relax the column to `column.string()`, but never
111
+ * remove values — existing rows could already hold them.
112
+ */
113
+ enum: (values) => createColumn("string", { enumValues: validateEnumValues(values, "enum column") }),
114
+ };
115
+ export function table(name, columns, options = {}) {
116
+ const entries = Object.entries(columns);
117
+ if (entries.length === 0)
118
+ throw new TypeError(`Table ${name} needs at least one column`);
119
+ const uniqueColumns = entries.filter(([, definition]) => definition.isUnique);
120
+ if (uniqueColumns.length > 1) {
121
+ throw new TypeError(`Table ${name} may name at most one unique column`);
122
+ }
123
+ const uniqueEntry = uniqueColumns[0];
124
+ if (uniqueEntry?.[1].isNullable === true) {
125
+ throw new TypeError(`Table ${name} unique column must not be nullable: ${uniqueEntry[0]}`);
126
+ }
127
+ for (const [columnName, definition] of entries) {
128
+ if (definition.defaultFn !== undefined && definition.isNullable) {
129
+ throw new TypeError(`Defaults require a non-nullable column: ${columnName}`);
130
+ }
131
+ const backfill = definition.backfillValue;
132
+ 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
+ }
144
+ }
145
+ if (backfill !== undefined && definition.isNullable) {
146
+ throw new TypeError(`A nullable column needs no backfill: ${name}.${columnName}. Rows without it already read NULL.`);
147
+ }
148
+ if (definition.reference?.onDelete === "set null" && !definition.isNullable) {
149
+ throw new TypeError(`ON DELETE SET NULL requires a nullable column: ${name}.${columnName}`);
150
+ }
151
+ const spec = definition.defaultSpec;
152
+ if (spec === undefined)
153
+ continue;
154
+ validateColumnDefault({
155
+ name: columnName,
156
+ type: definition.type,
157
+ nullable: definition.isNullable,
158
+ isUniqueKey: definition.isUnique,
159
+ ...(definition.enumValues === undefined ? {} : { enumValues: definition.enumValues }),
160
+ }, spec);
161
+ }
162
+ const checks = options.checks ?? [];
163
+ const checkNames = new Set();
164
+ for (const check of checks) {
165
+ if (check.name.length === 0)
166
+ throw new TypeError(`Table ${name} has an unnamed CHECK`);
167
+ if (checkNames.has(check.name)) {
168
+ throw new TypeError(`Duplicate CHECK in table ${name}: ${check.name}`);
169
+ }
170
+ checkNames.add(check.name);
171
+ if (check.sql.trim().length === 0) {
172
+ throw new TypeError(`CHECK ${check.name} in table ${name} has no expression`);
173
+ }
174
+ }
175
+ return {
176
+ kind: "table",
177
+ name,
178
+ columns,
179
+ checks,
180
+ "~standard": {
181
+ version: 1,
182
+ vendor: "minnow",
183
+ validate(value) {
184
+ if (typeof value !== "object" || value === null) {
185
+ return { issues: [{ message: "A row must be an object", path: [] }] };
186
+ }
187
+ const row = value;
188
+ const issues = [];
189
+ for (const [columnName, definition] of entries) {
190
+ 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) {
197
+ issues.push({ message: `Missing non-nullable column`, path: [columnName] });
198
+ }
199
+ continue;
200
+ }
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] });
206
+ }
207
+ else if (definition.enumValues !== undefined &&
208
+ typeof columnValue === "string" &&
209
+ !definition.enumValues.includes(columnValue)) {
210
+ issues.push({
211
+ message: `Expected one of: ${definition.enumValues.join(", ")}`,
212
+ path: [columnName],
213
+ });
214
+ }
215
+ }
216
+ for (const key of Object.keys(row)) {
217
+ if (!(key in columns))
218
+ issues.push({ message: "Unknown column", path: [key] });
219
+ }
220
+ return issues.length > 0 ? { issues } : { value: row };
221
+ },
222
+ },
223
+ };
224
+ }
225
+ /**
226
+ * The FOREIGN KEY constraints a table declares, in column order. This is the single derivation
227
+ * of "what relations does this table have" — `planMigration` diffs against it and `migrate()`
228
+ * creates from it, so the two can never disagree.
229
+ */
230
+ export function declaredForeignKeys(definition) {
231
+ const keys = [];
232
+ for (const [columnName, columnDefinition] of Object.entries(definition.columns)) {
233
+ const reference = columnDefinition.reference;
234
+ if (reference === undefined)
235
+ continue;
236
+ keys.push({
237
+ name: foreignKeyName(definition.name, columnName),
238
+ column: columnName,
239
+ parentTable: reference.table,
240
+ parentColumn: reference.column,
241
+ onDelete: reference.onDelete,
242
+ });
243
+ }
244
+ return keys;
245
+ }
246
+ /**
247
+ * Declares a view. Its columns use the same builders tables use, so a view row types exactly like
248
+ * a table row — but a view is read-only, and the typed facade will not let a write name one.
249
+ *
250
+ * ```ts
251
+ * const activeCustomers = view("active_customers", {
252
+ * sql: `SELECT customer_id, name FROM customers WHERE status = 'active'`,
253
+ * columns: { customer_id: column.number(), name: column.string() },
254
+ * });
255
+ * ```
256
+ */
257
+ export function view(name, definition) {
258
+ const entries = Object.entries(definition.columns);
259
+ if (entries.length === 0)
260
+ throw new TypeError(`View ${name} needs at least one column`);
261
+ if (definition.sql.trim().length === 0)
262
+ throw new TypeError(`View ${name} has no query`);
263
+ for (const [columnName, columnDefinition] of entries) {
264
+ if (columnDefinition.isUnique) {
265
+ throw new TypeError(`A view column cannot be a unique key: ${name}.${columnName}`);
266
+ }
267
+ if (columnDefinition.defaultSpec !== undefined || columnDefinition.defaultFn !== undefined) {
268
+ throw new TypeError(`A view column cannot have a default: ${name}.${columnName}`);
269
+ }
270
+ }
271
+ return { kind: "view", name, sql: definition.sql, columns: definition.columns };
272
+ }
273
+ export function schema(tables, options = {}) {
274
+ const views = (options.views ?? []);
275
+ const names = new Set();
276
+ for (const definition of tables) {
277
+ if (names.has(definition.name)) {
278
+ throw new TypeError(`Duplicate table in schema: ${definition.name}`);
279
+ }
280
+ names.add(definition.name);
281
+ }
282
+ for (const definition of views) {
283
+ if (names.has(definition.name)) {
284
+ throw new TypeError(`Duplicate name in schema: ${definition.name}`);
285
+ }
286
+ names.add(definition.name);
287
+ }
288
+ 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}`);
296
+ }
297
+ }
298
+ }
299
+ return { kind: "schema", tables, views };
300
+ }
301
+ /**
302
+ * Views are derived: nothing is stored under one, so a body change is a replace rather than a
303
+ * rewrite and needs none of the proofs a table alteration needs.
304
+ *
305
+ * Within the set of views a migration created, the schema is authoritative — removing a
306
+ * declaration drops the view. Outside it nothing is touched: a view created with `CREATE VIEW`,
307
+ * or one written before ownership was recorded, is not any schema's to remove. That is the
308
+ * difference between a declaration being the source of truth and a declaration being allowed to
309
+ * destroy things it never made.
310
+ */
311
+ function planViewSteps(catalog, definition, steps) {
312
+ const tableNames = new Set(catalog.tables.map(({ name }) => name));
313
+ const existing = new Map(catalog.views.map((record) => [record.name, record]));
314
+ const declared = new Set(definition.views.map(({ name }) => name));
315
+ for (const viewDefinition of definition.views) {
316
+ if (tableNames.has(viewDefinition.name)) {
317
+ throw new TypeError(`A table already exists with this name: ${viewDefinition.name}`);
318
+ }
319
+ if (existing.get(viewDefinition.name)?.sql === viewDefinition.sql)
320
+ continue;
321
+ steps.push({ kind: "replace-view", view: viewDefinition });
322
+ }
323
+ for (const record of catalog.views) {
324
+ if (declared.has(record.name) || !record.managed)
325
+ continue;
326
+ steps.push({ kind: "drop-view", viewName: record.name });
327
+ }
328
+ }
329
+ /**
330
+ * Creation order, so a table is created after the tables it references. A FOREIGN KEY names a
331
+ * parent that must already exist, which would otherwise make a schema fail purely because its
332
+ * tables were listed child-first. Declaration order is preserved among tables that do not
333
+ * constrain each other, and a reference cycle (or a self-reference, which the engine allows)
334
+ * falls back to declaration order rather than failing here — the engine still has the final say.
335
+ */
336
+ function orderedForCreation(tables) {
337
+ const byName = new Map(tables.map((definition) => [definition.name, definition]));
338
+ const ordered = [];
339
+ const state = new Map();
340
+ const visit = (definition) => {
341
+ const status = state.get(definition.name);
342
+ if (status !== undefined)
343
+ return; // done, or a cycle we decline to reorder
344
+ state.set(definition.name, "visiting");
345
+ for (const key of declaredForeignKeys(definition)) {
346
+ const parent = byName.get(key.parentTable);
347
+ if (parent !== undefined && parent !== definition)
348
+ visit(parent);
349
+ }
350
+ state.set(definition.name, "done");
351
+ ordered.push(definition);
352
+ };
353
+ for (const definition of tables)
354
+ visit(definition);
355
+ return ordered;
356
+ }
357
+ /**
358
+ * Whether a step destroys data a reader could still see. These are the steps `migrate()` refuses
359
+ * to apply unless the caller opts in, so that a schema file drifting out of sync cannot delete
360
+ * rows the next time an application opens.
361
+ */
362
+ export function isDestructiveStep(step) {
363
+ return step.kind === "drop-column" || step.kind === "drop-table";
364
+ }
365
+ /**
366
+ * A column can stop being projected without rewriting anything, but not while something else in
367
+ * the catalog still points at it. Each of these would leave a constraint or key referring to a
368
+ * column no longer there, so the drop is refused rather than silently invalidating them.
369
+ */
370
+ function assertColumnDroppable(record, column) {
371
+ const where = `${record.name}.${column.name}`;
372
+ if (record.uniqueKeyColumnId === column.id) {
373
+ throw new TypeError(`The unique key cannot be dropped: ${where}. Unique-key changes need the table recreated.`);
374
+ }
375
+ for (const key of record.foreignKeys) {
376
+ if (key.column === column.name) {
377
+ throw new TypeError(`FOREIGN KEY ${key.name} still uses this column: ${where}`);
378
+ }
379
+ }
380
+ for (const check of record.checks) {
381
+ let referenced;
382
+ try {
383
+ referenced = expressionColumns(compileCheckExpression(check.sql, check.name));
384
+ }
385
+ catch {
386
+ // A check the engine can no longer compile is not a licence to drop what it names.
387
+ throw new TypeError(`CHECK ${check.name} cannot be re-read, so ${where} is not droppable`);
388
+ }
389
+ if (referenced.includes(column.name)) {
390
+ throw new TypeError(`CHECK ${check.name} still uses this column: ${where}`);
391
+ }
392
+ }
393
+ }
394
+ /**
395
+ * Freezes a column's backfill. A generator runs exactly once — here, while the migration is being
396
+ * planned — so the catalog stores a value rather than a function and no two readers can disagree.
397
+ */
398
+ function resolveBackfill(definition) {
399
+ const declared = definition.backfillValue;
400
+ if (declared === undefined)
401
+ return undefined;
402
+ return typeof declared === "function" ? declared() : declared;
403
+ }
404
+ /**
405
+ * Constraints on an existing table cannot change through a metadata-only step. Attaching a
406
+ * FOREIGN KEY or CHECK to a table that already holds rows would claim something about those rows
407
+ * that nobody has verified, and no validation scan exists; dropping one is refused for the mirror
408
+ * reason, so that a constraint never disappears because a schema file drifted. Both get the same
409
+ * discipline every other unprovable change gets: an explicit error naming the fix.
410
+ */
411
+ function assertConstraintsUnchanged(record, definition) {
412
+ const describeKey = (key) => `${key.column} -> ${key.parentTable}.${key.parentColumn} ON DELETE ${key.onDelete}`;
413
+ const existingKeys = new Map(record.foreignKeys.map((key) => [key.name, key]));
414
+ const declaredKeys = new Map(declaredForeignKeys(definition).map((key) => [key.name, key]));
415
+ for (const [name, declared] of declaredKeys) {
416
+ const existing = existingKeys.get(name);
417
+ if (existing === undefined) {
418
+ throw new TypeError(`FOREIGN KEY cannot be added after creation: ${definition.name}.${declared.column}. ` +
419
+ `Existing rows are not known to satisfy it; recreate the table to add a relation.`);
420
+ }
421
+ if (describeKey(existing) !== describeKey(declared)) {
422
+ throw new TypeError(`FOREIGN KEY cannot change: ${definition.name}.${declared.column} is ` +
423
+ `${describeKey(existing)}, schema says ${describeKey(declared)}`);
424
+ }
425
+ }
426
+ for (const name of existingKeys.keys()) {
427
+ if (!declaredKeys.has(name)) {
428
+ throw new TypeError(`FOREIGN KEY cannot be dropped: ${definition.name} still has ${name}. ` +
429
+ `Declare the relation, or recreate the table without it.`);
430
+ }
431
+ }
432
+ const existingChecks = new Map(record.checks.map((check) => [check.name, check.sql]));
433
+ const declaredChecks = new Map(definition.checks.map((check) => [check.name, check.sql]));
434
+ for (const [name, sql] of declaredChecks) {
435
+ const existing = existingChecks.get(name);
436
+ if (existing === undefined) {
437
+ throw new TypeError(`CHECK cannot be added after creation: ${definition.name}.${name}. ` +
438
+ `Existing rows are not known to satisfy it; recreate the table to add a constraint.`);
439
+ }
440
+ if (existing !== sql) {
441
+ throw new TypeError(`CHECK cannot change: ${definition.name}.${name} is (${existing}), schema says (${sql})`);
442
+ }
443
+ }
444
+ for (const name of existingChecks.keys()) {
445
+ if (!declaredChecks.has(name)) {
446
+ throw new TypeError(`CHECK cannot be dropped: ${definition.name} still has ${name}. ` +
447
+ `Declare the constraint, or recreate the table without it.`);
448
+ }
449
+ }
450
+ }
451
+ export function planMigration(catalog, definition, options = {}) {
452
+ const steps = [];
453
+ const currentByName = new Map(catalog.tables.map((record) => [record.name, record]));
454
+ for (const tableDefinition of orderedForCreation(definition.tables)) {
455
+ const record = currentByName.get(tableDefinition.name);
456
+ if (record === undefined) {
457
+ steps.push({ kind: "create-table", table: tableDefinition });
458
+ continue;
459
+ }
460
+ const recordColumnsByName = new Map(record.columns.map((columnRecord) => [columnRecord.name, columnRecord]));
461
+ const definedNames = new Set(Object.keys(tableDefinition.columns));
462
+ const renameSources = new Set();
463
+ for (const [columnName, columnDefinition] of Object.entries(tableDefinition.columns)) {
464
+ const from = columnDefinition.renamedFromName;
465
+ let existing = recordColumnsByName.get(columnName);
466
+ if (existing === undefined && from !== undefined) {
467
+ const source = recordColumnsByName.get(from);
468
+ if (source !== undefined) {
469
+ if (definedNames.has(from)) {
470
+ throw new TypeError(`Rename source is still defined: ${tableDefinition.name}.${from}`);
471
+ }
472
+ steps.push({
473
+ kind: "rename-column",
474
+ tableName: tableDefinition.name,
475
+ from,
476
+ to: columnName,
477
+ });
478
+ renameSources.add(from);
479
+ existing = source;
480
+ }
481
+ }
482
+ if (existing === undefined) {
483
+ const backfill = resolveBackfill(columnDefinition);
484
+ if (!columnDefinition.isNullable && backfill === undefined) {
485
+ throw new TypeError(`Added columns must be nullable, or carry a backfill: ${tableDefinition.name}.${columnName}`);
486
+ }
487
+ if (columnDefinition.isUnique) {
488
+ throw new TypeError(`Unique keys cannot be added after creation: ${tableDefinition.name}.${columnName}`);
489
+ }
490
+ steps.push({
491
+ kind: "add-column",
492
+ tableName: tableDefinition.name,
493
+ columnName,
494
+ definition: columnDefinition,
495
+ ...(backfill === undefined ? {} : { backfill }),
496
+ });
497
+ continue;
498
+ }
499
+ if (existing.type !== columnDefinition.type) {
500
+ throw new TypeError(`Column types cannot change: ${tableDefinition.name}.${columnName} is ${existing.type}, schema says ${columnDefinition.type}`);
501
+ }
502
+ const existingEnum = existing.enumValues;
503
+ const definedEnum = columnDefinition.enumValues;
504
+ if (definedEnum !== undefined && existingEnum === undefined) {
505
+ throw new TypeError(`Plain string columns cannot tighten to an enum: ${tableDefinition.name}.${columnName}. Existing rows may hold values outside the set.`);
506
+ }
507
+ if (existingEnum !== undefined) {
508
+ if (definedEnum === undefined) {
509
+ steps.push({
510
+ kind: "widen-enum",
511
+ tableName: tableDefinition.name,
512
+ columnName,
513
+ enumValues: null,
514
+ });
515
+ }
516
+ else {
517
+ const removed = existingEnum.filter((value) => !definedEnum.includes(value));
518
+ if (removed.length > 0) {
519
+ throw new TypeError(`Enum values cannot be removed: ${tableDefinition.name}.${columnName} drops ${removed.join(", ")}`);
520
+ }
521
+ if (definedEnum.some((value) => !existingEnum.includes(value))) {
522
+ steps.push({
523
+ kind: "widen-enum",
524
+ tableName: tableDefinition.name,
525
+ columnName,
526
+ enumValues: [...definedEnum],
527
+ });
528
+ }
529
+ }
530
+ }
531
+ const existingIsKey = record.uniqueKeyColumnId === existing.id;
532
+ if (existingIsKey !== columnDefinition.isUnique) {
533
+ throw new TypeError(`Unique keys cannot change: ${tableDefinition.name}.${columnName}`);
534
+ }
535
+ if (existing.nullable && !columnDefinition.isNullable) {
536
+ steps.push({
537
+ kind: "tighten-nullable",
538
+ tableName: tableDefinition.name,
539
+ columnName,
540
+ });
541
+ }
542
+ if (!existing.nullable && columnDefinition.isNullable) {
543
+ steps.push({
544
+ kind: "widen-nullable",
545
+ tableName: tableDefinition.name,
546
+ columnName,
547
+ });
548
+ }
549
+ if (!columnDefaultsEqual(existing.defaultValue, columnDefinition.defaultSpec)) {
550
+ const wasAuto = existing.defaultValue?.kind === "autoincrement";
551
+ const isAuto = columnDefinition.defaultSpec?.kind === "autoincrement";
552
+ if (wasAuto !== isAuto) {
553
+ // Adopting one seeds the counter past the largest key already stored; dropping one
554
+ // simply stops generating. Neither rewrites a row.
555
+ steps.push({
556
+ kind: "set-auto-increment",
557
+ tableName: tableDefinition.name,
558
+ columnName,
559
+ enabled: isAuto,
560
+ });
561
+ continue;
562
+ }
563
+ steps.push({
564
+ kind: "alter-default",
565
+ tableName: tableDefinition.name,
566
+ columnName,
567
+ defaultValue: columnDefinition.defaultSpec ?? null,
568
+ });
569
+ }
570
+ }
571
+ for (const columnRecord of record.columns) {
572
+ if (definedNames.has(columnRecord.name) || renameSources.has(columnRecord.name))
573
+ continue;
574
+ assertColumnDroppable(record, columnRecord);
575
+ steps.push({
576
+ kind: "drop-column",
577
+ tableName: tableDefinition.name,
578
+ columnName: columnRecord.name,
579
+ });
580
+ }
581
+ assertConstraintsUnchanged(record, tableDefinition);
582
+ }
583
+ if (options.schemaOwnsDatabase === true) {
584
+ const declaredTables = new Set(definition.tables.map(({ name }) => name));
585
+ for (const record of catalog.tables) {
586
+ // Only tables a migration created; see `managed`. Rows are at stake, so an undeclared
587
+ // table someone else made is never a migration's to remove.
588
+ if (declaredTables.has(record.name) || !record.managed)
589
+ continue;
590
+ steps.push({ kind: "drop-table", tableName: record.name });
591
+ }
592
+ }
593
+ planViewSteps(catalog, definition, steps);
594
+ return { steps };
595
+ }
596
+ /**
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.
599
+ */
600
+ export function typedTable(database, definition) {
601
+ 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
+ })));
610
+ return {
611
+ 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)),
620
+ };
621
+ }
622
+ /** Applies one table's alteration steps onto its current column records. */
623
+ export function applyColumnSteps(record, steps, createId) {
624
+ const columns = structuredClone(record.columns);
625
+ for (const step of steps) {
626
+ if (step.kind === "rename-column") {
627
+ const target = columns.find(({ name }) => name === step.from);
628
+ if (target !== undefined)
629
+ target.name = step.to;
630
+ continue;
631
+ }
632
+ if (step.kind === "add-column") {
633
+ if (!columns.some(({ name }) => name === step.columnName)) {
634
+ columns.push({
635
+ id: createId(),
636
+ name: step.columnName,
637
+ type: step.definition.type,
638
+ // A backfill is what lets the column be non-nullable: every row has a value, either
639
+ // written or substituted at read time.
640
+ nullable: step.backfill === undefined ? true : step.definition.isNullable,
641
+ ...(step.backfill === undefined ? {} : { backfill: step.backfill }),
642
+ ...(step.definition.enumValues === undefined
643
+ ? {}
644
+ : { enumValues: [...step.definition.enumValues] }),
645
+ });
646
+ }
647
+ continue;
648
+ }
649
+ if (step.kind === "widen-enum") {
650
+ const target = columns.find(({ name }) => name === step.columnName);
651
+ if (target !== undefined) {
652
+ if (step.enumValues === null)
653
+ delete target.enumValues;
654
+ else
655
+ target.enumValues = [...step.enumValues];
656
+ }
657
+ continue;
658
+ }
659
+ if (step.kind === "widen-nullable") {
660
+ const target = columns.find(({ name }) => name === step.columnName);
661
+ if (target !== undefined)
662
+ target.nullable = true;
663
+ continue;
664
+ }
665
+ if (step.kind === "drop-column") {
666
+ // The column record goes; the stored blocks stay until compaction rewrites the segments
667
+ // that hold them, which is what keeps the drop a metadata step.
668
+ const index = columns.findIndex(({ name }) => name === step.columnName);
669
+ if (index !== -1)
670
+ columns.splice(index, 1);
671
+ continue;
672
+ }
673
+ if (step.kind === "tighten-nullable") {
674
+ // The proof happens before this runs; see MinnowDatabase#columnHoldsNull.
675
+ const target = columns.find(({ name }) => name === step.columnName);
676
+ if (target !== undefined)
677
+ target.nullable = false;
678
+ continue;
679
+ }
680
+ if (step.kind === "set-auto-increment") {
681
+ const target = columns.find(({ name }) => name === step.columnName);
682
+ if (target !== undefined) {
683
+ if (step.enabled)
684
+ target.defaultValue = { kind: "autoincrement" };
685
+ else
686
+ delete target.defaultValue;
687
+ }
688
+ continue;
689
+ }
690
+ if (step.kind === "alter-default") {
691
+ const target = columns.find(({ name }) => name === step.columnName);
692
+ if (target !== undefined) {
693
+ if (step.defaultValue === null)
694
+ delete target.defaultValue;
695
+ else
696
+ target.defaultValue = step.defaultValue;
697
+ }
698
+ }
699
+ }
700
+ return columns;
701
+ }
702
+ function columnDefaultsEqual(left, right) {
703
+ if (left === undefined || right === undefined)
704
+ return left === right;
705
+ if (left.kind !== right.kind)
706
+ return false;
707
+ return left.kind !== "literal" || right.kind !== "literal" || left.value === right.value;
708
+ }
709
+ //# sourceMappingURL=schema.js.map