@noego/proper 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/index.js CHANGED
@@ -66,23 +66,32 @@ var __async = (__this, __arguments, generator) => {
66
66
  // index.ts
67
67
  var index_exports = {};
68
68
  __export(index_exports, {
69
+ MigrationError: () => MigrationError,
69
70
  MigrationRunner: () => MySQLMigrationRunner,
70
71
  MigrationRunnerFactory: () => MigrationRunnerFactory,
72
+ PatchConflictError: () => PatchConflictError,
73
+ PatchCreator: () => PatchCreator,
74
+ PatchError: () => PatchError,
75
+ PatchExecutionError: () => PatchExecutionError,
76
+ PatchIntegrityError: () => PatchIntegrityError,
77
+ PatchRunner: () => PatchRunner,
78
+ PatchValidationError: () => PatchValidationError,
71
79
  PgRunner: () => PgRunner,
72
80
  SQLRunner: () => SQLRunner,
73
81
  SQLiteRunner: () => SQLiteRunner,
74
82
  createSeedFactory: () => createSeedFactory,
75
83
  loadMigrationConfig: () => loadMigrationConfig,
84
+ resolvePatchFolder: () => resolvePatchFolder,
85
+ resolvePatchTable: () => resolvePatchTable,
76
86
  runSeedsWithRunner: () => runSeedsWithRunner
77
87
  });
78
88
  module.exports = __toCommonJS(index_exports);
79
89
 
80
90
  // framework/MigrationRunner.ts
81
- var import_fs3 = __toESM(require("fs"));
91
+ var import_fs6 = __toESM(require("fs"));
82
92
 
83
93
  // framework/MigrationDirectoryReader.ts
84
- var import_fs = __toESM(require("fs"));
85
- var import_path = __toESM(require("path"));
94
+ var import_fs2 = __toESM(require("fs"));
86
95
 
87
96
  // framework/errors.ts
88
97
  var MigrationError = class _MigrationError extends Error {
@@ -197,6 +206,54 @@ ${originalError.message}`;
197
206
  return new _MigrationExecutionError(message);
198
207
  }
199
208
  };
209
+ var PatchError = class _PatchError extends MigrationError {
210
+ constructor(message, patchFile, patchKey) {
211
+ super(`Patch Error: ${message}`);
212
+ this.patchFile = patchFile;
213
+ this.patchKey = patchKey;
214
+ this.name = "PatchError";
215
+ Object.setPrototypeOf(this, _PatchError.prototype);
216
+ }
217
+ };
218
+ var PatchValidationError = class _PatchValidationError extends PatchError {
219
+ constructor(message, patchFile, patchKey) {
220
+ super(message, patchFile, patchKey);
221
+ this.name = "PatchValidationError";
222
+ Object.setPrototypeOf(this, _PatchValidationError.prototype);
223
+ }
224
+ };
225
+ var PatchIntegrityError = class _PatchIntegrityError extends PatchError {
226
+ constructor(message, patchFile, patchKey, expectedChecksum, actualChecksum) {
227
+ super(message, patchFile, patchKey);
228
+ this.expectedChecksum = expectedChecksum;
229
+ this.actualChecksum = actualChecksum;
230
+ this.name = "PatchIntegrityError";
231
+ Object.setPrototypeOf(this, _PatchIntegrityError.prototype);
232
+ }
233
+ };
234
+ var PatchConflictError = class _PatchConflictError extends PatchError {
235
+ constructor(message, patchFile, patchKey, operationIndex, operationVerb, migrationKeys, observedRowCounts) {
236
+ super(message, patchFile, patchKey);
237
+ this.operationIndex = operationIndex;
238
+ this.operationVerb = operationVerb;
239
+ this.migrationKeys = migrationKeys;
240
+ this.observedRowCounts = observedRowCounts;
241
+ this.name = "PatchConflictError";
242
+ Object.setPrototypeOf(this, _PatchConflictError.prototype);
243
+ }
244
+ };
245
+ var PatchExecutionError = class _PatchExecutionError extends PatchError {
246
+ constructor(message, patchFile, patchKey, operationIndex, operationVerb, originalError) {
247
+ super(originalError ? `${message}
248
+ Original Error:
249
+ ${originalError.message}` : message, patchFile, patchKey);
250
+ this.operationIndex = operationIndex;
251
+ this.operationVerb = operationVerb;
252
+ this.originalError = originalError;
253
+ this.name = "PatchExecutionError";
254
+ Object.setPrototypeOf(this, _PatchExecutionError.prototype);
255
+ }
256
+ };
200
257
  var CLIError = class _CLIError extends MigrationError {
201
258
  constructor(message) {
202
259
  super(`CLI Error: ${message}`);
@@ -359,6 +416,36 @@ var SqlMigrationBuilder = class {
359
416
  }
360
417
  };
361
418
 
419
+ // framework/MigrationManifest.ts
420
+ var import_fs = __toESM(require("fs"));
421
+ var import_path = __toESM(require("path"));
422
+ function canonicalMigrationKey(value) {
423
+ return value.replace(/(?:\.(mysql|sqlite|pg))?\.(up|down)\.(sql|js)$/i, "").toLowerCase();
424
+ }
425
+ function resolveMigrationFile(directory, baseName, direction, dialect) {
426
+ const dialectExt = dialect === "sql" ? "mysql" : dialect;
427
+ const dialectFile = import_path.default.join(directory, `${baseName}.${dialectExt}.${direction}.sql`);
428
+ if (import_fs.default.existsSync(dialectFile)) return dialectFile;
429
+ const genericFile = import_path.default.join(directory, `${baseName}.${direction}.sql`);
430
+ if (import_fs.default.existsSync(genericFile)) return genericFile;
431
+ return null;
432
+ }
433
+ function loadMigrationManifest(directory, dialect) {
434
+ const manifest = /* @__PURE__ */ new Map();
435
+ if (!import_fs.default.existsSync(directory)) return manifest;
436
+ const files = import_fs.default.readdirSync(directory, { withFileTypes: true }).filter((f) => f.isFile()).map((f) => f.name);
437
+ const uniqueKeys = /* @__PURE__ */ new Set();
438
+ files.forEach((file) => uniqueKeys.add(canonicalMigrationKey(file)));
439
+ uniqueKeys.forEach((key) => {
440
+ manifest.set(key, {
441
+ key,
442
+ upFile: resolveMigrationFile(directory, key, "up", dialect),
443
+ downFile: resolveMigrationFile(directory, key, "down", dialect)
444
+ });
445
+ });
446
+ return manifest;
447
+ }
448
+
362
449
  // framework/MigrationDirectoryReader.ts
363
450
  var MigrationDirectoryReader = class {
364
451
  constructor(directory, read_strategy, sqlrunner, dialect = "sql") {
@@ -373,12 +460,7 @@ var MigrationDirectoryReader = class {
373
460
  * File extensions: `.mysql.up.sql`, `.sqlite.up.sql`, `.pg.up.sql`.
374
461
  */
375
462
  resolveFile(baseName, direction) {
376
- const dialectExt = this.dialect === "sql" ? "mysql" : this.dialect;
377
- const dialectFile = import_path.default.join(this.directory, `${baseName}.${dialectExt}.${direction}.sql`);
378
- if (import_fs.default.existsSync(dialectFile)) return dialectFile;
379
- const genericFile = import_path.default.join(this.directory, `${baseName}.${direction}.sql`);
380
- if (import_fs.default.existsSync(genericFile)) return genericFile;
381
- return null;
463
+ return resolveMigrationFile(this.directory, baseName, direction, this.dialect);
382
464
  }
383
465
  /**
384
466
  * Checks if a file path is dialect-specific (contains .mysql. or .sqlite. in the name)
@@ -387,11 +469,11 @@ var MigrationDirectoryReader = class {
387
469
  return /\.(mysql|sqlite|pg)\.(up|down)\.sql$/i.test(filePath);
388
470
  }
389
471
  loadMigrations(table, connection) {
390
- import_fs.default.existsSync(this.directory) || import_fs.default.mkdirSync(this.directory);
391
- const dir_content = import_fs.default.readdirSync(this.directory, { withFileTypes: true }).filter((file) => file.isFile()).map((file) => file.name);
472
+ import_fs2.default.existsSync(this.directory) || import_fs2.default.mkdirSync(this.directory);
473
+ const dir_content = import_fs2.default.readdirSync(this.directory, { withFileTypes: true }).filter((file) => file.isFile()).map((file) => file.name);
392
474
  const uniqueKeys = /* @__PURE__ */ new Set();
393
475
  dir_content.forEach((file) => {
394
- const key = file.replace(/(?:\.(mysql|sqlite|pg))?\.(up|down)\.(sql|js)/i, "").toLowerCase();
476
+ const key = canonicalMigrationKey(file);
395
477
  uniqueKeys.add(key);
396
478
  });
397
479
  const migration_sorter = {};
@@ -431,14 +513,14 @@ var MigrationDirectoryReader = class {
431
513
  return builder;
432
514
  }
433
515
  sql_up(file) {
434
- let content = import_fs.default.readFileSync(file).toString();
516
+ let content = import_fs2.default.readFileSync(file).toString();
435
517
  if (!this.isDialectSpecific(file)) {
436
518
  content = this.read_strategy(content);
437
519
  }
438
520
  return content.trim();
439
521
  }
440
522
  sql_down(file) {
441
- let content = import_fs.default.readFileSync(file).toString();
523
+ let content = import_fs2.default.readFileSync(file).toString();
442
524
  if (!this.isDialectSpecific(file)) {
443
525
  content = this.read_strategy(content);
444
526
  }
@@ -447,7 +529,23 @@ var MigrationDirectoryReader = class {
447
529
  };
448
530
 
449
531
  // framework/MigrationSetup.ts
450
- var import_fs2 = __toESM(require("fs"));
532
+ var import_fs3 = __toESM(require("fs"));
533
+
534
+ // framework/PatchTypes.ts
535
+ var import_path2 = __toESM(require("path"));
536
+ var PATCH_FORMAT_VERSION = 1;
537
+ var DEFAULT_PATCH_TABLE = "proper_patches";
538
+ var PATCH_FILENAME_REGEX = new RegExp("^(?<stamp>[0-9]{13})_(?<name>[a-z0-9][a-z0-9_-]{0,119})\\.yaml$");
539
+ function resolvePatchFolder(config) {
540
+ if (config.patch_folder) return config.patch_folder;
541
+ const dir = import_path2.default.dirname(config.migration_folder);
542
+ return dir === "." && !config.migration_folder.includes(import_path2.default.sep) && !config.migration_folder.includes("/") ? "patches" : import_path2.default.join(dir, "patches");
543
+ }
544
+ function resolvePatchTable(config) {
545
+ return config.patch_table || DEFAULT_PATCH_TABLE;
546
+ }
547
+
548
+ // framework/MigrationSetup.ts
451
549
  var MigrationSetup = class {
452
550
  constructor(sqlrunner, config) {
453
551
  this.sqlrunner = sqlrunner;
@@ -455,7 +553,7 @@ var MigrationSetup = class {
455
553
  }
456
554
  setup() {
457
555
  return __async(this, null, function* () {
458
- import_fs2.default.existsSync(this.config.migration_folder) || import_fs2.default.mkdirSync(this.config.migration_folder);
556
+ import_fs3.default.existsSync(this.config.migration_folder) || import_fs3.default.mkdirSync(this.config.migration_folder);
459
557
  const tableName = this.config.migration_table;
460
558
  const createTableSql = this.config.database === "sqlite" ? `CREATE TABLE IF NOT EXISTS ${tableName} (
461
559
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -477,11 +575,39 @@ var MigrationSetup = class {
477
575
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
478
576
  )`;
479
577
  yield this.sqlrunner.query(createTableSql);
578
+ const patchTable = resolvePatchTable(this.config);
579
+ const createPatchTableSql = this.config.database === "sqlite" ? `CREATE TABLE IF NOT EXISTS ${patchTable} (
580
+ migration_table TEXT NOT NULL,
581
+ patch_key TEXT NOT NULL,
582
+ checksum TEXT NOT NULL,
583
+ format_version INTEGER NOT NULL,
584
+ description TEXT NOT NULL,
585
+ applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
586
+ PRIMARY KEY (migration_table, patch_key)
587
+ )` : this.config.database === "pg" ? `CREATE TABLE IF NOT EXISTS ${patchTable} (
588
+ migration_table TEXT NOT NULL,
589
+ patch_key TEXT NOT NULL,
590
+ checksum TEXT NOT NULL,
591
+ format_version INTEGER NOT NULL,
592
+ description TEXT NOT NULL,
593
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT now(),
594
+ PRIMARY KEY (migration_table, patch_key)
595
+ )` : `CREATE TABLE IF NOT EXISTS ${patchTable} (
596
+ migration_table VARCHAR(255) NOT NULL,
597
+ patch_key VARCHAR(255) NOT NULL,
598
+ checksum CHAR(64) NOT NULL,
599
+ format_version INT NOT NULL,
600
+ description TEXT NOT NULL,
601
+ applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
602
+ PRIMARY KEY (migration_table, patch_key)
603
+ )`;
604
+ yield this.sqlrunner.query(createPatchTableSql);
480
605
  });
481
606
  }
482
607
  teardown() {
483
608
  return __async(this, null, function* () {
484
609
  yield this.sqlrunner.execute(`DROP TABLE ${this.config.migration_table}`);
610
+ yield this.sqlrunner.execute(`DROP TABLE IF EXISTS ${resolvePatchTable(this.config)}`);
485
611
  });
486
612
  }
487
613
  };
@@ -558,6 +684,673 @@ function markerDialectParser(names) {
558
684
  var SqliteDialectParser = markerDialectParser(["sqlite"]);
559
685
  var PgDialectParser = markerDialectParser(["pg", "postgres", "postgresql"]);
560
686
 
687
+ // framework/PatchDirectoryReader.ts
688
+ var import_crypto = __toESM(require("crypto"));
689
+ var import_fs4 = __toESM(require("fs"));
690
+ var import_path3 = __toESM(require("path"));
691
+
692
+ // framework/PatchValidator.ts
693
+ var import_ajv = __toESM(require("ajv"));
694
+ var import_yaml = require("yaml");
695
+ var MAX_DESCRIPTION_LENGTH = 500;
696
+ var MAX_MIGRATION_KEY_LENGTH = 255;
697
+ var migrationKeySchema = {
698
+ type: "string",
699
+ minLength: 1,
700
+ maxLength: MAX_MIGRATION_KEY_LENGTH
701
+ };
702
+ var patchSchema = {
703
+ type: "object",
704
+ additionalProperties: false,
705
+ required: ["version", "description", "operations"],
706
+ properties: {
707
+ version: { type: "integer" },
708
+ description: { type: "string" },
709
+ operations: {
710
+ type: "array",
711
+ minItems: 1,
712
+ items: {
713
+ type: "object",
714
+ additionalProperties: false,
715
+ minProperties: 1,
716
+ maxProperties: 1,
717
+ properties: {
718
+ rename_migration: {
719
+ type: "object",
720
+ additionalProperties: false,
721
+ required: ["from", "to"],
722
+ properties: { from: migrationKeySchema, to: migrationKeySchema }
723
+ },
724
+ mark_applied: {
725
+ type: "object",
726
+ additionalProperties: false,
727
+ required: ["key"],
728
+ properties: { key: migrationKeySchema }
729
+ },
730
+ unmark_applied: {
731
+ type: "object",
732
+ additionalProperties: false,
733
+ required: ["key"],
734
+ properties: { key: migrationKeySchema }
735
+ }
736
+ }
737
+ }
738
+ }
739
+ }
740
+ };
741
+ var ajv = new import_ajv.default({ allErrors: true, strict: true });
742
+ var validateSchema = ajv.compile(patchSchema);
743
+ function fail(message, file, patchKey) {
744
+ throw new PatchValidationError(message, file, patchKey);
745
+ }
746
+ function checkMigrationKey(value, context, file, patchKey) {
747
+ if (value !== value.trim()) fail(`${context}: migration key has leading/trailing whitespace`, file, patchKey);
748
+ if (/[/\\]/.test(value)) fail(`${context}: migration key contains a path separator`, file, patchKey);
749
+ if (/[\x00-\x1f\x7f]/.test(value)) fail(`${context}: migration key contains control characters`, file, patchKey);
750
+ if (value.length === 0 || value.length > MAX_MIGRATION_KEY_LENGTH) {
751
+ fail(`${context}: migration key length out of bounds`, file, patchKey);
752
+ }
753
+ return canonicalMigrationKey(value);
754
+ }
755
+ function assertStrictYaml(doc, file, patchKey) {
756
+ if (doc.errors.length > 0) {
757
+ fail(`YAML parse error: ${doc.errors[0].message}`, file, patchKey);
758
+ }
759
+ if (doc.warnings.length > 0) {
760
+ fail(`YAML warning treated as error: ${doc.warnings[0].message}`, file, patchKey);
761
+ }
762
+ const visit = (node) => {
763
+ var _a, _b;
764
+ if (node == null || typeof node !== "object") return;
765
+ if ("source" in node && ((_a = node.constructor) == null ? void 0 : _a.name) === "Alias") {
766
+ fail("YAML aliases are not permitted in patch files", file, patchKey);
767
+ }
768
+ if (node.anchor) {
769
+ fail("YAML anchors are not permitted in patch files", file, patchKey);
770
+ }
771
+ if (node.tag && ![
772
+ "tag:yaml.org,2002:str",
773
+ "tag:yaml.org,2002:int",
774
+ "tag:yaml.org,2002:bool",
775
+ "tag:yaml.org,2002:null",
776
+ "tag:yaml.org,2002:map",
777
+ "tag:yaml.org,2002:seq"
778
+ ].includes(node.tag)) {
779
+ fail(`YAML tag '${node.tag}' is not permitted in patch files`, file, patchKey);
780
+ }
781
+ if (Array.isArray(node.items)) {
782
+ for (const item of node.items) {
783
+ if (item && typeof item === "object" && "key" in item) {
784
+ const keyValue = (_b = item.key) == null ? void 0 : _b.value;
785
+ if (keyValue === "<<") fail("YAML merge keys are not permitted in patch files", file, patchKey);
786
+ visit(item.key);
787
+ visit(item.value);
788
+ } else {
789
+ visit(item);
790
+ }
791
+ }
792
+ }
793
+ };
794
+ visit(doc.contents);
795
+ }
796
+ function parsePatchContent(content, fileName, patchKey) {
797
+ var _a;
798
+ const doc = (0, import_yaml.parseDocument)(content, {
799
+ uniqueKeys: true,
800
+ // duplicate mapping keys become errors
801
+ merge: false,
802
+ schema: "core",
803
+ version: "1.2"
804
+ });
805
+ assertStrictYaml(doc, fileName, patchKey);
806
+ const raw = doc.toJS({ mapAsMap: false });
807
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
808
+ fail("Patch document must be a YAML mapping", fileName, patchKey);
809
+ }
810
+ if (!validateSchema(raw)) {
811
+ const detail = ((_a = validateSchema.errors) != null ? _a : []).map((e) => `${e.instancePath || "/"} ${e.message}`).join("; ");
812
+ const anyRaw = raw;
813
+ if (typeof anyRaw.version === "number" && anyRaw.version !== PATCH_FORMAT_VERSION) {
814
+ fail(`Unknown patch format version: ${anyRaw.version} (supported: ${PATCH_FORMAT_VERSION})`, fileName, patchKey);
815
+ }
816
+ fail(`Schema validation failed: ${detail}`, fileName, patchKey);
817
+ }
818
+ const parsed = raw;
819
+ if (parsed.version !== PATCH_FORMAT_VERSION) {
820
+ fail(`Unknown patch format version: ${parsed.version} (supported: ${PATCH_FORMAT_VERSION})`, fileName, patchKey);
821
+ }
822
+ const description = parsed.description.trim();
823
+ if (description.length === 0) fail("description must be non-empty", fileName, patchKey);
824
+ if (description.length > MAX_DESCRIPTION_LENGTH) {
825
+ fail(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters`, fileName, patchKey);
826
+ }
827
+ const operations = parsed.operations.map((op, index) => {
828
+ const verbs = Object.keys(op);
829
+ const verb = verbs[0];
830
+ const context = `operation ${index} (${verb})`;
831
+ switch (verb) {
832
+ case "rename_migration": {
833
+ const from = checkMigrationKey(op.rename_migration.from, context, fileName, patchKey);
834
+ const to = checkMigrationKey(op.rename_migration.to, context, fileName, patchKey);
835
+ if (from === to) {
836
+ fail(`${context}: 'from' and 'to' are identical after canonicalization ('${from}')`, fileName, patchKey);
837
+ }
838
+ return { verb: "rename_migration", from, to };
839
+ }
840
+ case "mark_applied":
841
+ return { verb: "mark_applied", key: checkMigrationKey(op.mark_applied.key, context, fileName, patchKey) };
842
+ case "unmark_applied":
843
+ return { verb: "unmark_applied", key: checkMigrationKey(op.unmark_applied.key, context, fileName, patchKey) };
844
+ default:
845
+ fail(`operation ${index}: unknown verb '${verb}'`, fileName, patchKey);
846
+ }
847
+ });
848
+ return { version: parsed.version, description, operations };
849
+ }
850
+ function validatePatchPlan(patches, manifest) {
851
+ const renames = [];
852
+ for (const patch of patches) {
853
+ patch.operations.forEach((op, index) => {
854
+ if (op.verb === "rename_migration") {
855
+ renames.push({ from: op.from, to: op.to, file: patch.fileName });
856
+ } else if (op.verb === "mark_applied") {
857
+ if (!manifest.has(op.key)) {
858
+ throw new PatchValidationError(
859
+ `operation ${index} (mark_applied): key '${op.key}' is not present in the current migration manifest`,
860
+ patch.fileName,
861
+ patch.patchKey
862
+ );
863
+ }
864
+ }
865
+ });
866
+ }
867
+ if (renames.length === 0) return;
868
+ const mentioned = /* @__PURE__ */ new Set();
869
+ renames.forEach((r) => {
870
+ mentioned.add(r.from);
871
+ mentioned.add(r.to);
872
+ });
873
+ const finalKey = (start) => {
874
+ let current = start;
875
+ for (const r of renames) {
876
+ if (current === r.from) current = r.to;
877
+ }
878
+ return current;
879
+ };
880
+ for (const key of mentioned) {
881
+ const finish = finalKey(key);
882
+ if (manifest.has(key)) {
883
+ if (finish !== key) {
884
+ throw new PatchValidationError(
885
+ `rename plan moves current migration '${key}' to '${finish}', which would make its file incorrectly pending`
886
+ );
887
+ }
888
+ } else {
889
+ if (!manifest.has(finish)) {
890
+ throw new PatchValidationError(
891
+ `rename plan leaves historical key '${key}' at '${finish}', which is not present in the current migration manifest`
892
+ );
893
+ }
894
+ }
895
+ }
896
+ }
897
+
898
+ // framework/PatchDirectoryReader.ts
899
+ var PatchDirectoryReader = class {
900
+ constructor(directory) {
901
+ this.directory = directory;
902
+ }
903
+ loadPatches() {
904
+ if (!import_fs4.default.existsSync(this.directory)) return [];
905
+ const entries = import_fs4.default.readdirSync(this.directory, { withFileTypes: true });
906
+ const patchFiles = [];
907
+ for (const entry of entries) {
908
+ if (!entry.name.endsWith(".yaml")) continue;
909
+ if (!entry.isFile() || entry.isSymbolicLink()) {
910
+ if (entry.isSymbolicLink()) {
911
+ throw new PatchValidationError(`patch file must be a regular file, not a symlink`, entry.name);
912
+ }
913
+ continue;
914
+ }
915
+ patchFiles.push(entry.name);
916
+ }
917
+ patchFiles.sort((a, b) => {
918
+ const stampA = parseInt(a.slice(0, 13), 10);
919
+ const stampB = parseInt(b.slice(0, 13), 10);
920
+ if (!Number.isNaN(stampA) && !Number.isNaN(stampB) && stampA !== stampB) {
921
+ return stampA - stampB;
922
+ }
923
+ return a < b ? -1 : a > b ? 1 : 0;
924
+ });
925
+ return patchFiles.map((fileName) => {
926
+ const match = PATCH_FILENAME_REGEX.exec(fileName);
927
+ if (!match) {
928
+ throw new PatchValidationError(
929
+ `invalid patch filename (expected <13-digit-stamp>_<name>.yaml with name matching [a-z0-9][a-z0-9_-]{0,119})`,
930
+ fileName
931
+ );
932
+ }
933
+ const patchKey = fileName.slice(0, -".yaml".length);
934
+ const filePath = import_path3.default.join(this.directory, fileName);
935
+ const bytes = import_fs4.default.readFileSync(filePath);
936
+ const checksum = import_crypto.default.createHash("sha256").update(bytes).digest("hex");
937
+ const content = bytes.toString("utf8");
938
+ const { version, description, operations } = parsePatchContent(content, fileName, patchKey);
939
+ return { patchKey, fileName, filePath, checksum, version, description, operations };
940
+ });
941
+ }
942
+ };
943
+
944
+ // framework/PatchRunner.ts
945
+ function toError(error) {
946
+ if (error instanceof Error) return error;
947
+ return new Error(String(error));
948
+ }
949
+ function extractRows(result) {
950
+ if (!Array.isArray(result)) return [];
951
+ if (Array.isArray(result[0])) return result[0];
952
+ if (result.length === 2 && result[0] && typeof result[0] === "object" && result[1] && typeof result[1] === "object" && !("rows" in result[1])) {
953
+ return result;
954
+ }
955
+ if (result[0] == null) return [];
956
+ return [result[0]];
957
+ }
958
+ var PatchRunner = class {
959
+ constructor(sqlrunner, config) {
960
+ this.sqlrunner = sqlrunner;
961
+ this.config = config;
962
+ this.patchTable = resolvePatchTable(config);
963
+ this.migrationTable = config.migration_table;
964
+ this.dialect = config.database;
965
+ }
966
+ /**
967
+ * Discovers, validates, and applies every unapplied patch in order.
968
+ * Each unapplied patch is its own transaction; earlier committed patches
969
+ * remain committed if a later patch fails.
970
+ */
971
+ applyPending() {
972
+ return __async(this, null, function* () {
973
+ const reader = new PatchDirectoryReader(resolvePatchFolder(this.config));
974
+ const patches = reader.loadPatches();
975
+ const history = yield this.loadHistory();
976
+ if (patches.length === 0 && history.length === 0) {
977
+ return [];
978
+ }
979
+ const byKey = new Map(patches.map((p) => [p.patchKey, p]));
980
+ for (const row of history) {
981
+ const file = byKey.get(row.patch_key);
982
+ if (!file) {
983
+ throw new PatchIntegrityError(
984
+ `applied patch '${row.patch_key}' has no corresponding file in the patch folder; patch files are permanent and must never be renamed or deleted`,
985
+ void 0,
986
+ row.patch_key
987
+ );
988
+ }
989
+ if (file.checksum !== row.checksum) {
990
+ throw new PatchIntegrityError(
991
+ `applied patch '${row.patch_key}' content changed after application; patch files are immutable once recorded`,
992
+ file.fileName,
993
+ row.patch_key,
994
+ row.checksum,
995
+ file.checksum
996
+ );
997
+ }
998
+ }
999
+ const manifest = loadMigrationManifest(this.config.migration_folder, this.dialect);
1000
+ validatePatchPlan(patches, manifest);
1001
+ const appliedKeys = new Set(history.map((r) => r.patch_key));
1002
+ const results = [];
1003
+ for (const patch of patches) {
1004
+ if (appliedKeys.has(patch.patchKey)) {
1005
+ results.push({
1006
+ patchKey: patch.patchKey,
1007
+ fileName: patch.fileName,
1008
+ status: "already_applied",
1009
+ operations: []
1010
+ });
1011
+ continue;
1012
+ }
1013
+ results.push(yield this.applyOne(patch, manifest));
1014
+ }
1015
+ return results;
1016
+ });
1017
+ }
1018
+ loadHistory() {
1019
+ return __async(this, null, function* () {
1020
+ try {
1021
+ const result = yield this.sqlrunner.query(
1022
+ `SELECT patch_key, checksum FROM ${this.patchTable} WHERE migration_table = ?`,
1023
+ [this.migrationTable]
1024
+ );
1025
+ return extractRows(result);
1026
+ } catch (error) {
1027
+ throw new PatchExecutionError(
1028
+ `failed to read patch history from '${this.patchTable}'`,
1029
+ void 0,
1030
+ void 0,
1031
+ void 0,
1032
+ void 0,
1033
+ toError(error)
1034
+ );
1035
+ }
1036
+ });
1037
+ }
1038
+ beginSql() {
1039
+ switch (this.dialect) {
1040
+ case "sqlite":
1041
+ return "BEGIN IMMEDIATE";
1042
+ case "pg":
1043
+ return "BEGIN";
1044
+ default:
1045
+ return "START TRANSACTION";
1046
+ }
1047
+ }
1048
+ begin(patch) {
1049
+ return __async(this, null, function* () {
1050
+ const deadline = Date.now() + 1e4;
1051
+ while (true) {
1052
+ try {
1053
+ yield this.sqlrunner.execute(this.beginSql());
1054
+ return;
1055
+ } catch (error) {
1056
+ const message = toError(error).message;
1057
+ if (/SQLITE_BUSY|database is locked/i.test(message) && Date.now() < deadline) {
1058
+ yield new Promise((resolve) => setTimeout(resolve, 50));
1059
+ continue;
1060
+ }
1061
+ throw new PatchExecutionError(
1062
+ "failed to start patch transaction",
1063
+ patch.fileName,
1064
+ patch.patchKey,
1065
+ void 0,
1066
+ void 0,
1067
+ toError(error)
1068
+ );
1069
+ }
1070
+ }
1071
+ });
1072
+ }
1073
+ rollbackQuietly() {
1074
+ return __async(this, null, function* () {
1075
+ try {
1076
+ yield this.sqlrunner.execute("ROLLBACK");
1077
+ } catch (e) {
1078
+ }
1079
+ });
1080
+ }
1081
+ applyOne(patch, manifest) {
1082
+ return __async(this, null, function* () {
1083
+ yield this.begin(patch);
1084
+ try {
1085
+ yield this.sqlrunner.execute(
1086
+ `INSERT INTO ${this.patchTable} (migration_table, patch_key, checksum, format_version, description)
1087
+ VALUES (?, ?, ?, ?, ?)`,
1088
+ [this.migrationTable, patch.patchKey, patch.checksum, patch.version, patch.description]
1089
+ );
1090
+ } catch (claimError) {
1091
+ yield this.rollbackQuietly();
1092
+ const committed = yield this.findCommittedRow(patch.patchKey);
1093
+ if (committed) {
1094
+ if (committed.checksum === patch.checksum) {
1095
+ return {
1096
+ patchKey: patch.patchKey,
1097
+ fileName: patch.fileName,
1098
+ status: "already_applied",
1099
+ operations: []
1100
+ };
1101
+ }
1102
+ throw new PatchIntegrityError(
1103
+ `patch '${patch.patchKey}' was applied elsewhere with a different checksum`,
1104
+ patch.fileName,
1105
+ patch.patchKey,
1106
+ committed.checksum,
1107
+ patch.checksum
1108
+ );
1109
+ }
1110
+ throw new PatchExecutionError(
1111
+ "failed to claim patch-history row",
1112
+ patch.fileName,
1113
+ patch.patchKey,
1114
+ void 0,
1115
+ void 0,
1116
+ toError(claimError)
1117
+ );
1118
+ }
1119
+ const operationResults = [];
1120
+ try {
1121
+ for (let index = 0; index < patch.operations.length; index++) {
1122
+ operationResults.push(
1123
+ yield this.applyOperation(patch, patch.operations[index], index, manifest)
1124
+ );
1125
+ }
1126
+ yield this.sqlrunner.execute("COMMIT");
1127
+ } catch (error) {
1128
+ yield this.rollbackQuietly();
1129
+ if (error instanceof PatchConflictError || error instanceof PatchExecutionError || error instanceof PatchIntegrityError) {
1130
+ throw error;
1131
+ }
1132
+ throw new PatchExecutionError(
1133
+ "patch application failed",
1134
+ patch.fileName,
1135
+ patch.patchKey,
1136
+ void 0,
1137
+ void 0,
1138
+ toError(error)
1139
+ );
1140
+ }
1141
+ return {
1142
+ patchKey: patch.patchKey,
1143
+ fileName: patch.fileName,
1144
+ status: "applied",
1145
+ operations: operationResults
1146
+ };
1147
+ });
1148
+ }
1149
+ findCommittedRow(patchKey) {
1150
+ return __async(this, null, function* () {
1151
+ const result = yield this.sqlrunner.query(
1152
+ `SELECT patch_key, checksum FROM ${this.patchTable} WHERE migration_table = ? AND patch_key = ?`,
1153
+ [this.migrationTable, patchKey]
1154
+ );
1155
+ const list = extractRows(result);
1156
+ return list.length > 0 ? list[0] : null;
1157
+ });
1158
+ }
1159
+ countRows(key) {
1160
+ return __async(this, null, function* () {
1161
+ var _a, _b, _c;
1162
+ const result = yield this.sqlrunner.query(
1163
+ `SELECT COUNT(*) AS row_count FROM ${this.migrationTable} WHERE migration_key = ?`,
1164
+ [key]
1165
+ );
1166
+ const rows = extractRows(result);
1167
+ const value = (_c = (_a = rows[0]) == null ? void 0 : _a.row_count) != null ? _c : Object.values((_b = rows[0]) != null ? _b : {})[0];
1168
+ return Number(value != null ? value : 0);
1169
+ });
1170
+ }
1171
+ conflict(patch, index, verb, message, keys, counts) {
1172
+ throw new PatchConflictError(
1173
+ `operation ${index} (${verb}): ${message}`,
1174
+ patch.fileName,
1175
+ patch.patchKey,
1176
+ index,
1177
+ verb,
1178
+ keys,
1179
+ counts
1180
+ );
1181
+ }
1182
+ applyOperation(patch, op, index, manifest) {
1183
+ return __async(this, null, function* () {
1184
+ var _a, _b, _c, _d;
1185
+ try {
1186
+ switch (op.verb) {
1187
+ case "rename_migration": {
1188
+ const fromCount = yield this.countRows(op.from);
1189
+ const toCount = yield this.countRows(op.to);
1190
+ const counts = { [op.from]: fromCount, [op.to]: toCount };
1191
+ if (fromCount > 1 || toCount > 1) {
1192
+ this.conflict(
1193
+ patch,
1194
+ index,
1195
+ op.verb,
1196
+ `ledger corruption: duplicate rows for a migration key`,
1197
+ [op.from, op.to],
1198
+ counts
1199
+ );
1200
+ }
1201
+ if (fromCount === 1 && toCount === 1) {
1202
+ this.conflict(
1203
+ patch,
1204
+ index,
1205
+ op.verb,
1206
+ `both '${op.from}' and '${op.to}' exist in the ledger`,
1207
+ [op.from, op.to],
1208
+ counts
1209
+ );
1210
+ }
1211
+ if (fromCount === 0) {
1212
+ return { verb: op.verb, changed: false };
1213
+ }
1214
+ const target = manifest.get(op.to);
1215
+ if (target) {
1216
+ yield this.sqlrunner.execute(
1217
+ `UPDATE ${this.migrationTable} SET migration_key = ?, up = ?, down = ? WHERE migration_key = ?`,
1218
+ [op.to, (_a = target.upFile) != null ? _a : "", (_b = target.downFile) != null ? _b : "", op.from]
1219
+ );
1220
+ } else {
1221
+ yield this.sqlrunner.execute(
1222
+ `UPDATE ${this.migrationTable} SET migration_key = ? WHERE migration_key = ?`,
1223
+ [op.to, op.from]
1224
+ );
1225
+ }
1226
+ return { verb: op.verb, changed: true };
1227
+ }
1228
+ case "mark_applied": {
1229
+ const count = yield this.countRows(op.key);
1230
+ if (count > 1) {
1231
+ this.conflict(
1232
+ patch,
1233
+ index,
1234
+ op.verb,
1235
+ `ledger corruption: duplicate rows for '${op.key}'`,
1236
+ [op.key],
1237
+ { [op.key]: count }
1238
+ );
1239
+ }
1240
+ if (count === 1) {
1241
+ return { verb: op.verb, changed: false };
1242
+ }
1243
+ const entry = manifest.get(op.key);
1244
+ yield this.sqlrunner.execute(
1245
+ `INSERT INTO ${this.migrationTable} (migration_key, up, down) VALUES (?, ?, ?)`,
1246
+ [op.key, (_c = entry == null ? void 0 : entry.upFile) != null ? _c : "", (_d = entry == null ? void 0 : entry.downFile) != null ? _d : ""]
1247
+ );
1248
+ return { verb: op.verb, changed: true };
1249
+ }
1250
+ case "unmark_applied": {
1251
+ const count = yield this.countRows(op.key);
1252
+ if (count > 1) {
1253
+ this.conflict(
1254
+ patch,
1255
+ index,
1256
+ op.verb,
1257
+ `ledger corruption: duplicate rows for '${op.key}'`,
1258
+ [op.key],
1259
+ { [op.key]: count }
1260
+ );
1261
+ }
1262
+ if (count === 0) {
1263
+ return { verb: op.verb, changed: false };
1264
+ }
1265
+ yield this.sqlrunner.execute(
1266
+ `DELETE FROM ${this.migrationTable} WHERE migration_key = ?`,
1267
+ [op.key]
1268
+ );
1269
+ return { verb: op.verb, changed: true };
1270
+ }
1271
+ }
1272
+ } catch (error) {
1273
+ if (error instanceof PatchConflictError) throw error;
1274
+ throw new PatchExecutionError(
1275
+ `operation failed`,
1276
+ patch.fileName,
1277
+ patch.patchKey,
1278
+ index,
1279
+ op.verb,
1280
+ toError(error)
1281
+ );
1282
+ }
1283
+ });
1284
+ }
1285
+ };
1286
+
1287
+ // framework/PatchCreator.ts
1288
+ var import_fs5 = __toESM(require("fs"));
1289
+ var import_path4 = __toESM(require("path"));
1290
+ var MAX_NAME_LENGTH = 120;
1291
+ var SCAFFOLD = `version: 1
1292
+ description: TODO
1293
+ operations: []
1294
+ `;
1295
+ var PatchCreator = class _PatchCreator {
1296
+ constructor(patchFolder) {
1297
+ this.patchFolder = patchFolder;
1298
+ }
1299
+ /**
1300
+ * Normalizes a patch name: trim, whitespace runs -> `_`, lowercase.
1301
+ * Rejects empty results, path separators, `..`, control characters,
1302
+ * characters outside [a-z0-9_-], and names longer than 120 characters.
1303
+ */
1304
+ static normalizeName(name) {
1305
+ const normalized = (name != null ? name : "").trim().replace(/\s+/g, "_").toLowerCase();
1306
+ if (normalized.length === 0) {
1307
+ throw new CLIError("Patch name is required");
1308
+ }
1309
+ if (normalized.includes("/") || normalized.includes("\\")) {
1310
+ throw new CLIError("Patch name must not contain path separators");
1311
+ }
1312
+ if (normalized.includes("..")) {
1313
+ throw new CLIError("Patch name must not contain '..'");
1314
+ }
1315
+ if (/[\x00-\x1f\x7f]/.test(normalized)) {
1316
+ throw new CLIError("Patch name must not contain control characters");
1317
+ }
1318
+ if (!/^[a-z0-9_-]+$/.test(normalized)) {
1319
+ throw new CLIError("Patch name may only contain characters [a-z0-9_-]");
1320
+ }
1321
+ if (normalized.length > MAX_NAME_LENGTH) {
1322
+ throw new CLIError(`Patch name exceeds ${MAX_NAME_LENGTH} characters after normalization`);
1323
+ }
1324
+ return normalized;
1325
+ }
1326
+ /**
1327
+ * Creates `<patch_folder>/<stamp>_<normalized_name>.yaml` with exclusive
1328
+ * file creation. On a millisecond-stamp collision, mints a later stamp
1329
+ * and retries. Returns the created path.
1330
+ */
1331
+ create(name) {
1332
+ const normalized = _PatchCreator.normalizeName(name);
1333
+ if (!import_fs5.default.existsSync(this.patchFolder)) {
1334
+ import_fs5.default.mkdirSync(this.patchFolder, { recursive: true });
1335
+ }
1336
+ let stamp = Date.now();
1337
+ for (let attempt = 0; attempt < 1e3; attempt++) {
1338
+ const filePath = import_path4.default.join(this.patchFolder, `${stamp}_${normalized}.yaml`);
1339
+ try {
1340
+ import_fs5.default.writeFileSync(filePath, SCAFFOLD, { flag: "wx" });
1341
+ return filePath;
1342
+ } catch (error) {
1343
+ if (error && error.code === "EEXIST") {
1344
+ stamp += 1;
1345
+ continue;
1346
+ }
1347
+ throw error;
1348
+ }
1349
+ }
1350
+ throw new CLIError("Unable to create patch file: too many filename collisions");
1351
+ }
1352
+ };
1353
+
561
1354
  // framework/SQLRunner.ts
