@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/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