@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.mjs CHANGED
@@ -39,11 +39,10 @@ var __async = (__this, __arguments, generator) => {
39
39
  };
40
40
 
41
41
  // framework/MigrationRunner.ts
42
- import fs3 from "fs";
42
+ import fs6 from "fs";
43
43
 
44
44
  // framework/MigrationDirectoryReader.ts
45
- import fs from "fs";
46
- import path from "path";
45
+ import fs2 from "fs";
47
46
 
48
47
  // framework/errors.ts
49
48
  var MigrationError = class _MigrationError extends Error {
@@ -158,6 +157,54 @@ ${originalError.message}`;
158
157
  return new _MigrationExecutionError(message);
159
158
  }
160
159
  };
160
+ var PatchError = class _PatchError extends MigrationError {
161
+ constructor(message, patchFile, patchKey) {
162
+ super(`Patch Error: ${message}`);
163
+ this.patchFile = patchFile;
164
+ this.patchKey = patchKey;
165
+ this.name = "PatchError";
166
+ Object.setPrototypeOf(this, _PatchError.prototype);
167
+ }
168
+ };
169
+ var PatchValidationError = class _PatchValidationError extends PatchError {
170
+ constructor(message, patchFile, patchKey) {
171
+ super(message, patchFile, patchKey);
172
+ this.name = "PatchValidationError";
173
+ Object.setPrototypeOf(this, _PatchValidationError.prototype);
174
+ }
175
+ };
176
+ var PatchIntegrityError = class _PatchIntegrityError extends PatchError {
177
+ constructor(message, patchFile, patchKey, expectedChecksum, actualChecksum) {
178
+ super(message, patchFile, patchKey);
179
+ this.expectedChecksum = expectedChecksum;
180
+ this.actualChecksum = actualChecksum;
181
+ this.name = "PatchIntegrityError";
182
+ Object.setPrototypeOf(this, _PatchIntegrityError.prototype);
183
+ }
184
+ };
185
+ var PatchConflictError = class _PatchConflictError extends PatchError {
186
+ constructor(message, patchFile, patchKey, operationIndex, operationVerb, migrationKeys, observedRowCounts) {
187
+ super(message, patchFile, patchKey);
188
+ this.operationIndex = operationIndex;
189
+ this.operationVerb = operationVerb;
190
+ this.migrationKeys = migrationKeys;
191
+ this.observedRowCounts = observedRowCounts;
192
+ this.name = "PatchConflictError";
193
+ Object.setPrototypeOf(this, _PatchConflictError.prototype);
194
+ }
195
+ };
196
+ var PatchExecutionError = class _PatchExecutionError extends PatchError {
197
+ constructor(message, patchFile, patchKey, operationIndex, operationVerb, originalError) {
198
+ super(originalError ? `${message}
199
+ Original Error:
200
+ ${originalError.message}` : message, patchFile, patchKey);
201
+ this.operationIndex = operationIndex;
202
+ this.operationVerb = operationVerb;
203
+ this.originalError = originalError;
204
+ this.name = "PatchExecutionError";
205
+ Object.setPrototypeOf(this, _PatchExecutionError.prototype);
206
+ }
207
+ };
161
208
  var CLIError = class _CLIError extends MigrationError {
162
209
  constructor(message) {
163
210
  super(`CLI Error: ${message}`);
@@ -320,6 +367,40 @@ var SqlMigrationBuilder = class {
320
367
  }
321
368
  };
322
369
 
370
+ // framework/MigrationManifest.ts
371
+ import fs from "fs";
372
+ import path from "path";
373
+ var MIGRATION_FILE_REGEX = /(?:\.(mysql|sqlite|pg))?\.(up|down)\.(sql|js)$/i;
374
+ function isMigrationFile(fileName) {
375
+ return MIGRATION_FILE_REGEX.test(fileName);
376
+ }
377
+ function canonicalMigrationKey(value) {
378
+ return value.replace(MIGRATION_FILE_REGEX, "").toLowerCase();
379
+ }
380
+ function resolveMigrationFile(directory, baseName, direction, dialect) {
381
+ const dialectExt = dialect === "sql" ? "mysql" : dialect;
382
+ const dialectFile = path.join(directory, `${baseName}.${dialectExt}.${direction}.sql`);
383
+ if (fs.existsSync(dialectFile)) return dialectFile;
384
+ const genericFile = path.join(directory, `${baseName}.${direction}.sql`);
385
+ if (fs.existsSync(genericFile)) return genericFile;
386
+ return null;
387
+ }
388
+ function loadMigrationManifest(directory, dialect) {
389
+ const manifest = /* @__PURE__ */ new Map();
390
+ if (!fs.existsSync(directory)) return manifest;
391
+ const files = fs.readdirSync(directory, { withFileTypes: true }).filter((f) => f.isFile()).map((f) => f.name).filter(isMigrationFile);
392
+ const uniqueKeys = /* @__PURE__ */ new Set();
393
+ files.forEach((file) => uniqueKeys.add(canonicalMigrationKey(file)));
394
+ uniqueKeys.forEach((key) => {
395
+ manifest.set(key, {
396
+ key,
397
+ upFile: resolveMigrationFile(directory, key, "up", dialect),
398
+ downFile: resolveMigrationFile(directory, key, "down", dialect)
399
+ });
400
+ });
401
+ return manifest;
402
+ }
403
+
323
404
  // framework/MigrationDirectoryReader.ts
324
405
  var MigrationDirectoryReader = class {
325
406
  constructor(directory, read_strategy, sqlrunner, dialect = "sql") {
@@ -334,12 +415,7 @@ var MigrationDirectoryReader = class {
334
415
  * File extensions: `.mysql.up.sql`, `.sqlite.up.sql`, `.pg.up.sql`.
335
416
  */
336
417
  resolveFile(baseName, direction) {
337
- const dialectExt = this.dialect === "sql" ? "mysql" : this.dialect;
338
- const dialectFile = path.join(this.directory, `${baseName}.${dialectExt}.${direction}.sql`);
339
- if (fs.existsSync(dialectFile)) return dialectFile;
340
- const genericFile = path.join(this.directory, `${baseName}.${direction}.sql`);
341
- if (fs.existsSync(genericFile)) return genericFile;
342
- return null;
418
+ return resolveMigrationFile(this.directory, baseName, direction, this.dialect);
343
419
  }
344
420
  /**
345
421
  * Checks if a file path is dialect-specific (contains .mysql. or .sqlite. in the name)
@@ -348,11 +424,11 @@ var MigrationDirectoryReader = class {
348
424
  return /\.(mysql|sqlite|pg)\.(up|down)\.sql$/i.test(filePath);
349
425
  }
350
426
  loadMigrations(table, connection) {
351
- fs.existsSync(this.directory) || fs.mkdirSync(this.directory);
352
- const dir_content = fs.readdirSync(this.directory, { withFileTypes: true }).filter((file) => file.isFile()).map((file) => file.name);
427
+ fs2.existsSync(this.directory) || fs2.mkdirSync(this.directory);
428
+ const dir_content = fs2.readdirSync(this.directory, { withFileTypes: true }).filter((file) => file.isFile()).map((file) => file.name).filter(isMigrationFile);
353
429
  const uniqueKeys = /* @__PURE__ */ new Set();
354
430
  dir_content.forEach((file) => {
355
- const key = file.replace(/(?:\.(mysql|sqlite|pg))?\.(up|down)\.(sql|js)/i, "").toLowerCase();
431
+ const key = canonicalMigrationKey(file);
356
432
  uniqueKeys.add(key);
357
433
  });
358
434
  const migration_sorter = {};
@@ -392,14 +468,14 @@ var MigrationDirectoryReader = class {
392
468
  return builder;
393
469
  }
394
470
  sql_up(file) {
395
- let content = fs.readFileSync(file).toString();
471
+ let content = fs2.readFileSync(file).toString();
396
472
  if (!this.isDialectSpecific(file)) {
397
473
  content = this.read_strategy(content);
398
474
  }
399
475
  return content.trim();
400
476
  }
401
477
  sql_down(file) {
402
- let content = fs.readFileSync(file).toString();
478
+ let content = fs2.readFileSync(file).toString();
403
479
  if (!this.isDialectSpecific(file)) {
404
480
  content = this.read_strategy(content);
405
481
  }
@@ -408,7 +484,23 @@ var MigrationDirectoryReader = class {
408
484
  };
409
485
 
410
486
  // framework/MigrationSetup.ts
411
- import fs2 from "fs";
487
+ import fs3 from "fs";
488
+
489
+ // framework/PatchTypes.ts
490
+ import path2 from "path";
491
+ var PATCH_FORMAT_VERSION = 1;
492
+ var DEFAULT_PATCH_TABLE = "proper_patches";
493
+ var PATCH_FILENAME_REGEX = new RegExp("^(?<stamp>[0-9]{13})_(?<name>[a-z0-9][a-z0-9_-]{0,119})\\.yaml$");
494
+ function resolvePatchFolder(config) {
495
+ if (config.patch_folder) return config.patch_folder;
496
+ const dir = path2.dirname(config.migration_folder);
497
+ return dir === "." && !config.migration_folder.includes(path2.sep) && !config.migration_folder.includes("/") ? "patches" : path2.join(dir, "patches");
498
+ }
499
+ function resolvePatchTable(config) {
500
+ return config.patch_table || DEFAULT_PATCH_TABLE;
501
+ }
502
+
503
+ // framework/MigrationSetup.ts
412
504
  var MigrationSetup = class {
413
505
  constructor(sqlrunner, config) {
414
506
  this.sqlrunner = sqlrunner;
@@ -416,7 +508,7 @@ var MigrationSetup = class {
416
508
  }
417
509
  setup() {
418
510
  return __async(this, null, function* () {
419
- fs2.existsSync(this.config.migration_folder) || fs2.mkdirSync(this.config.migration_folder);
511
+ fs3.existsSync(this.config.migration_folder) || fs3.mkdirSync(this.config.migration_folder);
420
512
  const tableName = this.config.migration_table;
421
513
  const createTableSql = this.config.database === "sqlite" ? `CREATE TABLE IF NOT EXISTS ${tableName} (
422
514
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -438,11 +530,39 @@ var MigrationSetup = class {
438
530
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
439
531
  )`;
440
532
  yield this.sqlrunner.query(createTableSql);
533
+ const patchTable = resolvePatchTable(this.config);
534
+ const createPatchTableSql = this.config.database === "sqlite" ? `CREATE TABLE IF NOT EXISTS ${patchTable} (
535
+ migration_table TEXT NOT NULL,
536
+ patch_key TEXT NOT NULL,
537
+ checksum TEXT NOT NULL,
538
+ format_version INTEGER NOT NULL,
539
+ description TEXT NOT NULL,
540
+ applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
541
+ PRIMARY KEY (migration_table, patch_key)
542
+ )` : this.config.database === "pg" ? `CREATE TABLE IF NOT EXISTS ${patchTable} (
543
+ migration_table TEXT NOT NULL,
544
+ patch_key TEXT NOT NULL,
545
+ checksum TEXT NOT NULL,
546
+ format_version INTEGER NOT NULL,
547
+ description TEXT NOT NULL,
548
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT now(),
549
+ PRIMARY KEY (migration_table, patch_key)
550
+ )` : `CREATE TABLE IF NOT EXISTS ${patchTable} (
551
+ migration_table VARCHAR(255) NOT NULL,
552
+ patch_key VARCHAR(255) NOT NULL,
553
+ checksum CHAR(64) NOT NULL,
554
+ format_version INT NOT NULL,
555
+ description TEXT NOT NULL,
556
+ applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
557
+ PRIMARY KEY (migration_table, patch_key)
558
+ )`;
559
+ yield this.sqlrunner.query(createPatchTableSql);
441
560
  });
442
561
  }
443
562
  teardown() {
444
563
  return __async(this, null, function* () {
445
564
  yield this.sqlrunner.execute(`DROP TABLE ${this.config.migration_table}`);
565
+ yield this.sqlrunner.execute(`DROP TABLE IF EXISTS ${resolvePatchTable(this.config)}`);
446
566
  });
447
567
  }
448
568
  };
@@ -519,6 +639,673 @@ function markerDialectParser(names) {
519
639
  var SqliteDialectParser = markerDialectParser(["sqlite"]);
520
640
  var PgDialectParser = markerDialectParser(["pg", "postgres", "postgresql"]);
521
641
 
642
+ // framework/PatchDirectoryReader.ts
643
+ import crypto from "crypto";
644
+ import fs4 from "fs";
645
+ import path3 from "path";
646
+
647
+ // framework/PatchValidator.ts
648
+ import Ajv from "ajv";
649
+ import { parseDocument } from "yaml";
650
+ var MAX_DESCRIPTION_LENGTH = 500;
651
+ var MAX_MIGRATION_KEY_LENGTH = 255;
652
+ var migrationKeySchema = {
653
+ type: "string",
654
+ minLength: 1,
655
+ maxLength: MAX_MIGRATION_KEY_LENGTH
656
+ };
657
+ var patchSchema = {
658
+ type: "object",
659
+ additionalProperties: false,
660
+ required: ["version", "description", "operations"],
661
+ properties: {
662
+ version: { type: "integer" },
663
+ description: { type: "string" },
664
+ operations: {
665
+ type: "array",
666
+ minItems: 1,
667
+ items: {
668
+ type: "object",
669
+ additionalProperties: false,
670
+ minProperties: 1,
671
+ maxProperties: 1,
672
+ properties: {
673
+ rename_migration: {
674
+ type: "object",
675
+ additionalProperties: false,
676
+ required: ["from", "to"],
677
+ properties: { from: migrationKeySchema, to: migrationKeySchema }
678
+ },
679
+ mark_applied: {
680
+ type: "object",
681
+ additionalProperties: false,
682
+ required: ["key"],
683
+ properties: { key: migrationKeySchema }
684
+ },
685
+ unmark_applied: {
686
+ type: "object",
687
+ additionalProperties: false,
688
+ required: ["key"],
689
+ properties: { key: migrationKeySchema }
690
+ }
691
+ }
692
+ }
693
+ }
694
+ }
695
+ };
696
+ var ajv = new Ajv({ allErrors: true, strict: true });
697
+ var validateSchema = ajv.compile(patchSchema);
698
+ function fail(message, file, patchKey) {
699
+ throw new PatchValidationError(message, file, patchKey);
700
+ }
701
+ function checkMigrationKey(value, context, file, patchKey) {
702
+ if (value !== value.trim()) fail(`${context}: migration key has leading/trailing whitespace`, file, patchKey);
703
+ if (/[/\\]/.test(value)) fail(`${context}: migration key contains a path separator`, file, patchKey);
704
+ if (/[\x00-\x1f\x7f]/.test(value)) fail(`${context}: migration key contains control characters`, file, patchKey);
705
+ if (value.length === 0 || value.length > MAX_MIGRATION_KEY_LENGTH) {
706
+ fail(`${context}: migration key length out of bounds`, file, patchKey);
707
+ }
708
+ return canonicalMigrationKey(value);
709
+ }
710
+ function assertStrictYaml(doc, file, patchKey) {
711
+ if (doc.errors.length > 0) {
712
+ fail(`YAML parse error: ${doc.errors[0].message}`, file, patchKey);
713
+ }
714
+ if (doc.warnings.length > 0) {
715
+ fail(`YAML warning treated as error: ${doc.warnings[0].message}`, file, patchKey);
716
+ }
717
+ const visit = (node) => {
718
+ var _a, _b;
719
+ if (node == null || typeof node !== "object") return;
720
+ if ("source" in node && ((_a = node.constructor) == null ? void 0 : _a.name) === "Alias") {
721
+ fail("YAML aliases are not permitted in patch files", file, patchKey);
722
+ }
723
+ if (node.anchor) {
724
+ fail("YAML anchors are not permitted in patch files", file, patchKey);
725
+ }
726
+ if (node.tag && ![
727
+ "tag:yaml.org,2002:str",
728
+ "tag:yaml.org,2002:int",
729
+ "tag:yaml.org,2002:bool",
730
+ "tag:yaml.org,2002:null",
731
+ "tag:yaml.org,2002:map",
732
+ "tag:yaml.org,2002:seq"
733
+ ].includes(node.tag)) {
734
+ fail(`YAML tag '${node.tag}' is not permitted in patch files`, file, patchKey);
735
+ }
736
+ if (Array.isArray(node.items)) {
737
+ for (const item of node.items) {
738
+ if (item && typeof item === "object" && "key" in item) {
739
+ const keyValue = (_b = item.key) == null ? void 0 : _b.value;
740
+ if (keyValue === "<<") fail("YAML merge keys are not permitted in patch files", file, patchKey);
741
+ visit(item.key);
742
+ visit(item.value);
743
+ } else {
744
+ visit(item);
745
+ }
746
+ }
747
+ }
748
+ };
749
+ visit(doc.contents);
750
+ }
751
+ function parsePatchContent(content, fileName, patchKey) {
752
+ var _a;
753
+ const doc = parseDocument(content, {
754
+ uniqueKeys: true,
755
+ // duplicate mapping keys become errors
756
+ merge: false,
757
+ schema: "core",
758
+ version: "1.2"
759
+ });
760
+ assertStrictYaml(doc, fileName, patchKey);
761
+ const raw = doc.toJS({ mapAsMap: false });
762
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
763
+ fail("Patch document must be a YAML mapping", fileName, patchKey);
764
+ }
765
+ if (!validateSchema(raw)) {
766
+ const detail = ((_a = validateSchema.errors) != null ? _a : []).map((e) => `${e.instancePath || "/"} ${e.message}`).join("; ");
767
+ const anyRaw = raw;
768
+ if (typeof anyRaw.version === "number" && anyRaw.version !== PATCH_FORMAT_VERSION) {
769
+ fail(`Unknown patch format version: ${anyRaw.version} (supported: ${PATCH_FORMAT_VERSION})`, fileName, patchKey);
770
+ }
771
+ fail(`Schema validation failed: ${detail}`, fileName, patchKey);
772
+ }
773
+ const parsed = raw;
774
+ if (parsed.version !== PATCH_FORMAT_VERSION) {
775
+ fail(`Unknown patch format version: ${parsed.version} (supported: ${PATCH_FORMAT_VERSION})`, fileName, patchKey);
776
+ }
777
+ const description = parsed.description.trim();
778
+ if (description.length === 0) fail("description must be non-empty", fileName, patchKey);
779
+ if (description.length > MAX_DESCRIPTION_LENGTH) {
780
+ fail(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters`, fileName, patchKey);
781
+ }
782
+ const operations = parsed.operations.map((op, index) => {
783
+ const verbs = Object.keys(op);
784
+ const verb = verbs[0];
785
+ const context = `operation ${index} (${verb})`;
786
+ switch (verb) {
787
+ case "rename_migration": {
788
+ const from = checkMigrationKey(op.rename_migration.from, context, fileName, patchKey);
789
+ const to = checkMigrationKey(op.rename_migration.to, context, fileName, patchKey);
790
+ if (from === to) {
791
+ fail(`${context}: 'from' and 'to' are identical after canonicalization ('${from}')`, fileName, patchKey);
792
+ }
793
+ return { verb: "rename_migration", from, to };
794
+ }
795
+ case "mark_applied":
796
+ return { verb: "mark_applied", key: checkMigrationKey(op.mark_applied.key, context, fileName, patchKey) };
797
+ case "unmark_applied":
798
+ return { verb: "unmark_applied", key: checkMigrationKey(op.unmark_applied.key, context, fileName, patchKey) };
799
+ default:
800
+ fail(`operation ${index}: unknown verb '${verb}'`, fileName, patchKey);
801
+ }
802
+ });
803
+ return { version: parsed.version, description, operations };
804
+ }
805
+ function validatePatchPlan(patches, manifest) {
806
+ const renames = [];
807
+ for (const patch of patches) {
808
+ patch.operations.forEach((op, index) => {
809
+ if (op.verb === "rename_migration") {
810
+ renames.push({ from: op.from, to: op.to, file: patch.fileName });
811
+ } else if (op.verb === "mark_applied") {
812
+ if (!manifest.has(op.key)) {
813
+ throw new PatchValidationError(
814
+ `operation ${index} (mark_applied): key '${op.key}' is not present in the current migration manifest`,
815
+ patch.fileName,
816
+ patch.patchKey
817
+ );
818
+ }
819
+ }
820
+ });
821
+ }
822
+ if (renames.length === 0) return;
823
+ const mentioned = /* @__PURE__ */ new Set();
824
+ renames.forEach((r) => {
825
+ mentioned.add(r.from);
826
+ mentioned.add(r.to);
827
+ });
828
+ const finalKey = (start) => {
829
+ let current = start;
830
+ for (const r of renames) {
831
+ if (current === r.from) current = r.to;
832
+ }
833
+ return current;
834
+ };
835
+ for (const key of mentioned) {
836
+ const finish = finalKey(key);
837
+ if (manifest.has(key)) {
838
+ if (finish !== key) {
839
+ throw new PatchValidationError(
840
+ `rename plan moves current migration '${key}' to '${finish}', which would make its file incorrectly pending`
841
+ );
842
+ }
843
+ } else {
844
+ if (!manifest.has(finish)) {
845
+ throw new PatchValidationError(
846
+ `rename plan leaves historical key '${key}' at '${finish}', which is not present in the current migration manifest`
847
+ );
848
+ }
849
+ }
850
+ }
851
+ }
852
+
853
+ // framework/PatchDirectoryReader.ts
854
+ var PatchDirectoryReader = class {
855
+ constructor(directory) {
856
+ this.directory = directory;
857
+ }
858
+ loadPatches() {
859
+ if (!fs4.existsSync(this.directory)) return [];
860
+ const entries = fs4.readdirSync(this.directory, { withFileTypes: true });
861
+ const patchFiles = [];
862
+ for (const entry of entries) {
863
+ if (!entry.name.endsWith(".yaml")) continue;
864
+ if (!entry.isFile() || entry.isSymbolicLink()) {
865
+ if (entry.isSymbolicLink()) {
866
+ throw new PatchValidationError(`patch file must be a regular file, not a symlink`, entry.name);
867
+ }
868
+ continue;
869
+ }
870
+ patchFiles.push(entry.name);
871
+ }
872
+ patchFiles.sort((a, b) => {
873
+ const stampA = parseInt(a.slice(0, 13), 10);
874
+ const stampB = parseInt(b.slice(0, 13), 10);
875
+ if (!Number.isNaN(stampA) && !Number.isNaN(stampB) && stampA !== stampB) {
876
+ return stampA - stampB;
877
+ }
878
+ return a < b ? -1 : a > b ? 1 : 0;
879
+ });
880
+ return patchFiles.map((fileName) => {
881
+ const match = PATCH_FILENAME_REGEX.exec(fileName);
882
+ if (!match) {
883
+ throw new PatchValidationError(
884
+ `invalid patch filename (expected <13-digit-stamp>_<name>.yaml with name matching [a-z0-9][a-z0-9_-]{0,119})`,
885
+ fileName
886
+ );
887
+ }
888
+ const patchKey = fileName.slice(0, -".yaml".length);
889
+ const filePath = path3.join(this.directory, fileName);
890
+ const bytes = fs4.readFileSync(filePath);
891
+ const checksum = crypto.createHash("sha256").update(bytes).digest("hex");
892
+ const content = bytes.toString("utf8");
893
+ const { version, description, operations } = parsePatchContent(content, fileName, patchKey);
894
+ return { patchKey, fileName, filePath, checksum, version, description, operations };
895
+ });
896
+ }
897
+ };
898
+
899
+ // framework/PatchRunner.ts
900
+ function toError(error) {
901
+ if (error instanceof Error) return error;
902
+ return new Error(String(error));
903
+ }
904
+ function extractRows(result) {
905
+ if (!Array.isArray(result)) return [];
906
+ if (Array.isArray(result[0])) return result[0];
907
+ if (result.length === 2 && result[0] && typeof result[0] === "object" && result[1] && typeof result[1] === "object" && !("rows" in result[1])) {
908
+ return result;
909
+ }
910
+ if (result[0] == null) return [];
911
+ return [result[0]];
912
+ }
913
+ var PatchRunner = class {
914
+ constructor(sqlrunner, config) {
915
+ this.sqlrunner = sqlrunner;
916
+ this.config = config;
917
+ this.patchTable = resolvePatchTable(config);
918
+ this.migrationTable = config.migration_table;
919
+ this.dialect = config.database;
920
+ }
921
+ /**
922
+ * Discovers, validates, and applies every unapplied patch in order.
923
+ * Each unapplied patch is its own transaction; earlier committed patches
924
+ * remain committed if a later patch fails.
925
+ */
926
+ applyPending() {
927
+ return __async(this, null, function* () {
928
+ const reader = new PatchDirectoryReader(resolvePatchFolder(this.config));
929
+ const patches = reader.loadPatches();
930
+ const history = yield this.loadHistory();
931
+ if (patches.length === 0 && history.length === 0) {
932
+ return [];
933
+ }
934
+ const byKey = new Map(patches.map((p) => [p.patchKey, p]));
935
+ for (const row of history) {
936
+ const file = byKey.get(row.patch_key);
937
+ if (!file) {
938
+ throw new PatchIntegrityError(
939
+ `applied patch '${row.patch_key}' has no corresponding file in the patch folder; patch files are permanent and must never be renamed or deleted`,
940
+ void 0,
941
+ row.patch_key
942
+ );
943
+ }
944
+ if (file.checksum !== row.checksum) {
945
+ throw new PatchIntegrityError(
946
+ `applied patch '${row.patch_key}' content changed after application; patch files are immutable once recorded`,
947
+ file.fileName,
948
+ row.patch_key,
949
+ row.checksum,
950
+ file.checksum
951
+ );
952
+ }
953
+ }
954
+ const manifest = loadMigrationManifest(this.config.migration_folder, this.dialect);
955
+ validatePatchPlan(patches, manifest);
956
+ const appliedKeys = new Set(history.map((r) => r.patch_key));
957
+ const results = [];
958
+ for (const patch of patches) {
959
+ if (appliedKeys.has(patch.patchKey)) {
960
+ results.push({
961
+ patchKey: patch.patchKey,
962
+ fileName: patch.fileName,
963
+ status: "already_applied",
964
+ operations: []
965
+ });
966
+ continue;
967
+ }
968
+ results.push(yield this.applyOne(patch, manifest));
969
+ }
970
+ return results;
971
+ });
972
+ }
973
+ loadHistory() {
974
+ return __async(this, null, function* () {
975
+ try {
976
+ const result = yield this.sqlrunner.query(
977
+ `SELECT patch_key, checksum FROM ${this.patchTable} WHERE migration_table = ?`,
978
+ [this.migrationTable]
979
+ );
980
+ return extractRows(result);
981
+ } catch (error) {
982
+ throw new PatchExecutionError(
983
+ `failed to read patch history from '${this.patchTable}'`,
984
+ void 0,
985
+ void 0,
986
+ void 0,
987
+ void 0,
988
+ toError(error)
989
+ );
990
+ }
991
+ });
992
+ }
993
+ beginSql() {
994
+ switch (this.dialect) {
995
+ case "sqlite":
996
+ return "BEGIN IMMEDIATE";
997
+ case "pg":
998
+ return "BEGIN";
999
+ default:
1000
+ return "START TRANSACTION";
1001
+ }
1002
+ }
1003
+ begin(patch) {
1004
+ return __async(this, null, function* () {
1005
+ const deadline = Date.now() + 1e4;
1006
+ while (true) {
1007
+ try {
1008
+ yield this.sqlrunner.execute(this.beginSql());
1009
+ return;
1010
+ } catch (error) {
1011
+ const message = toError(error).message;
1012
+ if (/SQLITE_BUSY|database is locked/i.test(message) && Date.now() < deadline) {
1013
+ yield new Promise((resolve) => setTimeout(resolve, 50));
1014
+ continue;
1015
+ }
1016
+ throw new PatchExecutionError(
1017
+ "failed to start patch transaction",
1018
+ patch.fileName,
1019
+ patch.patchKey,
1020
+ void 0,
1021
+ void 0,
1022
+ toError(error)
1023
+ );
1024
+ }
1025
+ }
1026
+ });
1027
+ }
1028
+ rollbackQuietly() {
1029
+ return __async(this, null, function* () {
1030
+ try {
1031
+ yield this.sqlrunner.execute("ROLLBACK");
1032
+ } catch (e) {
1033
+ }
1034
+ });
1035
+ }
1036
+ applyOne(patch, manifest) {
1037
+ return __async(this, null, function* () {
1038
+ yield this.begin(patch);
1039
+ try {
1040
+ yield this.sqlrunner.execute(
1041
+ `INSERT INTO ${this.patchTable} (migration_table, patch_key, checksum, format_version, description)
1042
+ VALUES (?, ?, ?, ?, ?)`,
1043
+ [this.migrationTable, patch.patchKey, patch.checksum, patch.version, patch.description]
1044
+ );
1045
+ } catch (claimError) {
1046
+ yield this.rollbackQuietly();
1047
+ const committed = yield this.findCommittedRow(patch.patchKey);
1048
+ if (committed) {
1049
+ if (committed.checksum === patch.checksum) {
1050
+ return {
1051
+ patchKey: patch.patchKey,
1052
+ fileName: patch.fileName,
1053
+ status: "already_applied",
1054
+ operations: []
1055
+ };
1056
+ }
1057
+ throw new PatchIntegrityError(
1058
+ `patch '${patch.patchKey}' was applied elsewhere with a different checksum`,
1059
+ patch.fileName,
1060
+ patch.patchKey,
1061
+ committed.checksum,
1062
+ patch.checksum
1063
+ );
1064
+ }
1065
+ throw new PatchExecutionError(
1066
+ "failed to claim patch-history row",
1067
+ patch.fileName,
1068
+ patch.patchKey,
1069
+ void 0,
1070
+ void 0,
1071
+ toError(claimError)
1072
+ );
1073
+ }
1074
+ const operationResults = [];
1075
+ try {
1076
+ for (let index = 0; index < patch.operations.length; index++) {
1077
+ operationResults.push(
1078
+ yield this.applyOperation(patch, patch.operations[index], index, manifest)
1079
+ );
1080
+ }
1081
+ yield this.sqlrunner.execute("COMMIT");
1082
+ } catch (error) {
1083
+ yield this.rollbackQuietly();
1084
+ if (error instanceof PatchConflictError || error instanceof PatchExecutionError || error instanceof PatchIntegrityError) {
1085
+ throw error;
1086
+ }
1087
+ throw new PatchExecutionError(
1088
+ "patch application failed",
1089
+ patch.fileName,
1090
+ patch.patchKey,
1091
+ void 0,
1092
+ void 0,
1093
+ toError(error)
1094
+ );
1095
+ }
1096
+ return {
1097
+ patchKey: patch.patchKey,
1098
+ fileName: patch.fileName,
1099
+ status: "applied",
1100
+ operations: operationResults
1101
+ };
1102
+ });
1103
+ }
1104
+ findCommittedRow(patchKey) {
1105
+ return __async(this, null, function* () {
1106
+ const result = yield this.sqlrunner.query(
1107
+ `SELECT patch_key, checksum FROM ${this.patchTable} WHERE migration_table = ? AND patch_key = ?`,
1108
+ [this.migrationTable, patchKey]
1109
+ );
1110
+ const list = extractRows(result);
1111
+ return list.length > 0 ? list[0] : null;
1112
+ });
1113
+ }
1114
+ countRows(key) {
1115
+ return __async(this, null, function* () {
1116
+ var _a, _b, _c;
1117
+ const result = yield this.sqlrunner.query(
1118
+ `SELECT COUNT(*) AS row_count FROM ${this.migrationTable} WHERE migration_key = ?`,
1119
+ [key]
1120
+ );
1121
+ const rows = extractRows(result);
1122
+ const value = (_c = (_a = rows[0]) == null ? void 0 : _a.row_count) != null ? _c : Object.values((_b = rows[0]) != null ? _b : {})[0];
1123
+ return Number(value != null ? value : 0);
1124
+ });
1125
+ }
1126
+ conflict(patch, index, verb, message, keys, counts) {
1127
+ throw new PatchConflictError(
1128
+ `operation ${index} (${verb}): ${message}`,
1129
+ patch.fileName,
1130
+ patch.patchKey,
1131
+ index,
1132
+ verb,
1133
+ keys,
1134
+ counts
1135
+ );
1136
+ }
1137
+ applyOperation(patch, op, index, manifest) {
1138
+ return __async(this, null, function* () {
1139
+ var _a, _b, _c, _d;
1140
+ try {
1141
+ switch (op.verb) {
1142
+ case "rename_migration": {
1143
+ const fromCount = yield this.countRows(op.from);
1144
+ const toCount = yield this.countRows(op.to);
1145
+ const counts = { [op.from]: fromCount, [op.to]: toCount };
1146
+ if (fromCount > 1 || toCount > 1) {
1147
+ this.conflict(
1148
+ patch,
1149
+ index,
1150
+ op.verb,
1151
+ `ledger corruption: duplicate rows for a migration key`,
1152
+ [op.from, op.to],
1153
+ counts
1154
+ );
1155
+ }
1156
+ if (fromCount === 1 && toCount === 1) {
1157
+ this.conflict(
1158
+ patch,
1159
+ index,
1160
+ op.verb,
1161
+ `both '${op.from}' and '${op.to}' exist in the ledger`,
1162
+ [op.from, op.to],
1163
+ counts
1164
+ );
1165
+ }
1166
+ if (fromCount === 0) {
1167
+ return { verb: op.verb, changed: false };
1168
+ }
1169
+ const target = manifest.get(op.to);
1170
+ if (target) {
1171
+ yield this.sqlrunner.execute(
1172
+ `UPDATE ${this.migrationTable} SET migration_key = ?, up = ?, down = ? WHERE migration_key = ?`,
1173
+ [op.to, (_a = target.upFile) != null ? _a : "", (_b = target.downFile) != null ? _b : "", op.from]
1174
+ );
1175
+ } else {
1176
+ yield this.sqlrunner.execute(
1177
+ `UPDATE ${this.migrationTable} SET migration_key = ? WHERE migration_key = ?`,
1178
+ [op.to, op.from]
1179
+ );
1180
+ }
1181
+ return { verb: op.verb, changed: true };
1182
+ }
1183
+ case "mark_applied": {
1184
+ const count = yield this.countRows(op.key);
1185
+ if (count > 1) {
1186
+ this.conflict(
1187
+ patch,
1188
+ index,
1189
+ op.verb,
1190
+ `ledger corruption: duplicate rows for '${op.key}'`,
1191
+ [op.key],
1192
+ { [op.key]: count }
1193
+ );
1194
+ }
1195
+ if (count === 1) {
1196
+ return { verb: op.verb, changed: false };
1197
+ }
1198
+ const entry = manifest.get(op.key);
1199
+ yield this.sqlrunner.execute(
1200
+ `INSERT INTO ${this.migrationTable} (migration_key, up, down) VALUES (?, ?, ?)`,
1201
+ [op.key, (_c = entry == null ? void 0 : entry.upFile) != null ? _c : "", (_d = entry == null ? void 0 : entry.downFile) != null ? _d : ""]
1202
+ );
1203
+ return { verb: op.verb, changed: true };
1204
+ }
1205
+ case "unmark_applied": {
1206
+ const count = yield this.countRows(op.key);
1207
+ if (count > 1) {
1208
+ this.conflict(
1209
+ patch,
1210
+ index,
1211
+ op.verb,
1212
+ `ledger corruption: duplicate rows for '${op.key}'`,
1213
+ [op.key],
1214
+ { [op.key]: count }
1215
+ );
1216
+ }
1217
+ if (count === 0) {
1218
+ return { verb: op.verb, changed: false };
1219
+ }
1220
+ yield this.sqlrunner.execute(
1221
+ `DELETE FROM ${this.migrationTable} WHERE migration_key = ?`,
1222
+ [op.key]
1223
+ );
1224
+ return { verb: op.verb, changed: true };
1225
+ }
1226
+ }
1227
+ } catch (error) {
1228
+ if (error instanceof PatchConflictError) throw error;
1229
+ throw new PatchExecutionError(
1230
+ `operation failed`,
1231
+ patch.fileName,
1232
+ patch.patchKey,
1233
+ index,
1234
+ op.verb,
1235
+ toError(error)
1236
+ );
1237
+ }
1238
+ });
1239
+ }
1240
+ };
1241
+
1242
+ // framework/PatchCreator.ts
1243
+ import fs5 from "fs";
1244
+ import path4 from "path";
1245
+ var MAX_NAME_LENGTH = 120;
1246
+ var SCAFFOLD = `version: 1
1247
+ description: TODO
1248
+ operations: []
1249
+ `;
1250
+ var PatchCreator = class _PatchCreator {
1251
+ constructor(patchFolder) {
1252
+ this.patchFolder = patchFolder;
1253
+ }
1254
+ /**
1255
+ * Normalizes a patch name: trim, whitespace runs -> `_`, lowercase.
1256
+ * Rejects empty results, path separators, `..`, control characters,
1257
+ * characters outside [a-z0-9_-], and names longer than 120 characters.
1258
+ */
1259
+ static normalizeName(name) {
1260
+ const normalized = (name != null ? name : "").trim().replace(/\s+/g, "_").toLowerCase();
1261
+ if (normalized.length === 0) {
1262
+ throw new CLIError("Patch name is required");
1263
+ }
1264
+ if (normalized.includes("/") || normalized.includes("\\")) {
1265
+ throw new CLIError("Patch name must not contain path separators");
1266
+ }
1267
+ if (normalized.includes("..")) {
1268
+ throw new CLIError("Patch name must not contain '..'");
1269
+ }
1270
+ if (/[\x00-\x1f\x7f]/.test(normalized)) {
1271
+ throw new CLIError("Patch name must not contain control characters");
1272
+ }
1273
+ if (!/^[a-z0-9_-]+$/.test(normalized)) {
1274
+ throw new CLIError("Patch name may only contain characters [a-z0-9_-]");
1275
+ }
1276
+ if (normalized.length > MAX_NAME_LENGTH) {
1277
+ throw new CLIError(`Patch name exceeds ${MAX_NAME_LENGTH} characters after normalization`);
1278
+ }
1279
+ return normalized;
1280
+ }
1281
+ /**
1282
+ * Creates `<patch_folder>/<stamp>_<normalized_name>.yaml` with exclusive
1283
+ * file creation. On a millisecond-stamp collision, mints a later stamp
1284
+ * and retries. Returns the created path.
1285
+ */
1286
+ create(name) {
1287
+ const normalized = _PatchCreator.normalizeName(name);
1288
+ if (!fs5.existsSync(this.patchFolder)) {
1289
+ fs5.mkdirSync(this.patchFolder, { recursive: true });
1290
+ }
1291
+ let stamp = Date.now();
1292
+ for (let attempt = 0; attempt < 1e3; attempt++) {
1293
+ const filePath = path4.join(this.patchFolder, `${stamp}_${normalized}.yaml`);
1294
+ try {
1295
+ fs5.writeFileSync(filePath, SCAFFOLD, { flag: "wx" });
1296
+ return filePath;
1297
+ } catch (error) {
1298
+ if (error && error.code === "EEXIST") {
1299
+ stamp += 1;
1300
+ continue;
1301
+ }
1302
+ throw error;
1303
+ }
1304
+ }
1305
+ throw new CLIError("Unable to create patch file: too many filename collisions");
1306
+ }
1307
+ };
1308
+
522
1309
  // framework/SQLRunner.ts