562
1355
  var BaseSQLRunner = class {
563
1356
  /**
@@ -872,7 +1665,7 @@ var PgRunner = class _PgRunner extends BaseSQLRunner {
872
1665
  };
873
1666
 
874
1667
  // framework/MigrationRunner.ts
875
- function toError(error) {
1668
+ function toError2(error) {
876
1669
  if (error instanceof Error) return error;
877
1670
  return new Error(String(error));
878
1671
  }
@@ -900,10 +1693,12 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
900
1693
  return __async(this, null, function* () {
901
1694
  const configReader = new FileMigrationConfigReader(configFile);
902
1695
  const config = configReader.loadFile();
1696
+ let factoryOwnsConnection = false;
903
1697
  if (!conn) {
904
1698
  conn = yield this.createConnection(config);
1699
+ factoryOwnsConnection = true;
905
1700
  }
906
- return new _MigrationRunnerFactory().create(config, conn);
1701
+ return new _MigrationRunnerFactory().create(config, conn, factoryOwnsConnection);
907
1702
  });
908
1703
  }
909
1704
  static createConnection(config) {
@@ -923,7 +1718,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
923
1718
  conn = yield ((_b = (_a = mysql.default) == null ? void 0 : _a.createConnection) != null ? _b : mysql.createConnection)(settings);
924
1719
  return conn;
925
1720
  } catch (error) {
926
- throw DatabaseConnectionError.connectionFailed("sql", toError(error).message);
1721
+ throw DatabaseConnectionError.connectionFailed("sql", toError2(error).message);
927
1722
  }
928
1723
  case "sqlite":
929
1724
  if (!config.sqlite) {
@@ -938,7 +1733,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
938
1733
  });
939
1734
  return conn;
940
1735
  } catch (error) {
941
- throw DatabaseConnectionError.connectionFailed("sqlite", toError(error).message);
1736
+ throw DatabaseConnectionError.connectionFailed("sqlite", toError2(error).message);
942
1737
  }
943
1738
  case "pg":
944
1739
  if (!config.pg && !process.env.DATABASE_URL) {
@@ -954,7 +1749,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
954
1749
  yield conn.connect();
955
1750
  return conn;
956
1751
  } catch (error) {
957
- throw DatabaseConnectionError.connectionFailed("pg", toError(error).message);
1752
+ throw DatabaseConnectionError.connectionFailed("pg", toError2(error).message);
958
1753
  }
959
1754
  default:
960
1755
  throw ConfigurationError.unknownDatabaseType(config.database);
@@ -968,7 +1763,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
968
1763
  return new _MigrationRunnerFactory().createEmpty(config);
969
1764
  });
970
1765
  }
971
- create(config, conn) {
1766
+ create(config, conn, factoryOwnsConnection = false) {
972
1767
  return __async(this, null, function* () {
973
1768
  let sqlrunner;
974
1769
  let driverConnection = conn;
@@ -981,8 +1776,19 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
981
1776
  const setup = new MigrationSetup(sqlrunner, config);
982
1777
  const read_strategy = this.getReadStategy(config);
983
1778
  const migration_files = new MigrationDirectoryReader(config.migration_folder, read_strategy, sqlrunner, config.database);
984
- yield setup.setup();
985
- return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, driverConnection);
1779
+ const runner = new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, driverConnection);
1780
+ try {
1781
+ yield runner.setup();
1782
+ } catch (error) {
1783
+ if (factoryOwnsConnection) {
1784
+ try {
1785
+ yield sqlrunner.end();
1786
+ } catch (e) {
1787
+ }
1788
+ }
1789
+ throw error;
1790
+ }
1791
+ return runner;
986
1792
  });
987
1793
  }
988
1794
  createEmpty(config) {
@@ -992,7 +1798,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
992
1798
  const setup = new MigrationSetup(sqlrunner, config);
993
1799
  const read_strategy = this.getReadStategy(config);
994
1800
  const migration_files = new MigrationDirectoryReader(config.migration_folder, read_strategy, sqlrunner, config.database);
995
- return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, conn);
1801
+ return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, conn, false);
996
1802
  });
997
1803
  }
998
1804
  getReadStategy(config) {
@@ -1009,33 +1815,77 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
1009
1815
  }
1010
1816
  };
1011
1817
  var MySQLMigrationRunner = class {
1012
- constructor(config, directory, setupRunner, sqlrunner, connection) {
1818
+ constructor(config, directory, setupRunner, sqlrunner, connection, preflightEnabled = true) {
1013
1819
  this.config = config;
1014
1820
  this.directory = directory;
1015
1821
  this.setupRunner = setupRunner;
1016
1822
  this.sqlrunner = sqlrunner;
1017
1823
  this.connection = connection;
1824
+ this.preflightEnabled = preflightEnabled;
1825
+ /**
1826
+ * Memoized in-flight preflight promise. Simultaneous or repeated calls
1827
+ * to setup() on one runner execute the preflight (migration table setup
1828
+ * + patch application) exactly once. Cleared after rejection so a caller
1829
+ * may retry after fixing the cause.
1830
+ */
1831
+ this.preflightPromise = null;
1832
+ this.lastPatchResults = [];
1018
1833
  }
