@sedrino/db-schema 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -0
- package/dist/index.d.ts +1222 -0
- package/dist/index.js +1456 -0
- package/dist/index.js.map +1 -0
- package/docs/cli.md +70 -0
- package/docs/index.md +11 -0
- package/docs/migrations.md +65 -0
- package/docs/schema-document.md +48 -0
- package/package.json +41 -0
- package/src/apply.ts +234 -0
- package/src/cli.ts +209 -0
- package/src/drizzle.ts +178 -0
- package/src/index.ts +62 -0
- package/src/migration.ts +456 -0
- package/src/planner.ts +247 -0
- package/src/project.ts +79 -0
- package/src/schema.ts +53 -0
- package/src/sqlite.ts +172 -0
- package/src/types.ts +145 -0
- package/src/utils.ts +286 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1456 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var foreignKeyActionSchema = z.enum([
|
|
4
|
+
"cascade",
|
|
5
|
+
"restrict",
|
|
6
|
+
"set null",
|
|
7
|
+
"set default",
|
|
8
|
+
"no action"
|
|
9
|
+
]);
|
|
10
|
+
var fieldReferenceSpecSchema = z.object({
|
|
11
|
+
table: z.string().min(1),
|
|
12
|
+
field: z.string().min(1),
|
|
13
|
+
onDelete: foreignKeyActionSchema.optional(),
|
|
14
|
+
onUpdate: foreignKeyActionSchema.optional()
|
|
15
|
+
});
|
|
16
|
+
var logicalTypeSpecSchema = z.discriminatedUnion("kind", [
|
|
17
|
+
z.object({
|
|
18
|
+
kind: z.literal("id"),
|
|
19
|
+
prefix: z.string().min(1)
|
|
20
|
+
}),
|
|
21
|
+
z.object({
|
|
22
|
+
kind: z.literal("string"),
|
|
23
|
+
format: z.enum(["email", "url", "slug"]).optional()
|
|
24
|
+
}),
|
|
25
|
+
z.object({
|
|
26
|
+
kind: z.literal("text")
|
|
27
|
+
}),
|
|
28
|
+
z.object({
|
|
29
|
+
kind: z.literal("boolean")
|
|
30
|
+
}),
|
|
31
|
+
z.object({
|
|
32
|
+
kind: z.literal("integer")
|
|
33
|
+
}),
|
|
34
|
+
z.object({
|
|
35
|
+
kind: z.literal("number")
|
|
36
|
+
}),
|
|
37
|
+
z.object({
|
|
38
|
+
kind: z.literal("enum"),
|
|
39
|
+
values: z.array(z.string()).min(1)
|
|
40
|
+
}),
|
|
41
|
+
z.object({
|
|
42
|
+
kind: z.literal("json"),
|
|
43
|
+
tsType: z.string().min(1)
|
|
44
|
+
}),
|
|
45
|
+
z.object({
|
|
46
|
+
kind: z.literal("temporal.instant")
|
|
47
|
+
}),
|
|
48
|
+
z.object({
|
|
49
|
+
kind: z.literal("temporal.plainDate")
|
|
50
|
+
})
|
|
51
|
+
]);
|
|
52
|
+
var storageSpecSchema = z.discriminatedUnion("strategy", [
|
|
53
|
+
z.object({
|
|
54
|
+
strategy: z.literal("sqlite.text"),
|
|
55
|
+
column: z.string().min(1)
|
|
56
|
+
}),
|
|
57
|
+
z.object({
|
|
58
|
+
strategy: z.literal("sqlite.integer"),
|
|
59
|
+
column: z.string().min(1)
|
|
60
|
+
}),
|
|
61
|
+
z.object({
|
|
62
|
+
strategy: z.literal("sqlite.real"),
|
|
63
|
+
column: z.string().min(1)
|
|
64
|
+
}),
|
|
65
|
+
z.object({
|
|
66
|
+
strategy: z.literal("sqlite.temporalInstantEpochMs"),
|
|
67
|
+
column: z.string().min(1)
|
|
68
|
+
}),
|
|
69
|
+
z.object({
|
|
70
|
+
strategy: z.literal("sqlite.temporalPlainDateText"),
|
|
71
|
+
column: z.string().min(1)
|
|
72
|
+
})
|
|
73
|
+
]);
|
|
74
|
+
var defaultSpecSchema = z.discriminatedUnion("kind", [
|
|
75
|
+
z.object({
|
|
76
|
+
kind: z.literal("literal"),
|
|
77
|
+
value: z.union([z.string(), z.number(), z.boolean(), z.null()])
|
|
78
|
+
}),
|
|
79
|
+
z.object({
|
|
80
|
+
kind: z.literal("now")
|
|
81
|
+
}),
|
|
82
|
+
z.object({
|
|
83
|
+
kind: z.literal("generatedId"),
|
|
84
|
+
prefix: z.string().min(1)
|
|
85
|
+
})
|
|
86
|
+
]);
|
|
87
|
+
var fieldSpecSchema = z.object({
|
|
88
|
+
id: z.string().min(1),
|
|
89
|
+
name: z.string().min(1),
|
|
90
|
+
logical: logicalTypeSpecSchema,
|
|
91
|
+
storage: storageSpecSchema,
|
|
92
|
+
nullable: z.boolean().default(true),
|
|
93
|
+
default: defaultSpecSchema.optional(),
|
|
94
|
+
primaryKey: z.boolean().default(false),
|
|
95
|
+
unique: z.boolean().default(false),
|
|
96
|
+
description: z.string().min(1).optional(),
|
|
97
|
+
references: fieldReferenceSpecSchema.optional()
|
|
98
|
+
});
|
|
99
|
+
var indexSpecSchema = z.object({
|
|
100
|
+
name: z.string().min(1).optional(),
|
|
101
|
+
fields: z.array(z.string().min(1)).min(1)
|
|
102
|
+
});
|
|
103
|
+
var uniqueSpecSchema = z.object({
|
|
104
|
+
name: z.string().min(1).optional(),
|
|
105
|
+
fields: z.array(z.string().min(1)).min(1)
|
|
106
|
+
});
|
|
107
|
+
var tableSpecSchema = z.object({
|
|
108
|
+
id: z.string().min(1),
|
|
109
|
+
name: z.string().min(1),
|
|
110
|
+
description: z.string().min(1).optional(),
|
|
111
|
+
fields: z.array(fieldSpecSchema).default([]),
|
|
112
|
+
indexes: z.array(indexSpecSchema).default([]),
|
|
113
|
+
uniques: z.array(uniqueSpecSchema).default([])
|
|
114
|
+
});
|
|
115
|
+
var schemaDocumentSchema = z.object({
|
|
116
|
+
version: z.literal(1),
|
|
117
|
+
dialect: z.literal("sqlite"),
|
|
118
|
+
schemaId: z.string().min(1),
|
|
119
|
+
tables: z.array(tableSpecSchema).default([])
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// src/utils.ts
|
|
123
|
+
var SQLITE_EPOCH_MS_NOW = "CAST(strftime('%s','now') AS integer) * 1000 + CAST((strftime('%f','now') - strftime('%S','now')) * 1000 AS integer)";
|
|
124
|
+
function sqliteEpochMsNowSql() {
|
|
125
|
+
return SQLITE_EPOCH_MS_NOW;
|
|
126
|
+
}
|
|
127
|
+
function toSnakeCase(value) {
|
|
128
|
+
return value.replace(/-/g, "_").replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z]+)([A-Z][a-z0-9]+)/g, "$1_$2").toLowerCase();
|
|
129
|
+
}
|
|
130
|
+
function toPascalCase(value) {
|
|
131
|
+
return value.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/\s+/).filter(Boolean).map((part) => `${part[0].toUpperCase()}${part.slice(1)}`).join("");
|
|
132
|
+
}
|
|
133
|
+
function quoteIdentifier(value) {
|
|
134
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
135
|
+
}
|
|
136
|
+
function tableVariableName(tableName) {
|
|
137
|
+
return `table${toPascalCase(tableName)}`;
|
|
138
|
+
}
|
|
139
|
+
function fieldAutoId(tableName, fieldName) {
|
|
140
|
+
return `fld_${toSnakeCase(tableName)}_${toSnakeCase(fieldName)}`;
|
|
141
|
+
}
|
|
142
|
+
function tableAutoId(tableName) {
|
|
143
|
+
return `tbl_${toSnakeCase(tableName)}`;
|
|
144
|
+
}
|
|
145
|
+
function defaultStorageForLogical(logical, column) {
|
|
146
|
+
switch (logical.kind) {
|
|
147
|
+
case "id":
|
|
148
|
+
case "string":
|
|
149
|
+
case "text":
|
|
150
|
+
case "enum":
|
|
151
|
+
case "json":
|
|
152
|
+
return { strategy: "sqlite.text", column };
|
|
153
|
+
case "boolean":
|
|
154
|
+
case "integer":
|
|
155
|
+
return { strategy: "sqlite.integer", column };
|
|
156
|
+
case "number":
|
|
157
|
+
return { strategy: "sqlite.real", column };
|
|
158
|
+
case "temporal.instant":
|
|
159
|
+
return { strategy: "sqlite.temporalInstantEpochMs", column };
|
|
160
|
+
case "temporal.plainDate":
|
|
161
|
+
return { strategy: "sqlite.temporalPlainDateText", column };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function validateLogicalAndStorageCompatibility(logical, storage) {
|
|
165
|
+
switch (storage.strategy) {
|
|
166
|
+
case "sqlite.temporalInstantEpochMs":
|
|
167
|
+
return logical.kind === "temporal.instant" ? null : "sqlite.temporalInstantEpochMs storage requires logical kind temporal.instant";
|
|
168
|
+
case "sqlite.temporalPlainDateText":
|
|
169
|
+
return logical.kind === "temporal.plainDate" ? null : "sqlite.temporalPlainDateText storage requires logical kind temporal.plainDate";
|
|
170
|
+
case "sqlite.integer":
|
|
171
|
+
return logical.kind === "boolean" || logical.kind === "integer" ? null : "sqlite.integer storage requires logical kind boolean or integer";
|
|
172
|
+
case "sqlite.real":
|
|
173
|
+
return logical.kind === "number" ? null : "sqlite.real storage requires logical kind number";
|
|
174
|
+
case "sqlite.text":
|
|
175
|
+
return logical.kind === "id" || logical.kind === "string" || logical.kind === "text" || logical.kind === "enum" || logical.kind === "json" ? null : "sqlite.text storage requires logical kind id, string, text, enum, or json";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function validateDefaultCompatibility(field, defaultValue) {
|
|
179
|
+
if (defaultValue.kind === "generatedId") {
|
|
180
|
+
return field.logical.kind === "id" ? null : "generatedId default requires logical kind id";
|
|
181
|
+
}
|
|
182
|
+
if (defaultValue.kind === "now") {
|
|
183
|
+
return field.logical.kind === "temporal.instant" ? null : "now default requires logical kind temporal.instant";
|
|
184
|
+
}
|
|
185
|
+
if (field.logical.kind === "boolean" && typeof defaultValue.value !== "boolean") {
|
|
186
|
+
return "boolean fields require boolean literal defaults";
|
|
187
|
+
}
|
|
188
|
+
if (field.logical.kind === "integer" && !Number.isInteger(defaultValue.value)) {
|
|
189
|
+
return "integer fields require integer literal defaults";
|
|
190
|
+
}
|
|
191
|
+
if (field.logical.kind === "number" && typeof defaultValue.value !== "number") {
|
|
192
|
+
return "number fields require numeric literal defaults";
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
function validateSchemaDocumentCompatibility(schema) {
|
|
197
|
+
const issues = [];
|
|
198
|
+
const tableNames = /* @__PURE__ */ new Set();
|
|
199
|
+
for (const table of schema.tables) {
|
|
200
|
+
if (tableNames.has(table.name)) {
|
|
201
|
+
issues.push({
|
|
202
|
+
path: `tables.${table.name}`,
|
|
203
|
+
message: `Duplicate table name ${table.name}`
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
tableNames.add(table.name);
|
|
207
|
+
const fieldNames = /* @__PURE__ */ new Set();
|
|
208
|
+
let primaryKeyCount = 0;
|
|
209
|
+
for (const field of table.fields) {
|
|
210
|
+
if (fieldNames.has(field.name)) {
|
|
211
|
+
issues.push({
|
|
212
|
+
path: `tables.${table.name}.fields.${field.name}`,
|
|
213
|
+
message: `Duplicate field name ${field.name}`
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
fieldNames.add(field.name);
|
|
217
|
+
if (field.primaryKey) primaryKeyCount += 1;
|
|
218
|
+
if (field.primaryKey && field.nullable) {
|
|
219
|
+
issues.push({
|
|
220
|
+
path: `tables.${table.name}.fields.${field.name}`,
|
|
221
|
+
message: "Primary key fields cannot be nullable"
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
const storageIssue = validateLogicalAndStorageCompatibility(field.logical, field.storage);
|
|
225
|
+
if (storageIssue) {
|
|
226
|
+
issues.push({
|
|
227
|
+
path: `tables.${table.name}.fields.${field.name}`,
|
|
228
|
+
message: storageIssue
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
if (field.default) {
|
|
232
|
+
const defaultIssue = validateDefaultCompatibility(field, field.default);
|
|
233
|
+
if (defaultIssue) {
|
|
234
|
+
issues.push({
|
|
235
|
+
path: `tables.${table.name}.fields.${field.name}`,
|
|
236
|
+
message: defaultIssue
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (field.references && !tableNames.has(field.references.table)) {
|
|
241
|
+
const targetTableExists = schema.tables.some(
|
|
242
|
+
(candidate) => candidate.name === field.references.table
|
|
243
|
+
);
|
|
244
|
+
if (!targetTableExists) {
|
|
245
|
+
issues.push({
|
|
246
|
+
path: `tables.${table.name}.fields.${field.name}`,
|
|
247
|
+
message: `Referenced table ${field.references.table} does not exist`
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (field.references) {
|
|
252
|
+
const targetTable = schema.tables.find(
|
|
253
|
+
(candidate) => candidate.name === field.references.table
|
|
254
|
+
);
|
|
255
|
+
const targetFieldExists = targetTable?.fields.some(
|
|
256
|
+
(candidate) => candidate.name === field.references.field
|
|
257
|
+
);
|
|
258
|
+
if (targetTable && !targetFieldExists) {
|
|
259
|
+
issues.push({
|
|
260
|
+
path: `tables.${table.name}.fields.${field.name}`,
|
|
261
|
+
message: `Referenced field ${field.references.field} does not exist on table ${field.references.table}`
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (primaryKeyCount > 1) {
|
|
267
|
+
issues.push({
|
|
268
|
+
path: `tables.${table.name}`,
|
|
269
|
+
message: "Composite primary keys are not supported in v1"
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
for (const index of table.indexes) {
|
|
273
|
+
for (const fieldName of index.fields) {
|
|
274
|
+
if (!fieldNames.has(fieldName)) {
|
|
275
|
+
issues.push({
|
|
276
|
+
path: `tables.${table.name}.indexes.${index.name ?? fieldName}`,
|
|
277
|
+
message: `Index references missing field ${fieldName}`
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
for (const unique of table.uniques) {
|
|
283
|
+
for (const fieldName of unique.fields) {
|
|
284
|
+
if (!fieldNames.has(fieldName)) {
|
|
285
|
+
issues.push({
|
|
286
|
+
path: `tables.${table.name}.uniques.${unique.name ?? fieldName}`,
|
|
287
|
+
message: `Unique constraint references missing field ${fieldName}`
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return issues;
|
|
294
|
+
}
|
|
295
|
+
function stableStringify(value) {
|
|
296
|
+
return JSON.stringify(sortValue(value));
|
|
297
|
+
}
|
|
298
|
+
function sortValue(value) {
|
|
299
|
+
if (Array.isArray(value)) {
|
|
300
|
+
return value.map((entry) => sortValue(entry));
|
|
301
|
+
}
|
|
302
|
+
if (value && typeof value === "object") {
|
|
303
|
+
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, innerValue]) => [key, sortValue(innerValue)]);
|
|
304
|
+
return Object.fromEntries(entries);
|
|
305
|
+
}
|
|
306
|
+
return value;
|
|
307
|
+
}
|
|
308
|
+
function createSchemaHash(schema) {
|
|
309
|
+
const text = stableStringify(schema);
|
|
310
|
+
let hash = 2166136261;
|
|
311
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
312
|
+
hash ^= text.charCodeAt(index);
|
|
313
|
+
hash = Math.imul(hash, 16777619);
|
|
314
|
+
}
|
|
315
|
+
return `schema_${(hash >>> 0).toString(16).padStart(8, "0")}`;
|
|
316
|
+
}
|
|
317
|
+
function cloneSchema(value) {
|
|
318
|
+
return structuredClone(value);
|
|
319
|
+
}
|
|
320
|
+
function renameStorageColumn(field, newFieldName) {
|
|
321
|
+
const next = cloneSchema(field);
|
|
322
|
+
const column = toSnakeCase(newFieldName);
|
|
323
|
+
next.name = newFieldName;
|
|
324
|
+
next.storage = { ...next.storage, column };
|
|
325
|
+
return next;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/migration.ts
|
|
329
|
+
var MutableFieldBuilder = class {
|
|
330
|
+
constructor(field) {
|
|
331
|
+
this.field = field;
|
|
332
|
+
}
|
|
333
|
+
field;
|
|
334
|
+
required() {
|
|
335
|
+
this.field.nullable = false;
|
|
336
|
+
return this;
|
|
337
|
+
}
|
|
338
|
+
nullable() {
|
|
339
|
+
this.field.nullable = true;
|
|
340
|
+
return this;
|
|
341
|
+
}
|
|
342
|
+
unique() {
|
|
343
|
+
this.field.unique = true;
|
|
344
|
+
return this;
|
|
345
|
+
}
|
|
346
|
+
default(value) {
|
|
347
|
+
const nextDefault = {
|
|
348
|
+
kind: "literal",
|
|
349
|
+
value
|
|
350
|
+
};
|
|
351
|
+
const issue = validateDefaultCompatibility(this.field, nextDefault);
|
|
352
|
+
if (issue) throw new Error(issue);
|
|
353
|
+
this.field.default = nextDefault;
|
|
354
|
+
return this;
|
|
355
|
+
}
|
|
356
|
+
defaultNow() {
|
|
357
|
+
const nextDefault = {
|
|
358
|
+
kind: "now"
|
|
359
|
+
};
|
|
360
|
+
const issue = validateDefaultCompatibility(this.field, nextDefault);
|
|
361
|
+
if (issue) throw new Error(issue);
|
|
362
|
+
this.field.default = nextDefault;
|
|
363
|
+
return this;
|
|
364
|
+
}
|
|
365
|
+
references(reference) {
|
|
366
|
+
this.field.references = reference;
|
|
367
|
+
return this;
|
|
368
|
+
}
|
|
369
|
+
description(description) {
|
|
370
|
+
this.field.description = description;
|
|
371
|
+
return this;
|
|
372
|
+
}
|
|
373
|
+
column(column) {
|
|
374
|
+
this.field.storage = {
|
|
375
|
+
...this.field.storage,
|
|
376
|
+
column
|
|
377
|
+
};
|
|
378
|
+
return this;
|
|
379
|
+
}
|
|
380
|
+
build() {
|
|
381
|
+
return this.field;
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
function createFieldSpec(tableName, fieldName, logical, options = {}) {
|
|
385
|
+
const column = options.column ?? toSnakeCase(fieldName);
|
|
386
|
+
const storage = options.storage ?? defaultStorageForLogical(logical, column);
|
|
387
|
+
const storageIssue = validateLogicalAndStorageCompatibility(logical, storage);
|
|
388
|
+
if (storageIssue) throw new Error(storageIssue);
|
|
389
|
+
const field = {
|
|
390
|
+
id: fieldAutoId(tableName, fieldName),
|
|
391
|
+
name: fieldName,
|
|
392
|
+
logical,
|
|
393
|
+
storage,
|
|
394
|
+
nullable: true,
|
|
395
|
+
primaryKey: false,
|
|
396
|
+
unique: false,
|
|
397
|
+
description: options.description,
|
|
398
|
+
references: options.references
|
|
399
|
+
};
|
|
400
|
+
if (logical.kind === "id") {
|
|
401
|
+
field.primaryKey = true;
|
|
402
|
+
field.nullable = false;
|
|
403
|
+
field.default = {
|
|
404
|
+
kind: "generatedId",
|
|
405
|
+
prefix: logical.prefix
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
return field;
|
|
409
|
+
}
|
|
410
|
+
var TableCreateBuilder = class {
|
|
411
|
+
constructor(tableName) {
|
|
412
|
+
this.tableName = tableName;
|
|
413
|
+
}
|
|
414
|
+
tableName;
|
|
415
|
+
fields = [];
|
|
416
|
+
indexes = [];
|
|
417
|
+
uniques = [];
|
|
418
|
+
descriptionText;
|
|
419
|
+
description(text) {
|
|
420
|
+
this.descriptionText = text;
|
|
421
|
+
return this;
|
|
422
|
+
}
|
|
423
|
+
addField(field) {
|
|
424
|
+
this.fields.push(field);
|
|
425
|
+
return new MutableFieldBuilder(field);
|
|
426
|
+
}
|
|
427
|
+
id(fieldName, options) {
|
|
428
|
+
return this.addField(
|
|
429
|
+
createFieldSpec(this.tableName, fieldName, { kind: "id", prefix: options.prefix }, options)
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
string(fieldName, options = {}) {
|
|
433
|
+
return this.addField(
|
|
434
|
+
createFieldSpec(
|
|
435
|
+
this.tableName,
|
|
436
|
+
fieldName,
|
|
437
|
+
{ kind: "string", format: options.format },
|
|
438
|
+
options
|
|
439
|
+
)
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
text(fieldName, options = {}) {
|
|
443
|
+
return this.addField(createFieldSpec(this.tableName, fieldName, { kind: "text" }, options));
|
|
444
|
+
}
|
|
445
|
+
boolean(fieldName, options = {}) {
|
|
446
|
+
return this.addField(createFieldSpec(this.tableName, fieldName, { kind: "boolean" }, options));
|
|
447
|
+
}
|
|
448
|
+
integer(fieldName, options = {}) {
|
|
449
|
+
return this.addField(createFieldSpec(this.tableName, fieldName, { kind: "integer" }, options));
|
|
450
|
+
}
|
|
451
|
+
number(fieldName, options = {}) {
|
|
452
|
+
return this.addField(createFieldSpec(this.tableName, fieldName, { kind: "number" }, options));
|
|
453
|
+
}
|
|
454
|
+
enum(fieldName, values, options = {}) {
|
|
455
|
+
return this.addField(
|
|
456
|
+
createFieldSpec(this.tableName, fieldName, { kind: "enum", values }, options)
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
json(fieldName, tsType, options = {}) {
|
|
460
|
+
return this.addField(
|
|
461
|
+
createFieldSpec(this.tableName, fieldName, { kind: "json", tsType }, options)
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
temporalInstant(fieldName, options = {}) {
|
|
465
|
+
return this.addField(
|
|
466
|
+
createFieldSpec(this.tableName, fieldName, { kind: "temporal.instant" }, options)
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
temporalPlainDate(fieldName, options = {}) {
|
|
470
|
+
return this.addField(
|
|
471
|
+
createFieldSpec(this.tableName, fieldName, { kind: "temporal.plainDate" }, options)
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
reference(fieldName, options) {
|
|
475
|
+
return this.addField(
|
|
476
|
+
createFieldSpec(
|
|
477
|
+
this.tableName,
|
|
478
|
+
fieldName,
|
|
479
|
+
{ kind: "string" },
|
|
480
|
+
{ ...options, references: options.references }
|
|
481
|
+
)
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
index(fields, options = {}) {
|
|
485
|
+
this.indexes.push({
|
|
486
|
+
fields,
|
|
487
|
+
name: options.name
|
|
488
|
+
});
|
|
489
|
+
return this;
|
|
490
|
+
}
|
|
491
|
+
unique(fields, options = {}) {
|
|
492
|
+
this.uniques.push({
|
|
493
|
+
fields,
|
|
494
|
+
name: options.name
|
|
495
|
+
});
|
|
496
|
+
return this;
|
|
497
|
+
}
|
|
498
|
+
build() {
|
|
499
|
+
return {
|
|
500
|
+
id: tableAutoId(this.tableName),
|
|
501
|
+
name: this.tableName,
|
|
502
|
+
description: this.descriptionText,
|
|
503
|
+
fields: this.fields,
|
|
504
|
+
indexes: this.indexes,
|
|
505
|
+
uniques: this.uniques
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
var TableAlterBuilder = class {
|
|
510
|
+
constructor(tableName) {
|
|
511
|
+
this.tableName = tableName;
|
|
512
|
+
}
|
|
513
|
+
tableName;
|
|
514
|
+
operations = [];
|
|
515
|
+
addField(field) {
|
|
516
|
+
const operation = {
|
|
517
|
+
kind: "addField",
|
|
518
|
+
tableName: this.tableName,
|
|
519
|
+
field
|
|
520
|
+
};
|
|
521
|
+
this.operations.push(operation);
|
|
522
|
+
return new MutableFieldBuilder(field);
|
|
523
|
+
}
|
|
524
|
+
string(fieldName, options = {}) {
|
|
525
|
+
return this.addField(
|
|
526
|
+
createFieldSpec(
|
|
527
|
+
this.tableName,
|
|
528
|
+
fieldName,
|
|
529
|
+
{ kind: "string", format: options.format },
|
|
530
|
+
options
|
|
531
|
+
)
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
text(fieldName, options = {}) {
|
|
535
|
+
return this.addField(createFieldSpec(this.tableName, fieldName, { kind: "text" }, options));
|
|
536
|
+
}
|
|
537
|
+
boolean(fieldName, options = {}) {
|
|
538
|
+
return this.addField(createFieldSpec(this.tableName, fieldName, { kind: "boolean" }, options));
|
|
539
|
+
}
|
|
540
|
+
integer(fieldName, options = {}) {
|
|
541
|
+
return this.addField(createFieldSpec(this.tableName, fieldName, { kind: "integer" }, options));
|
|
542
|
+
}
|
|
543
|
+
number(fieldName, options = {}) {
|
|
544
|
+
return this.addField(createFieldSpec(this.tableName, fieldName, { kind: "number" }, options));
|
|
545
|
+
}
|
|
546
|
+
enum(fieldName, values, options = {}) {
|
|
547
|
+
return this.addField(
|
|
548
|
+
createFieldSpec(this.tableName, fieldName, { kind: "enum", values }, options)
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
json(fieldName, tsType, options = {}) {
|
|
552
|
+
return this.addField(
|
|
553
|
+
createFieldSpec(this.tableName, fieldName, { kind: "json", tsType }, options)
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
temporalInstant(fieldName, options = {}) {
|
|
557
|
+
return this.addField(
|
|
558
|
+
createFieldSpec(this.tableName, fieldName, { kind: "temporal.instant" }, options)
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
temporalPlainDate(fieldName, options = {}) {
|
|
562
|
+
return this.addField(
|
|
563
|
+
createFieldSpec(this.tableName, fieldName, { kind: "temporal.plainDate" }, options)
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
reference(fieldName, options) {
|
|
567
|
+
return this.addField(
|
|
568
|
+
createFieldSpec(
|
|
569
|
+
this.tableName,
|
|
570
|
+
fieldName,
|
|
571
|
+
{ kind: "string" },
|
|
572
|
+
{ ...options, references: options.references }
|
|
573
|
+
)
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
dropField(fieldName) {
|
|
577
|
+
this.operations.push({
|
|
578
|
+
kind: "dropField",
|
|
579
|
+
tableName: this.tableName,
|
|
580
|
+
fieldName
|
|
581
|
+
});
|
|
582
|
+
return this;
|
|
583
|
+
}
|
|
584
|
+
renameField(from, to) {
|
|
585
|
+
this.operations.push({
|
|
586
|
+
kind: "renameField",
|
|
587
|
+
tableName: this.tableName,
|
|
588
|
+
from,
|
|
589
|
+
to
|
|
590
|
+
});
|
|
591
|
+
return this;
|
|
592
|
+
}
|
|
593
|
+
addIndex(fields, options = {}) {
|
|
594
|
+
this.operations.push({
|
|
595
|
+
kind: "addIndex",
|
|
596
|
+
tableName: this.tableName,
|
|
597
|
+
index: {
|
|
598
|
+
fields,
|
|
599
|
+
name: options.name
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
return this;
|
|
603
|
+
}
|
|
604
|
+
dropIndex(indexName) {
|
|
605
|
+
this.operations.push({
|
|
606
|
+
kind: "dropIndex",
|
|
607
|
+
tableName: this.tableName,
|
|
608
|
+
indexName
|
|
609
|
+
});
|
|
610
|
+
return this;
|
|
611
|
+
}
|
|
612
|
+
addUnique(fields, options = {}) {
|
|
613
|
+
this.operations.push({
|
|
614
|
+
kind: "addUnique",
|
|
615
|
+
tableName: this.tableName,
|
|
616
|
+
unique: {
|
|
617
|
+
fields,
|
|
618
|
+
name: options.name
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
return this;
|
|
622
|
+
}
|
|
623
|
+
dropUnique(uniqueName) {
|
|
624
|
+
this.operations.push({
|
|
625
|
+
kind: "dropUnique",
|
|
626
|
+
tableName: this.tableName,
|
|
627
|
+
uniqueName
|
|
628
|
+
});
|
|
629
|
+
return this;
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
var MigrationBuilderImpl = class {
|
|
633
|
+
operations = [];
|
|
634
|
+
createTable(name, callback) {
|
|
635
|
+
const table = new TableCreateBuilder(name);
|
|
636
|
+
callback(table);
|
|
637
|
+
this.operations.push({
|
|
638
|
+
kind: "createTable",
|
|
639
|
+
table: table.build()
|
|
640
|
+
});
|
|
641
|
+
return this;
|
|
642
|
+
}
|
|
643
|
+
dropTable(tableName) {
|
|
644
|
+
this.operations.push({
|
|
645
|
+
kind: "dropTable",
|
|
646
|
+
tableName
|
|
647
|
+
});
|
|
648
|
+
return this;
|
|
649
|
+
}
|
|
650
|
+
renameTable(from, to) {
|
|
651
|
+
this.operations.push({
|
|
652
|
+
kind: "renameTable",
|
|
653
|
+
from,
|
|
654
|
+
to
|
|
655
|
+
});
|
|
656
|
+
return this;
|
|
657
|
+
}
|
|
658
|
+
alterTable(tableName, callback) {
|
|
659
|
+
const table = new TableAlterBuilder(tableName);
|
|
660
|
+
callback(table);
|
|
661
|
+
this.operations.push(...table.operations);
|
|
662
|
+
return this;
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
function createMigration(meta, callback) {
|
|
666
|
+
return {
|
|
667
|
+
meta,
|
|
668
|
+
buildOperations() {
|
|
669
|
+
const builder = new MigrationBuilderImpl();
|
|
670
|
+
callback(builder);
|
|
671
|
+
return builder.operations;
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// src/schema.ts
|
|
677
|
+
function createEmptySchema(schemaId = "schema") {
|
|
678
|
+
return schemaDocumentSchema.parse({
|
|
679
|
+
version: 1,
|
|
680
|
+
dialect: "sqlite",
|
|
681
|
+
schemaId,
|
|
682
|
+
tables: []
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
function parseSchemaDocument(input) {
|
|
686
|
+
return schemaDocumentSchema.parse(input);
|
|
687
|
+
}
|
|
688
|
+
function validateSchemaDocument(input) {
|
|
689
|
+
const schema = parseSchemaDocument(input);
|
|
690
|
+
return {
|
|
691
|
+
schema,
|
|
692
|
+
issues: validateSchemaDocumentCompatibility(schema)
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
function assertValidSchemaDocument(input) {
|
|
696
|
+
const { schema, issues } = validateSchemaDocument(input);
|
|
697
|
+
if (issues.length > 0) {
|
|
698
|
+
throw new Error(
|
|
699
|
+
`Schema validation failed:
|
|
700
|
+
${issues.map((issue) => `- ${issue.path}: ${issue.message}`).join("\n")}`
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
return schema;
|
|
704
|
+
}
|
|
705
|
+
function schemaHash(input) {
|
|
706
|
+
return createSchemaHash(input);
|
|
707
|
+
}
|
|
708
|
+
function findTable(schema, tableName) {
|
|
709
|
+
return schema.tables.find((table) => table.name === tableName) ?? null;
|
|
710
|
+
}
|
|
711
|
+
function findField(table, fieldName) {
|
|
712
|
+
return table.fields.find((field) => field.name === fieldName) ?? null;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/sqlite.ts
|
|
716
|
+
function compileSchemaToSqlite(schema) {
|
|
717
|
+
const statements = [];
|
|
718
|
+
for (const table of schema.tables) {
|
|
719
|
+
statements.push(renderCreateTableStatement(table));
|
|
720
|
+
statements.push(...renderCreateIndexStatements(table));
|
|
721
|
+
}
|
|
722
|
+
return `${statements.join("\n\n")}
|
|
723
|
+
`;
|
|
724
|
+
}
|
|
725
|
+
function renderSqliteMigration(operations) {
|
|
726
|
+
const statements = [];
|
|
727
|
+
const warnings = [];
|
|
728
|
+
for (const operation of operations) {
|
|
729
|
+
switch (operation.kind) {
|
|
730
|
+
case "createTable":
|
|
731
|
+
statements.push(renderCreateTableStatement(operation.table));
|
|
732
|
+
statements.push(...renderCreateIndexStatements(operation.table));
|
|
733
|
+
break;
|
|
734
|
+
case "dropTable":
|
|
735
|
+
statements.push(`DROP TABLE ${quoteIdentifier(operation.tableName)};`);
|
|
736
|
+
break;
|
|
737
|
+
case "renameTable":
|
|
738
|
+
statements.push(
|
|
739
|
+
`ALTER TABLE ${quoteIdentifier(operation.from)} RENAME TO ${quoteIdentifier(operation.to)};`
|
|
740
|
+
);
|
|
741
|
+
break;
|
|
742
|
+
case "addField":
|
|
743
|
+
statements.push(
|
|
744
|
+
`ALTER TABLE ${quoteIdentifier(operation.tableName)} ADD COLUMN ${renderColumnDefinition(operation.field)};`
|
|
745
|
+
);
|
|
746
|
+
break;
|
|
747
|
+
case "renameField":
|
|
748
|
+
statements.push(
|
|
749
|
+
`ALTER TABLE ${quoteIdentifier(operation.tableName)} RENAME COLUMN ${quoteIdentifier(toSnakeCase(operation.from))} TO ${quoteIdentifier(toSnakeCase(operation.to))};`
|
|
750
|
+
);
|
|
751
|
+
break;
|
|
752
|
+
case "addIndex":
|
|
753
|
+
statements.push(renderCreateIndexStatement(operation.tableName, operation.index));
|
|
754
|
+
break;
|
|
755
|
+
case "dropIndex":
|
|
756
|
+
statements.push(`DROP INDEX ${quoteIdentifier(operation.indexName)};`);
|
|
757
|
+
break;
|
|
758
|
+
case "addUnique":
|
|
759
|
+
statements.push(renderCreateUniqueStatement(operation.tableName, operation.unique));
|
|
760
|
+
break;
|
|
761
|
+
case "dropUnique":
|
|
762
|
+
statements.push(`DROP INDEX ${quoteIdentifier(operation.uniqueName)};`);
|
|
763
|
+
break;
|
|
764
|
+
case "dropField":
|
|
765
|
+
warnings.push(
|
|
766
|
+
`dropField ${operation.tableName}.${operation.fieldName} requires a table rebuild and is not emitted in v1`
|
|
767
|
+
);
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return {
|
|
772
|
+
statements,
|
|
773
|
+
warnings
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
function renderCreateTableStatement(table) {
|
|
777
|
+
const columnLines = table.fields.map((field) => ` ${renderColumnDefinition(field)}`);
|
|
778
|
+
return `CREATE TABLE ${quoteIdentifier(table.name)} (
|
|
779
|
+
${columnLines.join(",\n")}
|
|
780
|
+
);`;
|
|
781
|
+
}
|
|
782
|
+
function renderCreateIndexStatements(table) {
|
|
783
|
+
const statements = table.indexes.map(
|
|
784
|
+
(index) => renderCreateIndexStatement(table.name, index, table)
|
|
785
|
+
);
|
|
786
|
+
statements.push(
|
|
787
|
+
...table.uniques.map((unique) => renderCreateUniqueStatement(table.name, unique, table))
|
|
788
|
+
);
|
|
789
|
+
return statements;
|
|
790
|
+
}
|
|
791
|
+
function renderCreateIndexStatement(tableName, index, table) {
|
|
792
|
+
const name = index.name ?? `${tableName}_${index.fields.join("_")}_idx`;
|
|
793
|
+
return `CREATE INDEX ${quoteIdentifier(name)} ON ${quoteIdentifier(tableName)} (${index.fields.map((field) => quoteIdentifier(resolveColumnName(table, field))).join(", ")});`;
|
|
794
|
+
}
|
|
795
|
+
function renderCreateUniqueStatement(tableName, unique, table) {
|
|
796
|
+
const name = unique.name ?? `${tableName}_${unique.fields.join("_")}_unique`;
|
|
797
|
+
return `CREATE UNIQUE INDEX ${quoteIdentifier(name)} ON ${quoteIdentifier(tableName)} (${unique.fields.map((field) => quoteIdentifier(resolveColumnName(table, field))).join(", ")});`;
|
|
798
|
+
}
|
|
799
|
+
function renderColumnDefinition(field) {
|
|
800
|
+
const parts = [quoteIdentifier(field.storage.column), renderSqlType(field.storage)];
|
|
801
|
+
if (field.primaryKey) parts.push("PRIMARY KEY");
|
|
802
|
+
if (!field.nullable) parts.push("NOT NULL");
|
|
803
|
+
if (field.unique && !field.primaryKey) parts.push("UNIQUE");
|
|
804
|
+
const defaultSql = renderSqlDefault(field.default, field);
|
|
805
|
+
if (defaultSql) parts.push(`DEFAULT ${defaultSql}`);
|
|
806
|
+
if (field.references) {
|
|
807
|
+
parts.push(
|
|
808
|
+
`REFERENCES ${quoteIdentifier(field.references.table)}(${quoteIdentifier(toSnakeCase(field.references.field))})`
|
|
809
|
+
);
|
|
810
|
+
if (field.references.onDelete)
|
|
811
|
+
parts.push(`ON DELETE ${field.references.onDelete.toUpperCase()}`);
|
|
812
|
+
if (field.references.onUpdate)
|
|
813
|
+
parts.push(`ON UPDATE ${field.references.onUpdate.toUpperCase()}`);
|
|
814
|
+
}
|
|
815
|
+
return parts.join(" ");
|
|
816
|
+
}
|
|
817
|
+
function renderSqlType(storage) {
|
|
818
|
+
switch (storage.strategy) {
|
|
819
|
+
case "sqlite.text":
|
|
820
|
+
case "sqlite.temporalPlainDateText":
|
|
821
|
+
return "TEXT";
|
|
822
|
+
case "sqlite.integer":
|
|
823
|
+
case "sqlite.temporalInstantEpochMs":
|
|
824
|
+
return "INTEGER";
|
|
825
|
+
case "sqlite.real":
|
|
826
|
+
return "REAL";
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
function renderSqlDefault(defaultValue, field) {
|
|
830
|
+
if (!defaultValue) return null;
|
|
831
|
+
switch (defaultValue.kind) {
|
|
832
|
+
case "generatedId":
|
|
833
|
+
return null;
|
|
834
|
+
case "now":
|
|
835
|
+
return `(${sqliteEpochMsNowSql()})`;
|
|
836
|
+
case "literal":
|
|
837
|
+
if (defaultValue.value === null) return "NULL";
|
|
838
|
+
if (typeof defaultValue.value === "string") {
|
|
839
|
+
return `'${defaultValue.value.replace(/'/g, "''")}'`;
|
|
840
|
+
}
|
|
841
|
+
if (typeof defaultValue.value === "boolean") {
|
|
842
|
+
return field.storage.strategy === "sqlite.integer" ? defaultValue.value ? "1" : "0" : defaultValue.value ? "TRUE" : "FALSE";
|
|
843
|
+
}
|
|
844
|
+
return `${defaultValue.value}`;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
function resolveColumnName(table, fieldName) {
|
|
848
|
+
const field = table?.fields.find((candidate) => candidate.name === fieldName);
|
|
849
|
+
return field?.storage.column ?? toSnakeCase(fieldName);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// src/planner.ts
|
|
853
|
+
function planMigration(args) {
|
|
854
|
+
const currentSchema = args.currentSchema ? assertValidSchemaDocument(args.currentSchema) : createEmptySchema(args.migration.meta.id);
|
|
855
|
+
const operations = args.migration.buildOperations();
|
|
856
|
+
const nextSchema = applyOperationsToSchema(currentSchema, operations);
|
|
857
|
+
return {
|
|
858
|
+
migrationId: args.migration.meta.id,
|
|
859
|
+
migrationName: args.migration.meta.name,
|
|
860
|
+
fromSchemaHash: createSchemaHash(currentSchema),
|
|
861
|
+
toSchemaHash: createSchemaHash(nextSchema),
|
|
862
|
+
operations,
|
|
863
|
+
nextSchema,
|
|
864
|
+
sql: renderSqliteMigration(operations)
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
function materializeSchema(args) {
|
|
868
|
+
let schema = args.baseSchema ? assertValidSchemaDocument(args.baseSchema) : createEmptySchema();
|
|
869
|
+
const plans = [];
|
|
870
|
+
for (const migration of args.migrations) {
|
|
871
|
+
const plan = planMigration({
|
|
872
|
+
currentSchema: schema,
|
|
873
|
+
migration
|
|
874
|
+
});
|
|
875
|
+
plans.push(plan);
|
|
876
|
+
schema = plan.nextSchema;
|
|
877
|
+
}
|
|
878
|
+
return {
|
|
879
|
+
schema,
|
|
880
|
+
plans
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
function applyOperationsToSchema(schemaInput, operations) {
|
|
884
|
+
const schema = cloneSchema(schemaInput);
|
|
885
|
+
for (const operation of operations) {
|
|
886
|
+
switch (operation.kind) {
|
|
887
|
+
case "createTable":
|
|
888
|
+
if (findTable(schema, operation.table.name)) {
|
|
889
|
+
throw new Error(`Table ${operation.table.name} already exists`);
|
|
890
|
+
}
|
|
891
|
+
schema.tables.push(cloneSchema(operation.table));
|
|
892
|
+
break;
|
|
893
|
+
case "dropTable": {
|
|
894
|
+
const referencedBy = schema.tables.flatMap(
|
|
895
|
+
(table) => table.fields.filter((field) => field.references?.table === operation.tableName).map((field) => `${table.name}.${field.name}`)
|
|
896
|
+
);
|
|
897
|
+
if (referencedBy.length > 0) {
|
|
898
|
+
throw new Error(
|
|
899
|
+
`Cannot drop table ${operation.tableName}; still referenced by ${referencedBy.join(", ")}`
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
const index = schema.tables.findIndex((table) => table.name === operation.tableName);
|
|
903
|
+
if (index < 0) throw new Error(`Table ${operation.tableName} does not exist`);
|
|
904
|
+
schema.tables.splice(index, 1);
|
|
905
|
+
break;
|
|
906
|
+
}
|
|
907
|
+
case "renameTable": {
|
|
908
|
+
const table = findTable(schema, operation.from);
|
|
909
|
+
if (!table) throw new Error(`Table ${operation.from} does not exist`);
|
|
910
|
+
if (findTable(schema, operation.to)) {
|
|
911
|
+
throw new Error(`Table ${operation.to} already exists`);
|
|
912
|
+
}
|
|
913
|
+
table.name = operation.to;
|
|
914
|
+
for (const candidateTable of schema.tables) {
|
|
915
|
+
for (const field of candidateTable.fields) {
|
|
916
|
+
if (field.references?.table === operation.from) {
|
|
917
|
+
field.references = {
|
|
918
|
+
...field.references,
|
|
919
|
+
table: operation.to
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
break;
|
|
925
|
+
}
|
|
926
|
+
case "addField": {
|
|
927
|
+
const table = findTable(schema, operation.tableName);
|
|
928
|
+
if (!table) throw new Error(`Table ${operation.tableName} does not exist`);
|
|
929
|
+
if (findField(table, operation.field.name)) {
|
|
930
|
+
throw new Error(`Field ${operation.tableName}.${operation.field.name} already exists`);
|
|
931
|
+
}
|
|
932
|
+
table.fields.push(cloneSchema(operation.field));
|
|
933
|
+
break;
|
|
934
|
+
}
|
|
935
|
+
case "dropField": {
|
|
936
|
+
const table = findTable(schema, operation.tableName);
|
|
937
|
+
if (!table) throw new Error(`Table ${operation.tableName} does not exist`);
|
|
938
|
+
const fieldIndex = table.fields.findIndex((field) => field.name === operation.fieldName);
|
|
939
|
+
if (fieldIndex < 0) {
|
|
940
|
+
throw new Error(`Field ${operation.tableName}.${operation.fieldName} does not exist`);
|
|
941
|
+
}
|
|
942
|
+
const referencedBy = schema.tables.flatMap(
|
|
943
|
+
(candidateTable) => candidateTable.fields.filter(
|
|
944
|
+
(field) => field.references?.table === operation.tableName && field.references.field === operation.fieldName
|
|
945
|
+
).map((field) => `${candidateTable.name}.${field.name}`)
|
|
946
|
+
);
|
|
947
|
+
if (referencedBy.length > 0) {
|
|
948
|
+
throw new Error(
|
|
949
|
+
`Cannot drop field ${operation.tableName}.${operation.fieldName}; still referenced by ${referencedBy.join(", ")}`
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
table.fields.splice(fieldIndex, 1);
|
|
953
|
+
table.indexes = table.indexes.filter(
|
|
954
|
+
(index) => !index.fields.includes(operation.fieldName)
|
|
955
|
+
);
|
|
956
|
+
table.uniques = table.uniques.filter(
|
|
957
|
+
(unique) => !unique.fields.includes(operation.fieldName)
|
|
958
|
+
);
|
|
959
|
+
break;
|
|
960
|
+
}
|
|
961
|
+
case "renameField": {
|
|
962
|
+
const table = findTable(schema, operation.tableName);
|
|
963
|
+
if (!table) throw new Error(`Table ${operation.tableName} does not exist`);
|
|
964
|
+
const field = findField(table, operation.from);
|
|
965
|
+
if (!field)
|
|
966
|
+
throw new Error(`Field ${operation.tableName}.${operation.from} does not exist`);
|
|
967
|
+
if (findField(table, operation.to)) {
|
|
968
|
+
throw new Error(`Field ${operation.tableName}.${operation.to} already exists`);
|
|
969
|
+
}
|
|
970
|
+
const renamed = renameStorageColumn(field, operation.to);
|
|
971
|
+
const index = table.fields.findIndex((candidate) => candidate.id === field.id);
|
|
972
|
+
table.fields[index] = renamed;
|
|
973
|
+
table.indexes = renameFieldInIndexes(table.indexes, operation.from, operation.to);
|
|
974
|
+
table.uniques = renameFieldInUniques(table.uniques, operation.from, operation.to);
|
|
975
|
+
for (const candidateTable of schema.tables) {
|
|
976
|
+
for (const candidateField of candidateTable.fields) {
|
|
977
|
+
if (candidateField.references?.table === table.name && candidateField.references.field === operation.from) {
|
|
978
|
+
candidateField.references = {
|
|
979
|
+
...candidateField.references,
|
|
980
|
+
field: operation.to
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
break;
|
|
986
|
+
}
|
|
987
|
+
case "addIndex": {
|
|
988
|
+
const table = findTable(schema, operation.tableName);
|
|
989
|
+
if (!table) throw new Error(`Table ${operation.tableName} does not exist`);
|
|
990
|
+
table.indexes.push(cloneSchema(operation.index));
|
|
991
|
+
break;
|
|
992
|
+
}
|
|
993
|
+
case "dropIndex": {
|
|
994
|
+
const table = findTable(schema, operation.tableName);
|
|
995
|
+
if (!table) throw new Error(`Table ${operation.tableName} does not exist`);
|
|
996
|
+
const nextIndexes = table.indexes.filter(
|
|
997
|
+
(index) => resolveIndexName(table.name, index) !== operation.indexName
|
|
998
|
+
);
|
|
999
|
+
if (nextIndexes.length === table.indexes.length) {
|
|
1000
|
+
throw new Error(`Index ${operation.indexName} does not exist on ${table.name}`);
|
|
1001
|
+
}
|
|
1002
|
+
table.indexes = nextIndexes;
|
|
1003
|
+
break;
|
|
1004
|
+
}
|
|
1005
|
+
case "addUnique": {
|
|
1006
|
+
const table = findTable(schema, operation.tableName);
|
|
1007
|
+
if (!table) throw new Error(`Table ${operation.tableName} does not exist`);
|
|
1008
|
+
table.uniques.push(cloneSchema(operation.unique));
|
|
1009
|
+
break;
|
|
1010
|
+
}
|
|
1011
|
+
case "dropUnique": {
|
|
1012
|
+
const table = findTable(schema, operation.tableName);
|
|
1013
|
+
if (!table) throw new Error(`Table ${operation.tableName} does not exist`);
|
|
1014
|
+
const nextUniques = table.uniques.filter(
|
|
1015
|
+
(unique) => resolveUniqueName(table.name, unique) !== operation.uniqueName
|
|
1016
|
+
);
|
|
1017
|
+
if (nextUniques.length === table.uniques.length) {
|
|
1018
|
+
throw new Error(`Unique ${operation.uniqueName} does not exist on ${table.name}`);
|
|
1019
|
+
}
|
|
1020
|
+
table.uniques = nextUniques;
|
|
1021
|
+
break;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
return assertValidSchemaDocument(schema);
|
|
1026
|
+
}
|
|
1027
|
+
function renameFieldInIndexes(indexes, from, to) {
|
|
1028
|
+
return indexes.map((index) => ({
|
|
1029
|
+
...index,
|
|
1030
|
+
fields: index.fields.map((field) => field === from ? to : field)
|
|
1031
|
+
}));
|
|
1032
|
+
}
|
|
1033
|
+
function renameFieldInUniques(uniques, from, to) {
|
|
1034
|
+
return uniques.map((unique) => ({
|
|
1035
|
+
...unique,
|
|
1036
|
+
fields: unique.fields.map((field) => field === from ? to : field)
|
|
1037
|
+
}));
|
|
1038
|
+
}
|
|
1039
|
+
function resolveIndexName(tableName, index) {
|
|
1040
|
+
return index.name ?? `${tableName}_${index.fields.join("_")}_idx`;
|
|
1041
|
+
}
|
|
1042
|
+
function resolveUniqueName(tableName, unique) {
|
|
1043
|
+
return unique.name ?? `${tableName}_${unique.fields.join("_")}_unique`;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// src/drizzle.ts
|
|
1047
|
+
function compileSchemaToDrizzle(schema) {
|
|
1048
|
+
const sqliteImports = /* @__PURE__ */ new Set(["sqliteTable"]);
|
|
1049
|
+
const toolkitImports = /* @__PURE__ */ new Set();
|
|
1050
|
+
let needsUlid = false;
|
|
1051
|
+
for (const table of schema.tables) {
|
|
1052
|
+
for (const field of table.fields) {
|
|
1053
|
+
switch (field.logical.kind) {
|
|
1054
|
+
case "id":
|
|
1055
|
+
case "string":
|
|
1056
|
+
case "text":
|
|
1057
|
+
case "enum":
|
|
1058
|
+
case "json":
|
|
1059
|
+
sqliteImports.add("text");
|
|
1060
|
+
break;
|
|
1061
|
+
case "boolean":
|
|
1062
|
+
case "integer":
|
|
1063
|
+
sqliteImports.add("integer");
|
|
1064
|
+
break;
|
|
1065
|
+
case "number":
|
|
1066
|
+
sqliteImports.add("real");
|
|
1067
|
+
break;
|
|
1068
|
+
case "temporal.instant":
|
|
1069
|
+
toolkitImports.add("temporalInstantEpochMs");
|
|
1070
|
+
break;
|
|
1071
|
+
case "temporal.plainDate":
|
|
1072
|
+
toolkitImports.add("temporalPlainDateText");
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1075
|
+
if (field.default?.kind === "generatedId") needsUlid = true;
|
|
1076
|
+
if (field.default?.kind === "now") toolkitImports.add("epochMsNow");
|
|
1077
|
+
}
|
|
1078
|
+
if (table.indexes.length > 0) sqliteImports.add("index");
|
|
1079
|
+
if (table.uniques.length > 0) sqliteImports.add("uniqueIndex");
|
|
1080
|
+
}
|
|
1081
|
+
const lines = [];
|
|
1082
|
+
lines.push(
|
|
1083
|
+
`import { ${Array.from(sqliteImports).sort().join(", ")} } from "drizzle-orm/sqlite-core";`
|
|
1084
|
+
);
|
|
1085
|
+
if (toolkitImports.size > 0) {
|
|
1086
|
+
lines.push(
|
|
1087
|
+
`import { ${Array.from(toolkitImports).sort().join(", ")} } from "@sedrino/toolkit/drizzle/sqlite";`
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
if (needsUlid) {
|
|
1091
|
+
lines.push(`import { ulid } from "ulid";`);
|
|
1092
|
+
}
|
|
1093
|
+
lines.push("");
|
|
1094
|
+
for (const table of schema.tables) {
|
|
1095
|
+
lines.push(renderTable(table));
|
|
1096
|
+
lines.push("");
|
|
1097
|
+
}
|
|
1098
|
+
return lines.join("\n").trimEnd() + "\n";
|
|
1099
|
+
}
|
|
1100
|
+
function renderTable(table) {
|
|
1101
|
+
const variableName = tableVariableName(table.name);
|
|
1102
|
+
const fieldLines = table.fields.map((field) => ` ${field.name}: ${renderField(field)},`);
|
|
1103
|
+
const tableConfig = renderTableConfig(table);
|
|
1104
|
+
if (!tableConfig) {
|
|
1105
|
+
return `export const ${variableName} = sqliteTable("${table.name}", {
|
|
1106
|
+
${fieldLines.join("\n")}
|
|
1107
|
+
});`;
|
|
1108
|
+
}
|
|
1109
|
+
return `export const ${variableName} = sqliteTable("${table.name}", {
|
|
1110
|
+
${fieldLines.join(
|
|
1111
|
+
"\n"
|
|
1112
|
+
)}
|
|
1113
|
+
}, (table) => [
|
|
1114
|
+
${tableConfig}
|
|
1115
|
+
]);`;
|
|
1116
|
+
}
|
|
1117
|
+
function renderTableConfig(table) {
|
|
1118
|
+
const lines = [];
|
|
1119
|
+
for (const index of table.indexes) {
|
|
1120
|
+
lines.push(` ${renderIndex(table.name, index)},`);
|
|
1121
|
+
}
|
|
1122
|
+
for (const unique of table.uniques) {
|
|
1123
|
+
lines.push(` ${renderUnique(table.name, unique)},`);
|
|
1124
|
+
}
|
|
1125
|
+
return lines.length > 0 ? lines.join("\n") : null;
|
|
1126
|
+
}
|
|
1127
|
+
function renderIndex(tableName, index) {
|
|
1128
|
+
const name = index.name ?? `${tableName}_${index.fields.join("_")}_idx`;
|
|
1129
|
+
return `index("${name}").on(${index.fields.map((field) => `table.${field}`).join(", ")})`;
|
|
1130
|
+
}
|
|
1131
|
+
function renderUnique(tableName, unique) {
|
|
1132
|
+
const name = unique.name ?? `${tableName}_${unique.fields.join("_")}_unique`;
|
|
1133
|
+
return `uniqueIndex("${name}").on(${unique.fields.map((field) => `table.${field}`).join(", ")})`;
|
|
1134
|
+
}
|
|
1135
|
+
function renderField(field) {
|
|
1136
|
+
let expression = renderBaseColumn(field);
|
|
1137
|
+
if (field.logical.kind === "json") {
|
|
1138
|
+
expression += `.$type<${field.logical.tsType}>()`;
|
|
1139
|
+
}
|
|
1140
|
+
if (field.primaryKey) expression += ".primaryKey()";
|
|
1141
|
+
if (!field.nullable) expression += ".notNull()";
|
|
1142
|
+
if (field.unique && !field.primaryKey) expression += ".unique()";
|
|
1143
|
+
const defaultExpression = renderDrizzleDefault(field.default);
|
|
1144
|
+
if (defaultExpression) expression += `.default(${defaultExpression})`;
|
|
1145
|
+
if (field.default?.kind === "generatedId") {
|
|
1146
|
+
expression += `.$default(() => \`${field.default.prefix}-\${ulid()}\`)`;
|
|
1147
|
+
}
|
|
1148
|
+
if (field.references) {
|
|
1149
|
+
const targetTable = tableVariableName(field.references.table);
|
|
1150
|
+
const referenceParts = [`() => ${targetTable}.${field.references.field}`];
|
|
1151
|
+
const options = [];
|
|
1152
|
+
if (field.references.onDelete) options.push(`onDelete: "${field.references.onDelete}"`);
|
|
1153
|
+
if (field.references.onUpdate) options.push(`onUpdate: "${field.references.onUpdate}"`);
|
|
1154
|
+
if (options.length > 0) {
|
|
1155
|
+
referenceParts.push(`{ ${options.join(", ")} }`);
|
|
1156
|
+
}
|
|
1157
|
+
expression += `.references(${referenceParts.join(", ")})`;
|
|
1158
|
+
}
|
|
1159
|
+
return expression;
|
|
1160
|
+
}
|
|
1161
|
+
function renderBaseColumn(field) {
|
|
1162
|
+
switch (field.logical.kind) {
|
|
1163
|
+
case "id":
|
|
1164
|
+
case "string":
|
|
1165
|
+
case "text":
|
|
1166
|
+
return `text("${field.storage.column}")`;
|
|
1167
|
+
case "enum":
|
|
1168
|
+
return `text("${field.storage.column}", { enum: [${field.logical.values.map((value) => JSON.stringify(value)).join(", ")}] })`;
|
|
1169
|
+
case "json":
|
|
1170
|
+
return `text("${field.storage.column}", { mode: "json" })`;
|
|
1171
|
+
case "boolean":
|
|
1172
|
+
return `integer("${field.storage.column}", { mode: "boolean" })`;
|
|
1173
|
+
case "integer":
|
|
1174
|
+
return `integer("${field.storage.column}", { mode: "number" })`;
|
|
1175
|
+
case "number":
|
|
1176
|
+
return `real("${field.storage.column}")`;
|
|
1177
|
+
case "temporal.instant":
|
|
1178
|
+
return `temporalInstantEpochMs("${field.storage.column}")`;
|
|
1179
|
+
case "temporal.plainDate":
|
|
1180
|
+
return `temporalPlainDateText("${field.storage.column}")`;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
function renderDrizzleDefault(defaultValue) {
|
|
1184
|
+
if (!defaultValue) return null;
|
|
1185
|
+
switch (defaultValue.kind) {
|
|
1186
|
+
case "generatedId":
|
|
1187
|
+
return null;
|
|
1188
|
+
case "now":
|
|
1189
|
+
return "epochMsNow()";
|
|
1190
|
+
case "literal":
|
|
1191
|
+
return JSON.stringify(defaultValue.value);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// src/apply.ts
|
|
1196
|
+
import { createClient } from "@libsql/client";
|
|
1197
|
+
var MIGRATIONS_TABLE = "_sedrino_schema_migrations";
|
|
1198
|
+
var STATE_TABLE = "_sedrino_schema_state";
|
|
1199
|
+
function createLibsqlClient(options) {
|
|
1200
|
+
return createClient({
|
|
1201
|
+
url: options.url,
|
|
1202
|
+
authToken: options.authToken,
|
|
1203
|
+
concurrency: 0
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
async function applyMigrations(args) {
|
|
1207
|
+
const client = args.client ?? createLibsqlClient(assertConnection(args.connection));
|
|
1208
|
+
await ensureMetadataTables(client);
|
|
1209
|
+
const appliedRows = await listAppliedMigrations(client);
|
|
1210
|
+
const appliedIds = new Set(appliedRows.map((row) => row.migrationId));
|
|
1211
|
+
const migrationMap = new Map(args.migrations.map((migration) => [migration.meta.id, migration]));
|
|
1212
|
+
for (const applied of appliedRows) {
|
|
1213
|
+
if (!migrationMap.has(applied.migrationId)) {
|
|
1214
|
+
throw new Error(
|
|
1215
|
+
`Database contains applied migration ${applied.migrationId}, but it is not present locally`
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
const appliedLocalMigrations = args.migrations.filter(
|
|
1220
|
+
(migration) => appliedIds.has(migration.meta.id)
|
|
1221
|
+
);
|
|
1222
|
+
const expectedCurrent = materializeSchema({
|
|
1223
|
+
baseSchema: args.baseSchema,
|
|
1224
|
+
migrations: appliedLocalMigrations
|
|
1225
|
+
}).schema;
|
|
1226
|
+
const expectedHash = schemaHash(expectedCurrent);
|
|
1227
|
+
const currentState = await getSchemaState(client);
|
|
1228
|
+
if (currentState) {
|
|
1229
|
+
if (currentState.schemaHash !== expectedHash) {
|
|
1230
|
+
throw new Error(
|
|
1231
|
+
`Schema drift detected. Database hash ${currentState.schemaHash} does not match expected local hash ${expectedHash}`
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
} else if (appliedRows.length > 0) {
|
|
1235
|
+
throw new Error(
|
|
1236
|
+
`Database has applied migrations recorded in ${MIGRATIONS_TABLE} but is missing ${STATE_TABLE}`
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
const pendingMigrations = args.migrations.filter(
|
|
1240
|
+
(migration) => !appliedIds.has(migration.meta.id)
|
|
1241
|
+
);
|
|
1242
|
+
const appliedPlans = [];
|
|
1243
|
+
let currentSchema = expectedCurrent;
|
|
1244
|
+
for (const migration of pendingMigrations) {
|
|
1245
|
+
const plan = planMigration({
|
|
1246
|
+
currentSchema,
|
|
1247
|
+
migration
|
|
1248
|
+
});
|
|
1249
|
+
if (plan.sql.warnings.length > 0) {
|
|
1250
|
+
throw new Error(
|
|
1251
|
+
`Migration ${plan.migrationId} cannot be applied safely:
|
|
1252
|
+
${plan.sql.warnings.map((warning) => `- ${warning}`).join("\n")}`
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
await executePlan(client, plan);
|
|
1256
|
+
currentSchema = plan.nextSchema;
|
|
1257
|
+
appliedPlans.push(plan);
|
|
1258
|
+
}
|
|
1259
|
+
return {
|
|
1260
|
+
appliedPlans,
|
|
1261
|
+
skippedMigrationIds: appliedLocalMigrations.map((migration) => migration.meta.id),
|
|
1262
|
+
currentSchema,
|
|
1263
|
+
currentSchemaHash: schemaHash(currentSchema)
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
async function listAppliedMigrations(client) {
|
|
1267
|
+
const result = await client.execute(
|
|
1268
|
+
`SELECT migration_id, migration_name, schema_hash, applied_at
|
|
1269
|
+
FROM ${MIGRATIONS_TABLE}
|
|
1270
|
+
ORDER BY applied_at ASC, migration_id ASC`
|
|
1271
|
+
);
|
|
1272
|
+
return result.rows.map((row) => ({
|
|
1273
|
+
migrationId: getString(row.migration_id) ?? "",
|
|
1274
|
+
migrationName: getString(row.migration_name) ?? "",
|
|
1275
|
+
schemaHash: getString(row.schema_hash) ?? "",
|
|
1276
|
+
appliedAt: getNumber(row.applied_at) ?? 0
|
|
1277
|
+
}));
|
|
1278
|
+
}
|
|
1279
|
+
async function getSchemaState(client) {
|
|
1280
|
+
const result = await client.execute(
|
|
1281
|
+
`SELECT schema_hash, schema_json
|
|
1282
|
+
FROM ${STATE_TABLE}
|
|
1283
|
+
WHERE singleton_id = 1`
|
|
1284
|
+
);
|
|
1285
|
+
const row = result.rows[0];
|
|
1286
|
+
if (!row) return null;
|
|
1287
|
+
const schemaHashValue = getString(row.schema_hash);
|
|
1288
|
+
const schemaJsonValue = getString(row.schema_json);
|
|
1289
|
+
if (!schemaHashValue || !schemaJsonValue) return null;
|
|
1290
|
+
return {
|
|
1291
|
+
schemaHash: schemaHashValue,
|
|
1292
|
+
schemaJson: schemaJsonValue
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
async function ensureMetadataTables(client) {
|
|
1296
|
+
await client.batch(
|
|
1297
|
+
[
|
|
1298
|
+
"PRAGMA foreign_keys = ON",
|
|
1299
|
+
`
|
|
1300
|
+
CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
|
|
1301
|
+
migration_id TEXT PRIMARY KEY,
|
|
1302
|
+
migration_name TEXT NOT NULL,
|
|
1303
|
+
schema_hash TEXT NOT NULL,
|
|
1304
|
+
applied_at INTEGER NOT NULL,
|
|
1305
|
+
sql_statements_json TEXT NOT NULL
|
|
1306
|
+
)
|
|
1307
|
+
`,
|
|
1308
|
+
`
|
|
1309
|
+
CREATE TABLE IF NOT EXISTS ${STATE_TABLE} (
|
|
1310
|
+
singleton_id INTEGER PRIMARY KEY CHECK (singleton_id = 1),
|
|
1311
|
+
schema_hash TEXT NOT NULL,
|
|
1312
|
+
schema_json TEXT NOT NULL,
|
|
1313
|
+
updated_at INTEGER NOT NULL
|
|
1314
|
+
)
|
|
1315
|
+
`
|
|
1316
|
+
],
|
|
1317
|
+
"write"
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
async function executePlan(client, plan) {
|
|
1321
|
+
const appliedAt = Date.now();
|
|
1322
|
+
const statements = [
|
|
1323
|
+
...plan.sql.statements,
|
|
1324
|
+
{
|
|
1325
|
+
sql: `INSERT INTO ${MIGRATIONS_TABLE} (
|
|
1326
|
+
migration_id,
|
|
1327
|
+
migration_name,
|
|
1328
|
+
schema_hash,
|
|
1329
|
+
applied_at,
|
|
1330
|
+
sql_statements_json
|
|
1331
|
+
) VALUES (?, ?, ?, ?, ?)`,
|
|
1332
|
+
args: [
|
|
1333
|
+
plan.migrationId,
|
|
1334
|
+
plan.migrationName,
|
|
1335
|
+
plan.toSchemaHash,
|
|
1336
|
+
appliedAt,
|
|
1337
|
+
JSON.stringify(plan.sql.statements)
|
|
1338
|
+
]
|
|
1339
|
+
},
|
|
1340
|
+
{
|
|
1341
|
+
sql: `INSERT INTO ${STATE_TABLE} (
|
|
1342
|
+
singleton_id,
|
|
1343
|
+
schema_hash,
|
|
1344
|
+
schema_json,
|
|
1345
|
+
updated_at
|
|
1346
|
+
) VALUES (1, ?, ?, ?)
|
|
1347
|
+
ON CONFLICT(singleton_id) DO UPDATE SET
|
|
1348
|
+
schema_hash = excluded.schema_hash,
|
|
1349
|
+
schema_json = excluded.schema_json,
|
|
1350
|
+
updated_at = excluded.updated_at`,
|
|
1351
|
+
args: [plan.toSchemaHash, JSON.stringify(plan.nextSchema), appliedAt]
|
|
1352
|
+
}
|
|
1353
|
+
];
|
|
1354
|
+
await client.batch(statements, "write");
|
|
1355
|
+
}
|
|
1356
|
+
function assertConnection(connection) {
|
|
1357
|
+
if (!connection?.url) {
|
|
1358
|
+
throw new Error("Missing database connection. Provide --url or pass a connection object.");
|
|
1359
|
+
}
|
|
1360
|
+
return connection;
|
|
1361
|
+
}
|
|
1362
|
+
function getString(value) {
|
|
1363
|
+
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
1364
|
+
}
|
|
1365
|
+
function getNumber(value) {
|
|
1366
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1367
|
+
if (typeof value === "bigint") return Number(value);
|
|
1368
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
1369
|
+
const parsed = Number(value);
|
|
1370
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1371
|
+
}
|
|
1372
|
+
return null;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// src/project.ts
|
|
1376
|
+
import { mkdir, readdir, writeFile } from "fs/promises";
|
|
1377
|
+
import path from "path";
|
|
1378
|
+
import { pathToFileURL } from "url";
|
|
1379
|
+
var MIGRATION_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".mts", ".js", ".mjs"]);
|
|
1380
|
+
function resolveDbProjectLayout(dbDir = "db") {
|
|
1381
|
+
const absoluteDbDir = path.resolve(dbDir);
|
|
1382
|
+
return {
|
|
1383
|
+
dbDir: absoluteDbDir,
|
|
1384
|
+
migrationsDir: path.join(absoluteDbDir, "migrations"),
|
|
1385
|
+
schemaDir: path.join(absoluteDbDir, "schema"),
|
|
1386
|
+
snapshotPath: path.join(absoluteDbDir, "schema", "schema.snapshot.json"),
|
|
1387
|
+
drizzlePath: path.join(absoluteDbDir, "schema", "schema.generated.ts")
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
async function loadMigrationDefinitionsFromDirectory(migrationsDir) {
|
|
1391
|
+
const files = await readdir(migrationsDir, { withFileTypes: true });
|
|
1392
|
+
const migrationFiles = files.filter((entry) => entry.isFile()).map((entry) => path.join(migrationsDir, entry.name)).filter((filePath) => MIGRATION_EXTENSIONS.has(path.extname(filePath))).sort((left, right) => path.basename(left).localeCompare(path.basename(right)));
|
|
1393
|
+
const migrations = [];
|
|
1394
|
+
for (const filePath of migrationFiles) {
|
|
1395
|
+
const imported = await import(pathToFileURL(filePath).href);
|
|
1396
|
+
const definition = imported.default;
|
|
1397
|
+
if (!definition || !definition.meta || typeof definition.meta.id !== "string" || typeof definition.meta.name !== "string" || typeof definition.buildOperations !== "function") {
|
|
1398
|
+
throw new Error(`Migration file ${filePath} does not export a valid default migration`);
|
|
1399
|
+
}
|
|
1400
|
+
migrations.push(definition);
|
|
1401
|
+
}
|
|
1402
|
+
return migrations;
|
|
1403
|
+
}
|
|
1404
|
+
async function materializeProjectMigrations(layout) {
|
|
1405
|
+
const migrations = await loadMigrationDefinitionsFromDirectory(layout.migrationsDir);
|
|
1406
|
+
const materialized = materializeSchema({ migrations });
|
|
1407
|
+
return {
|
|
1408
|
+
...materialized,
|
|
1409
|
+
migrations
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
async function writeSchemaSnapshot(schema, snapshotPath) {
|
|
1413
|
+
await mkdir(path.dirname(snapshotPath), { recursive: true });
|
|
1414
|
+
await writeFile(snapshotPath, `${JSON.stringify(schema, null, 2)}
|
|
1415
|
+
`, "utf8");
|
|
1416
|
+
}
|
|
1417
|
+
async function writeDrizzleSchema(schema, drizzlePath) {
|
|
1418
|
+
await mkdir(path.dirname(drizzlePath), { recursive: true });
|
|
1419
|
+
await writeFile(drizzlePath, compileSchemaToDrizzle(schema), "utf8");
|
|
1420
|
+
}
|
|
1421
|
+
export {
|
|
1422
|
+
applyMigrations,
|
|
1423
|
+
applyOperationsToSchema,
|
|
1424
|
+
assertValidSchemaDocument,
|
|
1425
|
+
compileSchemaToDrizzle,
|
|
1426
|
+
compileSchemaToSqlite,
|
|
1427
|
+
createEmptySchema,
|
|
1428
|
+
createLibsqlClient,
|
|
1429
|
+
createMigration,
|
|
1430
|
+
defaultSpecSchema,
|
|
1431
|
+
fieldReferenceSpecSchema,
|
|
1432
|
+
fieldSpecSchema,
|
|
1433
|
+
findField,
|
|
1434
|
+
findTable,
|
|
1435
|
+
foreignKeyActionSchema,
|
|
1436
|
+
getSchemaState,
|
|
1437
|
+
indexSpecSchema,
|
|
1438
|
+
listAppliedMigrations,
|
|
1439
|
+
loadMigrationDefinitionsFromDirectory,
|
|
1440
|
+
logicalTypeSpecSchema,
|
|
1441
|
+
materializeProjectMigrations,
|
|
1442
|
+
materializeSchema,
|
|
1443
|
+
parseSchemaDocument,
|
|
1444
|
+
planMigration,
|
|
1445
|
+
renderSqliteMigration,
|
|
1446
|
+
resolveDbProjectLayout,
|
|
1447
|
+
schemaDocumentSchema,
|
|
1448
|
+
schemaHash,
|
|
1449
|
+
storageSpecSchema,
|
|
1450
|
+
tableSpecSchema,
|
|
1451
|
+
uniqueSpecSchema,
|
|
1452
|
+
validateSchemaDocument,
|
|
1453
|
+
writeDrizzleSchema,
|
|
1454
|
+
writeSchemaSnapshot
|
|
1455
|
+
};
|
|
1456
|
+
//# sourceMappingURL=index.js.map
|