@sedrino/db-schema 0.1.1 → 0.1.2
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 +62 -6
- package/dist/cli.js +1904 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +365 -85
- package/dist/index.js +1108 -187
- package/dist/index.js.map +1 -1
- package/docs/cli.md +93 -0
- package/docs/expressions-and-transforms.md +165 -0
- package/docs/index.md +5 -2
- package/docs/migrations.md +183 -3
- package/docs/planning-and-apply.md +200 -0
- package/docs/relations.md +130 -0
- package/docs/schema-document.md +62 -0
- package/package.json +3 -2
- package/src/apply.ts +67 -0
- package/src/cli.ts +105 -7
- package/src/drizzle.ts +348 -1
- package/src/index.ts +38 -1
- package/src/migration.ts +315 -3
- package/src/operations.ts +278 -0
- package/src/planner.ts +7 -190
- package/src/project.ts +157 -1
- package/src/sql-expression.ts +123 -0
- package/src/sqlite.ts +150 -9
- package/src/transforms.ts +94 -0
- package/src/utils.ts +54 -0
package/dist/index.js
CHANGED
|
@@ -130,6 +130,29 @@ function toSnakeCase(value) {
|
|
|
130
130
|
function toPascalCase(value) {
|
|
131
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
132
|
}
|
|
133
|
+
function toCamelCase(value) {
|
|
134
|
+
const pascal = toPascalCase(value);
|
|
135
|
+
return pascal.length > 0 ? `${pascal[0].toLowerCase()}${pascal.slice(1)}` : pascal;
|
|
136
|
+
}
|
|
137
|
+
function pluralize(value) {
|
|
138
|
+
if (value.endsWith("s")) return `${value}es`;
|
|
139
|
+
if (value.endsWith("y") && !/[aeiou]y$/i.test(value)) {
|
|
140
|
+
return `${value.slice(0, -1)}ies`;
|
|
141
|
+
}
|
|
142
|
+
return `${value}s`;
|
|
143
|
+
}
|
|
144
|
+
function singularize(value) {
|
|
145
|
+
if (value.endsWith("ies") && value.length > 3) {
|
|
146
|
+
return `${value.slice(0, -3)}y`;
|
|
147
|
+
}
|
|
148
|
+
if (value.endsWith("ses") && value.length > 3) {
|
|
149
|
+
return value.slice(0, -2);
|
|
150
|
+
}
|
|
151
|
+
if (value.endsWith("s") && !value.endsWith("ss") && value.length > 1) {
|
|
152
|
+
return value.slice(0, -1);
|
|
153
|
+
}
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
133
156
|
function quoteIdentifier(value) {
|
|
134
157
|
return `"${value.replace(/"/g, '""')}"`;
|
|
135
158
|
}
|
|
@@ -142,23 +165,23 @@ function fieldAutoId(tableName, fieldName) {
|
|
|
142
165
|
function tableAutoId(tableName) {
|
|
143
166
|
return `tbl_${toSnakeCase(tableName)}`;
|
|
144
167
|
}
|
|
145
|
-
function defaultStorageForLogical(logical,
|
|
168
|
+
function defaultStorageForLogical(logical, column2) {
|
|
146
169
|
switch (logical.kind) {
|
|
147
170
|
case "id":
|
|
148
171
|
case "string":
|
|
149
172
|
case "text":
|
|
150
173
|
case "enum":
|
|
151
174
|
case "json":
|
|
152
|
-
return { strategy: "sqlite.text", column };
|
|
175
|
+
return { strategy: "sqlite.text", column: column2 };
|
|
153
176
|
case "boolean":
|
|
154
177
|
case "integer":
|
|
155
|
-
return { strategy: "sqlite.integer", column };
|
|
178
|
+
return { strategy: "sqlite.integer", column: column2 };
|
|
156
179
|
case "number":
|
|
157
|
-
return { strategy: "sqlite.real", column };
|
|
180
|
+
return { strategy: "sqlite.real", column: column2 };
|
|
158
181
|
case "temporal.instant":
|
|
159
|
-
return { strategy: "sqlite.temporalInstantEpochMs", column };
|
|
182
|
+
return { strategy: "sqlite.temporalInstantEpochMs", column: column2 };
|
|
160
183
|
case "temporal.plainDate":
|
|
161
|
-
return { strategy: "sqlite.temporalPlainDateText", column };
|
|
184
|
+
return { strategy: "sqlite.temporalPlainDateText", column: column2 };
|
|
162
185
|
}
|
|
163
186
|
}
|
|
164
187
|
function validateLogicalAndStorageCompatibility(logical, storage) {
|
|
@@ -196,6 +219,7 @@ function validateDefaultCompatibility(field, defaultValue) {
|
|
|
196
219
|
function validateSchemaDocumentCompatibility(schema) {
|
|
197
220
|
const issues = [];
|
|
198
221
|
const tableNames = /* @__PURE__ */ new Set();
|
|
222
|
+
const indexNames = /* @__PURE__ */ new Set();
|
|
199
223
|
for (const table of schema.tables) {
|
|
200
224
|
if (tableNames.has(table.name)) {
|
|
201
225
|
issues.push({
|
|
@@ -205,6 +229,7 @@ function validateSchemaDocumentCompatibility(schema) {
|
|
|
205
229
|
}
|
|
206
230
|
tableNames.add(table.name);
|
|
207
231
|
const fieldNames = /* @__PURE__ */ new Set();
|
|
232
|
+
const columnNames = /* @__PURE__ */ new Set();
|
|
208
233
|
let primaryKeyCount = 0;
|
|
209
234
|
for (const field of table.fields) {
|
|
210
235
|
if (fieldNames.has(field.name)) {
|
|
@@ -214,6 +239,13 @@ function validateSchemaDocumentCompatibility(schema) {
|
|
|
214
239
|
});
|
|
215
240
|
}
|
|
216
241
|
fieldNames.add(field.name);
|
|
242
|
+
if (columnNames.has(field.storage.column)) {
|
|
243
|
+
issues.push({
|
|
244
|
+
path: `tables.${table.name}.fields.${field.name}`,
|
|
245
|
+
message: `Duplicate column name ${field.storage.column}`
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
columnNames.add(field.storage.column);
|
|
217
249
|
if (field.primaryKey) primaryKeyCount += 1;
|
|
218
250
|
if (field.primaryKey && field.nullable) {
|
|
219
251
|
issues.push({
|
|
@@ -270,6 +302,14 @@ function validateSchemaDocumentCompatibility(schema) {
|
|
|
270
302
|
});
|
|
271
303
|
}
|
|
272
304
|
for (const index of table.indexes) {
|
|
305
|
+
const indexName = index.name ?? `${table.name}_${index.fields.join("_")}_idx`;
|
|
306
|
+
if (indexNames.has(indexName)) {
|
|
307
|
+
issues.push({
|
|
308
|
+
path: `tables.${table.name}.indexes.${indexName}`,
|
|
309
|
+
message: `Duplicate index name ${indexName}`
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
indexNames.add(indexName);
|
|
273
313
|
for (const fieldName of index.fields) {
|
|
274
314
|
if (!fieldNames.has(fieldName)) {
|
|
275
315
|
issues.push({
|
|
@@ -280,6 +320,14 @@ function validateSchemaDocumentCompatibility(schema) {
|
|
|
280
320
|
}
|
|
281
321
|
}
|
|
282
322
|
for (const unique of table.uniques) {
|
|
323
|
+
const uniqueName = unique.name ?? `${table.name}_${unique.fields.join("_")}_unique`;
|
|
324
|
+
if (indexNames.has(uniqueName)) {
|
|
325
|
+
issues.push({
|
|
326
|
+
path: `tables.${table.name}.uniques.${uniqueName}`,
|
|
327
|
+
message: `Duplicate index name ${uniqueName}`
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
indexNames.add(uniqueName);
|
|
283
331
|
for (const fieldName of unique.fields) {
|
|
284
332
|
if (!fieldNames.has(fieldName)) {
|
|
285
333
|
issues.push({
|
|
@@ -319,18 +367,20 @@ function cloneSchema(value) {
|
|
|
319
367
|
}
|
|
320
368
|
function renameStorageColumn(field, newFieldName) {
|
|
321
369
|
const next = cloneSchema(field);
|
|
322
|
-
const
|
|
370
|
+
const column2 = toSnakeCase(newFieldName);
|
|
323
371
|
next.name = newFieldName;
|
|
324
|
-
next.storage = { ...next.storage, column };
|
|
372
|
+
next.storage = { ...next.storage, column: column2 };
|
|
325
373
|
return next;
|
|
326
374
|
}
|
|
327
375
|
|
|
328
376
|
// src/migration.ts
|
|
329
377
|
var MutableFieldBuilder = class {
|
|
330
|
-
constructor(field) {
|
|
378
|
+
constructor(field, hooks = {}) {
|
|
331
379
|
this.field = field;
|
|
380
|
+
this.hooks = hooks;
|
|
332
381
|
}
|
|
333
382
|
field;
|
|
383
|
+
hooks;
|
|
334
384
|
required() {
|
|
335
385
|
this.field.nullable = false;
|
|
336
386
|
return this;
|
|
@@ -370,20 +420,136 @@ var MutableFieldBuilder = class {
|
|
|
370
420
|
this.field.description = description;
|
|
371
421
|
return this;
|
|
372
422
|
}
|
|
373
|
-
column(
|
|
423
|
+
column(column2) {
|
|
374
424
|
this.field.storage = {
|
|
375
425
|
...this.field.storage,
|
|
376
|
-
column
|
|
426
|
+
column: column2
|
|
377
427
|
};
|
|
378
428
|
return this;
|
|
379
429
|
}
|
|
430
|
+
backfillSql(expression) {
|
|
431
|
+
const value = normalizeSqlExpression(expression);
|
|
432
|
+
if (!this.hooks.setBackfillSql) {
|
|
433
|
+
throw new Error("backfillSql is only supported for fields added in alterTable(...)");
|
|
434
|
+
}
|
|
435
|
+
this.hooks.setBackfillSql(value.sql);
|
|
436
|
+
return this;
|
|
437
|
+
}
|
|
438
|
+
backfill(expression) {
|
|
439
|
+
return this.backfillSql(expression);
|
|
440
|
+
}
|
|
380
441
|
build() {
|
|
381
442
|
return this.field;
|
|
382
443
|
}
|
|
383
444
|
};
|
|
445
|
+
var AlterFieldBuilder = class {
|
|
446
|
+
patch = {};
|
|
447
|
+
transform;
|
|
448
|
+
required() {
|
|
449
|
+
this.patch.nullable = false;
|
|
450
|
+
return this;
|
|
451
|
+
}
|
|
452
|
+
nullable() {
|
|
453
|
+
this.patch.nullable = true;
|
|
454
|
+
return this;
|
|
455
|
+
}
|
|
456
|
+
unique() {
|
|
457
|
+
this.patch.unique = true;
|
|
458
|
+
return this;
|
|
459
|
+
}
|
|
460
|
+
notUnique() {
|
|
461
|
+
this.patch.unique = false;
|
|
462
|
+
return this;
|
|
463
|
+
}
|
|
464
|
+
default(value) {
|
|
465
|
+
this.patch.default = {
|
|
466
|
+
kind: "literal",
|
|
467
|
+
value
|
|
468
|
+
};
|
|
469
|
+
return this;
|
|
470
|
+
}
|
|
471
|
+
defaultNow() {
|
|
472
|
+
this.patch.default = {
|
|
473
|
+
kind: "now"
|
|
474
|
+
};
|
|
475
|
+
return this;
|
|
476
|
+
}
|
|
477
|
+
dropDefault() {
|
|
478
|
+
this.patch.default = null;
|
|
479
|
+
return this;
|
|
480
|
+
}
|
|
481
|
+
references(reference) {
|
|
482
|
+
this.patch.references = reference;
|
|
483
|
+
return this;
|
|
484
|
+
}
|
|
485
|
+
dropReferences() {
|
|
486
|
+
this.patch.references = null;
|
|
487
|
+
return this;
|
|
488
|
+
}
|
|
489
|
+
description(description) {
|
|
490
|
+
this.patch.description = description;
|
|
491
|
+
return this;
|
|
492
|
+
}
|
|
493
|
+
dropDescription() {
|
|
494
|
+
this.patch.description = null;
|
|
495
|
+
return this;
|
|
496
|
+
}
|
|
497
|
+
column(column2) {
|
|
498
|
+
this.patch.column = column2;
|
|
499
|
+
return this;
|
|
500
|
+
}
|
|
501
|
+
storage(storage) {
|
|
502
|
+
this.patch.storage = storage;
|
|
503
|
+
return this;
|
|
504
|
+
}
|
|
505
|
+
usingSql(expression) {
|
|
506
|
+
this.transform = normalizeSqlExpression(expression);
|
|
507
|
+
return this;
|
|
508
|
+
}
|
|
509
|
+
using(expression) {
|
|
510
|
+
return this.usingSql(expression);
|
|
511
|
+
}
|
|
512
|
+
logical(logical, options = {}) {
|
|
513
|
+
this.patch.logical = logical;
|
|
514
|
+
if (options.column !== void 0) this.patch.column = options.column;
|
|
515
|
+
if (options.storage !== void 0) this.patch.storage = options.storage;
|
|
516
|
+
return this;
|
|
517
|
+
}
|
|
518
|
+
string(options = {}) {
|
|
519
|
+
return this.logical({ kind: "string", format: options.format }, options);
|
|
520
|
+
}
|
|
521
|
+
text(options = {}) {
|
|
522
|
+
return this.logical({ kind: "text" }, options);
|
|
523
|
+
}
|
|
524
|
+
boolean(options = {}) {
|
|
525
|
+
return this.logical({ kind: "boolean" }, options);
|
|
526
|
+
}
|
|
527
|
+
integer(options = {}) {
|
|
528
|
+
return this.logical({ kind: "integer" }, options);
|
|
529
|
+
}
|
|
530
|
+
number(options = {}) {
|
|
531
|
+
return this.logical({ kind: "number" }, options);
|
|
532
|
+
}
|
|
533
|
+
enum(values, options = {}) {
|
|
534
|
+
return this.logical({ kind: "enum", values }, options);
|
|
535
|
+
}
|
|
536
|
+
json(tsType, options = {}) {
|
|
537
|
+
return this.logical({ kind: "json", tsType }, options);
|
|
538
|
+
}
|
|
539
|
+
temporalInstant(options = {}) {
|
|
540
|
+
return this.logical({ kind: "temporal.instant" }, options);
|
|
541
|
+
}
|
|
542
|
+
temporalPlainDate(options = {}) {
|
|
543
|
+
return this.logical({ kind: "temporal.plainDate" }, options);
|
|
544
|
+
}
|
|
545
|
+
reference(options) {
|
|
546
|
+
this.patch.references = options.references;
|
|
547
|
+
return this.logical({ kind: "string" }, options);
|
|
548
|
+
}
|
|
549
|
+
};
|
|
384
550
|
function createFieldSpec(tableName, fieldName, logical, options = {}) {
|
|
385
|
-
const
|
|
386
|
-
const storage = options.storage ?? defaultStorageForLogical(logical,
|
|
551
|
+
const column2 = options.column ?? toSnakeCase(fieldName);
|
|
552
|
+
const storage = options.storage ?? defaultStorageForLogical(logical, column2);
|
|
387
553
|
const storageIssue = validateLogicalAndStorageCompatibility(logical, storage);
|
|
388
554
|
if (storageIssue) throw new Error(storageIssue);
|
|
389
555
|
const field = {
|
|
@@ -481,6 +647,24 @@ var TableCreateBuilder = class {
|
|
|
481
647
|
)
|
|
482
648
|
);
|
|
483
649
|
}
|
|
650
|
+
belongsTo(targetTable, options = {}) {
|
|
651
|
+
const fieldName = resolveBelongsToFieldName(targetTable, options.fieldName);
|
|
652
|
+
const builder = this.reference(fieldName, {
|
|
653
|
+
...options,
|
|
654
|
+
references: {
|
|
655
|
+
table: targetTable,
|
|
656
|
+
field: options.referencesField ?? fieldName,
|
|
657
|
+
onDelete: options.onDelete,
|
|
658
|
+
onUpdate: options.onUpdate
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
if (options.required) builder.required();
|
|
662
|
+
if (options.unique) builder.unique();
|
|
663
|
+
if ((options.index ?? !options.unique) === true) {
|
|
664
|
+
this.index([fieldName], options.indexName ? { name: options.indexName } : {});
|
|
665
|
+
}
|
|
666
|
+
return builder;
|
|
667
|
+
}
|
|
484
668
|
index(fields, options = {}) {
|
|
485
669
|
this.indexes.push({
|
|
486
670
|
fields,
|
|
@@ -519,7 +703,11 @@ var TableAlterBuilder = class {
|
|
|
519
703
|
field
|
|
520
704
|
};
|
|
521
705
|
this.operations.push(operation);
|
|
522
|
-
return new MutableFieldBuilder(field
|
|
706
|
+
return new MutableFieldBuilder(field, {
|
|
707
|
+
setBackfillSql: (sql) => {
|
|
708
|
+
operation.backfill = { sql };
|
|
709
|
+
}
|
|
710
|
+
});
|
|
523
711
|
}
|
|
524
712
|
string(fieldName, options = {}) {
|
|
525
713
|
return this.addField(
|
|
@@ -573,6 +761,24 @@ var TableAlterBuilder = class {
|
|
|
573
761
|
)
|
|
574
762
|
);
|
|
575
763
|
}
|
|
764
|
+
belongsTo(targetTable, options = {}) {
|
|
765
|
+
const fieldName = resolveBelongsToFieldName(targetTable, options.fieldName);
|
|
766
|
+
const builder = this.reference(fieldName, {
|
|
767
|
+
...options,
|
|
768
|
+
references: {
|
|
769
|
+
table: targetTable,
|
|
770
|
+
field: options.referencesField ?? fieldName,
|
|
771
|
+
onDelete: options.onDelete,
|
|
772
|
+
onUpdate: options.onUpdate
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
if (options.required) builder.required();
|
|
776
|
+
if (options.unique) builder.unique();
|
|
777
|
+
if ((options.index ?? !options.unique) === true) {
|
|
778
|
+
this.addIndex([fieldName], options.indexName ? { name: options.indexName } : {});
|
|
779
|
+
}
|
|
780
|
+
return builder;
|
|
781
|
+
}
|
|
576
782
|
dropField(fieldName) {
|
|
577
783
|
this.operations.push({
|
|
578
784
|
kind: "dropField",
|
|
@@ -590,6 +796,21 @@ var TableAlterBuilder = class {
|
|
|
590
796
|
});
|
|
591
797
|
return this;
|
|
592
798
|
}
|
|
799
|
+
alterField(fieldName, callback) {
|
|
800
|
+
const field = new AlterFieldBuilder();
|
|
801
|
+
callback(field);
|
|
802
|
+
if (Object.keys(field.patch).length === 0 && !field.transform) {
|
|
803
|
+
throw new Error(`alterField(${fieldName}) requires at least one field change or usingSql(...)`);
|
|
804
|
+
}
|
|
805
|
+
this.operations.push({
|
|
806
|
+
kind: "alterField",
|
|
807
|
+
tableName: this.tableName,
|
|
808
|
+
fieldName,
|
|
809
|
+
patch: field.patch,
|
|
810
|
+
transform: field.transform
|
|
811
|
+
});
|
|
812
|
+
return this;
|
|
813
|
+
}
|
|
593
814
|
addIndex(fields, options = {}) {
|
|
594
815
|
this.operations.push({
|
|
595
816
|
kind: "addIndex",
|
|
@@ -640,6 +861,34 @@ var MigrationBuilderImpl = class {
|
|
|
640
861
|
});
|
|
641
862
|
return this;
|
|
642
863
|
}
|
|
864
|
+
createJunctionTable(name, options) {
|
|
865
|
+
return this.createTable(name, (table) => {
|
|
866
|
+
if (options.description) table.description(options.description);
|
|
867
|
+
const leftFieldName = resolveBelongsToFieldName(options.left.table, options.left.fieldName);
|
|
868
|
+
const rightFieldName = resolveBelongsToFieldName(options.right.table, options.right.fieldName);
|
|
869
|
+
table.belongsTo(options.left.table, {
|
|
870
|
+
fieldName: leftFieldName,
|
|
871
|
+
referencesField: options.left.referencesField,
|
|
872
|
+
description: options.left.description,
|
|
873
|
+
onDelete: options.left.onDelete ?? "cascade",
|
|
874
|
+
onUpdate: options.left.onUpdate,
|
|
875
|
+
required: true,
|
|
876
|
+
index: options.indexes ?? true
|
|
877
|
+
});
|
|
878
|
+
table.belongsTo(options.right.table, {
|
|
879
|
+
fieldName: rightFieldName,
|
|
880
|
+
referencesField: options.right.referencesField,
|
|
881
|
+
description: options.right.description,
|
|
882
|
+
onDelete: options.right.onDelete ?? "cascade",
|
|
883
|
+
onUpdate: options.right.onUpdate,
|
|
884
|
+
required: true,
|
|
885
|
+
index: options.indexes ?? true
|
|
886
|
+
});
|
|
887
|
+
if (options.unique ?? true) {
|
|
888
|
+
table.unique([leftFieldName, rightFieldName], options.uniqueName ? { name: options.uniqueName } : {});
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
}
|
|
643
892
|
dropTable(tableName) {
|
|
644
893
|
this.operations.push({
|
|
645
894
|
kind: "dropTable",
|
|
@@ -672,6 +921,15 @@ function createMigration(meta, callback) {
|
|
|
672
921
|
}
|
|
673
922
|
};
|
|
674
923
|
}
|
|
924
|
+
function normalizeSqlExpression(expression) {
|
|
925
|
+
const value = typeof expression === "string" ? expression.trim() : expression.sql.trim();
|
|
926
|
+
if (!value) throw new Error("SQL expression must be non-empty");
|
|
927
|
+
return { sql: value };
|
|
928
|
+
}
|
|
929
|
+
function resolveBelongsToFieldName(targetTable, fieldName) {
|
|
930
|
+
if (fieldName) return fieldName;
|
|
931
|
+
return `${toCamelCase(singularize(targetTable))}Id`;
|
|
932
|
+
}
|
|
675
933
|
|
|
676
934
|
// src/schema.ts
|
|
677
935
|
function createEmptySchema(schemaId = "schema") {
|
|
@@ -712,6 +970,223 @@ function findField(table, fieldName) {
|
|
|
712
970
|
return table.fields.find((field) => field.name === fieldName) ?? null;
|
|
713
971
|
}
|
|
714
972
|
|
|
973
|
+
// src/operations.ts
|
|
974
|
+
function applyOperationsToSchema(schemaInput, operations) {
|
|
975
|
+
const schema = cloneSchema(schemaInput);
|
|
976
|
+
for (const operation of operations) {
|
|
977
|
+
applyOperationToSchemaMutating(schema, operation);
|
|
978
|
+
}
|
|
979
|
+
return assertValidSchemaDocument(schema);
|
|
980
|
+
}
|
|
981
|
+
function applyOperationToSchema(schemaInput, operation) {
|
|
982
|
+
const schema = cloneSchema(schemaInput);
|
|
983
|
+
applyOperationToSchemaMutating(schema, operation);
|
|
984
|
+
return assertValidSchemaDocument(schema);
|
|
985
|
+
}
|
|
986
|
+
function applyOperationToSchemaMutating(schema, operation) {
|
|
987
|
+
switch (operation.kind) {
|
|
988
|
+
case "createTable":
|
|
989
|
+
if (findTable(schema, operation.table.name)) {
|
|
990
|
+
throw new Error(`Table ${operation.table.name} already exists`);
|
|
991
|
+
}
|
|
992
|
+
schema.tables.push(cloneSchema(operation.table));
|
|
993
|
+
return;
|
|
994
|
+
case "dropTable": {
|
|
995
|
+
const referencedBy = schema.tables.flatMap(
|
|
996
|
+
(table) => table.fields.filter((field) => field.references?.table === operation.tableName).map((field) => `${table.name}.${field.name}`)
|
|
997
|
+
);
|
|
998
|
+
if (referencedBy.length > 0) {
|
|
999
|
+
throw new Error(
|
|
1000
|
+
`Cannot drop table ${operation.tableName}; still referenced by ${referencedBy.join(", ")}`
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
const index = schema.tables.findIndex((table) => table.name === operation.tableName);
|
|
1004
|
+
if (index < 0) throw new Error(`Table ${operation.tableName} does not exist`);
|
|
1005
|
+
schema.tables.splice(index, 1);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
case "renameTable": {
|
|
1009
|
+
const table = findTable(schema, operation.from);
|
|
1010
|
+
if (!table) throw new Error(`Table ${operation.from} does not exist`);
|
|
1011
|
+
if (findTable(schema, operation.to)) {
|
|
1012
|
+
throw new Error(`Table ${operation.to} already exists`);
|
|
1013
|
+
}
|
|
1014
|
+
table.name = operation.to;
|
|
1015
|
+
for (const candidateTable of schema.tables) {
|
|
1016
|
+
for (const field of candidateTable.fields) {
|
|
1017
|
+
if (field.references?.table === operation.from) {
|
|
1018
|
+
field.references = {
|
|
1019
|
+
...field.references,
|
|
1020
|
+
table: operation.to
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
case "addField": {
|
|
1028
|
+
const table = findTable(schema, operation.tableName);
|
|
1029
|
+
if (!table) throw new Error(`Table ${operation.tableName} does not exist`);
|
|
1030
|
+
if (findField(table, operation.field.name)) {
|
|
1031
|
+
throw new Error(`Field ${operation.tableName}.${operation.field.name} already exists`);
|
|
1032
|
+
}
|
|
1033
|
+
table.fields.push(cloneSchema(operation.field));
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
case "dropField": {
|
|
1037
|
+
const table = findTable(schema, operation.tableName);
|
|
1038
|
+
if (!table) throw new Error(`Table ${operation.tableName} does not exist`);
|
|
1039
|
+
const fieldIndex = table.fields.findIndex((field) => field.name === operation.fieldName);
|
|
1040
|
+
if (fieldIndex < 0) {
|
|
1041
|
+
throw new Error(`Field ${operation.tableName}.${operation.fieldName} does not exist`);
|
|
1042
|
+
}
|
|
1043
|
+
const referencedBy = schema.tables.flatMap(
|
|
1044
|
+
(candidateTable) => candidateTable.fields.filter(
|
|
1045
|
+
(field) => field.references?.table === operation.tableName && field.references.field === operation.fieldName
|
|
1046
|
+
).map((field) => `${candidateTable.name}.${field.name}`)
|
|
1047
|
+
);
|
|
1048
|
+
if (referencedBy.length > 0) {
|
|
1049
|
+
throw new Error(
|
|
1050
|
+
`Cannot drop field ${operation.tableName}.${operation.fieldName}; still referenced by ${referencedBy.join(", ")}`
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
table.fields.splice(fieldIndex, 1);
|
|
1054
|
+
table.indexes = table.indexes.filter((index) => !index.fields.includes(operation.fieldName));
|
|
1055
|
+
table.uniques = table.uniques.filter((unique) => !unique.fields.includes(operation.fieldName));
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
case "renameField": {
|
|
1059
|
+
const table = findTable(schema, operation.tableName);
|
|
1060
|
+
if (!table) throw new Error(`Table ${operation.tableName} does not exist`);
|
|
1061
|
+
const field = findField(table, operation.from);
|
|
1062
|
+
if (!field) throw new Error(`Field ${operation.tableName}.${operation.from} does not exist`);
|
|
1063
|
+
if (findField(table, operation.to)) {
|
|
1064
|
+
throw new Error(`Field ${operation.tableName}.${operation.to} already exists`);
|
|
1065
|
+
}
|
|
1066
|
+
const renamed = renameStorageColumn(field, operation.to);
|
|
1067
|
+
const index = table.fields.findIndex((candidate) => candidate.id === field.id);
|
|
1068
|
+
table.fields[index] = renamed;
|
|
1069
|
+
table.indexes = renameFieldInIndexes(table.indexes, operation.from, operation.to);
|
|
1070
|
+
table.uniques = renameFieldInUniques(table.uniques, operation.from, operation.to);
|
|
1071
|
+
for (const candidateTable of schema.tables) {
|
|
1072
|
+
for (const candidateField of candidateTable.fields) {
|
|
1073
|
+
if (candidateField.references?.table === table.name && candidateField.references.field === operation.from) {
|
|
1074
|
+
candidateField.references = {
|
|
1075
|
+
...candidateField.references,
|
|
1076
|
+
field: operation.to
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
case "alterField": {
|
|
1084
|
+
const table = findTable(schema, operation.tableName);
|
|
1085
|
+
if (!table) throw new Error(`Table ${operation.tableName} does not exist`);
|
|
1086
|
+
const field = findField(table, operation.fieldName);
|
|
1087
|
+
if (!field) {
|
|
1088
|
+
throw new Error(`Field ${operation.tableName}.${operation.fieldName} does not exist`);
|
|
1089
|
+
}
|
|
1090
|
+
const nextField = applyAlterFieldPatch(operation.tableName, field, operation.patch);
|
|
1091
|
+
const fieldIndex = table.fields.findIndex((candidate) => candidate.id === field.id);
|
|
1092
|
+
table.fields[fieldIndex] = nextField;
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
case "addIndex": {
|
|
1096
|
+
const table = findTable(schema, operation.tableName);
|
|
1097
|
+
if (!table) throw new Error(`Table ${operation.tableName} does not exist`);
|
|
1098
|
+
table.indexes.push(cloneSchema(operation.index));
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
case "dropIndex": {
|
|
1102
|
+
const table = findTable(schema, operation.tableName);
|
|
1103
|
+
if (!table) throw new Error(`Table ${operation.tableName} does not exist`);
|
|
1104
|
+
const nextIndexes = table.indexes.filter(
|
|
1105
|
+
(index) => resolveIndexName(table.name, index) !== operation.indexName
|
|
1106
|
+
);
|
|
1107
|
+
if (nextIndexes.length === table.indexes.length) {
|
|
1108
|
+
throw new Error(`Index ${operation.indexName} does not exist on ${table.name}`);
|
|
1109
|
+
}
|
|
1110
|
+
table.indexes = nextIndexes;
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
case "addUnique": {
|
|
1114
|
+
const table = findTable(schema, operation.tableName);
|
|
1115
|
+
if (!table) throw new Error(`Table ${operation.tableName} does not exist`);
|
|
1116
|
+
table.uniques.push(cloneSchema(operation.unique));
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
case "dropUnique": {
|
|
1120
|
+
const table = findTable(schema, operation.tableName);
|
|
1121
|
+
if (!table) throw new Error(`Table ${operation.tableName} does not exist`);
|
|
1122
|
+
const nextUniques = table.uniques.filter(
|
|
1123
|
+
(unique) => resolveUniqueName(table.name, unique) !== operation.uniqueName
|
|
1124
|
+
);
|
|
1125
|
+
if (nextUniques.length === table.uniques.length) {
|
|
1126
|
+
throw new Error(`Unique ${operation.uniqueName} does not exist on ${table.name}`);
|
|
1127
|
+
}
|
|
1128
|
+
table.uniques = nextUniques;
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
function applyAlterFieldPatch(tableName, field, patch) {
|
|
1134
|
+
if (field.primaryKey) {
|
|
1135
|
+
throw new Error(`Field ${tableName}.${field.name} is a primary key and cannot be altered in v1`);
|
|
1136
|
+
}
|
|
1137
|
+
if (patch.logical?.kind === "id") {
|
|
1138
|
+
throw new Error(`Field ${tableName}.${field.name} cannot be altered into an id field in v1`);
|
|
1139
|
+
}
|
|
1140
|
+
const nextLogical = patch.logical ?? field.logical;
|
|
1141
|
+
const nextColumn = patch.column ?? patch.storage?.column ?? field.storage.column;
|
|
1142
|
+
const nextStorage = patch.storage ?? (patch.logical ? defaultStorageForLogical(nextLogical, nextColumn) : { ...field.storage, column: nextColumn });
|
|
1143
|
+
const nextField = {
|
|
1144
|
+
...cloneSchema(field),
|
|
1145
|
+
logical: cloneSchema(nextLogical),
|
|
1146
|
+
storage: cloneSchema(nextStorage)
|
|
1147
|
+
};
|
|
1148
|
+
if (patch.nullable !== void 0) nextField.nullable = patch.nullable;
|
|
1149
|
+
if (patch.default !== void 0) nextField.default = patch.default ?? void 0;
|
|
1150
|
+
if (patch.unique !== void 0) nextField.unique = patch.unique;
|
|
1151
|
+
if (patch.description !== void 0) nextField.description = patch.description ?? void 0;
|
|
1152
|
+
if (patch.references !== void 0) nextField.references = patch.references ?? void 0;
|
|
1153
|
+
const storageIssue = validateLogicalAndStorageCompatibility(nextField.logical, nextField.storage);
|
|
1154
|
+
if (storageIssue) {
|
|
1155
|
+
throw new Error(`Field ${tableName}.${field.name}: ${storageIssue}`);
|
|
1156
|
+
}
|
|
1157
|
+
if (nextField.default) {
|
|
1158
|
+
const defaultIssue = validateDefaultCompatibility(nextField, nextField.default);
|
|
1159
|
+
if (defaultIssue) {
|
|
1160
|
+
throw new Error(`Field ${tableName}.${field.name}: ${defaultIssue}`);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
return nextField;
|
|
1164
|
+
}
|
|
1165
|
+
function fieldStorageStrategyChanged(before, after) {
|
|
1166
|
+
return before.storage.strategy !== after.storage.strategy;
|
|
1167
|
+
}
|
|
1168
|
+
function fieldNullabilityTightened(before, after) {
|
|
1169
|
+
return before.nullable && !after.nullable;
|
|
1170
|
+
}
|
|
1171
|
+
function resolveIndexName(tableName, index) {
|
|
1172
|
+
return index.name ?? `${tableName}_${index.fields.join("_")}_idx`;
|
|
1173
|
+
}
|
|
1174
|
+
function resolveUniqueName(tableName, unique) {
|
|
1175
|
+
return unique.name ?? `${tableName}_${unique.fields.join("_")}_unique`;
|
|
1176
|
+
}
|
|
1177
|
+
function renameFieldInIndexes(indexes, from, to) {
|
|
1178
|
+
return indexes.map((index) => ({
|
|
1179
|
+
...index,
|
|
1180
|
+
fields: index.fields.map((field) => field === from ? to : field)
|
|
1181
|
+
}));
|
|
1182
|
+
}
|
|
1183
|
+
function renameFieldInUniques(uniques, from, to) {
|
|
1184
|
+
return uniques.map((unique) => ({
|
|
1185
|
+
...unique,
|
|
1186
|
+
fields: unique.fields.map((field) => field === from ? to : field)
|
|
1187
|
+
}));
|
|
1188
|
+
}
|
|
1189
|
+
|
|
715
1190
|
// src/sqlite.ts
|
|
716
1191
|
function compileSchemaToSqlite(schema) {
|
|
717
1192
|
const statements = [];
|
|
@@ -722,10 +1197,13 @@ function compileSchemaToSqlite(schema) {
|
|
|
722
1197
|
return `${statements.join("\n\n")}
|
|
723
1198
|
`;
|
|
724
1199
|
}
|
|
725
|
-
function renderSqliteMigration(operations) {
|
|
1200
|
+
function renderSqliteMigration(operations, options = {}) {
|
|
726
1201
|
const statements = [];
|
|
727
1202
|
const warnings = [];
|
|
1203
|
+
let workingSchema = options.currentSchema ?? createEmptySchema();
|
|
1204
|
+
let rebuildSequence = 0;
|
|
728
1205
|
for (const operation of operations) {
|
|
1206
|
+
const nextSchema = applyOperationToSchema(workingSchema, operation);
|
|
729
1207
|
switch (operation.kind) {
|
|
730
1208
|
case "createTable":
|
|
731
1209
|
statements.push(renderCreateTableStatement(operation.table));
|
|
@@ -739,11 +1217,6 @@ function renderSqliteMigration(operations) {
|
|
|
739
1217
|
`ALTER TABLE ${quoteIdentifier(operation.from)} RENAME TO ${quoteIdentifier(operation.to)};`
|
|
740
1218
|
);
|
|
741
1219
|
break;
|
|
742
|
-
case "addField":
|
|
743
|
-
statements.push(
|
|
744
|
-
`ALTER TABLE ${quoteIdentifier(operation.tableName)} ADD COLUMN ${renderColumnDefinition(operation.field)};`
|
|
745
|
-
);
|
|
746
|
-
break;
|
|
747
1220
|
case "renameField":
|
|
748
1221
|
statements.push(
|
|
749
1222
|
`ALTER TABLE ${quoteIdentifier(operation.tableName)} RENAME COLUMN ${quoteIdentifier(toSnakeCase(operation.from))} TO ${quoteIdentifier(toSnakeCase(operation.to))};`
|
|
@@ -761,12 +1234,31 @@ function renderSqliteMigration(operations) {
|
|
|
761
1234
|
case "dropUnique":
|
|
762
1235
|
statements.push(`DROP INDEX ${quoteIdentifier(operation.uniqueName)};`);
|
|
763
1236
|
break;
|
|
1237
|
+
case "addField":
|
|
764
1238
|
case "dropField":
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
);
|
|
1239
|
+
case "alterField": {
|
|
1240
|
+
const tableName = operation.tableName;
|
|
1241
|
+
const beforeTable = findTable(workingSchema, tableName);
|
|
1242
|
+
const afterTable = findTable(nextSchema, tableName);
|
|
1243
|
+
if (!beforeTable || !afterTable) {
|
|
1244
|
+
warnings.push(
|
|
1245
|
+
`operation ${operation.kind} on ${tableName} could not be emitted because the table state was incomplete`
|
|
1246
|
+
);
|
|
1247
|
+
break;
|
|
1248
|
+
}
|
|
1249
|
+
const rebuild = renderTableRebuild({
|
|
1250
|
+
beforeTable,
|
|
1251
|
+
afterTable,
|
|
1252
|
+
operation,
|
|
1253
|
+
sequence: rebuildSequence
|
|
1254
|
+
});
|
|
1255
|
+
rebuildSequence += 1;
|
|
1256
|
+
statements.push(...rebuild.statements);
|
|
1257
|
+
warnings.push(...rebuild.warnings);
|
|
768
1258
|
break;
|
|
1259
|
+
}
|
|
769
1260
|
}
|
|
1261
|
+
workingSchema = nextSchema;
|
|
770
1262
|
}
|
|
771
1263
|
return {
|
|
772
1264
|
statements,
|
|
@@ -848,6 +1340,73 @@ function resolveColumnName(table, fieldName) {
|
|
|
848
1340
|
const field = table?.fields.find((candidate) => candidate.name === fieldName);
|
|
849
1341
|
return field?.storage.column ?? toSnakeCase(fieldName);
|
|
850
1342
|
}
|
|
1343
|
+
function renderTableRebuild(args) {
|
|
1344
|
+
const warnings = [];
|
|
1345
|
+
const tempName = `__sedrino_rebuild_${args.afterTable.name}_${args.sequence}`;
|
|
1346
|
+
const insertColumns = args.afterTable.fields.map((field) => quoteIdentifier(field.storage.column));
|
|
1347
|
+
const selectExpressions = args.afterTable.fields.map(
|
|
1348
|
+
(field) => renderRebuildSelectExpression({
|
|
1349
|
+
beforeTable: args.beforeTable,
|
|
1350
|
+
afterTable: args.afterTable,
|
|
1351
|
+
operation: args.operation,
|
|
1352
|
+
targetField: field,
|
|
1353
|
+
warnings
|
|
1354
|
+
})
|
|
1355
|
+
);
|
|
1356
|
+
const statements = [
|
|
1357
|
+
"PRAGMA foreign_keys = OFF;",
|
|
1358
|
+
`ALTER TABLE ${quoteIdentifier(args.beforeTable.name)} RENAME TO ${quoteIdentifier(tempName)};`,
|
|
1359
|
+
renderCreateTableStatement(args.afterTable),
|
|
1360
|
+
...renderCreateIndexStatements(args.afterTable),
|
|
1361
|
+
`INSERT INTO ${quoteIdentifier(args.afterTable.name)} (${insertColumns.join(", ")}) SELECT ${selectExpressions.join(", ")} FROM ${quoteIdentifier(tempName)};`,
|
|
1362
|
+
`DROP TABLE ${quoteIdentifier(tempName)};`,
|
|
1363
|
+
"PRAGMA foreign_keys = ON;"
|
|
1364
|
+
];
|
|
1365
|
+
return {
|
|
1366
|
+
statements,
|
|
1367
|
+
warnings
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
function renderRebuildSelectExpression(args) {
|
|
1371
|
+
const sourceField = args.beforeTable.fields.find((candidate) => candidate.id === args.targetField.id) ?? null;
|
|
1372
|
+
if (!sourceField) {
|
|
1373
|
+
if (args.operation.kind === "addField" && args.operation.field.id === args.targetField.id && args.operation.backfill) {
|
|
1374
|
+
return `(${args.operation.backfill.sql}) AS ${quoteIdentifier(args.targetField.storage.column)}`;
|
|
1375
|
+
}
|
|
1376
|
+
const defaultSql = renderSqlDefault(args.targetField.default, args.targetField);
|
|
1377
|
+
if (defaultSql) return `${defaultSql} AS ${quoteIdentifier(args.targetField.storage.column)}`;
|
|
1378
|
+
if (args.targetField.nullable) {
|
|
1379
|
+
return `NULL AS ${quoteIdentifier(args.targetField.storage.column)}`;
|
|
1380
|
+
}
|
|
1381
|
+
args.warnings.push(
|
|
1382
|
+
`addField ${args.afterTable.name}.${args.targetField.name} is required with no default and cannot be backfilled safely`
|
|
1383
|
+
);
|
|
1384
|
+
return `NULL AS ${quoteIdentifier(args.targetField.storage.column)}`;
|
|
1385
|
+
}
|
|
1386
|
+
if (fieldStorageStrategyChanged(sourceField, args.targetField)) {
|
|
1387
|
+
if (args.operation.kind === "alterField" && args.operation.transform && args.operation.fieldName === args.targetField.name) {
|
|
1388
|
+
return `(${args.operation.transform.sql}) AS ${quoteIdentifier(args.targetField.storage.column)}`;
|
|
1389
|
+
}
|
|
1390
|
+
args.warnings.push(
|
|
1391
|
+
`alterField ${args.afterTable.name}.${args.targetField.name} changes storage strategy from ${sourceField.storage.strategy} to ${args.targetField.storage.strategy}; explicit data transforms are not supported in v1`
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
let expression = quoteIdentifier(sourceField.storage.column);
|
|
1395
|
+
if (args.operation.kind === "alterField" && args.operation.transform && args.operation.fieldName === args.targetField.name) {
|
|
1396
|
+
expression = `(${args.operation.transform.sql})`;
|
|
1397
|
+
}
|
|
1398
|
+
if (fieldNullabilityTightened(sourceField, args.targetField) && !(args.operation.kind === "alterField" && args.operation.transform && args.operation.fieldName === args.targetField.name)) {
|
|
1399
|
+
const defaultSql = renderSqlDefault(args.targetField.default, args.targetField);
|
|
1400
|
+
if (defaultSql) {
|
|
1401
|
+
expression = `COALESCE(${expression}, ${defaultSql})`;
|
|
1402
|
+
} else {
|
|
1403
|
+
args.warnings.push(
|
|
1404
|
+
`alterField ${args.afterTable.name}.${args.targetField.name} makes a nullable field required without a default; existing NULL rows would fail during rebuild`
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
return `${expression} AS ${quoteIdentifier(args.targetField.storage.column)}`;
|
|
1409
|
+
}
|
|
851
1410
|
|
|
852
1411
|
// src/planner.ts
|
|
853
1412
|
function planMigration(args) {
|
|
@@ -861,7 +1420,7 @@ function planMigration(args) {
|
|
|
861
1420
|
toSchemaHash: createSchemaHash(nextSchema),
|
|
862
1421
|
operations,
|
|
863
1422
|
nextSchema,
|
|
864
|
-
sql: renderSqliteMigration(operations)
|
|
1423
|
+
sql: renderSqliteMigration(operations, { currentSchema })
|
|
865
1424
|
};
|
|
866
1425
|
}
|
|
867
1426
|
function materializeSchema(args) {
|
|
@@ -880,173 +1439,12 @@ function materializeSchema(args) {
|
|
|
880
1439
|
plans
|
|
881
1440
|
};
|
|
882
1441
|
}
|
|
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
1442
|
|
|
1046
1443
|
// src/drizzle.ts
|
|
1047
1444
|
function compileSchemaToDrizzle(schema) {
|
|
1048
1445
|
const sqliteImports = /* @__PURE__ */ new Set(["sqliteTable"]);
|
|
1049
1446
|
const toolkitImports = /* @__PURE__ */ new Set();
|
|
1447
|
+
let needsRelationsImport = false;
|
|
1050
1448
|
let needsUlid = false;
|
|
1051
1449
|
for (const table of schema.tables) {
|
|
1052
1450
|
for (const field of table.fields) {
|
|
@@ -1074,11 +1472,15 @@ function compileSchemaToDrizzle(schema) {
|
|
|
1074
1472
|
}
|
|
1075
1473
|
if (field.default?.kind === "generatedId") needsUlid = true;
|
|
1076
1474
|
if (field.default?.kind === "now") toolkitImports.add("epochMsNow");
|
|
1475
|
+
if (field.references) needsRelationsImport = true;
|
|
1077
1476
|
}
|
|
1078
1477
|
if (table.indexes.length > 0) sqliteImports.add("index");
|
|
1079
1478
|
if (table.uniques.length > 0) sqliteImports.add("uniqueIndex");
|
|
1080
1479
|
}
|
|
1081
1480
|
const lines = [];
|
|
1481
|
+
if (needsRelationsImport) {
|
|
1482
|
+
lines.push(`import { defineRelations } from "drizzle-orm";`);
|
|
1483
|
+
}
|
|
1082
1484
|
lines.push(
|
|
1083
1485
|
`import { ${Array.from(sqliteImports).sort().join(", ")} } from "drizzle-orm/sqlite-core";`
|
|
1084
1486
|
);
|
|
@@ -1095,8 +1497,47 @@ function compileSchemaToDrizzle(schema) {
|
|
|
1095
1497
|
lines.push(renderTable(table));
|
|
1096
1498
|
lines.push("");
|
|
1097
1499
|
}
|
|
1500
|
+
const relationsSource = compileSchemaToDrizzleRelations(schema);
|
|
1501
|
+
if (relationsSource) {
|
|
1502
|
+
lines.push(relationsSource);
|
|
1503
|
+
lines.push("");
|
|
1504
|
+
}
|
|
1098
1505
|
return lines.join("\n").trimEnd() + "\n";
|
|
1099
1506
|
}
|
|
1507
|
+
function compileSchemaToDrizzleRelations(schema) {
|
|
1508
|
+
const directRelations = inferDirectRelations(schema);
|
|
1509
|
+
const manyToManyRelations = inferManyToManyRelations(schema, directRelations.usedKeysByTable);
|
|
1510
|
+
if (directRelations.relations.length === 0 && manyToManyRelations.length === 0) return "";
|
|
1511
|
+
const schemaEntries = schema.tables.map((table) => ` ${tableSchemaKey(table.name)}: ${tableVariableName(table.name)},`).join("\n");
|
|
1512
|
+
const tableBlocks = schema.tables.map((table) => {
|
|
1513
|
+
const tableKey = tableSchemaKey(table.name);
|
|
1514
|
+
const relationLines = directRelations.relations.filter(
|
|
1515
|
+
(relation) => relation.sourceTable.name === table.name || relation.targetTable.name === table.name
|
|
1516
|
+
).flatMap((relation) => {
|
|
1517
|
+
const lines = [];
|
|
1518
|
+
if (relation.sourceTable.name === table.name) {
|
|
1519
|
+
lines.push(` ${relation.forwardKey}: ${renderOneRelation(relation)},`);
|
|
1520
|
+
}
|
|
1521
|
+
if (relation.targetTable.name === table.name && !relation.skipReverse) {
|
|
1522
|
+
lines.push(` ${relation.reverseKey}: ${renderReverseRelation(relation)},`);
|
|
1523
|
+
}
|
|
1524
|
+
return lines;
|
|
1525
|
+
}).concat(
|
|
1526
|
+
manyToManyRelations.filter((relation) => relation.leftTable.name === table.name || relation.rightTable.name === table.name).map(
|
|
1527
|
+
(relation) => relation.leftTable.name === table.name ? ` ${relation.leftKey}: ${renderManyToManyRelation(relation, "left")},` : ` ${relation.rightKey}: ${renderManyToManyRelation(relation, "right")},`
|
|
1528
|
+
)
|
|
1529
|
+
);
|
|
1530
|
+
if (relationLines.length === 0) return null;
|
|
1531
|
+
return ` ${tableKey}: {
|
|
1532
|
+
${relationLines.join("\n")}
|
|
1533
|
+
},`;
|
|
1534
|
+
}).filter((block) => block !== null).join("\n");
|
|
1535
|
+
return `export const relations = defineRelations({
|
|
1536
|
+
${schemaEntries}
|
|
1537
|
+
}, (r) => ({
|
|
1538
|
+
${tableBlocks}
|
|
1539
|
+
}));`;
|
|
1540
|
+
}
|
|
1100
1541
|
function renderTable(table) {
|
|
1101
1542
|
const variableName = tableVariableName(table.name);
|
|
1102
1543
|
const fieldLines = table.fields.map((field) => ` ${field.name}: ${renderField(field)},`);
|
|
@@ -1191,6 +1632,373 @@ function renderDrizzleDefault(defaultValue) {
|
|
|
1191
1632
|
return JSON.stringify(defaultValue.value);
|
|
1192
1633
|
}
|
|
1193
1634
|
}
|
|
1635
|
+
function inferDirectRelations(schema) {
|
|
1636
|
+
const junctionTableNames = new Set(inferJunctionCandidates(schema).map((candidate) => candidate.junctionTable.name));
|
|
1637
|
+
const raw2 = schema.tables.flatMap(
|
|
1638
|
+
(sourceTable) => sourceTable.fields.filter((field) => field.references).map((field) => {
|
|
1639
|
+
const targetTable = schema.tables.find((table) => table.name === field.references.table);
|
|
1640
|
+
if (!targetTable) {
|
|
1641
|
+
throw new Error(
|
|
1642
|
+
`Cannot infer relation for ${sourceTable.name}.${field.name}; missing target table ${field.references.table}`
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
return {
|
|
1646
|
+
sourceTable,
|
|
1647
|
+
targetTable,
|
|
1648
|
+
sourceField: field,
|
|
1649
|
+
targetFieldName: field.references.field,
|
|
1650
|
+
sourceTableKey: tableSchemaKey(sourceTable.name),
|
|
1651
|
+
targetTableKey: tableSchemaKey(targetTable.name),
|
|
1652
|
+
forwardBase: inferForwardRelationKey(field, targetTable),
|
|
1653
|
+
reverseBase: tableSchemaKey(pluralize(sourceTable.name))
|
|
1654
|
+
};
|
|
1655
|
+
})
|
|
1656
|
+
);
|
|
1657
|
+
const duplicatePairCounts = /* @__PURE__ */ new Map();
|
|
1658
|
+
for (const relation of raw2) {
|
|
1659
|
+
const key = `${relation.sourceTable.name}::${relation.targetTable.name}`;
|
|
1660
|
+
duplicatePairCounts.set(key, (duplicatePairCounts.get(key) ?? 0) + 1);
|
|
1661
|
+
}
|
|
1662
|
+
const usedByTable = /* @__PURE__ */ new Map();
|
|
1663
|
+
return {
|
|
1664
|
+
relations: raw2.map((relation) => {
|
|
1665
|
+
const skipReverse = junctionTableNames.has(relation.sourceTable.name);
|
|
1666
|
+
const pairKey = `${relation.sourceTable.name}::${relation.targetTable.name}`;
|
|
1667
|
+
const needsAlias = (duplicatePairCounts.get(pairKey) ?? 0) > 1;
|
|
1668
|
+
const alias = needsAlias ? relation.forwardBase : null;
|
|
1669
|
+
const sourceUsed = ensureUsedSet(usedByTable, relation.sourceTable.name);
|
|
1670
|
+
const targetUsed = ensureUsedSet(usedByTable, relation.targetTable.name);
|
|
1671
|
+
const reverseKind = inferReverseKind(relation.sourceTable, relation.sourceField);
|
|
1672
|
+
const forwardKey = ensureUniqueKey(
|
|
1673
|
+
sourceUsed,
|
|
1674
|
+
relation.forwardBase,
|
|
1675
|
+
`${relation.forwardBase}${toPascalCase(relation.targetTable.name)}`
|
|
1676
|
+
);
|
|
1677
|
+
const reversePreferred = needsAlias ? `${relation.forwardBase}${toPascalCase(pluralize(relation.sourceTable.name))}` : relation.reverseBase;
|
|
1678
|
+
const reverseFallback = `${relation.forwardBase}${toPascalCase(pluralize(relation.sourceTable.name))}`;
|
|
1679
|
+
const reverseKey = skipReverse ? reversePreferred : ensureUniqueKey(targetUsed, reversePreferred, reverseFallback);
|
|
1680
|
+
return {
|
|
1681
|
+
sourceTable: relation.sourceTable,
|
|
1682
|
+
targetTable: relation.targetTable,
|
|
1683
|
+
sourceField: relation.sourceField,
|
|
1684
|
+
targetFieldName: relation.targetFieldName,
|
|
1685
|
+
sourceTableKey: relation.sourceTableKey,
|
|
1686
|
+
targetTableKey: relation.targetTableKey,
|
|
1687
|
+
forwardKey,
|
|
1688
|
+
reverseKey,
|
|
1689
|
+
alias,
|
|
1690
|
+
reverseKind,
|
|
1691
|
+
skipReverse
|
|
1692
|
+
};
|
|
1693
|
+
}),
|
|
1694
|
+
usedKeysByTable: usedByTable
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
function inferManyToManyRelations(schema, usedByTable) {
|
|
1698
|
+
const candidates = inferJunctionCandidates(schema);
|
|
1699
|
+
const pairCounts = /* @__PURE__ */ new Map();
|
|
1700
|
+
for (const candidate of candidates) {
|
|
1701
|
+
const pairKey = createPairKey(candidate.leftTable.name, candidate.rightTable.name);
|
|
1702
|
+
pairCounts.set(pairKey, (pairCounts.get(pairKey) ?? 0) + 1);
|
|
1703
|
+
}
|
|
1704
|
+
return candidates.map((candidate) => {
|
|
1705
|
+
const pairKey = createPairKey(candidate.leftTable.name, candidate.rightTable.name);
|
|
1706
|
+
const duplicatePair = (pairCounts.get(pairKey) ?? 0) > 1;
|
|
1707
|
+
const selfRelation = candidate.leftTable.name === candidate.rightTable.name;
|
|
1708
|
+
const alias = duplicatePair || selfRelation ? tableSchemaKey(candidate.junctionTable.name) : null;
|
|
1709
|
+
const leftUsed = ensureUsedSet(usedByTable, candidate.leftTable.name);
|
|
1710
|
+
const rightUsed = ensureUsedSet(usedByTable, candidate.rightTable.name);
|
|
1711
|
+
const leftPreferred = inferManyToManyKey(candidate.rightTable, candidate.rightField, selfRelation);
|
|
1712
|
+
const rightPreferred = inferManyToManyKey(candidate.leftTable, candidate.leftField, selfRelation);
|
|
1713
|
+
const leftFallback = `${leftPreferred}${toPascalCase(candidate.junctionTable.name)}`;
|
|
1714
|
+
const rightFallback = `${rightPreferred}${toPascalCase(candidate.junctionTable.name)}`;
|
|
1715
|
+
return {
|
|
1716
|
+
junctionTable: candidate.junctionTable,
|
|
1717
|
+
leftTable: candidate.leftTable,
|
|
1718
|
+
rightTable: candidate.rightTable,
|
|
1719
|
+
leftField: candidate.leftField,
|
|
1720
|
+
rightField: candidate.rightField,
|
|
1721
|
+
leftTargetFieldName: candidate.leftField.references.field,
|
|
1722
|
+
rightTargetFieldName: candidate.rightField.references.field,
|
|
1723
|
+
junctionTableKey: tableSchemaKey(candidate.junctionTable.name),
|
|
1724
|
+
leftTableKey: tableSchemaKey(candidate.leftTable.name),
|
|
1725
|
+
rightTableKey: tableSchemaKey(candidate.rightTable.name),
|
|
1726
|
+
leftKey: ensureUniqueKey(leftUsed, leftPreferred, leftFallback),
|
|
1727
|
+
rightKey: ensureUniqueKey(rightUsed, rightPreferred, rightFallback),
|
|
1728
|
+
alias
|
|
1729
|
+
};
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
function inferJunctionCandidates(schema) {
|
|
1733
|
+
return schema.tables.flatMap((junctionTable) => {
|
|
1734
|
+
const referencedFields = junctionTable.fields.filter((field) => field.references);
|
|
1735
|
+
if (referencedFields.length !== 2) return [];
|
|
1736
|
+
if (junctionTable.fields.length !== 2) return [];
|
|
1737
|
+
const compositeUnique = junctionTable.uniques.some(
|
|
1738
|
+
(unique) => unique.fields.length === 2 && referencedFields.every((field) => unique.fields.includes(field.name))
|
|
1739
|
+
);
|
|
1740
|
+
if (!compositeUnique) return [];
|
|
1741
|
+
const [leftField, rightField] = referencedFields;
|
|
1742
|
+
const leftTable = schema.tables.find((table) => table.name === leftField.references.table);
|
|
1743
|
+
const rightTable = schema.tables.find((table) => table.name === rightField.references.table);
|
|
1744
|
+
if (!leftTable || !rightTable) return [];
|
|
1745
|
+
return [
|
|
1746
|
+
{
|
|
1747
|
+
junctionTable,
|
|
1748
|
+
leftField,
|
|
1749
|
+
rightField,
|
|
1750
|
+
leftTable,
|
|
1751
|
+
rightTable
|
|
1752
|
+
}
|
|
1753
|
+
];
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
function renderOneRelation(relation) {
|
|
1757
|
+
const options = [
|
|
1758
|
+
`from: r.${relation.sourceTableKey}.${relation.sourceField.name}`,
|
|
1759
|
+
`to: r.${relation.targetTableKey}.${relation.targetFieldName}`
|
|
1760
|
+
];
|
|
1761
|
+
if (relation.alias) {
|
|
1762
|
+
options.push(`alias: "${relation.alias}"`);
|
|
1763
|
+
}
|
|
1764
|
+
return `r.one.${relation.targetTableKey}({ ${options.join(", ")} })`;
|
|
1765
|
+
}
|
|
1766
|
+
function renderReverseRelation(relation) {
|
|
1767
|
+
const options = [
|
|
1768
|
+
`from: r.${relation.targetTableKey}.${relation.targetFieldName}`,
|
|
1769
|
+
`to: r.${relation.sourceTableKey}.${relation.sourceField.name}`
|
|
1770
|
+
];
|
|
1771
|
+
if (relation.alias) {
|
|
1772
|
+
options.push(`alias: "${relation.alias}"`);
|
|
1773
|
+
}
|
|
1774
|
+
return `r.${relation.reverseKind}.${relation.sourceTableKey}({ ${options.join(", ")} })`;
|
|
1775
|
+
}
|
|
1776
|
+
function renderManyToManyRelation(relation, side) {
|
|
1777
|
+
const isLeft = side === "left";
|
|
1778
|
+
const currentTableKey = isLeft ? relation.leftTableKey : relation.rightTableKey;
|
|
1779
|
+
const currentTargetFieldName = isLeft ? relation.leftTargetFieldName : relation.rightTargetFieldName;
|
|
1780
|
+
const currentJunctionFieldName = isLeft ? relation.leftField.name : relation.rightField.name;
|
|
1781
|
+
const otherTableKey = isLeft ? relation.rightTableKey : relation.leftTableKey;
|
|
1782
|
+
const otherTargetFieldName = isLeft ? relation.rightTargetFieldName : relation.leftTargetFieldName;
|
|
1783
|
+
const otherJunctionFieldName = isLeft ? relation.rightField.name : relation.leftField.name;
|
|
1784
|
+
const options = [
|
|
1785
|
+
`from: r.${currentTableKey}.${currentTargetFieldName}.through(r.${relation.junctionTableKey}.${currentJunctionFieldName})`,
|
|
1786
|
+
`to: r.${otherTableKey}.${otherTargetFieldName}.through(r.${relation.junctionTableKey}.${otherJunctionFieldName})`
|
|
1787
|
+
];
|
|
1788
|
+
if (relation.alias) {
|
|
1789
|
+
options.push(`alias: "${relation.alias}"`);
|
|
1790
|
+
}
|
|
1791
|
+
return `r.many.${otherTableKey}({ ${options.join(", ")} })`;
|
|
1792
|
+
}
|
|
1793
|
+
function inferForwardRelationKey(field, targetTable) {
|
|
1794
|
+
const trimmed2 = field.name.replace(/Id$/, "").replace(/Ids$/, "").replace(/Ref$/, "");
|
|
1795
|
+
const candidate = trimmed2.length > 0 ? trimmed2 : tableSchemaKey(targetTable.name);
|
|
1796
|
+
return toCamelCase(candidate);
|
|
1797
|
+
}
|
|
1798
|
+
function inferManyToManyKey(targetTable, targetField, selfRelation) {
|
|
1799
|
+
if (!selfRelation) {
|
|
1800
|
+
return tableSchemaKey(pluralize(singularize(targetTable.name)));
|
|
1801
|
+
}
|
|
1802
|
+
return tableSchemaKey(pluralize(inferForwardRelationKey(targetField, targetTable)));
|
|
1803
|
+
}
|
|
1804
|
+
function inferReverseKind(sourceTable, sourceField) {
|
|
1805
|
+
if (sourceField.primaryKey || sourceField.unique) return "one";
|
|
1806
|
+
const hasSingleFieldUnique = sourceTable.uniques.some(
|
|
1807
|
+
(unique) => unique.fields.length === 1 && unique.fields[0] === sourceField.name
|
|
1808
|
+
);
|
|
1809
|
+
return hasSingleFieldUnique ? "one" : "many";
|
|
1810
|
+
}
|
|
1811
|
+
function ensureUsedSet(map, key) {
|
|
1812
|
+
const existing = map.get(key);
|
|
1813
|
+
if (existing) return existing;
|
|
1814
|
+
const created = /* @__PURE__ */ new Set();
|
|
1815
|
+
map.set(key, created);
|
|
1816
|
+
return created;
|
|
1817
|
+
}
|
|
1818
|
+
function ensureUniqueKey(used, preferred, fallback) {
|
|
1819
|
+
if (!used.has(preferred)) {
|
|
1820
|
+
used.add(preferred);
|
|
1821
|
+
return preferred;
|
|
1822
|
+
}
|
|
1823
|
+
if (!used.has(fallback)) {
|
|
1824
|
+
used.add(fallback);
|
|
1825
|
+
return fallback;
|
|
1826
|
+
}
|
|
1827
|
+
let counter = 2;
|
|
1828
|
+
while (used.has(`${fallback}${counter}`)) {
|
|
1829
|
+
counter += 1;
|
|
1830
|
+
}
|
|
1831
|
+
const candidate = `${fallback}${counter}`;
|
|
1832
|
+
used.add(candidate);
|
|
1833
|
+
return candidate;
|
|
1834
|
+
}
|
|
1835
|
+
function tableSchemaKey(tableName) {
|
|
1836
|
+
return toCamelCase(tableName);
|
|
1837
|
+
}
|
|
1838
|
+
function createPairKey(left, right) {
|
|
1839
|
+
return [left, right].sort((a, b) => a.localeCompare(b)).join("::");
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
// src/sql-expression.ts
|
|
1843
|
+
var SqlExpressionBuilder = class {
|
|
1844
|
+
constructor(text) {
|
|
1845
|
+
this.text = text;
|
|
1846
|
+
}
|
|
1847
|
+
text;
|
|
1848
|
+
toSqlExpression() {
|
|
1849
|
+
return { sql: this.text };
|
|
1850
|
+
}
|
|
1851
|
+
toString() {
|
|
1852
|
+
return this.text;
|
|
1853
|
+
}
|
|
1854
|
+
};
|
|
1855
|
+
function sqlExpression(sql) {
|
|
1856
|
+
const value = sql.trim();
|
|
1857
|
+
if (!value) throw new Error("SQL expression must be non-empty");
|
|
1858
|
+
return { sql: value };
|
|
1859
|
+
}
|
|
1860
|
+
function column(fieldName) {
|
|
1861
|
+
return sqlExpression(quoteIdentifier2(toSnakeCase(fieldName)));
|
|
1862
|
+
}
|
|
1863
|
+
function raw(sql) {
|
|
1864
|
+
return sqlExpression(sql);
|
|
1865
|
+
}
|
|
1866
|
+
function literal(value) {
|
|
1867
|
+
if (value === null) return sqlExpression("NULL");
|
|
1868
|
+
if (typeof value === "string") {
|
|
1869
|
+
return sqlExpression(`'${value.replace(/'/g, "''")}'`);
|
|
1870
|
+
}
|
|
1871
|
+
if (typeof value === "boolean") {
|
|
1872
|
+
return sqlExpression(value ? "TRUE" : "FALSE");
|
|
1873
|
+
}
|
|
1874
|
+
return sqlExpression(String(value));
|
|
1875
|
+
}
|
|
1876
|
+
function lower(expression) {
|
|
1877
|
+
return sqlExpression(`lower(${normalizeExpression(expression)})`);
|
|
1878
|
+
}
|
|
1879
|
+
function trim(expression) {
|
|
1880
|
+
return sqlExpression(`trim(${normalizeExpression(expression)})`);
|
|
1881
|
+
}
|
|
1882
|
+
function replace(expression, search, replacement) {
|
|
1883
|
+
return sqlExpression(
|
|
1884
|
+
`replace(${normalizeExpression(expression)}, ${literal(search).sql}, ${literal(replacement).sql})`
|
|
1885
|
+
);
|
|
1886
|
+
}
|
|
1887
|
+
function cast(expression, sqlType) {
|
|
1888
|
+
return sqlExpression(`CAST(${normalizeExpression(expression)} AS ${sqlType})`);
|
|
1889
|
+
}
|
|
1890
|
+
function unixepoch(expression) {
|
|
1891
|
+
return sqlExpression(`unixepoch(${normalizeExpression(expression)})`);
|
|
1892
|
+
}
|
|
1893
|
+
function date(expression) {
|
|
1894
|
+
return sqlExpression(`date(${normalizeExpression(expression)})`);
|
|
1895
|
+
}
|
|
1896
|
+
function multiply(expression, factor) {
|
|
1897
|
+
return sqlExpression(`${normalizeExpression(expression)} * ${factor}`);
|
|
1898
|
+
}
|
|
1899
|
+
function coalesce(...expressions) {
|
|
1900
|
+
if (expressions.length === 0) {
|
|
1901
|
+
throw new Error("coalesce requires at least one expression");
|
|
1902
|
+
}
|
|
1903
|
+
return sqlExpression(`COALESCE(${expressions.map((value) => normalizeExpression(value)).join(", ")})`);
|
|
1904
|
+
}
|
|
1905
|
+
function concat(...expressions) {
|
|
1906
|
+
if (expressions.length === 0) {
|
|
1907
|
+
throw new Error("concat requires at least one expression");
|
|
1908
|
+
}
|
|
1909
|
+
return sqlExpression(expressions.map((value) => normalizeExpression(value)).join(" || "));
|
|
1910
|
+
}
|
|
1911
|
+
var sqlExpr = {
|
|
1912
|
+
raw,
|
|
1913
|
+
column,
|
|
1914
|
+
literal,
|
|
1915
|
+
lower,
|
|
1916
|
+
trim,
|
|
1917
|
+
replace,
|
|
1918
|
+
cast,
|
|
1919
|
+
unixepoch,
|
|
1920
|
+
date,
|
|
1921
|
+
multiply,
|
|
1922
|
+
coalesce,
|
|
1923
|
+
concat,
|
|
1924
|
+
build(sql) {
|
|
1925
|
+
return new SqlExpressionBuilder(sql).toSqlExpression();
|
|
1926
|
+
}
|
|
1927
|
+
};
|
|
1928
|
+
function normalizeExpression(expression) {
|
|
1929
|
+
return typeof expression === "string" ? sqlExpression(expression).sql : expression.sql;
|
|
1930
|
+
}
|
|
1931
|
+
function quoteIdentifier2(value) {
|
|
1932
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
// src/transforms.ts
|
|
1936
|
+
function copy(fieldName) {
|
|
1937
|
+
return column(fieldName);
|
|
1938
|
+
}
|
|
1939
|
+
function lowercase(source) {
|
|
1940
|
+
return lower(resolveSource(source));
|
|
1941
|
+
}
|
|
1942
|
+
function trimmed(source) {
|
|
1943
|
+
return trim(resolveSource(source));
|
|
1944
|
+
}
|
|
1945
|
+
function slugFrom(source, options = {}) {
|
|
1946
|
+
const separator = options.separator ?? "-";
|
|
1947
|
+
return lower(replace(trim(resolveSource(source)), " ", separator));
|
|
1948
|
+
}
|
|
1949
|
+
function concatFields(fieldNames, options = {}) {
|
|
1950
|
+
if (fieldNames.length === 0) {
|
|
1951
|
+
throw new Error("concatFields requires at least one field name");
|
|
1952
|
+
}
|
|
1953
|
+
const separator = options.separator ?? "";
|
|
1954
|
+
const expressions = [];
|
|
1955
|
+
for (const [index, fieldName] of fieldNames.entries()) {
|
|
1956
|
+
if (index > 0 && separator.length > 0) {
|
|
1957
|
+
expressions.push(literal(separator));
|
|
1958
|
+
}
|
|
1959
|
+
expressions.push(column(fieldName));
|
|
1960
|
+
}
|
|
1961
|
+
return concat(...expressions);
|
|
1962
|
+
}
|
|
1963
|
+
function coalesceFields(fieldNames, fallback) {
|
|
1964
|
+
if (fieldNames.length === 0) {
|
|
1965
|
+
throw new Error("coalesceFields requires at least one field name");
|
|
1966
|
+
}
|
|
1967
|
+
const expressions = fieldNames.map((fieldName) => column(fieldName));
|
|
1968
|
+
if (fallback !== void 0) expressions.push(resolveFallback(fallback));
|
|
1969
|
+
return coalesce(...expressions);
|
|
1970
|
+
}
|
|
1971
|
+
function epochMsFromIsoString(source) {
|
|
1972
|
+
return multiply(cast(unixepoch(resolveSource(source)), "INTEGER"), 1e3);
|
|
1973
|
+
}
|
|
1974
|
+
function plainDateFromIsoString(source) {
|
|
1975
|
+
return date(resolveSource(source));
|
|
1976
|
+
}
|
|
1977
|
+
function integerFromText(source) {
|
|
1978
|
+
return cast(trim(resolveSource(source)), "INTEGER");
|
|
1979
|
+
}
|
|
1980
|
+
function realFromText(source) {
|
|
1981
|
+
return cast(trim(resolveSource(source)), "REAL");
|
|
1982
|
+
}
|
|
1983
|
+
var transforms = {
|
|
1984
|
+
copy,
|
|
1985
|
+
lowercase,
|
|
1986
|
+
trimmed,
|
|
1987
|
+
slugFrom,
|
|
1988
|
+
concatFields,
|
|
1989
|
+
coalesceFields,
|
|
1990
|
+
epochMsFromIsoString,
|
|
1991
|
+
plainDateFromIsoString,
|
|
1992
|
+
integerFromText,
|
|
1993
|
+
realFromText
|
|
1994
|
+
};
|
|
1995
|
+
function resolveSource(source) {
|
|
1996
|
+
return typeof source === "string" ? column(source) : source;
|
|
1997
|
+
}
|
|
1998
|
+
function resolveFallback(fallback) {
|
|
1999
|
+
if (typeof fallback === "object" && fallback !== null && "sql" in fallback) return fallback;
|
|
2000
|
+
return literal(fallback);
|
|
2001
|
+
}
|
|
1194
2002
|
|
|
1195
2003
|
// src/apply.ts
|
|
1196
2004
|
import { createClient } from "@libsql/client";
|
|
@@ -1276,6 +2084,35 @@ async function listAppliedMigrations(client) {
|
|
|
1276
2084
|
appliedAt: getNumber(row.applied_at) ?? 0
|
|
1277
2085
|
}));
|
|
1278
2086
|
}
|
|
2087
|
+
async function inspectMigrationStatus(args) {
|
|
2088
|
+
const client = args.client ?? createLibsqlClient(assertConnection(args.connection));
|
|
2089
|
+
const metadataTablesPresent = await hasMetadataTables(client);
|
|
2090
|
+
const appliedRows = metadataTablesPresent ? await listAppliedMigrations(client) : [];
|
|
2091
|
+
const appliedIds = new Set(appliedRows.map((row) => row.migrationId));
|
|
2092
|
+
const localMigrationIds = args.migrations.map((migration) => migration.meta.id);
|
|
2093
|
+
const pendingMigrationIds = localMigrationIds.filter((id) => !appliedIds.has(id));
|
|
2094
|
+
const unexpectedDatabaseMigrationIds = appliedRows.map((row) => row.migrationId).filter((id) => !localMigrationIds.includes(id));
|
|
2095
|
+
const appliedLocalMigrations = args.migrations.filter((migration) => appliedIds.has(migration.meta.id));
|
|
2096
|
+
const expectedCurrent = materializeSchema({
|
|
2097
|
+
baseSchema: args.baseSchema,
|
|
2098
|
+
migrations: appliedLocalMigrations
|
|
2099
|
+
}).schema;
|
|
2100
|
+
const currentState = metadataTablesPresent ? await getSchemaState(client) : null;
|
|
2101
|
+
const localSchemaHash = schemaHash(expectedCurrent);
|
|
2102
|
+
const databaseSchemaHash = currentState?.schemaHash ?? null;
|
|
2103
|
+
return {
|
|
2104
|
+
localMigrationIds,
|
|
2105
|
+
appliedMigrationIds: appliedRows.map((row) => row.migrationId),
|
|
2106
|
+
pendingMigrationIds,
|
|
2107
|
+
unexpectedDatabaseMigrationIds,
|
|
2108
|
+
schemaHash: {
|
|
2109
|
+
local: localSchemaHash,
|
|
2110
|
+
database: databaseSchemaHash,
|
|
2111
|
+
driftDetected: databaseSchemaHash !== null && databaseSchemaHash !== localSchemaHash
|
|
2112
|
+
},
|
|
2113
|
+
metadataTablesPresent
|
|
2114
|
+
};
|
|
2115
|
+
}
|
|
1279
2116
|
async function getSchemaState(client) {
|
|
1280
2117
|
const result = await client.execute(
|
|
1281
2118
|
`SELECT schema_hash, schema_json
|
|
@@ -1317,6 +2154,17 @@ async function ensureMetadataTables(client) {
|
|
|
1317
2154
|
"write"
|
|
1318
2155
|
);
|
|
1319
2156
|
}
|
|
2157
|
+
async function hasMetadataTables(client) {
|
|
2158
|
+
const result = await client.execute({
|
|
2159
|
+
sql: `SELECT name FROM sqlite_master
|
|
2160
|
+
WHERE type = 'table' AND name IN (?, ?)`,
|
|
2161
|
+
args: [MIGRATIONS_TABLE, STATE_TABLE]
|
|
2162
|
+
});
|
|
2163
|
+
const names = new Set(
|
|
2164
|
+
result.rows.map((row) => getString(row.name)).filter((value) => value !== null)
|
|
2165
|
+
);
|
|
2166
|
+
return names.has(MIGRATIONS_TABLE) && names.has(STATE_TABLE);
|
|
2167
|
+
}
|
|
1320
2168
|
async function executePlan(client, plan) {
|
|
1321
2169
|
const appliedAt = Date.now();
|
|
1322
2170
|
const statements = [
|
|
@@ -1373,7 +2221,7 @@ function getNumber(value) {
|
|
|
1373
2221
|
}
|
|
1374
2222
|
|
|
1375
2223
|
// src/project.ts
|
|
1376
|
-
import { mkdir, readdir, writeFile } from "fs/promises";
|
|
2224
|
+
import { mkdir, readFile, readdir, writeFile } from "fs/promises";
|
|
1377
2225
|
import path from "path";
|
|
1378
2226
|
import { pathToFileURL } from "url";
|
|
1379
2227
|
var MIGRATION_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".mts", ".js", ".mjs"]);
|
|
@@ -1403,12 +2251,38 @@ async function loadMigrationDefinitionsFromDirectory(migrationsDir) {
|
|
|
1403
2251
|
}
|
|
1404
2252
|
async function materializeProjectMigrations(layout) {
|
|
1405
2253
|
const migrations = await loadMigrationDefinitionsFromDirectory(layout.migrationsDir);
|
|
2254
|
+
assertUniqueMigrationIds(migrations);
|
|
1406
2255
|
const materialized = materializeSchema({ migrations });
|
|
1407
2256
|
return {
|
|
1408
2257
|
...materialized,
|
|
1409
2258
|
migrations
|
|
1410
2259
|
};
|
|
1411
2260
|
}
|
|
2261
|
+
async function validateDbProject(layout) {
|
|
2262
|
+
const materialized = await materializeProjectMigrations(layout);
|
|
2263
|
+
const expectedSnapshot = `${JSON.stringify(materialized.schema, null, 2)}
|
|
2264
|
+
`;
|
|
2265
|
+
const expectedDrizzle = compileSchemaToDrizzle(materialized.schema);
|
|
2266
|
+
const warnings = materialized.plans.flatMap(
|
|
2267
|
+
(plan) => plan.sql.warnings.map((warning) => `${plan.migrationId}: ${warning}`)
|
|
2268
|
+
);
|
|
2269
|
+
const [snapshotContents, drizzleContents] = await Promise.all([
|
|
2270
|
+
readTextIfExists(layout.snapshotPath),
|
|
2271
|
+
readTextIfExists(layout.drizzlePath)
|
|
2272
|
+
]);
|
|
2273
|
+
return {
|
|
2274
|
+
...materialized,
|
|
2275
|
+
warnings,
|
|
2276
|
+
expectedSnapshot,
|
|
2277
|
+
expectedDrizzle,
|
|
2278
|
+
artifacts: {
|
|
2279
|
+
snapshotExists: snapshotContents !== null,
|
|
2280
|
+
drizzleExists: drizzleContents !== null,
|
|
2281
|
+
snapshotUpToDate: snapshotContents === expectedSnapshot,
|
|
2282
|
+
drizzleUpToDate: drizzleContents === expectedDrizzle
|
|
2283
|
+
}
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
1412
2286
|
async function writeSchemaSnapshot(schema, snapshotPath) {
|
|
1413
2287
|
await mkdir(path.dirname(snapshotPath), { recursive: true });
|
|
1414
2288
|
await writeFile(snapshotPath, `${JSON.stringify(schema, null, 2)}
|
|
@@ -1418,16 +2292,45 @@ async function writeDrizzleSchema(schema, drizzlePath) {
|
|
|
1418
2292
|
await mkdir(path.dirname(drizzlePath), { recursive: true });
|
|
1419
2293
|
await writeFile(drizzlePath, compileSchemaToDrizzle(schema), "utf8");
|
|
1420
2294
|
}
|
|
2295
|
+
function assertUniqueMigrationIds(migrations) {
|
|
2296
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2297
|
+
for (const migration of migrations) {
|
|
2298
|
+
if (seen.has(migration.meta.id)) {
|
|
2299
|
+
throw new Error(`Duplicate migration id ${migration.meta.id}`);
|
|
2300
|
+
}
|
|
2301
|
+
seen.add(migration.meta.id);
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
async function readTextIfExists(filePath) {
|
|
2305
|
+
try {
|
|
2306
|
+
return await readFile(filePath, "utf8");
|
|
2307
|
+
} catch (error) {
|
|
2308
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
2309
|
+
return null;
|
|
2310
|
+
}
|
|
2311
|
+
throw error;
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
1421
2314
|
export {
|
|
1422
2315
|
applyMigrations,
|
|
1423
2316
|
applyOperationsToSchema,
|
|
1424
2317
|
assertValidSchemaDocument,
|
|
2318
|
+
cast,
|
|
2319
|
+
coalesce,
|
|
2320
|
+
coalesceFields,
|
|
2321
|
+
column,
|
|
1425
2322
|
compileSchemaToDrizzle,
|
|
2323
|
+
compileSchemaToDrizzleRelations,
|
|
1426
2324
|
compileSchemaToSqlite,
|
|
2325
|
+
concat,
|
|
2326
|
+
concatFields,
|
|
2327
|
+
copy,
|
|
1427
2328
|
createEmptySchema,
|
|
1428
2329
|
createLibsqlClient,
|
|
1429
2330
|
createMigration,
|
|
2331
|
+
date,
|
|
1430
2332
|
defaultSpecSchema,
|
|
2333
|
+
epochMsFromIsoString,
|
|
1431
2334
|
fieldReferenceSpecSchema,
|
|
1432
2335
|
fieldSpecSchema,
|
|
1433
2336
|
findField,
|
|
@@ -1435,20 +2338,38 @@ export {
|
|
|
1435
2338
|
foreignKeyActionSchema,
|
|
1436
2339
|
getSchemaState,
|
|
1437
2340
|
indexSpecSchema,
|
|
2341
|
+
inspectMigrationStatus,
|
|
2342
|
+
integerFromText,
|
|
1438
2343
|
listAppliedMigrations,
|
|
2344
|
+
literal,
|
|
1439
2345
|
loadMigrationDefinitionsFromDirectory,
|
|
1440
2346
|
logicalTypeSpecSchema,
|
|
2347
|
+
lower,
|
|
2348
|
+
lowercase,
|
|
1441
2349
|
materializeProjectMigrations,
|
|
1442
2350
|
materializeSchema,
|
|
2351
|
+
multiply,
|
|
1443
2352
|
parseSchemaDocument,
|
|
2353
|
+
plainDateFromIsoString,
|
|
1444
2354
|
planMigration,
|
|
2355
|
+
raw,
|
|
2356
|
+
realFromText,
|
|
1445
2357
|
renderSqliteMigration,
|
|
2358
|
+
replace,
|
|
1446
2359
|
resolveDbProjectLayout,
|
|
1447
2360
|
schemaDocumentSchema,
|
|
1448
2361
|
schemaHash,
|
|
2362
|
+
slugFrom,
|
|
2363
|
+
sqlExpr,
|
|
2364
|
+
sqlExpression,
|
|
1449
2365
|
storageSpecSchema,
|
|
1450
2366
|
tableSpecSchema,
|
|
2367
|
+
transforms,
|
|
2368
|
+
trim,
|
|
2369
|
+
trimmed,
|
|
1451
2370
|
uniqueSpecSchema,
|
|
2371
|
+
unixepoch,
|
|
2372
|
+
validateDbProject,
|
|
1452
2373
|
validateSchemaDocument,
|
|
1453
2374
|
writeDrizzleSchema,
|
|
1454
2375
|
writeSchemaSnapshot
|