@noego/proper 0.1.0 → 0.2.1

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