@noego/proper 0.1.0 → 0.2.0

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