523
1310
  var BaseSQLRunner = class {
524
1311
  /**
@@ -833,7 +1620,7 @@ var PgRunner = class _PgRunner extends BaseSQLRunner {
833
1620
  };
834
1621
 
835
1622
  // framework/MigrationRunner.ts
836
- function toError(error) {
1623
+ function toError2(error) {
837
1624
  if (error instanceof Error) return error;
838
1625
  return new Error(String(error));
839
1626
  }
@@ -861,10 +1648,12 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
861
1648
  return __async(this, null, function* () {
862
1649
  const configReader = new FileMigrationConfigReader(configFile);
863
1650
  const config = configReader.loadFile();
1651
+ let factoryOwnsConnection = false;
864
1652
  if (!conn) {
865
1653
  conn = yield this.createConnection(config);
1654
+ factoryOwnsConnection = true;
866
1655
  }
867
- return new _MigrationRunnerFactory().create(config, conn);
1656
+ return new _MigrationRunnerFactory().create(config, conn, factoryOwnsConnection);
868
1657
  });
869
1658
  }
870
1659
  static createConnection(config) {
@@ -884,7 +1673,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
884
1673
  conn = yield ((_b = (_a = mysql.default) == null ? void 0 : _a.createConnection) != null ? _b : mysql.createConnection)(settings);
885
1674
  return conn;
886
1675
  } catch (error) {
887
- throw DatabaseConnectionError.connectionFailed("sql", toError(error).message);
1676
+ throw DatabaseConnectionError.connectionFailed("sql", toError2(error).message);
888
1677
  }
889
1678
  case "sqlite":
890
1679
  if (!config.sqlite) {
@@ -899,7 +1688,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
899
1688
  });
900
1689
  return conn;
901
1690
  } catch (error) {
902
- throw DatabaseConnectionError.connectionFailed("sqlite", toError(error).message);
1691
+ throw DatabaseConnectionError.connectionFailed("sqlite", toError2(error).message);
903
1692
  }
904
1693
  case "pg":
905
1694
  if (!config.pg && !process.env.DATABASE_URL) {
@@ -915,7 +1704,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
915
1704
  yield conn.connect();
916
1705
  return conn;
917
1706
  } catch (error) {
918
- throw DatabaseConnectionError.connectionFailed("pg", toError(error).message);
1707
+ throw DatabaseConnectionError.connectionFailed("pg", toError2(error).message);
919
1708
  }
920
1709
  default:
921
1710
  throw ConfigurationError.unknownDatabaseType(config.database);
@@ -929,7 +1718,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
929
1718
  return new _MigrationRunnerFactory().createEmpty(config);
930
1719
  });
931
1720
  }
932
- create(config, conn) {
1721
+ create(config, conn, factoryOwnsConnection = false) {
933
1722
  return __async(this, null, function* () {
934
1723
  let sqlrunner;
935
1724
  let driverConnection = conn;
@@ -942,8 +1731,19 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
942
1731
  const setup = new MigrationSetup(sqlrunner, config);
943
1732
  const read_strategy = this.getReadStategy(config);
944
1733
  const migration_files = new MigrationDirectoryReader(config.migration_folder, read_strategy, sqlrunner, config.database);
945
- yield setup.setup();
946
- return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, driverConnection);
1734
+ const runner = new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, driverConnection);
1735
+ try {
1736
+ yield runner.setup();
1737
+ } catch (error) {
1738
+ if (factoryOwnsConnection) {
1739
+ try {
1740
+ yield sqlrunner.end();
1741
+ } catch (e) {
1742
+ }
1743
+ }
1744
+ throw error;
1745
+ }
1746
+ return runner;
947
1747
  });
948
1748
  }
949
1749
  createEmpty(config) {
@@ -953,7 +1753,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
953
1753
  const setup = new MigrationSetup(sqlrunner, config);
954
1754
  const read_strategy = this.getReadStategy(config);
955
1755
  const migration_files = new MigrationDirectoryReader(config.migration_folder, read_strategy, sqlrunner, config.database);
956
- return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, conn);
1756
+ return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, conn, false);
957
1757
  });
958
1758
  }
959
1759
  getReadStategy(config) {
@@ -970,33 +1770,77 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
970
1770
  }
971
1771
  };
972
1772
  var MySQLMigrationRunner = class {
973
- constructor(config, directory, setupRunner, sqlrunner, connection) {
1773
+ constructor(config, directory, setupRunner, sqlrunner, connection, preflightEnabled = true) {
974
1774
  this.config = config;
975
1775
  this.directory = directory;
976
1776
  this.setupRunner = setupRunner;
977
1777
  this.sqlrunner = sqlrunner;
978
1778
  this.connection = connection;
1779
+ this.preflightEnabled = preflightEnabled;
1780
+ /**
1781
+ * Memoized in-flight preflight promise. Simultaneous or repeated calls
1782
+ * to setup() on one runner execute the preflight (migration table setup
1783
+ * + patch application) exactly once. Cleared after rejection so a caller
1784
+ * may retry after fixing the cause.
1785
+ */
1786
+ this.preflightPromise = null;
1787
+ this.lastPatchResults = [];
979
1788
  }
