@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/cli.mjs CHANGED
@@ -121,7 +121,7 @@ var init_MigrationFilter = __esm({
121
121
  });
122
122
 
123
123
  // framework/errors.ts
124
- var MigrationError, ConfigurationError, DatabaseConnectionError, MigrationExecutionError, CLIError;
124
+ var MigrationError, ConfigurationError, DatabaseConnectionError, MigrationExecutionError, PatchError, PatchValidationError, PatchIntegrityError, PatchConflictError, PatchExecutionError, CLIError;
125
125
  var init_errors = __esm({
126
126
  "framework/errors.ts"() {
127
127
  MigrationError = class _MigrationError extends Error {
@@ -236,6 +236,54 @@ ${originalError.message}`;
236
236
  return new _MigrationExecutionError(message);
237
237
  }
238
238
  };
239
+ PatchError = class _PatchError extends MigrationError {
240
+ constructor(message, patchFile, patchKey) {
241
+ super(`Patch Error: ${message}`);
242
+ this.patchFile = patchFile;
243
+ this.patchKey = patchKey;
244
+ this.name = "PatchError";
245
+ Object.setPrototypeOf(this, _PatchError.prototype);
246
+ }
247
+ };
248
+ PatchValidationError = class _PatchValidationError extends PatchError {
249
+ constructor(message, patchFile, patchKey) {
250
+ super(message, patchFile, patchKey);
251
+ this.name = "PatchValidationError";
252
+ Object.setPrototypeOf(this, _PatchValidationError.prototype);
253
+ }
254
+ };
255
+ PatchIntegrityError = class _PatchIntegrityError extends PatchError {
256
+ constructor(message, patchFile, patchKey, expectedChecksum, actualChecksum) {
257
+ super(message, patchFile, patchKey);
258
+ this.expectedChecksum = expectedChecksum;
259
+ this.actualChecksum = actualChecksum;
260
+ this.name = "PatchIntegrityError";
261
+ Object.setPrototypeOf(this, _PatchIntegrityError.prototype);
262
+ }
263
+ };
264
+ PatchConflictError = class _PatchConflictError extends PatchError {
265
+ constructor(message, patchFile, patchKey, operationIndex, operationVerb, migrationKeys, observedRowCounts) {
266
+ super(message, patchFile, patchKey);
267
+ this.operationIndex = operationIndex;
268
+ this.operationVerb = operationVerb;
269
+ this.migrationKeys = migrationKeys;
270
+ this.observedRowCounts = observedRowCounts;
271
+ this.name = "PatchConflictError";
272
+ Object.setPrototypeOf(this, _PatchConflictError.prototype);
273
+ }
274
+ };
275
+ PatchExecutionError = class _PatchExecutionError extends PatchError {
276
+ constructor(message, patchFile, patchKey, operationIndex, operationVerb, originalError) {
277
+ super(originalError ? `${message}
278
+ Original Error:
279
+ ${originalError.message}` : message, patchFile, patchKey);
280
+ this.operationIndex = operationIndex;
281
+ this.operationVerb = operationVerb;
282
+ this.originalError = originalError;
283
+ this.name = "PatchExecutionError";
284
+ Object.setPrototypeOf(this, _PatchExecutionError.prototype);
285
+ }
286
+ };
239
287
  CLIError = class _CLIError extends MigrationError {
240
288
  constructor(message) {
241
289
  super(`CLI Error: ${message}`);
@@ -412,13 +460,47 @@ var init_SqlMigrationBuilder = __esm({
412
460
  }
413
461
  });
414
462
 
415
- // framework/MigrationDirectoryReader.ts
463
+ // framework/MigrationManifest.ts
416
464
  import fs from "fs";
417
465
  import path from "path";
466
+ function canonicalMigrationKey(value) {
467
+ return value.replace(/(?:\.(mysql|sqlite|pg))?\.(up|down)\.(sql|js)$/i, "").toLowerCase();
468
+ }
469
+ function resolveMigrationFile(directory, baseName, direction, dialect) {
470
+ const dialectExt = dialect === "sql" ? "mysql" : dialect;
471
+ const dialectFile = path.join(directory, `${baseName}.${dialectExt}.${direction}.sql`);
472
+ if (fs.existsSync(dialectFile)) return dialectFile;
473
+ const genericFile = path.join(directory, `${baseName}.${direction}.sql`);
474
+ if (fs.existsSync(genericFile)) return genericFile;
475
+ return null;
476
+ }
477
+ function loadMigrationManifest(directory, dialect) {
478
+ const manifest = /* @__PURE__ */ new Map();
479
+ if (!fs.existsSync(directory)) return manifest;
480
+ const files = fs.readdirSync(directory, { withFileTypes: true }).filter((f) => f.isFile()).map((f) => f.name);
481
+ const uniqueKeys = /* @__PURE__ */ new Set();
482
+ files.forEach((file) => uniqueKeys.add(canonicalMigrationKey(file)));
483
+ uniqueKeys.forEach((key) => {
484
+ manifest.set(key, {
485
+ key,
486
+ upFile: resolveMigrationFile(directory, key, "up", dialect),
487
+ downFile: resolveMigrationFile(directory, key, "down", dialect)
488
+ });
489
+ });
490
+ return manifest;
491
+ }
492
+ var init_MigrationManifest = __esm({
493
+ "framework/MigrationManifest.ts"() {
494
+ }
495
+ });
496
+
497
+ // framework/MigrationDirectoryReader.ts
498
+ import fs2 from "fs";
418
499
  var MigrationDirectoryReader;
419
500
  var init_MigrationDirectoryReader = __esm({
420
501
  "framework/MigrationDirectoryReader.ts"() {
421
502
  init_SqlMigrationBuilder();
503
+ init_MigrationManifest();
422
504
  MigrationDirectoryReader = class {
423
505
  constructor(directory, read_strategy, sqlrunner, dialect = "sql") {
424
506
  this.directory = directory;
@@ -429,27 +511,23 @@ var init_MigrationDirectoryReader = __esm({
429
511
  /**
430
512
  * Resolves the appropriate file for a migration based on dialect.
431
513
  * Priority: dialect-specific file > generic file
514
+ * File extensions: `.mysql.up.sql`, `.sqlite.up.sql`, `.pg.up.sql`.
432
515
  */
433
516
  resolveFile(baseName, direction) {
434
- const dialectExt = this.dialect === "sql" ? "mysql" : "sqlite";
435
- const dialectFile = path.join(this.directory, `${baseName}.${dialectExt}.${direction}.sql`);
436
- if (fs.existsSync(dialectFile)) return dialectFile;
437
- const genericFile = path.join(this.directory, `${baseName}.${direction}.sql`);
438
- if (fs.existsSync(genericFile)) return genericFile;
439
- return null;
517
+ return resolveMigrationFile(this.directory, baseName, direction, this.dialect);
440
518
  }
441
519
  /**
442
520
  * Checks if a file path is dialect-specific (contains .mysql. or .sqlite. in the name)
443
521
  */
444
522
  isDialectSpecific(filePath) {
445
- return /\.(mysql|sqlite)\.(up|down)\.sql$/i.test(filePath);
523
+ return /\.(mysql|sqlite|pg)\.(up|down)\.sql$/i.test(filePath);
446
524
  }
447
525
  loadMigrations(table, connection) {
448
- fs.existsSync(this.directory) || fs.mkdirSync(this.directory);
449
- const dir_content = fs.readdirSync(this.directory, { withFileTypes: true }).filter((file) => file.isFile()).map((file) => file.name);
526
+ fs2.existsSync(this.directory) || fs2.mkdirSync(this.directory);
527
+ const dir_content = fs2.readdirSync(this.directory, { withFileTypes: true }).filter((file) => file.isFile()).map((file) => file.name);
450
528
  const uniqueKeys = /* @__PURE__ */ new Set();
451
529
  dir_content.forEach((file) => {
452
- const key = file.replace(/(?:\.(mysql|sqlite))?\.(up|down)\.(sql|js)/i, "").toLowerCase();
530
+ const key = canonicalMigrationKey(file);
453
531
  uniqueKeys.add(key);
454
532
  });
455
533
  const migration_sorter = {};
@@ -489,14 +567,14 @@ var init_MigrationDirectoryReader = __esm({
489
567
  return builder;
490
568
  }
491
569
  sql_up(file) {
492
- let content = fs.readFileSync(file).toString();
570
+ let content = fs2.readFileSync(file).toString();
493
571
  if (!this.isDialectSpecific(file)) {
494
572
  content = this.read_strategy(content);
495
573
  }
496
574
  return content.trim();
497
575
  }
498
576
  sql_down(file) {
499
- let content = fs.readFileSync(file).toString();
577
+ let content = fs2.readFileSync(file).toString();
500
578
  if (!this.isDialectSpecific(file)) {
501
579
  content = this.read_strategy(content);
502
580
  }
@@ -506,11 +584,31 @@ var init_MigrationDirectoryReader = __esm({
506
584
  }
507
585
  });
508
586
 
587
+ // framework/PatchTypes.ts
588
+ import path2 from "path";
589
+ function resolvePatchFolder(config) {
590
+ if (config.patch_folder) return config.patch_folder;
591
+ const dir = path2.dirname(config.migration_folder);
592
+ return dir === "." && !config.migration_folder.includes(path2.sep) && !config.migration_folder.includes("/") ? "patches" : path2.join(dir, "patches");
593
+ }
594
+ function resolvePatchTable(config) {
595
+ return config.patch_table || DEFAULT_PATCH_TABLE;
596
+ }
597
+ var PATCH_FORMAT_VERSION, DEFAULT_PATCH_TABLE, PATCH_FILENAME_REGEX;
598
+ var init_PatchTypes = __esm({
599
+ "framework/PatchTypes.ts"() {
600
+ PATCH_FORMAT_VERSION = 1;
601
+ DEFAULT_PATCH_TABLE = "proper_patches";
602
+ PATCH_FILENAME_REGEX = new RegExp("^(?<stamp>[0-9]{13})_(?<name>[a-z0-9][a-z0-9_-]{0,119})\\.yaml$");
603
+ }
604
+ });
605
+
509
606
  // framework/MigrationSetup.ts
510
- import fs2 from "fs";
607
+ import fs3 from "fs";
511
608
  var MigrationSetup;
512
609
  var init_MigrationSetup = __esm({
513
610
  "framework/MigrationSetup.ts"() {
611
+ init_PatchTypes();
514
612
  MigrationSetup = class {
515
613
  constructor(sqlrunner, config) {
516
614
  this.sqlrunner = sqlrunner;
@@ -518,7 +616,7 @@ var init_MigrationSetup = __esm({
518
616
  }
519
617
  setup() {
520
618
  return __async(this, null, function* () {
521
- fs2.existsSync(this.config.migration_folder) || fs2.mkdirSync(this.config.migration_folder);
619
+ fs3.existsSync(this.config.migration_folder) || fs3.mkdirSync(this.config.migration_folder);
522
620
  const tableName = this.config.migration_table;
523
621
  const createTableSql = this.config.database === "sqlite" ? `CREATE TABLE IF NOT EXISTS ${tableName} (
524
622
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -526,6 +624,12 @@ var init_MigrationSetup = __esm({
526
624
  up TEXT,
527
625
  down TEXT,
528
626
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
627
+ )` : this.config.database === "pg" ? `CREATE TABLE IF NOT EXISTS ${tableName} (
628
+ id SERIAL PRIMARY KEY,
629
+ migration_key TEXT,
630
+ up TEXT,
631
+ down TEXT,
632
+ created_at TIMESTAMPTZ DEFAULT now()
529
633
  )` : `CREATE TABLE IF NOT EXISTS ${tableName} (
530
634
  id INT AUTO_INCREMENT PRIMARY KEY,
531
635
  migration_key VARCHAR(255),
@@ -534,11 +638,39 @@ var init_MigrationSetup = __esm({
534
638
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
535
639
  )`;
536
640
  yield this.sqlrunner.query(createTableSql);
641
+ const patchTable = resolvePatchTable(this.config);
642
+ const createPatchTableSql = this.config.database === "sqlite" ? `CREATE TABLE IF NOT EXISTS ${patchTable} (
643
+ migration_table TEXT NOT NULL,
644
+ patch_key TEXT NOT NULL,
645
+ checksum TEXT NOT NULL,
646
+ format_version INTEGER NOT NULL,
647
+ description TEXT NOT NULL,
648
+ applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
649
+ PRIMARY KEY (migration_table, patch_key)
650
+ )` : this.config.database === "pg" ? `CREATE TABLE IF NOT EXISTS ${patchTable} (
651
+ migration_table TEXT NOT NULL,
652
+ patch_key TEXT NOT NULL,
653
+ checksum TEXT NOT NULL,
654
+ format_version INTEGER NOT NULL,
655
+ description TEXT NOT NULL,
656
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT now(),
657
+ PRIMARY KEY (migration_table, patch_key)
658
+ )` : `CREATE TABLE IF NOT EXISTS ${patchTable} (
659
+ migration_table VARCHAR(255) NOT NULL,
660
+ patch_key VARCHAR(255) NOT NULL,
661
+ checksum CHAR(64) NOT NULL,
662
+ format_version INT NOT NULL,
663
+ description TEXT NOT NULL,
664
+ applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
665
+ PRIMARY KEY (migration_table, patch_key)
666
+ )`;
667
+ yield this.sqlrunner.query(createPatchTableSql);
537
668
  });
538
669
  }
539
670
  teardown() {
540
671
  return __async(this, null, function* () {
541
672
  yield this.sqlrunner.execute(`DROP TABLE ${this.config.migration_table}`);
673
+ yield this.sqlrunner.execute(`DROP TABLE IF EXISTS ${resolvePatchTable(this.config)}`);
542
674
  });
543
675
  }
544
676
  };
@@ -572,33 +704,735 @@ function MySqlDialectParser(sql) {
572
704
  }
573
705
  return result;
574
706
  }
575
- function SqliteDialectParser(sql) {
576
- const lines = sql.split("\n");
577
- let result = "";
578
- let isInSqliteBlock = true;
579
- const startSqliteRegex = /^\s*--\s*\[\s*sqlite\s*\]\s*$/i;
707
+ function markerDialectParser(names) {
708
+ const startRegex = new RegExp(`^\\s*--\\s*\\[\\s*(${names.join("|")})\\s*\\]\\s*$`, "i");
580
709
  const anyDialectRegex = /^\s*--\s*\[\s*\w+\s*\]\s*$/i;
581
- for (const line of lines) {
582
- const trimmed = line.trim();
583
- if (startSqliteRegex.test(trimmed)) {
584
- isInSqliteBlock = true;
585
- result += line + "\n";
586
- continue;
587
- } else if (anyDialectRegex.test(trimmed) && !startSqliteRegex.test(trimmed)) {
588
- isInSqliteBlock = false;
589
- continue;
710
+ return function(sql) {
711
+ const lines = sql.split("\n");
712
+ let result = "";
713
+ let capturing = true;
714
+ for (const line of lines) {
715
+ const trimmed = line.trim();
716
+ if (startRegex.test(trimmed)) {
717
+ capturing = true;
718
+ result += line + "\n";
719
+ continue;
720
+ } else if (anyDialectRegex.test(trimmed)) {
721
+ capturing = false;
722
+ continue;
723
+ }
724
+ if (!capturing && line.toLowerCase().includes("create index")) {
725
+ capturing = true;
726
+ }
727
+ if (capturing) {
728
+ result += line + "\n";
729
+ }
590
730
  }
591
- if (!isInSqliteBlock && line.toLowerCase().includes("create index")) {
592
- isInSqliteBlock = true;
731
+ return result;
732
+ };
733
+ }
734
+ var SqliteDialectParser, PgDialectParser;
735
+ var init_MigrationDialectParser = __esm({
736
+ "framework/MigrationDialectParser.ts"() {
737
+ SqliteDialectParser = markerDialectParser(["sqlite"]);
738
+ PgDialectParser = markerDialectParser(["pg", "postgres", "postgresql"]);
739
+ }
740
+ });
741
+
742
+ // framework/PatchValidator.ts
743
+ import Ajv from "ajv";
744
+ import { parseDocument } from "yaml";
745
+ function fail(message, file, patchKey) {
746
+ throw new PatchValidationError(message, file, patchKey);
747
+ }
748
+ function checkMigrationKey(value, context, file, patchKey) {
749
+ if (value !== value.trim()) fail(`${context}: migration key has leading/trailing whitespace`, file, patchKey);
750
+ if (/[/\\]/.test(value)) fail(`${context}: migration key contains a path separator`, file, patchKey);
751
+ if (/[\x00-\x1f\x7f]/.test(value)) fail(`${context}: migration key contains control characters`, file, patchKey);
752
+ if (value.length === 0 || value.length > MAX_MIGRATION_KEY_LENGTH) {
753
+ fail(`${context}: migration key length out of bounds`, file, patchKey);
754
+ }
755
+ return canonicalMigrationKey(value);
756
+ }
757
+ function assertStrictYaml(doc, file, patchKey) {
758
+ if (doc.errors.length > 0) {
759
+ fail(`YAML parse error: ${doc.errors[0].message}`, file, patchKey);
760
+ }
761
+ if (doc.warnings.length > 0) {
762
+ fail(`YAML warning treated as error: ${doc.warnings[0].message}`, file, patchKey);
763
+ }
764
+ const visit = (node) => {
765
+ var _a, _b;
766
+ if (node == null || typeof node !== "object") return;
767
+ if ("source" in node && ((_a = node.constructor) == null ? void 0 : _a.name) === "Alias") {
768
+ fail("YAML aliases are not permitted in patch files", file, patchKey);
593
769
  }
594
- if (isInSqliteBlock) {
595
- result += line + "\n";
770
+ if (node.anchor) {
771
+ fail("YAML anchors are not permitted in patch files", file, patchKey);
772
+ }
773
+ if (node.tag && ![
774
+ "tag:yaml.org,2002:str",
775
+ "tag:yaml.org,2002:int",
776
+ "tag:yaml.org,2002:bool",
777
+ "tag:yaml.org,2002:null",
778
+ "tag:yaml.org,2002:map",
779
+ "tag:yaml.org,2002:seq"
780
+ ].includes(node.tag)) {
781
+ fail(`YAML tag '${node.tag}' is not permitted in patch files`, file, patchKey);
782
+ }
783
+ if (Array.isArray(node.items)) {
784
+ for (const item of node.items) {
785
+ if (item && typeof item === "object" && "key" in item) {
786
+ const keyValue = (_b = item.key) == null ? void 0 : _b.value;
787
+ if (keyValue === "<<") fail("YAML merge keys are not permitted in patch files", file, patchKey);
788
+ visit(item.key);
789
+ visit(item.value);
790
+ } else {
791
+ visit(item);
792
+ }
793
+ }
794
+ }
795
+ };
796
+ visit(doc.contents);
797
+ }
798
+ function parsePatchContent(content, fileName, patchKey) {
799
+ var _a;
800
+ const doc = parseDocument(content, {
801
+ uniqueKeys: true,
802
+ // duplicate mapping keys become errors
803
+ merge: false,
804
+ schema: "core",
805
+ version: "1.2"
806
+ });
807
+ assertStrictYaml(doc, fileName, patchKey);
808
+ const raw = doc.toJS({ mapAsMap: false });
809
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
810
+ fail("Patch document must be a YAML mapping", fileName, patchKey);
811
+ }
812
+ if (!validateSchema(raw)) {
813
+ const detail = ((_a = validateSchema.errors) != null ? _a : []).map((e) => `${e.instancePath || "/"} ${e.message}`).join("; ");
814
+ const anyRaw = raw;
815
+ if (typeof anyRaw.version === "number" && anyRaw.version !== PATCH_FORMAT_VERSION) {
816
+ fail(`Unknown patch format version: ${anyRaw.version} (supported: ${PATCH_FORMAT_VERSION})`, fileName, patchKey);
596
817
  }
818
+ fail(`Schema validation failed: ${detail}`, fileName, patchKey);
597
819
  }
598
- return result;
820
+ const parsed = raw;
821
+ if (parsed.version !== PATCH_FORMAT_VERSION) {
822
+ fail(`Unknown patch format version: ${parsed.version} (supported: ${PATCH_FORMAT_VERSION})`, fileName, patchKey);
823
+ }
824
+ const description = parsed.description.trim();
825
+ if (description.length === 0) fail("description must be non-empty", fileName, patchKey);
826
+ if (description.length > MAX_DESCRIPTION_LENGTH) {
827
+ fail(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters`, fileName, patchKey);
828
+ }
829
+ const operations = parsed.operations.map((op, index) => {
830
+ const verbs = Object.keys(op);
831
+ const verb = verbs[0];
832
+ const context = `operation ${index} (${verb})`;
833
+ switch (verb) {
834
+ case "rename_migration": {
835
+ const from = checkMigrationKey(op.rename_migration.from, context, fileName, patchKey);
836
+ const to = checkMigrationKey(op.rename_migration.to, context, fileName, patchKey);
837
+ if (from === to) {
838
+ fail(`${context}: 'from' and 'to' are identical after canonicalization ('${from}')`, fileName, patchKey);
839
+ }
840
+ return { verb: "rename_migration", from, to };
841
+ }
842
+ case "mark_applied":
843
+ return { verb: "mark_applied", key: checkMigrationKey(op.mark_applied.key, context, fileName, patchKey) };
844
+ case "unmark_applied":
845
+ return { verb: "unmark_applied", key: checkMigrationKey(op.unmark_applied.key, context, fileName, patchKey) };
846
+ default:
847
+ fail(`operation ${index}: unknown verb '${verb}'`, fileName, patchKey);
848
+ }
849
+ });
850
+ return { version: parsed.version, description, operations };
599
851
  }
600
- var init_MigrationDialectParser = __esm({
601
- "framework/MigrationDialectParser.ts"() {
852
+ function validatePatchPlan(patches, manifest) {
853
+ const renames = [];
854
+ for (const patch of patches) {
855
+ patch.operations.forEach((op, index) => {
856
+ if (op.verb === "rename_migration") {
857
+ renames.push({ from: op.from, to: op.to, file: patch.fileName });
858
+ } else if (op.verb === "mark_applied") {
859
+ if (!manifest.has(op.key)) {
860
+ throw new PatchValidationError(
861
+ `operation ${index} (mark_applied): key '${op.key}' is not present in the current migration manifest`,
862
+ patch.fileName,
863
+ patch.patchKey
864
+ );
865
+ }
866
+ }
867
+ });
868
+ }
869
+ if (renames.length === 0) return;
870
+ const mentioned = /* @__PURE__ */ new Set();
871
+ renames.forEach((r) => {
872
+ mentioned.add(r.from);
873
+ mentioned.add(r.to);
874
+ });
875
+ const finalKey = (start) => {
876
+ let current = start;
877
+ for (const r of renames) {
878
+ if (current === r.from) current = r.to;
879
+ }
880
+ return current;
881
+ };
882
+ for (const key of mentioned) {
883
+ const finish = finalKey(key);
884
+ if (manifest.has(key)) {
885
+ if (finish !== key) {
886
+ throw new PatchValidationError(
887
+ `rename plan moves current migration '${key}' to '${finish}', which would make its file incorrectly pending`
888
+ );
889
+ }
890
+ } else {
891
+ if (!manifest.has(finish)) {
892
+ throw new PatchValidationError(
893
+ `rename plan leaves historical key '${key}' at '${finish}', which is not present in the current migration manifest`
894
+ );
895
+ }
896
+ }
897
+ }
898
+ }
899
+ var MAX_DESCRIPTION_LENGTH, MAX_MIGRATION_KEY_LENGTH, migrationKeySchema, patchSchema, ajv, validateSchema;
900
+ var init_PatchValidator = __esm({
901
+ "framework/PatchValidator.ts"() {
902
+ init_errors();
903
+ init_MigrationManifest();
904
+ init_PatchTypes();
905
+ MAX_DESCRIPTION_LENGTH = 500;
906
+ MAX_MIGRATION_KEY_LENGTH = 255;
907
+ migrationKeySchema = {
908
+ type: "string",
909
+ minLength: 1,
910
+ maxLength: MAX_MIGRATION_KEY_LENGTH
911
+ };
912
+ patchSchema = {
913
+ type: "object",
914
+ additionalProperties: false,
915
+ required: ["version", "description", "operations"],
916
+ properties: {
917
+ version: { type: "integer" },
918
+ description: { type: "string" },
919
+ operations: {
920
+ type: "array",
921
+ minItems: 1,
922
+ items: {
923
+ type: "object",
924
+ additionalProperties: false,
925
+ minProperties: 1,
926
+ maxProperties: 1,
927
+ properties: {
928
+ rename_migration: {
929
+ type: "object",
930
+ additionalProperties: false,
931
+ required: ["from", "to"],
932
+ properties: { from: migrationKeySchema, to: migrationKeySchema }
933
+ },
934
+ mark_applied: {
935
+ type: "object",
936
+ additionalProperties: false,
937
+ required: ["key"],
938
+ properties: { key: migrationKeySchema }
939
+ },
940
+ unmark_applied: {
941
+ type: "object",
942
+ additionalProperties: false,
943
+ required: ["key"],
944
+ properties: { key: migrationKeySchema }
945
+ }
946
+ }
947
+ }
948
+ }
949
+ }
950
+ };
951
+ ajv = new Ajv({ allErrors: true, strict: true });
952
+ validateSchema = ajv.compile(patchSchema);
953
+ }
954
+ });
955
+
956
+ // framework/PatchDirectoryReader.ts
957
+ import crypto from "crypto";
958
+ import fs4 from "fs";
959
+ import path3 from "path";
960
+ var PatchDirectoryReader;
961
+ var init_PatchDirectoryReader = __esm({
962
+ "framework/PatchDirectoryReader.ts"() {
963
+ init_errors();
964
+ init_PatchValidator();
965
+ init_PatchTypes();
966
+ PatchDirectoryReader = class {
967
+ constructor(directory) {
968
+ this.directory = directory;
969
+ }
970
+ loadPatches() {
971
+ if (!fs4.existsSync(this.directory)) return [];
972
+ const entries = fs4.readdirSync(this.directory, { withFileTypes: true });
973
+ const patchFiles = [];
974
+ for (const entry of entries) {
975
+ if (!entry.name.endsWith(".yaml")) continue;
976
+ if (!entry.isFile() || entry.isSymbolicLink()) {
977
+ if (entry.isSymbolicLink()) {
978
+ throw new PatchValidationError(`patch file must be a regular file, not a symlink`, entry.name);
979
+ }
980
+ continue;
981
+ }
982
+ patchFiles.push(entry.name);
983
+ }
984
+ patchFiles.sort((a, b) => {
985
+ const stampA = parseInt(a.slice(0, 13), 10);
986
+ const stampB = parseInt(b.slice(0, 13), 10);
987
+ if (!Number.isNaN(stampA) && !Number.isNaN(stampB) && stampA !== stampB) {
988
+ return stampA - stampB;
989
+ }
990
+ return a < b ? -1 : a > b ? 1 : 0;
991
+ });
992
+ return patchFiles.map((fileName) => {
993
+ const match = PATCH_FILENAME_REGEX.exec(fileName);
994
+ if (!match) {
995
+ throw new PatchValidationError(
996
+ `invalid patch filename (expected <13-digit-stamp>_<name>.yaml with name matching [a-z0-9][a-z0-9_-]{0,119})`,
997
+ fileName
998
+ );
999
+ }
1000
+ const patchKey = fileName.slice(0, -".yaml".length);
1001
+ const filePath = path3.join(this.directory, fileName);
1002
+ const bytes = fs4.readFileSync(filePath);
1003
+ const checksum = crypto.createHash("sha256").update(bytes).digest("hex");
1004
+ const content = bytes.toString("utf8");
1005
+ const { version, description, operations } = parsePatchContent(content, fileName, patchKey);
1006
+ return { patchKey, fileName, filePath, checksum, version, description, operations };
1007
+ });
1008
+ }
1009
+ };
1010
+ }
1011
+ });
1012
+
1013
+ // framework/PatchRunner.ts
1014
+ function toError(error) {
1015
+ if (error instanceof Error) return error;
1016
+ return new Error(String(error));
1017
+ }
1018
+ function extractRows(result) {
1019
+ if (!Array.isArray(result)) return [];
1020
+ if (Array.isArray(result[0])) return result[0];
1021
+ if (result.length === 2 && result[0] && typeof result[0] === "object" && result[1] && typeof result[1] === "object" && !("rows" in result[1])) {
1022
+ return result;
1023
+ }
1024
+ if (result[0] == null) return [];
1025
+ return [result[0]];
1026
+ }
1027
+ var PatchRunner;
1028
+ var init_PatchRunner = __esm({
1029
+ "framework/PatchRunner.ts"() {
1030
+ init_PatchDirectoryReader();
1031
+ init_PatchValidator();
1032
+ init_MigrationManifest();
1033
+ init_PatchTypes();
1034
+ init_errors();
1035
+ PatchRunner = class {
1036
+ constructor(sqlrunner, config) {
1037
+ this.sqlrunner = sqlrunner;
1038
+ this.config = config;
1039
+ this.patchTable = resolvePatchTable(config);
1040
+ this.migrationTable = config.migration_table;
1041
+ this.dialect = config.database;
1042
+ }
1043
+ /**
1044
+ * Discovers, validates, and applies every unapplied patch in order.
1045
+ * Each unapplied patch is its own transaction; earlier committed patches
1046
+ * remain committed if a later patch fails.
1047
+ */
1048
+ applyPending() {
1049
+ return __async(this, null, function* () {
1050
+ const reader = new PatchDirectoryReader(resolvePatchFolder(this.config));
1051
+ const patches = reader.loadPatches();
1052
+ const history = yield this.loadHistory();
1053
+ if (patches.length === 0 && history.length === 0) {
1054
+ return [];
1055
+ }
1056
+ const byKey = new Map(patches.map((p) => [p.patchKey, p]));
1057
+ for (const row of history) {
1058
+ const file = byKey.get(row.patch_key);
1059
+ if (!file) {
1060
+ throw new PatchIntegrityError(
1061
+ `applied patch '${row.patch_key}' has no corresponding file in the patch folder; patch files are permanent and must never be renamed or deleted`,
1062
+ void 0,
1063
+ row.patch_key
1064
+ );
1065
+ }
1066
+ if (file.checksum !== row.checksum) {
1067
+ throw new PatchIntegrityError(
1068
+ `applied patch '${row.patch_key}' content changed after application; patch files are immutable once recorded`,
1069
+ file.fileName,
1070
+ row.patch_key,
1071
+ row.checksum,
1072
+ file.checksum
1073
+ );
1074
+ }
1075
+ }
1076
+ const manifest = loadMigrationManifest(this.config.migration_folder, this.dialect);
1077
+ validatePatchPlan(patches, manifest);
1078
+ const appliedKeys = new Set(history.map((r) => r.patch_key));
1079
+ const results = [];
1080
+ for (const patch of patches) {
1081
+ if (appliedKeys.has(patch.patchKey)) {
1082
+ results.push({
1083
+ patchKey: patch.patchKey,
1084
+ fileName: patch.fileName,
1085
+ status: "already_applied",
1086
+ operations: []
1087
+ });
1088
+ continue;
1089
+ }
1090
+ results.push(yield this.applyOne(patch, manifest));
1091
+ }
1092
+ return results;
1093
+ });
1094
+ }
1095
+ loadHistory() {
1096
+ return __async(this, null, function* () {
1097
+ try {
1098
+ const result = yield this.sqlrunner.query(
1099
+ `SELECT patch_key, checksum FROM ${this.patchTable} WHERE migration_table = ?`,
1100
+ [this.migrationTable]
1101
+ );
1102
+ return extractRows(result);
1103
+ } catch (error) {
1104
+ throw new PatchExecutionError(
1105
+ `failed to read patch history from '${this.patchTable}'`,
1106
+ void 0,
1107
+ void 0,
1108
+ void 0,
1109
+ void 0,
1110
+ toError(error)
1111
+ );
1112
+ }
1113
+ });
1114
+ }
1115
+ beginSql() {
1116
+ switch (this.dialect) {
1117
+ case "sqlite":
1118
+ return "BEGIN IMMEDIATE";
1119
+ case "pg":
1120
+ return "BEGIN";
1121
+ default:
1122
+ return "START TRANSACTION";
1123
+ }
1124
+ }
1125
+ begin(patch) {
1126
+ return __async(this, null, function* () {
1127
+ const deadline = Date.now() + 1e4;
1128
+ while (true) {
1129
+ try {
1130
+ yield this.sqlrunner.execute(this.beginSql());
1131
+ return;
1132
+ } catch (error) {
1133
+ const message = toError(error).message;
1134
+ if (/SQLITE_BUSY|database is locked/i.test(message) && Date.now() < deadline) {
1135
+ yield new Promise((resolve) => setTimeout(resolve, 50));
1136
+ continue;
1137
+ }
1138
+ throw new PatchExecutionError(
1139
+ "failed to start patch transaction",
1140
+ patch.fileName,
1141
+ patch.patchKey,
1142
+ void 0,
1143
+ void 0,
1144
+ toError(error)
1145
+ );
1146
+ }
1147
+ }
1148
+ });
1149
+ }
1150
+ rollbackQuietly() {
1151
+ return __async(this, null, function* () {
1152
+ try {
1153
+ yield this.sqlrunner.execute("ROLLBACK");
1154
+ } catch (e) {
1155
+ }
1156
+ });
1157
+ }
1158
+ applyOne(patch, manifest) {
1159
+ return __async(this, null, function* () {
1160
+ yield this.begin(patch);
1161
+ try {
1162
+ yield this.sqlrunner.execute(
1163
+ `INSERT INTO ${this.patchTable} (migration_table, patch_key, checksum, format_version, description)
1164
+ VALUES (?, ?, ?, ?, ?)`,
1165
+ [this.migrationTable, patch.patchKey, patch.checksum, patch.version, patch.description]
1166
+ );
1167
+ } catch (claimError) {
1168
+ yield this.rollbackQuietly();
1169
+ const committed = yield this.findCommittedRow(patch.patchKey);
1170
+ if (committed) {
1171
+ if (committed.checksum === patch.checksum) {
1172
+ return {
1173
+ patchKey: patch.patchKey,
1174
+ fileName: patch.fileName,
1175
+ status: "already_applied",
1176
+ operations: []
1177
+ };
1178
+ }
1179
+ throw new PatchIntegrityError(
1180
+ `patch '${patch.patchKey}' was applied elsewhere with a different checksum`,
1181
+ patch.fileName,
1182
+ patch.patchKey,
1183
+ committed.checksum,
1184
+ patch.checksum
1185
+ );
1186
+ }
1187
+ throw new PatchExecutionError(
1188
+ "failed to claim patch-history row",
1189
+ patch.fileName,
1190
+ patch.patchKey,
1191
+ void 0,
1192
+ void 0,
1193
+ toError(claimError)
1194
+ );
1195
+ }
1196
+ const operationResults = [];
1197
+ try {
1198
+ for (let index = 0; index < patch.operations.length; index++) {
1199
+ operationResults.push(
1200
+ yield this.applyOperation(patch, patch.operations[index], index, manifest)
1201
+ );
1202
+ }
1203
+ yield this.sqlrunner.execute("COMMIT");
1204
+ } catch (error) {
1205
+ yield this.rollbackQuietly();
1206
+ if (error instanceof PatchConflictError || error instanceof PatchExecutionError || error instanceof PatchIntegrityError) {
1207
+ throw error;
1208
+ }
1209
+ throw new PatchExecutionError(
1210
+ "patch application failed",
1211
+ patch.fileName,
1212
+ patch.patchKey,
1213
+ void 0,
1214
+ void 0,
1215
+ toError(error)
1216
+ );
1217
+ }
1218
+ return {
1219
+ patchKey: patch.patchKey,
1220
+ fileName: patch.fileName,
1221
+ status: "applied",
1222
+ operations: operationResults
1223
+ };
1224
+ });
1225
+ }
1226
+ findCommittedRow(patchKey) {
1227
+ return __async(this, null, function* () {
1228
+ const result = yield this.sqlrunner.query(
1229
+ `SELECT patch_key, checksum FROM ${this.patchTable} WHERE migration_table = ? AND patch_key = ?`,
1230
+ [this.migrationTable, patchKey]
1231
+ );
1232
+ const list = extractRows(result);
1233
+ return list.length > 0 ? list[0] : null;
1234
+ });
1235
+ }
1236
+ countRows(key) {
1237
+ return __async(this, null, function* () {
1238
+ var _a, _b, _c;
1239
+ const result = yield this.sqlrunner.query(
1240
+ `SELECT COUNT(*) AS row_count FROM ${this.migrationTable} WHERE migration_key = ?`,
1241
+ [key]
1242
+ );
1243
+ const rows = extractRows(result);
1244
+ const value = (_c = (_a = rows[0]) == null ? void 0 : _a.row_count) != null ? _c : Object.values((_b = rows[0]) != null ? _b : {})[0];
1245
+ return Number(value != null ? value : 0);
1246
+ });
1247
+ }
1248
+ conflict(patch, index, verb, message, keys, counts) {
1249
+ throw new PatchConflictError(
1250
+ `operation ${index} (${verb}): ${message}`,
1251
+ patch.fileName,
1252
+ patch.patchKey,
1253
+ index,
1254
+ verb,
1255
+ keys,
1256
+ counts
1257
+ );
1258
+ }
1259
+ applyOperation(patch, op, index, manifest) {
1260
+ return __async(this, null, function* () {
1261
+ var _a, _b, _c, _d;
1262
+ try {
1263
+ switch (op.verb) {
1264
+ case "rename_migration": {
1265
+ const fromCount = yield this.countRows(op.from);
1266
+ const toCount = yield this.countRows(op.to);
1267
+ const counts = { [op.from]: fromCount, [op.to]: toCount };
1268
+ if (fromCount > 1 || toCount > 1) {
1269
+ this.conflict(
1270
+ patch,
1271
+ index,
1272
+ op.verb,
1273
+ `ledger corruption: duplicate rows for a migration key`,
1274
+ [op.from, op.to],
1275
+ counts
1276
+ );
1277
+ }
1278
+ if (fromCount === 1 && toCount === 1) {
1279
+ this.conflict(
1280
+ patch,
1281
+ index,
1282
+ op.verb,
1283
+ `both '${op.from}' and '${op.to}' exist in the ledger`,
1284
+ [op.from, op.to],
1285
+ counts
1286
+ );
1287
+ }
1288
+ if (fromCount === 0) {
1289
+ return { verb: op.verb, changed: false };
1290
+ }
1291
+ const target = manifest.get(op.to);
1292
+ if (target) {
1293
+ yield this.sqlrunner.execute(
1294
+ `UPDATE ${this.migrationTable} SET migration_key = ?, up = ?, down = ? WHERE migration_key = ?`,
1295
+ [op.to, (_a = target.upFile) != null ? _a : "", (_b = target.downFile) != null ? _b : "", op.from]
1296
+ );
1297
+ } else {
1298
+ yield this.sqlrunner.execute(
1299
+ `UPDATE ${this.migrationTable} SET migration_key = ? WHERE migration_key = ?`,
1300
+ [op.to, op.from]
1301
+ );
1302
+ }
1303
+ return { verb: op.verb, changed: true };
1304
+ }
1305
+ case "mark_applied": {
1306
+ const count = yield this.countRows(op.key);
1307
+ if (count > 1) {
1308
+ this.conflict(
1309
+ patch,
1310
+ index,
1311
+ op.verb,
1312
+ `ledger corruption: duplicate rows for '${op.key}'`,
1313
+ [op.key],
1314
+ { [op.key]: count }
1315
+ );
1316
+ }
1317
+ if (count === 1) {
1318
+ return { verb: op.verb, changed: false };
1319
+ }
1320
+ const entry = manifest.get(op.key);
1321
+ yield this.sqlrunner.execute(
1322
+ `INSERT INTO ${this.migrationTable} (migration_key, up, down) VALUES (?, ?, ?)`,
1323
+ [op.key, (_c = entry == null ? void 0 : entry.upFile) != null ? _c : "", (_d = entry == null ? void 0 : entry.downFile) != null ? _d : ""]
1324
+ );
1325
+ return { verb: op.verb, changed: true };
1326
+ }
1327
+ case "unmark_applied": {
1328
+ const count = yield this.countRows(op.key);
1329
+ if (count > 1) {
1330
+ this.conflict(
1331
+ patch,
1332
+ index,
1333
+ op.verb,
1334
+ `ledger corruption: duplicate rows for '${op.key}'`,
1335
+ [op.key],
1336
+ { [op.key]: count }
1337
+ );
1338
+ }
1339
+ if (count === 0) {
1340
+ return { verb: op.verb, changed: false };
1341
+ }
1342
+ yield this.sqlrunner.execute(
1343
+ `DELETE FROM ${this.migrationTable} WHERE migration_key = ?`,
1344
+ [op.key]
1345
+ );
1346
+ return { verb: op.verb, changed: true };
1347
+ }
1348
+ }
1349
+ } catch (error) {
1350
+ if (error instanceof PatchConflictError) throw error;
1351
+ throw new PatchExecutionError(
1352
+ `operation failed`,
1353
+ patch.fileName,
1354
+ patch.patchKey,
1355
+ index,
1356
+ op.verb,
1357
+ toError(error)
1358
+ );
1359
+ }
1360
+ });
1361
+ }
1362
+ };
1363
+ }
1364
+ });
1365
+
1366
+ // framework/PatchCreator.ts
1367
+ import fs5 from "fs";
1368
+ import path4 from "path";
1369
+ var MAX_NAME_LENGTH, SCAFFOLD, PatchCreator;
1370
+ var init_PatchCreator = __esm({
1371
+ "framework/PatchCreator.ts"() {
1372
+ init_errors();
1373
+ MAX_NAME_LENGTH = 120;
1374
+ SCAFFOLD = `version: 1
1375
+ description: TODO
1376
+ operations: []
1377
+ `;
1378
+ PatchCreator = class _PatchCreator {
1379
+ constructor(patchFolder) {
1380
+ this.patchFolder = patchFolder;
1381
+ }
1382
+ /**
1383
+ * Normalizes a patch name: trim, whitespace runs -> `_`, lowercase.
1384
+ * Rejects empty results, path separators, `..`, control characters,
1385
+ * characters outside [a-z0-9_-], and names longer than 120 characters.
1386
+ */
1387
+ static normalizeName(name) {
1388
+ const normalized = (name != null ? name : "").trim().replace(/\s+/g, "_").toLowerCase();
1389
+ if (normalized.length === 0) {
1390
+ throw new CLIError("Patch name is required");
1391
+ }
1392
+ if (normalized.includes("/") || normalized.includes("\\")) {
1393
+ throw new CLIError("Patch name must not contain path separators");
1394
+ }
1395
+ if (normalized.includes("..")) {
1396
+ throw new CLIError("Patch name must not contain '..'");
1397
+ }
1398
+ if (/[\x00-\x1f\x7f]/.test(normalized)) {
1399
+ throw new CLIError("Patch name must not contain control characters");
1400
+ }
1401
+ if (!/^[a-z0-9_-]+$/.test(normalized)) {
1402
+ throw new CLIError("Patch name may only contain characters [a-z0-9_-]");
1403
+ }
1404
+ if (normalized.length > MAX_NAME_LENGTH) {
1405
+ throw new CLIError(`Patch name exceeds ${MAX_NAME_LENGTH} characters after normalization`);
1406
+ }
1407
+ return normalized;
1408
+ }
1409
+ /**
1410
+ * Creates `<patch_folder>/<stamp>_<normalized_name>.yaml` with exclusive
1411
+ * file creation. On a millisecond-stamp collision, mints a later stamp
1412
+ * and retries. Returns the created path.
1413
+ */
1414
+ create(name) {
1415
+ const normalized = _PatchCreator.normalizeName(name);
1416
+ if (!fs5.existsSync(this.patchFolder)) {
1417
+ fs5.mkdirSync(this.patchFolder, { recursive: true });
1418
+ }
1419
+ let stamp = Date.now();
1420
+ for (let attempt = 0; attempt < 1e3; attempt++) {
1421
+ const filePath = path4.join(this.patchFolder, `${stamp}_${normalized}.yaml`);
1422
+ try {
1423
+ fs5.writeFileSync(filePath, SCAFFOLD, { flag: "wx" });
1424
+ return filePath;
1425
+ } catch (error) {
1426
+ if (error && error.code === "EEXIST") {
1427
+ stamp += 1;
1428
+ continue;
1429
+ }
1430
+ throw error;
1431
+ }
1432
+ }
1433
+ throw new CLIError("Unable to create patch file: too many filename collisions");
1434
+ }
1435
+ };
602
1436
  }
603
1437
  });
604
1438
 
@@ -606,7 +1440,7 @@ var init_MigrationDialectParser = __esm({
606
1440
  function isPromiseLike(value) {
607
1441
  return !!value && typeof value.then === "function";
608
1442
  }
609
- var BaseSQLRunner, SQLRunner, SQLiteRunner;
1443
+ var BaseSQLRunner, SQLRunner, SQLiteRunner, PgRunner;
610
1444
  var init_SQLRunner = __esm({
611
1445
  "framework/SQLRunner.ts"() {
612
1446
  BaseSQLRunner = class {
@@ -819,18 +1653,125 @@ ${sql}
819
1653
  });
820
1654
  }
821
1655
  };
1656
+ PgRunner = class _PgRunner extends BaseSQLRunner {
1657
+ constructor(connection) {
1658
+ super();
1659
+ this.connection = connection;
1660
+ }
1661
+ /** `?` -> `$1`, `$2`, ... outside of string literals / comments. */
1662
+ static toPositional(sql) {
1663
+ let out = "";
1664
+ let n = 0;
1665
+ let inSingle = false;
1666
+ let inDouble = false;
1667
+ let inLineComment = false;
1668
+ let inBlockComment = false;
1669
+ for (let i = 0; i < sql.length; i++) {
1670
+ const ch = sql[i];
1671
+ const next = sql[i + 1];
1672
+ if (inLineComment) {
1673
+ out += ch;
1674
+ if (ch === "\n") inLineComment = false;
1675
+ continue;
1676
+ }
1677
+ if (inBlockComment) {
1678
+ out += ch;
1679
+ if (ch === "*" && next === "/") {
1680
+ out += next;
1681
+ i++;
1682
+ inBlockComment = false;
1683
+ }
1684
+ continue;
1685
+ }
1686
+ if (inSingle) {
1687
+ out += ch;
1688
+ if (ch === "'") inSingle = false;
1689
+ continue;
1690
+ }
1691
+ if (inDouble) {
1692
+ out += ch;
1693
+ if (ch === '"') inDouble = false;
1694
+ continue;
1695
+ }
1696
+ if (ch === "-" && next === "-") {
1697
+ out += ch;
1698
+ inLineComment = true;
1699
+ continue;
1700
+ }
1701
+ if (ch === "/" && next === "*") {
1702
+ out += ch + next;
1703
+ i++;
1704
+ inBlockComment = true;
1705
+ continue;
1706
+ }
1707
+ if (ch === "'") {
1708
+ out += ch;
1709
+ inSingle = true;
1710
+ continue;
1711
+ }
1712
+ if (ch === '"') {
1713
+ out += ch;
1714
+ inDouble = true;
1715
+ continue;
1716
+ }
1717
+ if (ch === "?") {
1718
+ out += `$${++n}`;
1719
+ continue;
1720
+ }
1721
+ out += ch;
1722
+ }
1723
+ return out;
1724
+ }
1725
+ run(sql, params) {
1726
+ return __async(this, null, function* () {
1727
+ if (params.length > 0) {
1728
+ return yield this.connection.query(_PgRunner.toPositional(sql), params);
1729
+ }
1730
+ return yield this.connection.query(sql);
1731
+ });
1732
+ }
1733
+ _query(_0) {
1734
+ return __async(this, arguments, function* (sql, params = []) {
1735
+ const result = yield this.run(sql, params);
1736
+ return [result.rows, result];
1737
+ });
1738
+ }
1739
+ _execute(_0) {
1740
+ return __async(this, arguments, function* (sql, params = []) {
1741
+ var _a;
1742
+ const result = yield this.run(sql, params);
1743
+ return [{ changes: (_a = result.rowCount) != null ? _a : 0, lastID: void 0 }, result];
1744
+ });
1745
+ }
1746
+ _end() {
1747
+ return __async(this, null, function* () {
1748
+ if (this.connection && typeof this.connection.end === "function") {
1749
+ yield this.connection.end();
1750
+ }
1751
+ });
1752
+ }
1753
+ };
822
1754
  }
823
1755
  });
824
1756
 
825
1757
  // framework/MigrationRunner.ts
826
- import fs3 from "fs";
827
- import mysql from "mysql2/promise";
828
- import * as sqlite from "sqlite";
829
- import * as sqlite3 from "sqlite3";
830
- function toError(error) {
1758
+ import fs6 from "fs";
1759
+ function toError2(error) {
831
1760
  if (error instanceof Error) return error;
832
1761
  return new Error(String(error));
833
1762
  }
1763
+ function makeRunner(database, conn) {
1764
+ switch (database) {
1765
+ case "sql":
1766
+ return new SQLRunner(conn);
1767
+ case "sqlite":
1768
+ return new SQLiteRunner(conn);
1769
+ case "pg":
1770
+ return new PgRunner(conn);
1771
+ default:
1772
+ throw ConfigurationError.unknownDatabaseType(database);
1773
+ }
1774
+ }
834
1775
  var MigrationRunnerFactory, MySQLMigrationRunner, FileMigrationConfigReader, MigrationCreator;
835
1776
  var init_MigrationRunner = __esm({
836
1777
  "framework/MigrationRunner.ts"() {
@@ -838,6 +1779,9 @@ var init_MigrationRunner = __esm({
838
1779
  init_MigrationSetup();
839
1780
  init_MigrationFilter();
840
1781
  init_MigrationDialectParser();
1782
+ init_PatchRunner();
1783
+ init_PatchCreator();
1784
+ init_PatchTypes();
841
1785
  init_SQLRunner();
842
1786
  init_errors();
843
1787
  MigrationRunnerFactory = class _MigrationRunnerFactory {
@@ -848,14 +1792,17 @@ var init_MigrationRunner = __esm({
848
1792
  return __async(this, null, function* () {
849
1793
  const configReader = new FileMigrationConfigReader(configFile);
850
1794
  const config = configReader.loadFile();
1795
+ let factoryOwnsConnection = false;
851
1796
  if (!conn) {
852
1797
  conn = yield this.createConnection(config);
1798
+ factoryOwnsConnection = true;
853
1799
  }
854
- return new _MigrationRunnerFactory().create(config, conn);
1800
+ return new _MigrationRunnerFactory().create(config, conn, factoryOwnsConnection);
855
1801
  });
856
1802
  }
857
1803
  static createConnection(config) {
858
1804
  return __async(this, null, function* () {
1805
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
859
1806
  let conn = null;
860
1807
  switch (config.database) {
861
1808
  case "sql":
@@ -866,23 +1813,42 @@ var init_MigrationRunner = __esm({
866
1813
  password: process.env.SQL_PASSWORD
867
1814
  }, config.sql);
868
1815
  try {
869
- conn = yield mysql.createConnection(settings);
1816
+ const mysql = yield import("mysql2/promise");
1817
+ conn = yield ((_b = (_a = mysql.default) == null ? void 0 : _a.createConnection) != null ? _b : mysql.createConnection)(settings);
870
1818
  return conn;
871
1819
  } catch (error) {
872
- throw DatabaseConnectionError.connectionFailed("sql", toError(error).message);
1820
+ throw DatabaseConnectionError.connectionFailed("sql", toError2(error).message);
873
1821
  }
874
1822
  case "sqlite":
875
1823
  if (!config.sqlite) {
876
1824
  throw ConfigurationError.missingDatabaseConfiguration("sqlite");
877
1825
  }
878
1826
  try {
879
- conn = yield sqlite.open({
1827
+ const sqlite = yield import("sqlite");
1828
+ const sqlite3 = yield import("sqlite3");
1829
+ conn = yield ((_d = (_c = sqlite.default) == null ? void 0 : _c.open) != null ? _d : sqlite.open)({
880
1830
  filename: config.sqlite.database,
881
- driver: sqlite3.Database
1831
+ driver: (_f = (_e = sqlite3.default) == null ? void 0 : _e.Database) != null ? _f : sqlite3.Database
882
1832
  });
883
1833
  return conn;
884
1834
  } catch (error) {
885
- throw DatabaseConnectionError.connectionFailed("sqlite", toError(error).message);
1835
+ throw DatabaseConnectionError.connectionFailed("sqlite", toError2(error).message);
1836
+ }
1837
+ case "pg":
1838
+ if (!config.pg && !process.env.DATABASE_URL) {
1839
+ throw ConfigurationError.missingDatabaseConfiguration("pg");
1840
+ }
1841
+ try {
1842
+ const pgcfg = (_g = config.pg) != null ? _g : {};
1843
+ const connectionString = (_h = pgcfg.connectionString) != null ? _h : process.env.DATABASE_URL;
1844
+ const settings2 = connectionString ? { connectionString, ssl: pgcfg.ssl } : __spreadProps(__spreadValues({}, pgcfg), { password: (_i = pgcfg.password) != null ? _i : process.env.PG_PASSWORD });
1845
+ const pg = yield import("pg");
1846
+ const Client = (_k = (_j = pg.default) == null ? void 0 : _j.Client) != null ? _k : pg.Client;
1847
+ conn = new Client(settings2);
1848
+ yield conn.connect();
1849
+ return conn;
1850
+ } catch (error) {
1851
+ throw DatabaseConnectionError.connectionFailed("pg", toError2(error).message);
886
1852
  }
887
1853
  default:
888
1854
  throw ConfigurationError.unknownDatabaseType(config.database);
@@ -896,7 +1862,7 @@ var init_MigrationRunner = __esm({
896
1862
  return new _MigrationRunnerFactory().createEmpty(config);
897
1863
  });
898
1864
  }
899
- create(config, conn) {
1865
+ create(config, conn, factoryOwnsConnection = false) {
900
1866
  return __async(this, null, function* () {
901
1867
  let sqlrunner;
902
1868
  let driverConnection = conn;
@@ -904,42 +1870,34 @@ var init_MigrationRunner = __esm({
904
1870
  sqlrunner = conn;
905
1871
  driverConnection = null;
906
1872
  } else {
907
- switch (config.database) {
908
- case "sql":
909
- sqlrunner = new SQLRunner(conn);
910
- break;
911
- case "sqlite":
912
- sqlrunner = new SQLiteRunner(conn);
913
- break;
914
- default:
915
- throw ConfigurationError.unknownDatabaseType(config.database);
916
- }
1873
+ sqlrunner = makeRunner(config.database, conn);
917
1874
  }
918
1875
  const setup = new MigrationSetup(sqlrunner, config);
919
1876
  const read_strategy = this.getReadStategy(config);
920
1877
  const migration_files = new MigrationDirectoryReader(config.migration_folder, read_strategy, sqlrunner, config.database);
921
- yield setup.setup();
922
- return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, driverConnection);
1878
+ const runner = new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, driverConnection);
1879
+ try {
1880
+ yield runner.setup();
1881
+ } catch (error) {
1882
+ if (factoryOwnsConnection) {
1883
+ try {
1884
+ yield sqlrunner.end();
1885
+ } catch (e) {
1886
+ }
1887
+ }
1888
+ throw error;
1889
+ }
1890
+ return runner;
923
1891
  });
924
1892
  }
925
1893
  createEmpty(config) {
926
1894
  return __async(this, null, function* () {
927
1895
  const conn = null;
928
- let sqlrunner;
929
- switch (config.database) {
930
- case "sql":
931
- sqlrunner = new SQLRunner(conn);
932
- break;
933
- case "sqlite":
934
- sqlrunner = new SQLiteRunner(conn);
935
- break;
936
- default:
937
- throw ConfigurationError.unknownDatabaseType(config.database);
938
- }
1896
+ const sqlrunner = makeRunner(config.database, conn);
939
1897
  const setup = new MigrationSetup(sqlrunner, config);
940
1898
  const read_strategy = this.getReadStategy(config);
941
1899
  const migration_files = new MigrationDirectoryReader(config.migration_folder, read_strategy, sqlrunner, config.database);
942
- return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, conn);
1900
+ return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, conn, false);
943
1901
  });
944
1902
  }
945
1903
  getReadStategy(config) {
@@ -948,39 +1906,85 @@ var init_MigrationRunner = __esm({
948
1906
  return MySqlDialectParser;
949
1907
  case "sqlite":
950
1908
  return SqliteDialectParser;
1909
+ case "pg":
1910
+ return PgDialectParser;
951
1911
  default:
952
1912
  throw ConfigurationError.unknownDatabaseType(config.database);
953
1913
  }
954
1914
  }
955
1915
  };
956
1916
  MySQLMigrationRunner = class {
957
- constructor(config, directory, setupRunner, sqlrunner, connection) {
1917
+ constructor(config, directory, setupRunner, sqlrunner, connection, preflightEnabled = true) {
958
1918
  this.config = config;
959
1919
  this.directory = directory;
960
1920
  this.setupRunner = setupRunner;
961
1921
  this.sqlrunner = sqlrunner;
962
1922
  this.connection = connection;
1923
+ this.preflightEnabled = preflightEnabled;
1924
+ /**
1925
+ * Memoized in-flight preflight promise. Simultaneous or repeated calls
1926
+ * to setup() on one runner execute the preflight (migration table setup
1927
+ * + patch application) exactly once. Cleared after rejection so a caller
1928
+ * may retry after fixing the cause.
1929
+ */
1930
+ this.preflightPromise = null;
1931
+ this.lastPatchResults = [];
963
1932
  }
964
1933
  setup() {
1934
+ return __async(this, null, function* () {
1935
+ if (!this.preflightEnabled) {
1936
+ return;
1937
+ }
1938
+ if (!this.preflightPromise) {
1939
+ this.preflightPromise = this.runPreflight();
1940
+ this.preflightPromise.catch(() => {
1941
+ this.preflightPromise = null;
1942
+ });
1943
+ }
1944
+ return this.preflightPromise;
1945
+ });
1946
+ }
1947
+ runPreflight() {
965
1948
  return __async(this, null, function* () {
966
1949
  try {
967
1950
  yield this.setupRunner.setup();
968
1951
  } catch (error) {
969
- throw new MigrationExecutionError("Failed to set up migration database", void 0, void 0, toError(error));
1952
+ throw new MigrationExecutionError("Failed to set up migration database", void 0, void 0, toError2(error));
970
1953
  }
1954
+ const patchRunner = new PatchRunner(this.sqlrunner, this.config);
1955
+ this.lastPatchResults = yield patchRunner.applyPending();
1956
+ });
1957
+ }
1958
+ /**
1959
+ * Delegates to the same idempotent preflight; returns the results of the
1960
+ * patch pass that ran (or is running) for this runner.
1961
+ */
1962
+ applyPendingPatches() {
1963
+ return __async(this, null, function* () {
1964
+ yield this.setup();
1965
+ return this.lastPatchResults;
971
1966
  });
972
1967
  }
1968
+ /**
1969
+ * Scaffolds a new ledger patch file and returns the created path.
1970
+ * Never connects to a database.
1971
+ */
1972
+ createPatch(name) {
1973
+ const creator = new PatchCreator(resolvePatchFolder(this.config));
1974
+ return creator.create(name);
1975
+ }
973
1976
  terminate() {
974
1977
  return __async(this, null, function* () {
975
1978
  try {
976
1979
  yield this.setupRunner.teardown();
977
1980
  } catch (error) {
978
- throw new MigrationExecutionError("Failed to tear down migration database", void 0, void 0, toError(error));
1981
+ throw new MigrationExecutionError("Failed to tear down migration database", void 0, void 0, toError2(error));
979
1982
  }
980
1983
  });
981
1984
  }
982
1985
  getMigrationsHistory() {
983
1986
  return __async(this, null, function* () {
1987
+ yield this.setup();
984
1988
  try {
985
1989
  const results = yield this.sqlrunner.query(`
986
1990
  select *
@@ -988,16 +1992,17 @@ var init_MigrationRunner = __esm({
988
1992
  `);
989
1993
  return results[0];
990
1994
  } catch (error) {
991
- throw new MigrationExecutionError("Failed to get migration history", void 0, void 0, toError(error));
1995
+ throw new MigrationExecutionError("Failed to get migration history", void 0, void 0, toError2(error));
992
1996
  }
993
1997
  });
994
1998
  }
995
1999
  getMigrations() {
996
2000
  return __async(this, null, function* () {
2001
+ yield this.setup();
997
2002
  try {
998
2003
  return this.directory.loadMigrations(this.config.migration_table, this.connection);
999
2004
  } catch (error) {
1000
- throw new MigrationExecutionError("Failed to load migrations", void 0, void 0, toError(error));
2005
+ throw new MigrationExecutionError("Failed to load migrations", void 0, void 0, toError2(error));
1001
2006
  }
1002
2007
  });
1003
2008
  }
@@ -1010,7 +2015,7 @@ var init_MigrationRunner = __esm({
1010
2015
  if (error instanceof MigrationExecutionError) {
1011
2016
  throw error;
1012
2017
  }
1013
- throw new MigrationExecutionError("Failed to get pending migrations", void 0, void 0, toError(error));
2018
+ throw new MigrationExecutionError("Failed to get pending migrations", void 0, void 0, toError2(error));
1014
2019
  }
1015
2020
  });
1016
2021
  }
@@ -1023,12 +2028,13 @@ var init_MigrationRunner = __esm({
1023
2028
  if (error instanceof MigrationExecutionError) {
1024
2029
  throw error;
1025
2030
  }
1026
- throw new MigrationExecutionError("Failed to get completed migrations", void 0, void 0, toError(error));
2031
+ throw new MigrationExecutionError("Failed to get completed migrations", void 0, void 0, toError2(error));
1027
2032
  }
1028
2033
  });
1029
2034
  }
1030
2035
  migrate(migrationNodes, forward) {
1031
2036
  return __async(this, null, function* () {
2037
+ yield this.setup();
1032
2038
  for (let node of migrationNodes) {
1033
2039
  try {
1034
2040
  if (forward) {
@@ -1041,7 +2047,7 @@ var init_MigrationRunner = __esm({
1041
2047
  `Failed to ${forward ? "apply" : "rollback"} migration`,
1042
2048
  node.name || String(node),
1043
2049
  forward ? node.up_sql() : node.down_sql(),
1044
- toError(error)
2050
+ toError2(error)
1045
2051
  );
1046
2052
  }
1047
2053
  }
@@ -1049,6 +2055,7 @@ var init_MigrationRunner = __esm({
1049
2055
  }
1050
2056
  reset() {
1051
2057
  return __async(this, null, function* () {
2058
+ yield this.setup();
1052
2059
  try {
1053
2060
  let migrations = yield this.getMigrations();
1054
2061
  const rollback = yield migration_filter(migrations, true);
@@ -1060,7 +2067,7 @@ var init_MigrationRunner = __esm({
1060
2067
  if (error instanceof MigrationExecutionError) {
1061
2068
  throw error;
1062
2069
  }
1063
- throw new MigrationExecutionError("Failed to reset migrations", void 0, void 0, toError(error));
2070
+ throw new MigrationExecutionError("Failed to reset migrations", void 0, void 0, toError2(error));
1064
2071
  }
1065
2072
  });
1066
2073
  }
@@ -1069,7 +2076,7 @@ var init_MigrationRunner = __esm({
1069
2076
  const creator = new MigrationCreator(this.config);
1070
2077
  creator.create(name);
1071
2078
  } catch (error) {
1072
- throw new MigrationExecutionError(`Failed to create migration: ${name}`, void 0, void 0, toError(error));
2079
+ throw new MigrationExecutionError(`Failed to create migration: ${name}`, void 0, void 0, toError2(error));
1073
2080
  }
1074
2081
  }
1075
2082
  close() {
@@ -1078,7 +2085,7 @@ var init_MigrationRunner = __esm({
1078
2085
  try {
1079
2086
  yield this.sqlrunner.end();
1080
2087
  } catch (error) {
1081
- throw new DatabaseConnectionError(`Failed to close database connection: ${toError(error).message}`);
2088
+ throw new DatabaseConnectionError(`Failed to close database connection: ${toError2(error).message}`);
1082
2089
  }
1083
2090
  }
1084
2091
  });
@@ -1092,7 +2099,7 @@ var init_MigrationRunner = __esm({
1092
2099
  return __async(this, null, function* () {
1093
2100
  try {
1094
2101
  console.log(`Checking for ${config_file}`);
1095
- const config_exist = fs3.existsSync(config_file);
2102
+ const config_exist = fs6.existsSync(config_file);
1096
2103
  if (!config_exist) {
1097
2104
  console.log(`Creating ${config_file}`);
1098
2105
  const default_config = {
@@ -1106,11 +2113,11 @@ var init_MigrationRunner = __esm({
1106
2113
  "password": ""
1107
2114
  }
1108
2115
  };
1109
- fs3.writeFileSync(config_file, JSON.stringify(default_config, null, 2));
2116
+ fs6.writeFileSync(config_file, JSON.stringify(default_config, null, 2));
1110
2117
  console.log(`Created ${config_file}`);
1111
2118
  }
1112
2119
  } catch (error) {
1113
- throw new ConfigurationError(`Failed to initialize config file: ${toError(error).message}`);
2120
+ throw new ConfigurationError(`Failed to initialize config file: ${toError2(error).message}`);
1114
2121
  }
1115
2122
  });
1116
2123
  }
@@ -1121,7 +2128,7 @@ var init_MigrationRunner = __esm({
1121
2128
  }
1122
2129
  loadFile() {
1123
2130
  try {
1124
- const fileContent = fs3.readFileSync(this.configFile);
2131
+ const fileContent = fs6.readFileSync(this.configFile);
1125
2132
  const config = JSON.parse(fileContent.toString());
1126
2133
  if (!config.migration_folder) {
1127
2134
  throw ConfigurationError.missingRequiredProperty("migration_folder");
@@ -1134,6 +2141,8 @@ var init_MigrationRunner = __esm({
1134
2141
  config.database = "sql";
1135
2142
  } else if (config.sqlite) {
1136
2143
  config.database = "sqlite";
2144
+ } else if (config.pg) {
2145
+ config.database = "pg";
1137
2146
  } else {
1138
2147
  throw ConfigurationError.missingRequiredProperty("database");
1139
2148
  }
@@ -1143,7 +2152,7 @@ var init_MigrationRunner = __esm({
1143
2152
  if (error instanceof ConfigurationError) {
1144
2153
  throw error;
1145
2154
  }
1146
- const err = toError(error);
2155
+ const err = toError2(error);
1147
2156
  if (err.message.includes("ENOENT")) {
1148
2157
  throw new ConfigurationError(`Config file not found: ${this.configFile}`);
1149
2158
  }
@@ -1160,16 +2169,16 @@ var init_MigrationRunner = __esm({
1160
2169
  throw new CLIError("Migration name is required");
1161
2170
  }
1162
2171
  try {
1163
- if (!fs3.existsSync(this.config.migration_folder)) {
1164
- fs3.mkdirSync(this.config.migration_folder, { recursive: true });
2172
+ if (!fs6.existsSync(this.config.migration_folder)) {
2173
+ fs6.mkdirSync(this.config.migration_folder, { recursive: true });
1165
2174
  }
1166
2175
  const now_timestamp = Date.now();
1167
2176
  const filename_up = `${now_timestamp}_${name}.up.sql`;
1168
2177
  const filename_down = `${now_timestamp}_${name}.down.sql`;
1169
- fs3.writeFileSync(`${this.config.migration_folder}/${filename_up}`, `
2178
+ fs6.writeFileSync(`${this.config.migration_folder}/${filename_up}`, `
1170
2179
  -- Write your up migration here
1171
2180
  `.trim());
1172
- fs3.writeFileSync(`${this.config.migration_folder}/${filename_down}`, `
2181
+ fs6.writeFileSync(`${this.config.migration_folder}/${filename_down}`, `
1173
2182
  -- Write your down migration here
1174
2183
  `.trim());
1175
2184
  console.log(`Created migration files:`);
@@ -1179,7 +2188,7 @@ var init_MigrationRunner = __esm({
1179
2188
  if (error instanceof CLIError) {
1180
2189
  throw error;
1181
2190
  }
1182
- throw new MigrationExecutionError(`Failed to create migration files: ${toError(error).message}`);
2191
+ throw new MigrationExecutionError(`Failed to create migration files: ${toError2(error).message}`);
1183
2192
  }
1184
2193
  }
1185
2194
  };
@@ -1187,10 +2196,10 @@ var init_MigrationRunner = __esm({
1187
2196
  });
1188
2197
 
1189
2198
  // framework/SeedRunner.ts
1190
- import fs4 from "fs";
1191
- import path2 from "path";
2199
+ import fs7 from "fs";
2200
+ import path5 from "path";
1192
2201
  import { pathToFileURL } from "url";
1193
- import Ajv from "ajv";
2202
+ import Ajv2 from "ajv";
1194
2203
  import addFormats from "ajv-formats";
1195
2204
  import { tsImport } from "tsx/esm/api";
1196
2205
  function resolveAlias(name, aliasMap) {
@@ -1199,10 +2208,10 @@ function resolveAlias(name, aliasMap) {
1199
2208
  return (_a = aliasMap[name]) != null ? _a : name;
1200
2209
  }
1201
2210
  function walkForFile(rootDir, fileName) {
1202
- if (!fs4.existsSync(rootDir)) return null;
1203
- const entries = fs4.readdirSync(rootDir, { withFileTypes: true });
2211
+ if (!fs7.existsSync(rootDir)) return null;
2212
+ const entries = fs7.readdirSync(rootDir, { withFileTypes: true });
1204
2213
  for (const entry of entries) {
1205
- const full = path2.join(rootDir, entry.name);
2214
+ const full = path5.join(rootDir, entry.name);
1206
2215
  if (entry.isDirectory()) {
1207
2216
  const found = walkForFile(full, fileName);
1208
2217
  if (found) return found;
@@ -1296,39 +2305,39 @@ function resolveSeed(name, migrationConfig, options) {
1296
2305
  }
1297
2306
  function loadJson(filePath) {
1298
2307
  return __async(this, null, function* () {
1299
- const content = yield fs4.promises.readFile(filePath, "utf8");
2308
+ const content = yield fs7.promises.readFile(filePath, "utf8");
1300
2309
  return JSON.parse(content);
1301
2310
  });
1302
2311
  }
1303
2312
  function createValidator() {
1304
- const ajv = new Ajv({ allErrors: true, strict: false });
1305
- addFormats(ajv);
1306
- return ajv;
2313
+ const ajv2 = new Ajv2({ allErrors: true, strict: false });
2314
+ addFormats(ajv2);
2315
+ return ajv2;
1307
2316
  }
1308
2317
  function validateData(schemaPath, data, validate, log) {
1309
2318
  return __async(this, null, function* () {
1310
2319
  if (!validate || !schemaPath) return;
1311
- const content = yield fs4.promises.readFile(schemaPath, "utf8");
2320
+ const content = yield fs7.promises.readFile(schemaPath, "utf8");
1312
2321
  const schema = JSON.parse(content);
1313
- const ajv = createValidator();
1314
- const validateFn = ajv.compile(schema);
2322
+ const ajv2 = createValidator();
2323
+ const validateFn = ajv2.compile(schema);
1315
2324
  const ok = validateFn(data);
1316
2325
  if (!ok) {
1317
2326
  log == null ? void 0 : log(`Validation failed for seed data (${schemaPath})`);
1318
- throw new Error(`Seed data validation failed: ${ajv.errorsText(validateFn.errors || [])}`);
2327
+ throw new Error(`Seed data validation failed: ${ajv2.errorsText(validateFn.errors || [])}`);
1319
2328
  }
1320
2329
  });
1321
2330
  }
1322
2331
  function runSqlSeed(runner, resolved, direction) {
1323
2332
  return __async(this, null, function* () {
1324
2333
  const sqlPath = direction === "up" ? resolved.upPath : resolved.downPath;
1325
- const sql = yield fs4.promises.readFile(sqlPath, "utf8");
2334
+ const sql = yield fs7.promises.readFile(sqlPath, "utf8");
1326
2335
  yield runner.query(sql);
1327
2336
  });
1328
2337
  }
1329
2338
  function loadSeedModule(modulePath) {
1330
2339
  return __async(this, null, function* () {
1331
- const resolved = path2.resolve(modulePath);
2340
+ const resolved = path5.resolve(modulePath);
1332
2341
  if (resolved.endsWith(".ts")) {
1333
2342
  const fileUrl = pathToFileURL(resolved).href;
1334
2343
  return tsImport(fileUrl, fileUrl);
@@ -1432,6 +2441,10 @@ var require_cli = __commonJS({
1432
2441
  init_errors();
1433
2442
  init_SeedRunner();
1434
2443
  var args = MigrationCLIFactory.setup(process.argv);
2444
+ if (args.flags.help) {
2445
+ printUsage();
2446
+ process.exit(0);
2447
+ }
1435
2448
  if (!args.commands || args.commands.length === 0) {
1436
2449
  console.error("Error: No command specified");
1437
2450
  printUsage();
@@ -1439,7 +2452,7 @@ var require_cli = __commonJS({
1439
2452
  }
1440
2453
  var commands = args.commands;
1441
2454
  var command = commands[0];
1442
- var load_database = !["init", "create", "help"].includes(command.toLowerCase());
2455
+ var load_database = !["init", "create", "help", "patch"].includes(command.toLowerCase());
1443
2456
  var config_file = args.flags.config || "proper.json";
1444
2457
  if (command.toLowerCase() === "help") {
1445
2458
  printUsage();
@@ -1448,9 +2461,17 @@ var require_cli = __commonJS({
1448
2461
  console.log(`Loading database: ${load_database}`);
1449
2462
  var pending_runner = load_database ? MigrationRunnerFactory.create(config_file) : MigrationRunnerFactory.createEmpty(config_file);
1450
2463
  pending_runner.then((runner) => __async(null, null, function* () {
2464
+ let failed = false;
1451
2465
  try {
1452
2466
  if (load_database) {
1453
- yield runner.setup();
2467
+ const patch_results = yield runner.applyPendingPatches();
2468
+ for (const patch of patch_results) {
2469
+ if (patch.status === "applied") {
2470
+ const changed = patch.operations.filter((op) => op.changed).length;
2471
+ const noop = patch.operations.length - changed;
2472
+ console.log(`Applied patch ${patch.fileName}: ${changed} changed, ${noop} no-op operation(s)`);
2473
+ }
2474
+ }
1454
2475
  }
1455
2476
  switch (command.toLowerCase()) {
1456
2477
  case "up":
@@ -1470,7 +2491,6 @@ var require_cli = __commonJS({
1470
2491
  } else {
1471
2492
  console.log("No pending migrations");
1472
2493
  }
1473
- yield runner.close();
1474
2494
  break;
1475
2495
  case "down":
1476
2496
  const migrations_rollback = yield runner.getMigrations();
@@ -1491,13 +2511,11 @@ var require_cli = __commonJS({
1491
2511
  } else {
1492
2512
  console.log("No migrations to roll back");
1493
2513
  }
1494
- yield runner.close();
1495
2514
  break;
1496
2515
  case "reset":
1497
2516
  console.log("Resetting all migrations...");
1498
2517
  yield runner.reset();
1499
2518
  console.log("Reset completed successfully");
1500
- yield runner.close();
1501
2519
  break;
1502
2520
  case "create":
1503
2521
  let filename = args.flags.name || commands[1];
@@ -1506,11 +2524,22 @@ var require_cli = __commonJS({
1506
2524
  }
1507
2525
  filename = filename.replace(/\s/g, "_");
1508
2526
  runner.createMigration(filename);
1509
- runner.close();
1510
2527
  break;
2528
+ case "patch": {
2529
+ const patch_name = args.flags.name || commands[1];
2530
+ if (!patch_name) {
2531
+ throw new CLIError("Patch name is required. Usage: proper patch <name>");
2532
+ }
2533
+ const created_path = runner.createPatch(patch_name);
2534
+ console.log(`Created patch file:`);
2535
+ console.log(` ${created_path}`);
2536
+ console.log("");
2537
+ console.log("Complete this file before running another database-backed");
2538
+ console.log("Proper command: 'operations: []' is intentionally not runnable.");
2539
+ break;
2540
+ }
1511
2541
  case "init":
1512
2542
  yield runner.init(config_file);
1513
- runner.close();
1514
2543
  break;
1515
2544
  case "status":
1516
2545
  const { printTable } = __require("console-table-printer");
@@ -1524,7 +2553,6 @@ var require_cli = __commonJS({
1524
2553
  };
1525
2554
  }));
1526
2555
  printTable(yield Promise.all(table));
1527
- runner.close();
1528
2556
  break;
1529
2557
  case "query":
1530
2558
  const sql_query = args.flags.query || commands[1];
@@ -1568,7 +2596,6 @@ Returned ${results.length} row(s)`);
1568
2596
  } catch (error) {
1569
2597
  throw new CLIError(`Query execution failed: ${error.message}`);
1570
2598
  }
1571
- yield runner.close();
1572
2599
  break;
1573
2600
  case "seed": {
1574
2601
  const subCommands = commands.slice(1);
@@ -1601,7 +2628,6 @@ Returned ${results.length} row(s)`);
1601
2628
  const reader = new FileMigrationConfigReader(config_file);
1602
2629
  const migrationConfig = reader.loadFile();
1603
2630
  yield runSeedsWithRunner(runner, migrationConfig, action, seedOptions);
1604
- yield runner.close();
1605
2631
  break;
1606
2632
  }
1607
2633
  default:
@@ -1612,9 +2638,25 @@ Returned ${results.length} row(s)`);
1612
2638
  if (error.stack && process.env.DEBUG) {
1613
2639
  console.error(error.stack);
1614
2640
  }
2641
+ failed = true;
2642
+ } finally {
2643
+ try {
2644
+ yield runner.close();
2645
+ } catch (closeError) {
2646
+ console.error(`Error closing connection: ${closeError.message}`);
2647
+ failed = true;
2648
+ }
2649
+ }
2650
+ if (failed) {
1615
2651
  process.exit(1);
1616
2652
  }
1617
- }));
2653
+ })).catch((error) => {
2654
+ console.error(`Error: ${error.message}`);
2655
+ if (error.stack && process.env.DEBUG) {
2656
+ console.error(error.stack);
2657
+ }
2658
+ process.exit(1);
2659
+ });
1618
2660
  function printUsage() {
1619
2661
  console.log(`
1620
2662
  SQL Proper - Database migration tool
@@ -1627,6 +2669,7 @@ Commands:
1627
2669
  down Roll back completed migrations
1628
2670
  reset Roll back all migrations and reapply them
1629
2671
  create Create a new migration
2672
+ patch Create a new ledger patch file (repairs migration history; no database access)
1630
2673
  init Initialize a new config file
1631
2674
  status Show migration status
1632
2675
  query Execute a SQL query and display results
@@ -1645,6 +2688,7 @@ Examples:
1645
2688
  proper down --increment 3 Roll back the last 3 applied migrations
1646
2689
  proper down --all Roll back all completed migrations
1647
2690
  proper create my_migration Create a new migration named "my_migration"
2691
+ proper patch fix_renamed_keys Scaffold a ledger patch file in the patch folder
1648
2692
  proper init Create a new config file
1649
2693
  proper status Show the status of all migrations
1650
2694
  proper query "select * from users" Execute a SQL query