1019
1834
  setup() {
1835
+ return __async(this, null, function* () {
1836
+ if (!this.preflightEnabled) {
1837
+ return;
1838
+ }
1839
+ if (!this.preflightPromise) {
1840
+ this.preflightPromise = this.runPreflight();
1841
+ this.preflightPromise.catch(() => {
1842
+ this.preflightPromise = null;
1843
+ });
1844
+ }
1845
+ return this.preflightPromise;
1846
+ });
1847
+ }
1848
+ runPreflight() {
1020
1849
  return __async(this, null, function* () {
1021
1850
  try {
1022
1851
  yield this.setupRunner.setup();
1023
1852
  } catch (error) {
1024
- throw new MigrationExecutionError("Failed to set up migration database", void 0, void 0, toError(error));
1853
+ throw new MigrationExecutionError("Failed to set up migration database", void 0, void 0, toError2(error));
1025
1854
  }
1855
+ const patchRunner = new PatchRunner(this.sqlrunner, this.config);
1856
+ this.lastPatchResults = yield patchRunner.applyPending();
1857
+ });
1858
+ }
1859
+ /**
1860
+ * Delegates to the same idempotent preflight; returns the results of the
1861
+ * patch pass that ran (or is running) for this runner.
1862
+ */
1863
+ applyPendingPatches() {
1864
+ return __async(this, null, function* () {
1865
+ yield this.setup();
1866
+ return this.lastPatchResults;
1026
1867
  });
1027
1868
  }