980
1789
  setup() {
1790
+ return __async(this, null, function* () {
1791
+ if (!this.preflightEnabled) {
1792
+ return;
1793
+ }
1794
+ if (!this.preflightPromise) {
1795
+ this.preflightPromise = this.runPreflight();
1796
+ this.preflightPromise.catch(() => {
1797
+ this.preflightPromise = null;
1798
+ });
1799
+ }
1800
+ return this.preflightPromise;
1801
+ });
1802
+ }
1803
+ runPreflight() {
981
1804
  return __async(this, null, function* () {
982
1805
  try {
983
1806
  yield this.setupRunner.setup();
984
1807
  } catch (error) {
985
- throw new MigrationExecutionError("Failed to set up migration database", void 0, void 0, toError(error));
1808
+ throw new MigrationExecutionError("Failed to set up migration database", void 0, void 0, toError2(error));
986
1809
  }
1810
+ const patchRunner = new PatchRunner(this.sqlrunner, this.config);
1811
+ this.lastPatchResults = yield patchRunner.applyPending();
1812
+ });
1813
+ }
1814
+ /**
1815
+ * Delegates to the same idempotent preflight; returns the results of the
1816
+ * patch pass that ran (or is running) for this runner.
1817
+ */
1818
+ applyPendingPatches() {
1819
+ return __async(this, null, function* () {
1820
+ yield this.setup();
1821
+ return this.lastPatchResults;
987
1822
  });
988
1823
  }
