@noego/proper 0.0.9 → 0.2.0

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