1869
+ /**
1870
+ * Scaffolds a new ledger patch file and returns the created path.
1871
+ * Never connects to a database.
1872
+ */
1873
+ createPatch(name) {
1874
+ const creator = new PatchCreator(resolvePatchFolder(this.config));
1875
+ return creator.create(name);
1876
+ }
1028
1877
  terminate() {
1029
1878
  return __async(this, null, function* () {
1030
1879
  try {
1031
1880
  yield this.setupRunner.teardown();
1032
1881
  } catch (error) {
1033
- throw new MigrationExecutionError("Failed to tear down migration database", void 0, void 0, toError(error));
1882
+ throw new MigrationExecutionError("Failed to tear down migration database", void 0, void 0, toError2(error));
1034
1883
  }
1035
1884
  });
1036
1885
  }
1037
1886
  getMigrationsHistory() {
1038
1887
  return __async(this, null, function* () {
1888
+ yield this.setup();
1039
1889
  try {
1040
1890
  const results = yield this.sqlrunner.query(`
1041
1891
  select *
@@ -1043,16 +1893,17 @@ var MySQLMigrationRunner = class {
1043
1893
  `);
1044
1894
  return results[0];
1045
1895
  } catch (error) {
1046
- throw new MigrationExecutionError("Failed to get migration history", void 0, void 0, toError(error));
1896
+ throw new MigrationExecutionError("Failed to get migration history", void 0, void 0, toError2(error));
1047
1897
  }
1048
1898
  });
1049
1899
  }