1824
+ /**
1825
+ * Scaffolds a new ledger patch file and returns the created path.
1826
+ * Never connects to a database.
1827
+ */
1828
+ createPatch(name) {
1829
+ const creator = new PatchCreator(resolvePatchFolder(this.config));
1830
+ return creator.create(name);
1831
+ }
989
1832
  terminate() {
990
1833
  return __async(this, null, function* () {
991
1834
  try {
992
1835
  yield this.setupRunner.teardown();
993
1836
  } catch (error) {
994
- throw new MigrationExecutionError("Failed to tear down migration database", void 0, void 0, toError(error));
1837
+ throw new MigrationExecutionError("Failed to tear down migration database", void 0, void 0, toError2(error));
995
1838
  }
996
1839
  });
997
1840
  }
998
1841
  getMigrationsHistory() {
999
1842
  return __async(this, null, function* () {
1843
+ yield this.setup();
1000
1844
  try {
1001
1845
  const results = yield this.sqlrunner.query(`
1002
1846
  select *
@@ -1004,16 +1848,17 @@ var MySQLMigrationRunner = class {
1004
1848
  `);
1005
1849
  return results[0];
1006
1850
  } catch (error) {
1007
- throw new MigrationExecutionError("Failed to get migration history", void 0, void 0, toError(error));
1851
+ throw new MigrationExecutionError("Failed to get migration history", void 0, void 0, toError2(error));
1008
1852
  }
1009
1853
  });
1010
1854
  }
