@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/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, column) {
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 column = toSnakeCase(newFieldName);
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(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 column = options.column ?? toSnakeCase(fieldName);
386
- const storage = options.storage ?? defaultStorageForLogical(logical, column);
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
- warnings.push(
766
- `dropField ${operation.tableName}.${operation.fieldName} requires a table rebuild and is not emitted in v1`
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