1050
1900
  getMigrations() {
1051
1901
  return __async(this, null, function* () {
1902
+ yield this.setup();
1052
1903
  try {
1053
1904
  return this.directory.loadMigrations(this.config.migration_table, this.connection);
1054
1905
  } catch (error) {
1055
- throw new MigrationExecutionError("Failed to load migrations", void 0, void 0, toError(error));
1906
+ throw new MigrationExecutionError("Failed to load migrations", void 0, void 0, toError2(error));
1056
1907
  }
1057
1908
  });
1058
1909
  }
@@ -1065,7 +1916,7 @@ var MySQLMigrationRunner = class {
1065
1916
  if (error instanceof MigrationExecutionError) {
1066
1917
  throw error;
1067
1918
  }
1068
- throw new MigrationExecutionError("Failed to get pending migrations", void 0, void 0, toError(error));
1919
+ throw new MigrationExecutionError("Failed to get pending migrations", void 0, void 0, toError2(error));
1069
1920
  }
1070
1921
  });
1071
1922
  }
@@ -1078,12 +1929,13 @@ var MySQLMigrationRunner = class {
1078
1929
  if (error instanceof MigrationExecutionError) {
1079
1930
  throw error;
1080
1931
  }
1081
- throw new MigrationExecutionError("Failed to get completed migrations", void 0, void 0, toError(error));
1932
+ throw new MigrationExecutionError("Failed to get completed migrations", void 0, void 0, toError2(error));
1082
1933
  }