1011
1855
  getMigrations() {
1012
1856
  return __async(this, null, function* () {
1857
+ yield this.setup();
1013
1858
  try {
1014
1859
  return this.directory.loadMigrations(this.config.migration_table, this.connection);
1015
1860
  } catch (error) {
1016
- throw new MigrationExecutionError("Failed to load migrations", void 0, void 0, toError(error));
1861
+ throw new MigrationExecutionError("Failed to load migrations", void 0, void 0, toError2(error));
1017
1862
  }
1018
1863
  });
1019
1864
  }
@@ -1026,7 +1871,7 @@ var MySQLMigrationRunner = class {
1026
1871
  if (error instanceof MigrationExecutionError) {
1027
1872
  throw error;
1028
1873
  }
1029
- throw new MigrationExecutionError("Failed to get pending migrations", void 0, void 0, toError(error));
1874
+ throw new MigrationExecutionError("Failed to get pending migrations", void 0, void 0, toError2(error));
1030
1875
  }
1031
1876
  });
1032
1877
  }
@@ -1039,12 +1884,13 @@ var MySQLMigrationRunner = class {
1039
1884
  if (error instanceof MigrationExecutionError) {
1040
1885
  throw error;
1041
1886
  }
1042
- throw new MigrationExecutionError("Failed to get completed migrations", void 0, void 0, toError(error));
1887
+ throw new MigrationExecutionError("Failed to get completed migrations", void 0, void 0, toError2(error));
1043
1888
  }