1083
1934
  });
1084
1935
  }
1085
1936
  migrate(migrationNodes, forward) {
1086
1937
  return __async(this, null, function* () {
1938
+ yield this.setup();
1087
1939
  for (let node of migrationNodes) {
1088
1940
  try {
1089
1941
  if (forward) {
@@ -1096,7 +1948,7 @@ var MySQLMigrationRunner = class {
1096
1948
  `Failed to ${forward ? "apply" : "rollback"} migration`,
1097
1949
  node.name || String(node),
1098
1950
  forward ? node.up_sql() : node.down_sql(),
1099
- toError(error)
1951
+ toError2(error)
1100
1952
  );
1101
1953
  }
1102
1954
  }
@@ -1104,6 +1956,7 @@ var MySQLMigrationRunner = class {
1104
1956
  }
1105
1957
  reset() {
1106
1958
  return __async(this, null, function* () {
1959
+ yield this.setup();
1107
1960
  try {
1108
1961
  let migrations = yield this.getMigrations();
1109
1962
  const rollback = yield migration_filter(migrations, true);
@@ -1115,7 +1968,7 @@ var MySQLMigrationRunner = class {
1115
1968
  if (error instanceof MigrationExecutionError) {
1116
1969
  throw error;
1117
1970
  }
1118
- throw new MigrationExecutionError("Failed to reset migrations", void 0, void 0, toError(error));
1971
+ throw new MigrationExecutionError("Failed to reset migrations", void 0, void 0, toError2(error));
1119
1972
  }
1120
1973
  });
1121
1974
  }
@@ -1124,7 +1977,7 @@ var MySQLMigrationRunner = class {
1124
1977
  const creator = new MigrationCreator(this.config);
1125
1978
  creator.create(name);
1126
1979
  } catch (error) {
1127
- throw new MigrationExecutionError(`Failed to create migration: ${name}`, void 0, void 0, toError(error));
1980
+ throw new MigrationExecutionError(`Failed to create migration: ${name}`, void 0, void 0, toError2(error));
1128
1981
  }
1129
1982
  }
1130
1983
  close() {
@@ -1133,7 +1986,7 @@ var MySQLMigrationRunner = class {
1133
1986
  try {
1134
1987
  yield this.sqlrunner.end();
1135
1988
  } catch (error) {
1136
- throw new DatabaseConnectionError(`Failed to close database connection: ${toError(error).message}`);
1989
+ throw new DatabaseConnectionError(`Failed to close database connection: ${toError2(error).message}`);
1137
1990
  }
1138
1991
  }
1139
1992
  });
@@ -1147,7 +2000,7 @@ var MySQLMigrationRunner = class {
1147
2000
  return __async(this, null, function* () {
1148
2001
  try {
1149
2002
  console.log(`Checking for ${config_file}`);
1150
- const config_exist = import_fs3.default.existsSync(config_file);
2003
+ const config_exist = import_fs6.default.existsSync(config_file);
1151
2004
  if (!config_exist) {
1152
2005
  console.log(`Creating ${config_file}`);
1153
2006
  const default_config = {
@@ -1161,11 +2014,11 @@ var MySQLMigrationRunner = class {
1161
2014
  "password": ""
1162
2015
  }
1163
2016
  };
1164
- import_fs3.default.writeFileSync(config_file, JSON.stringify(default_config, null, 2));
2017
+ import_fs6.default.writeFileSync(config_file, JSON.stringify(default_config, null, 2));
1165
2018
  console.log(`Created ${config_file}`);
1166
2019
  }
1167
2020
  } catch (error) {
1168
- throw new ConfigurationError(`Failed to initialize config file: ${toError(error).message}`);
2021
+ throw new ConfigurationError(`Failed to initialize config file: ${toError2(error).message}`);
1169
2022
  }
1170
2023
  });
1171
2024
  }
@@ -1176,7 +2029,7 @@ var FileMigrationConfigReader = class {
1176
2029
  }
1177
2030
  loadFile() {
1178
2031
  try {
1179
- const fileContent = import_fs3.default.readFileSync(this.configFile);
2032
+ const fileContent = import_fs6.default.readFileSync(this.configFile);
1180
2033
  const config = JSON.parse(fileContent.toString());
1181
2034
  if (!config.migration_folder) {
1182
2035
  throw ConfigurationError.missingRequiredProperty("migration_folder");
@@ -1200,7 +2053,7 @@ var FileMigrationConfigReader = class {
1200
2053
  if (error instanceof ConfigurationError) {
1201
2054
  throw error;
1202
2055
  }
1203
- const err = toError(error);
2056
+ const err = toError2(error);
1204
2057
  if (err.message.includes("ENOENT")) {
1205
2058
  throw new ConfigurationError(`Config file not found: ${this.configFile}`);
1206
2059
  }
@@ -1217,16 +2070,16 @@ var MigrationCreator = class {
1217
2070
  throw new CLIError("Migration name is required");
1218
2071
  }
1219
2072
  try {
1220
- if (!import_fs3.default.existsSync(this.config.migration_folder)) {
1221
- import_fs3.default.mkdirSync(this.config.migration_folder, { recursive: true });
2073
+ if (!import_fs6.default.existsSync(this.config.migration_folder)) {
2074
+ import_fs6.default.mkdirSync(this.config.migration_folder, { recursive: true });
1222
2075
  }
1223
2076
  const now_timestamp = Date.now();
1224
2077
  const filename_up = `${now_timestamp}_${name}.up.sql`;
1225
2078
  const filename_down = `${now_timestamp}_${name}.down.sql`;
1226
- import_fs3.default.writeFileSync(`${this.config.migration_folder}/${filename_up}`, `
2079
+ import_fs6.default.writeFileSync(`${this.config.migration_folder}/${filename_up}`, `
1227
2080
  -- Write your up migration here
1228
2081
  `.trim());
1229
- import_fs3.default.writeFileSync(`${this.config.migration_folder}/${filename_down}`, `
2082
+ import_fs6.default.writeFileSync(`${this.config.migration_folder}/${filename_down}`, `
1230
2083
  -- Write your down migration here
1231
2084
  `.trim());
1232
2085
  console.log(`Created migration files:`);
@@ -1236,16 +2089,16 @@ var MigrationCreator = class {
1236
2089
  if (error instanceof CLIError) {
1237
2090
  throw error;
1238
2091
  }
1239
- throw new MigrationExecutionError(`Failed to create migration files: ${toError(error).message}`);
2092
+ throw new MigrationExecutionError(`Failed to create migration files: ${toError2(error).message}`);
1240
2093
  }
1241
2094
  }
1242
2095
  };
1243
2096
 
1244
2097
  // framework/SeedRunner.ts
1245
- var import_fs4 = __toESM(require("fs"));
1246
- var import_path2 = __toESM(require("path"));
2098
+ var import_fs7 = __toESM(require("fs"));
2099
+ var import_path5 = __toESM(require("path"));
1247
2100
  var import_url = require("url");
1248
- var import_ajv = __toESM(require("ajv"));
2101
+ var import_ajv2 = __toESM(require("ajv"));
1249
2102
  var import_ajv_formats = __toESM(require("ajv-formats"));
1250
2103
  var import_api = require("tsx/esm/api");
1251
2104
  function resolveAlias(name, aliasMap) {
@@ -1254,10 +2107,10 @@ function resolveAlias(name, aliasMap) {
1254
2107
  return (_a = aliasMap[name]) != null ? _a : name;
1255
2108
  }
1256
2109
  function walkForFile(rootDir, fileName) {
1257
- if (!import_fs4.default.existsSync(rootDir)) return null;
1258
- const entries = import_fs4.default.readdirSync(rootDir, { withFileTypes: true });
2110
+ if (!import_fs7.default.existsSync(rootDir)) return null;
2111
+ const entries = import_fs7.default.readdirSync(rootDir, { withFileTypes: true });
1259
2112
  for (const entry of entries) {
1260
- const full = import_path2.default.join(rootDir, entry.name);
2113
+ const full = import_path5.default.join(rootDir, entry.name);
1261
2114
  if (entry.isDirectory()) {
1262
2115
  const found = walkForFile(full, fileName);
1263
2116
  if (found) return found;
@@ -1351,39 +2204,39 @@ function resolveSeed(name, migrationConfig, options) {
1351
2204
  }
1352
2205
  function loadJson(filePath) {
1353
2206
  return __async(this, null, function* () {
1354
- const content = yield import_fs4.default.promises.readFile(filePath, "utf8");
2207
+ const content = yield import_fs7.default.promises.readFile(filePath, "utf8");
1355
2208
  return JSON.parse(content);
1356
2209
  });
1357
2210
  }
1358
2211
  function createValidator() {
1359
- const ajv = new import_ajv.default({ allErrors: true, strict: false });
1360
- (0, import_ajv_formats.default)(ajv);
1361
- return ajv;
2212
+ const ajv2 = new import_ajv2.default({ allErrors: true, strict: false });
2213
+ (0, import_ajv_formats.default)(ajv2);
2214
+ return ajv2;
1362
2215
  }
1363
2216
  function validateData(schemaPath, data, validate, log) {
1364
2217
  return __async(this, null, function* () {
1365
2218
  if (!validate || !schemaPath) return;
1366
- const content = yield import_fs4.default.promises.readFile(schemaPath, "utf8");
2219
+ const content = yield import_fs7.default.promises.readFile(schemaPath, "utf8");
1367
2220
  const schema = JSON.parse(content);
1368
- const ajv = createValidator();
1369
- const validateFn = ajv.compile(schema);
2221
+ const ajv2 = createValidator();
2222
+ const validateFn = ajv2.compile(schema);
1370
2223
  const ok = validateFn(data);
1371
2224
  if (!ok) {
1372
2225
  log == null ? void 0 : log(`Validation failed for seed data (${schemaPath})`);
1373
- throw new Error(`Seed data validation failed: ${ajv.errorsText(validateFn.errors || [])}`);
2226
+ throw new Error(`Seed data validation failed: ${ajv2.errorsText(validateFn.errors || [])}`);
1374
2227
  }
1375
2228
  });
1376
2229
  }
1377
2230
  function runSqlSeed(runner, resolved, direction) {
1378
2231
  return __async(this, null, function* () {
1379
2232
  const sqlPath = direction === "up" ? resolved.upPath : resolved.downPath;
1380
- const sql = yield import_fs4.default.promises.readFile(sqlPath, "utf8");
2233
+ const sql = yield import_fs7.default.promises.readFile(sqlPath, "utf8");
1381
2234
  yield runner.query(sql);
1382
2235
  });
1383
2236
  }
1384
2237
  function loadSeedModule(modulePath) {
1385
2238
  return __async(this, null, function* () {
1386
- const resolved = import_path2.default.resolve(modulePath);
2239
+ const resolved = import_path5.default.resolve(modulePath);
1387
2240
  if (resolved.endsWith(".ts")) {
1388
2241
  const fileUrl = (0, import_url.pathToFileURL)(resolved).href;
1389
2242
  return (0, import_api.tsImport)(fileUrl, fileUrl);
@@ -1504,13 +2357,23 @@ function createSeedFactory(options) {
1504
2357
  }
1505
2358
  // Annotate the CommonJS export names for ESM import in node:
1506
2359
  0 && (module.exports = {
2360
+ MigrationError,
1507
2361
  MigrationRunner,
1508
2362
  MigrationRunnerFactory,
2363
+ PatchConflictError,
2364
+ PatchCreator,
2365
+ PatchError,
2366
+ PatchExecutionError,
2367
+ PatchIntegrityError,
2368
+ PatchRunner,
2369
+ PatchValidationError,
1509
2370
  PgRunner,
1510
2371
  SQLRunner,
1511
2372
  SQLiteRunner,
1512
2373
  createSeedFactory,
1513
2374
  loadMigrationConfig,
2375
+ resolvePatchFolder,
2376
+ resolvePatchTable,
1514
2377
  runSeedsWithRunner
1515
2378
  });
1516
2379
  //# sourceMappingURL=index.js.map