1044
1889
  });
1045
1890
  }
1046
1891
  migrate(migrationNodes, forward) {
1047
1892
  return __async(this, null, function* () {
1893
+ yield this.setup();
1048
1894
  for (let node of migrationNodes) {
1049
1895
  try {
1050
1896
  if (forward) {
@@ -1057,7 +1903,7 @@ var MySQLMigrationRunner = class {
1057
1903
  `Failed to ${forward ? "apply" : "rollback"} migration`,
1058
1904
  node.name || String(node),
1059
1905
  forward ? node.up_sql() : node.down_sql(),
1060
- toError(error)
1906
+ toError2(error)
1061
1907
  );
1062
1908
  }
1063
1909
  }
@@ -1065,6 +1911,7 @@ var MySQLMigrationRunner = class {
1065
1911
  }
1066
1912
  reset() {
1067
1913
  return __async(this, null, function* () {
1914
+ yield this.setup();
1068
1915
  try {
1069
1916
  let migrations = yield this.getMigrations();
1070
1917
  const rollback = yield migration_filter(migrations, true);
@@ -1076,7 +1923,7 @@ var MySQLMigrationRunner = class {
1076
1923
  if (error instanceof MigrationExecutionError) {
1077
1924
  throw error;
1078
1925
  }
1079
- throw new MigrationExecutionError("Failed to reset migrations", void 0, void 0, toError(error));
1926
+ throw new MigrationExecutionError("Failed to reset migrations", void 0, void 0, toError2(error));
1080
1927
  }
1081
1928
  });
1082
1929
  }
@@ -1085,7 +1932,7 @@ var MySQLMigrationRunner = class {
1085
1932
  const creator = new MigrationCreator(this.config);
1086
1933
  creator.create(name);
1087
1934
  } catch (error) {
1088
- throw new MigrationExecutionError(`Failed to create migration: ${name}`, void 0, void 0, toError(error));
1935
+ throw new MigrationExecutionError(`Failed to create migration: ${name}`, void 0, void 0, toError2(error));
1089
1936
  }
1090
1937
  }
1091
1938
  close() {
@@ -1094,7 +1941,7 @@ var MySQLMigrationRunner = class {
1094
1941
  try {
1095
1942
  yield this.sqlrunner.end();
1096
1943
  } catch (error) {
1097
- throw new DatabaseConnectionError(`Failed to close database connection: ${toError(error).message}`);
1944
+ throw new DatabaseConnectionError(`Failed to close database connection: ${toError2(error).message}`);
1098
1945
  }
1099
1946
  }
1100
1947
  });
@@ -1108,7 +1955,7 @@ var MySQLMigrationRunner = class {
1108
1955
  return __async(this, null, function* () {
1109
1956
  try {
1110
1957
  console.log(`Checking for ${config_file}`);
1111
- const config_exist = fs3.existsSync(config_file);
1958
+ const config_exist = fs6.existsSync(config_file);
1112
1959
  if (!config_exist) {
1113
1960
  console.log(`Creating ${config_file}`);
1114
1961
  const default_config = {
@@ -1122,11 +1969,11 @@ var MySQLMigrationRunner = class {
1122
1969
  "password": ""
1123
1970
  }
1124
1971
  };
1125
- fs3.writeFileSync(config_file, JSON.stringify(default_config, null, 2));
1972
+ fs6.writeFileSync(config_file, JSON.stringify(default_config, null, 2));
1126
1973
  console.log(`Created ${config_file}`);
1127
1974
  }
1128
1975
  } catch (error) {
1129
- throw new ConfigurationError(`Failed to initialize config file: ${toError(error).message}`);
1976
+ throw new ConfigurationError(`Failed to initialize config file: ${toError2(error).message}`);
1130
1977
  }
1131
1978
  });
1132
1979
  }
@@ -1137,7 +1984,7 @@ var FileMigrationConfigReader = class {
1137
1984
  }
1138
1985
  loadFile() {
1139
1986
  try {
1140
- const fileContent = fs3.readFileSync(this.configFile);
1987
+ const fileContent = fs6.readFileSync(this.configFile);
1141
1988
  const config = JSON.parse(fileContent.toString());
1142
1989
  if (!config.migration_folder) {
1143
1990
  throw ConfigurationError.missingRequiredProperty("migration_folder");
@@ -1161,7 +2008,7 @@ var FileMigrationConfigReader = class {
1161
2008
  if (error instanceof ConfigurationError) {
1162
2009
  throw error;
1163
2010
  }
1164
- const err = toError(error);
2011
+ const err = toError2(error);
1165
2012
  if (err.message.includes("ENOENT")) {
1166
2013
  throw new ConfigurationError(`Config file not found: ${this.configFile}`);
1167
2014
  }
@@ -1178,16 +2025,16 @@ var MigrationCreator = class {
1178
2025
  throw new CLIError("Migration name is required");
1179
2026
  }
1180
2027
  try {
1181
- if (!fs3.existsSync(this.config.migration_folder)) {
1182
- fs3.mkdirSync(this.config.migration_folder, { recursive: true });
2028
+ if (!fs6.existsSync(this.config.migration_folder)) {
2029
+ fs6.mkdirSync(this.config.migration_folder, { recursive: true });
1183
2030
  }
1184
2031
  const now_timestamp = Date.now();
1185
2032
  const filename_up = `${now_timestamp}_${name}.up.sql`;
1186
2033
  const filename_down = `${now_timestamp}_${name}.down.sql`;
1187
- fs3.writeFileSync(`${this.config.migration_folder}/${filename_up}`, `
2034
+ fs6.writeFileSync(`${this.config.migration_folder}/${filename_up}`, `
1188
2035
  -- Write your up migration here
1189
2036
  `.trim());
1190
- fs3.writeFileSync(`${this.config.migration_folder}/${filename_down}`, `
2037
+ fs6.writeFileSync(`${this.config.migration_folder}/${filename_down}`, `
1191
2038
  -- Write your down migration here
1192
2039
  `.trim());
1193
2040
  console.log(`Created migration files:`);
@@ -1197,16 +2044,16 @@ var MigrationCreator = class {
1197
2044
  if (error instanceof CLIError) {
1198
2045
  throw error;
1199
2046
  }
1200
- throw new MigrationExecutionError(`Failed to create migration files: ${toError(error).message}`);
2047
+ throw new MigrationExecutionError(`Failed to create migration files: ${toError2(error).message}`);
1201
2048
  }
1202
2049
  }
1203
2050
  };
1204
2051
 
1205
2052
  // framework/SeedRunner.ts
1206
- import fs4 from "fs";
1207
- import path2 from "path";
2053
+ import fs7 from "fs";
2054
+ import path5 from "path";
1208
2055
  import { pathToFileURL } from "url";
1209
- import Ajv from "ajv";
2056
+ import Ajv2 from "ajv";
1210
2057
  import addFormats from "ajv-formats";
1211
2058
  import { tsImport } from "tsx/esm/api";
1212
2059
  function resolveAlias(name, aliasMap) {
@@ -1215,10 +2062,10 @@ function resolveAlias(name, aliasMap) {
1215
2062
  return (_a = aliasMap[name]) != null ? _a : name;
1216
2063
  }
1217
2064
  function walkForFile(rootDir, fileName) {
1218
- if (!fs4.existsSync(rootDir)) return null;
1219
- const entries = fs4.readdirSync(rootDir, { withFileTypes: true });
2065
+ if (!fs7.existsSync(rootDir)) return null;
2066
+ const entries = fs7.readdirSync(rootDir, { withFileTypes: true });
1220
2067
  for (const entry of entries) {
1221
- const full = path2.join(rootDir, entry.name);
2068
+ const full = path5.join(rootDir, entry.name);
1222
2069
  if (entry.isDirectory()) {
1223
2070
  const found = walkForFile(full, fileName);
1224
2071
  if (found) return found;
@@ -1312,39 +2159,39 @@ function resolveSeed(name, migrationConfig, options) {
1312
2159
  }
1313
2160
  function loadJson(filePath) {
1314
2161
  return __async(this, null, function* () {
1315
- const content = yield fs4.promises.readFile(filePath, "utf8");
2162
+ const content = yield fs7.promises.readFile(filePath, "utf8");
1316
2163
  return JSON.parse(content);
1317
2164
  });
1318
2165
  }
1319
2166
  function createValidator() {
1320
- const ajv = new Ajv({ allErrors: true, strict: false });
1321
- addFormats(ajv);
1322
- return ajv;
2167
+ const ajv2 = new Ajv2({ allErrors: true, strict: false });
2168
+ addFormats(ajv2);
2169
+ return ajv2;
1323
2170
  }
1324
2171
  function validateData(schemaPath, data, validate, log) {
1325
2172
  return __async(this, null, function* () {
1326
2173
  if (!validate || !schemaPath) return;
1327
- const content = yield fs4.promises.readFile(schemaPath, "utf8");
2174
+ const content = yield fs7.promises.readFile(schemaPath, "utf8");
1328
2175
  const schema = JSON.parse(content);
1329
- const ajv = createValidator();
1330
- const validateFn = ajv.compile(schema);
2176
+ const ajv2 = createValidator();
2177
+ const validateFn = ajv2.compile(schema);
1331
2178
  const ok = validateFn(data);
1332
2179
  if (!ok) {
1333
2180
  log == null ? void 0 : log(`Validation failed for seed data (${schemaPath})`);
1334
- throw new Error(`Seed data validation failed: ${ajv.errorsText(validateFn.errors || [])}`);
2181
+ throw new Error(`Seed data validation failed: ${ajv2.errorsText(validateFn.errors || [])}`);
1335
2182
  }
1336
2183
  });
1337
2184
  }
1338
2185
  function runSqlSeed(runner, resolved, direction) {
1339
2186
  return __async(this, null, function* () {
1340
2187
  const sqlPath = direction === "up" ? resolved.upPath : resolved.downPath;
1341
- const sql = yield fs4.promises.readFile(sqlPath, "utf8");
2188
+ const sql = yield fs7.promises.readFile(sqlPath, "utf8");
1342
2189
  yield runner.query(sql);
1343
2190
  });
1344
2191
  }
1345
2192
  function loadSeedModule(modulePath) {
1346
2193
  return __async(this, null, function* () {
1347
- const resolved = path2.resolve(modulePath);
2194
+ const resolved = path5.resolve(modulePath);
1348
2195
  if (resolved.endsWith(".ts")) {
1349
2196
  const fileUrl = pathToFileURL(resolved).href;
1350
2197
  return tsImport(fileUrl, fileUrl);
@@ -1464,13 +2311,23 @@ function createSeedFactory(options) {
1464
2311
  };
1465
2312
  }
1466
2313
  export {
2314
+ MigrationError,
1467
2315
  MySQLMigrationRunner as MigrationRunner,
1468
2316
  MigrationRunnerFactory,
2317
+ PatchConflictError,
2318
+ PatchCreator,
2319
+ PatchError,
2320
+ PatchExecutionError,
2321
+ PatchIntegrityError,
2322
+ PatchRunner,
2323
+ PatchValidationError,
1469
2324
  PgRunner,
1470
2325
  SQLRunner,
1471
2326
  SQLiteRunner,
1472
2327
  createSeedFactory,
1473
2328
  loadMigrationConfig,
2329
+ resolvePatchFolder,
2330
+ resolvePatchTable,
1474
2331
  runSeedsWithRunner
1475
2332
  };
1476
2333
  //# sourceMappingURL=index.mjs.map