@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.js +1120 -119
- package/bin/cli.js.map +1 -1
- package/bin/cli.mjs +1166 -122
- package/bin/cli.mjs.map +1 -1
- package/bin/index.d.mts +284 -4
- package/bin/index.d.ts +284 -4
- package/bin/index.js +1100 -108
- package/bin/index.js.map +1 -1
- package/bin/index.mjs +1087 -108
- package/bin/index.mjs.map +1 -1
- package/lib/runner.js.map +1 -1
- package/lib/runner.mjs.map +1 -1
- package/package.json +6 -3
- package/readme.md +92 -0
package/bin/cli.js
CHANGED
|
@@ -123,14 +123,10 @@ function migration_filter(_0) {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
// framework/MigrationRunner.ts
|
|
126
|
-
var
|
|
127
|
-
var import_promise = __toESM(require("mysql2/promise"));
|
|
128
|
-
var sqlite = __toESM(require("sqlite"));
|
|
129
|
-
var sqlite3 = __toESM(require("sqlite3"));
|
|
126
|
+
var import_fs6 = __toESM(require("fs"));
|
|
130
127
|
|
|
131
128
|
// framework/MigrationDirectoryReader.ts
|
|
132
|
-
var
|
|
133
|
-
var import_path = __toESM(require("path"));
|
|
129
|
+
var import_fs2 = __toESM(require("fs"));
|
|
134
130
|
|
|
135
131
|
// framework/errors.ts
|
|
136
132
|
var MigrationError = class _MigrationError extends Error {
|
|
@@ -245,6 +241,54 @@ ${originalError.message}`;
|
|
|
245
241
|
return new _MigrationExecutionError(message);
|
|
246
242
|
}
|
|
247
243
|
};
|
|
244
|
+
var PatchError = class _PatchError extends MigrationError {
|
|
245
|
+
constructor(message, patchFile, patchKey) {
|
|
246
|
+
super(`Patch Error: ${message}`);
|
|
247
|
+
this.patchFile = patchFile;
|
|
248
|
+
this.patchKey = patchKey;
|
|
249
|
+
this.name = "PatchError";
|
|
250
|
+
Object.setPrototypeOf(this, _PatchError.prototype);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
var PatchValidationError = class _PatchValidationError extends PatchError {
|
|
254
|
+
constructor(message, patchFile, patchKey) {
|
|
255
|
+
super(message, patchFile, patchKey);
|
|
256
|
+
this.name = "PatchValidationError";
|
|
257
|
+
Object.setPrototypeOf(this, _PatchValidationError.prototype);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
var PatchIntegrityError = class _PatchIntegrityError extends PatchError {
|
|
261
|
+
constructor(message, patchFile, patchKey, expectedChecksum, actualChecksum) {
|
|
262
|
+
super(message, patchFile, patchKey);
|
|
263
|
+
this.expectedChecksum = expectedChecksum;
|
|
264
|
+
this.actualChecksum = actualChecksum;
|
|
265
|
+
this.name = "PatchIntegrityError";
|
|
266
|
+
Object.setPrototypeOf(this, _PatchIntegrityError.prototype);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
var PatchConflictError = class _PatchConflictError extends PatchError {
|
|
270
|
+
constructor(message, patchFile, patchKey, operationIndex, operationVerb, migrationKeys, observedRowCounts) {
|
|
271
|
+
super(message, patchFile, patchKey);
|
|
272
|
+
this.operationIndex = operationIndex;
|
|
273
|
+
this.operationVerb = operationVerb;
|
|
274
|
+
this.migrationKeys = migrationKeys;
|
|
275
|
+
this.observedRowCounts = observedRowCounts;
|
|
276
|
+
this.name = "PatchConflictError";
|
|
277
|
+
Object.setPrototypeOf(this, _PatchConflictError.prototype);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
var PatchExecutionError = class _PatchExecutionError extends PatchError {
|
|
281
|
+
constructor(message, patchFile, patchKey, operationIndex, operationVerb, originalError) {
|
|
282
|
+
super(originalError ? `${message}
|
|
283
|
+
Original Error:
|
|
284
|
+
${originalError.message}` : message, patchFile, patchKey);
|
|
285
|
+
this.operationIndex = operationIndex;
|
|
286
|
+
this.operationVerb = operationVerb;
|
|
287
|
+
this.originalError = originalError;
|
|
288
|
+
this.name = "PatchExecutionError";
|
|
289
|
+
Object.setPrototypeOf(this, _PatchExecutionError.prototype);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
248
292
|
var CLIError = class _CLIError extends MigrationError {
|
|
249
293
|
constructor(message) {
|
|
250
294
|
super(`CLI Error: ${message}`);
|
|
@@ -407,6 +451,36 @@ var SqlMigrationBuilder = class {
|
|
|
407
451
|
}
|
|
408
452
|
};
|
|
409
453
|
|
|
454
|
+
// framework/MigrationManifest.ts
|
|
455
|
+
var import_fs = __toESM(require("fs"));
|
|
456
|
+
var import_path = __toESM(require("path"));
|
|
457
|
+
function canonicalMigrationKey(value) {
|
|
458
|
+
return value.replace(/(?:\.(mysql|sqlite|pg))?\.(up|down)\.(sql|js)$/i, "").toLowerCase();
|
|
459
|
+
}
|
|
460
|
+
function resolveMigrationFile(directory, baseName, direction, dialect) {
|
|
461
|
+
const dialectExt = dialect === "sql" ? "mysql" : dialect;
|
|
462
|
+
const dialectFile = import_path.default.join(directory, `${baseName}.${dialectExt}.${direction}.sql`);
|
|
463
|
+
if (import_fs.default.existsSync(dialectFile)) return dialectFile;
|
|
464
|
+
const genericFile = import_path.default.join(directory, `${baseName}.${direction}.sql`);
|
|
465
|
+
if (import_fs.default.existsSync(genericFile)) return genericFile;
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
function loadMigrationManifest(directory, dialect) {
|
|
469
|
+
const manifest = /* @__PURE__ */ new Map();
|
|
470
|
+
if (!import_fs.default.existsSync(directory)) return manifest;
|
|
471
|
+
const files = import_fs.default.readdirSync(directory, { withFileTypes: true }).filter((f) => f.isFile()).map((f) => f.name);
|
|
472
|
+
const uniqueKeys = /* @__PURE__ */ new Set();
|
|
473
|
+
files.forEach((file) => uniqueKeys.add(canonicalMigrationKey(file)));
|
|
474
|
+
uniqueKeys.forEach((key) => {
|
|
475
|
+
manifest.set(key, {
|
|
476
|
+
key,
|
|
477
|
+
upFile: resolveMigrationFile(directory, key, "up", dialect),
|
|
478
|
+
downFile: resolveMigrationFile(directory, key, "down", dialect)
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
return manifest;
|
|
482
|
+
}
|
|
483
|
+
|
|
410
484
|
// framework/MigrationDirectoryReader.ts
|
|
411
485
|
var MigrationDirectoryReader = class {
|
|
412
486
|
constructor(directory, read_strategy, sqlrunner, dialect = "sql") {
|
|
@@ -418,27 +492,23 @@ var MigrationDirectoryReader = class {
|
|
|
418
492
|
/**
|
|
419
493
|
* Resolves the appropriate file for a migration based on dialect.
|
|
420
494
|
* Priority: dialect-specific file > generic file
|
|
495
|
+
* File extensions: `.mysql.up.sql`, `.sqlite.up.sql`, `.pg.up.sql`.
|
|
421
496
|
*/
|
|
422
497
|
resolveFile(baseName, direction) {
|
|
423
|
-
|
|
424
|
-
const dialectFile = import_path.default.join(this.directory, `${baseName}.${dialectExt}.${direction}.sql`);
|
|
425
|
-
if (import_fs.default.existsSync(dialectFile)) return dialectFile;
|
|
426
|
-
const genericFile = import_path.default.join(this.directory, `${baseName}.${direction}.sql`);
|
|
427
|
-
if (import_fs.default.existsSync(genericFile)) return genericFile;
|
|
428
|
-
return null;
|
|
498
|
+
return resolveMigrationFile(this.directory, baseName, direction, this.dialect);
|
|
429
499
|
}
|
|
430
500
|
/**
|
|
431
501
|
* Checks if a file path is dialect-specific (contains .mysql. or .sqlite. in the name)
|
|
432
502
|
*/
|
|
433
503
|
isDialectSpecific(filePath) {
|
|
434
|
-
return /\.(mysql|sqlite)\.(up|down)\.sql$/i.test(filePath);
|
|
504
|
+
return /\.(mysql|sqlite|pg)\.(up|down)\.sql$/i.test(filePath);
|
|
435
505
|
}
|
|
436
506
|
loadMigrations(table, connection) {
|
|
437
|
-
|
|
438
|
-
const dir_content =
|
|
507
|
+
import_fs2.default.existsSync(this.directory) || import_fs2.default.mkdirSync(this.directory);
|
|
508
|
+
const dir_content = import_fs2.default.readdirSync(this.directory, { withFileTypes: true }).filter((file) => file.isFile()).map((file) => file.name);
|
|
439
509
|
const uniqueKeys = /* @__PURE__ */ new Set();
|
|
440
510
|
dir_content.forEach((file) => {
|
|
441
|
-
const key = file
|
|
511
|
+
const key = canonicalMigrationKey(file);
|
|
442
512
|
uniqueKeys.add(key);
|
|
443
513
|
});
|
|
444
514
|
const migration_sorter = {};
|
|
@@ -478,14 +548,14 @@ var MigrationDirectoryReader = class {
|
|
|
478
548
|
return builder;
|
|
479
549
|
}
|
|
480
550
|
sql_up(file) {
|
|
481
|
-
let content =
|
|
551
|
+
let content = import_fs2.default.readFileSync(file).toString();
|
|
482
552
|
if (!this.isDialectSpecific(file)) {
|
|
483
553
|
content = this.read_strategy(content);
|
|
484
554
|
}
|
|
485
555
|
return content.trim();
|
|
486
556
|
}
|
|
487
557
|
sql_down(file) {
|
|
488
|
-
let content =
|
|
558
|
+
let content = import_fs2.default.readFileSync(file).toString();
|
|
489
559
|
if (!this.isDialectSpecific(file)) {
|
|
490
560
|
content = this.read_strategy(content);
|
|
491
561
|
}
|
|
@@ -494,7 +564,23 @@ var MigrationDirectoryReader = class {
|
|
|
494
564
|
};
|
|
495
565
|
|
|
496
566
|
// framework/MigrationSetup.ts
|
|
497
|
-
var
|
|
567
|
+
var import_fs3 = __toESM(require("fs"));
|
|
568
|
+
|
|
569
|
+
// framework/PatchTypes.ts
|
|
570
|
+
var import_path2 = __toESM(require("path"));
|
|
571
|
+
var PATCH_FORMAT_VERSION = 1;
|
|
572
|
+
var DEFAULT_PATCH_TABLE = "proper_patches";
|
|
573
|
+
var PATCH_FILENAME_REGEX = new RegExp("^(?<stamp>[0-9]{13})_(?<name>[a-z0-9][a-z0-9_-]{0,119})\\.yaml$");
|
|
574
|
+
function resolvePatchFolder(config) {
|
|
575
|
+
if (config.patch_folder) return config.patch_folder;
|
|
576
|
+
const dir = import_path2.default.dirname(config.migration_folder);
|
|
577
|
+
return dir === "." && !config.migration_folder.includes(import_path2.default.sep) && !config.migration_folder.includes("/") ? "patches" : import_path2.default.join(dir, "patches");
|
|
578
|
+
}
|
|
579
|
+
function resolvePatchTable(config) {
|
|
580
|
+
return config.patch_table || DEFAULT_PATCH_TABLE;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// framework/MigrationSetup.ts
|
|
498
584
|
var MigrationSetup = class {
|
|
499
585
|
constructor(sqlrunner, config) {
|
|
500
586
|
this.sqlrunner = sqlrunner;
|
|
@@ -502,7 +588,7 @@ var MigrationSetup = class {
|
|
|
502
588
|
}
|
|
503
589
|
setup() {
|
|
504
590
|
return __async(this, null, function* () {
|
|
505
|
-
|
|
591
|
+
import_fs3.default.existsSync(this.config.migration_folder) || import_fs3.default.mkdirSync(this.config.migration_folder);
|
|
506
592
|
const tableName = this.config.migration_table;
|
|
507
593
|
const createTableSql = this.config.database === "sqlite" ? `CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
508
594
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -510,6 +596,12 @@ var MigrationSetup = class {
|
|
|
510
596
|
up TEXT,
|
|
511
597
|
down TEXT,
|
|
512
598
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
599
|
+
)` : this.config.database === "pg" ? `CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
600
|
+
id SERIAL PRIMARY KEY,
|
|
601
|
+
migration_key TEXT,
|
|
602
|
+
up TEXT,
|
|
603
|
+
down TEXT,
|
|
604
|
+
created_at TIMESTAMPTZ DEFAULT now()
|
|
513
605
|
)` : `CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
514
606
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
515
607
|
migration_key VARCHAR(255),
|
|
@@ -518,11 +610,39 @@ var MigrationSetup = class {
|
|
|
518
610
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
519
611
|
)`;
|
|
520
612
|
yield this.sqlrunner.query(createTableSql);
|
|
613
|
+
const patchTable = resolvePatchTable(this.config);
|
|
614
|
+
const createPatchTableSql = this.config.database === "sqlite" ? `CREATE TABLE IF NOT EXISTS ${patchTable} (
|
|
615
|
+
migration_table TEXT NOT NULL,
|
|
616
|
+
patch_key TEXT NOT NULL,
|
|
617
|
+
checksum TEXT NOT NULL,
|
|
618
|
+
format_version INTEGER NOT NULL,
|
|
619
|
+
description TEXT NOT NULL,
|
|
620
|
+
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
621
|
+
PRIMARY KEY (migration_table, patch_key)
|
|
622
|
+
)` : this.config.database === "pg" ? `CREATE TABLE IF NOT EXISTS ${patchTable} (
|
|
623
|
+
migration_table TEXT NOT NULL,
|
|
624
|
+
patch_key TEXT NOT NULL,
|
|
625
|
+
checksum TEXT NOT NULL,
|
|
626
|
+
format_version INTEGER NOT NULL,
|
|
627
|
+
description TEXT NOT NULL,
|
|
628
|
+
applied_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
629
|
+
PRIMARY KEY (migration_table, patch_key)
|
|
630
|
+
)` : `CREATE TABLE IF NOT EXISTS ${patchTable} (
|
|
631
|
+
migration_table VARCHAR(255) NOT NULL,
|
|
632
|
+
patch_key VARCHAR(255) NOT NULL,
|
|
633
|
+
checksum CHAR(64) NOT NULL,
|
|
634
|
+
format_version INT NOT NULL,
|
|
635
|
+
description TEXT NOT NULL,
|
|
636
|
+
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
637
|
+
PRIMARY KEY (migration_table, patch_key)
|
|
638
|
+
)`;
|
|
639
|
+
yield this.sqlrunner.query(createPatchTableSql);
|
|
521
640
|
});
|
|
522
641
|
}
|
|
523
642
|
teardown() {
|
|
524
643
|
return __async(this, null, function* () {
|
|
525
644
|
yield this.sqlrunner.execute(`DROP TABLE ${this.config.migration_table}`);
|
|
645
|
+
yield this.sqlrunner.execute(`DROP TABLE IF EXISTS ${resolvePatchTable(this.config)}`);
|
|
526
646
|
});
|
|
527
647
|
}
|
|
528
648
|
};
|
|
@@ -554,32 +674,703 @@ function MySqlDialectParser(sql) {
|
|
|
554
674
|
}
|
|
555
675
|
return result;
|
|
556
676
|
}
|
|
557
|
-
function
|
|
558
|
-
const
|
|
559
|
-
let result = "";
|
|
560
|
-
let isInSqliteBlock = true;
|
|
561
|
-
const startSqliteRegex = /^\s*--\s*\[\s*sqlite\s*\]\s*$/i;
|
|
677
|
+
function markerDialectParser(names) {
|
|
678
|
+
const startRegex = new RegExp(`^\\s*--\\s*\\[\\s*(${names.join("|")})\\s*\\]\\s*$`, "i");
|
|
562
679
|
const anyDialectRegex = /^\s*--\s*\[\s*\w+\s*\]\s*$/i;
|
|
563
|
-
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
680
|
+
return function(sql) {
|
|
681
|
+
const lines = sql.split("\n");
|
|
682
|
+
let result = "";
|
|
683
|
+
let capturing = true;
|
|
684
|
+
for (const line of lines) {
|
|
685
|
+
const trimmed = line.trim();
|
|
686
|
+
if (startRegex.test(trimmed)) {
|
|
687
|
+
capturing = true;
|
|
688
|
+
result += line + "\n";
|
|
689
|
+
continue;
|
|
690
|
+
} else if (anyDialectRegex.test(trimmed)) {
|
|
691
|
+
capturing = false;
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
if (!capturing && line.toLowerCase().includes("create index")) {
|
|
695
|
+
capturing = true;
|
|
696
|
+
}
|
|
697
|
+
if (capturing) {
|
|
698
|
+
result += line + "\n";
|
|
699
|
+
}
|
|
572
700
|
}
|
|
573
|
-
|
|
574
|
-
|
|
701
|
+
return result;
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
var SqliteDialectParser = markerDialectParser(["sqlite"]);
|
|
705
|
+
var PgDialectParser = markerDialectParser(["pg", "postgres", "postgresql"]);
|
|
706
|
+
|
|
707
|
+
// framework/PatchDirectoryReader.ts
|
|
708
|
+
var import_crypto = __toESM(require("crypto"));
|
|
709
|
+
var import_fs4 = __toESM(require("fs"));
|
|
710
|
+
var import_path3 = __toESM(require("path"));
|
|
711
|
+
|
|
712
|
+
// framework/PatchValidator.ts
|
|
713
|
+
var import_ajv = __toESM(require("ajv"));
|
|
714
|
+
var import_yaml = require("yaml");
|
|
715
|
+
var MAX_DESCRIPTION_LENGTH = 500;
|
|
716
|
+
var MAX_MIGRATION_KEY_LENGTH = 255;
|
|
717
|
+
var migrationKeySchema = {
|
|
718
|
+
type: "string",
|
|
719
|
+
minLength: 1,
|
|
720
|
+
maxLength: MAX_MIGRATION_KEY_LENGTH
|
|
721
|
+
};
|
|
722
|
+
var patchSchema = {
|
|
723
|
+
type: "object",
|
|
724
|
+
additionalProperties: false,
|
|
725
|
+
required: ["version", "description", "operations"],
|
|
726
|
+
properties: {
|
|
727
|
+
version: { type: "integer" },
|
|
728
|
+
description: { type: "string" },
|
|
729
|
+
operations: {
|
|
730
|
+
type: "array",
|
|
731
|
+
minItems: 1,
|
|
732
|
+
items: {
|
|
733
|
+
type: "object",
|
|
734
|
+
additionalProperties: false,
|
|
735
|
+
minProperties: 1,
|
|
736
|
+
maxProperties: 1,
|
|
737
|
+
properties: {
|
|
738
|
+
rename_migration: {
|
|
739
|
+
type: "object",
|
|
740
|
+
additionalProperties: false,
|
|
741
|
+
required: ["from", "to"],
|
|
742
|
+
properties: { from: migrationKeySchema, to: migrationKeySchema }
|
|
743
|
+
},
|
|
744
|
+
mark_applied: {
|
|
745
|
+
type: "object",
|
|
746
|
+
additionalProperties: false,
|
|
747
|
+
required: ["key"],
|
|
748
|
+
properties: { key: migrationKeySchema }
|
|
749
|
+
},
|
|
750
|
+
unmark_applied: {
|
|
751
|
+
type: "object",
|
|
752
|
+
additionalProperties: false,
|
|
753
|
+
required: ["key"],
|
|
754
|
+
properties: { key: migrationKeySchema }
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
575
758
|
}
|
|
576
|
-
|
|
577
|
-
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
var ajv = new import_ajv.default({ allErrors: true, strict: true });
|
|
762
|
+
var validateSchema = ajv.compile(patchSchema);
|
|
763
|
+
function fail(message, file, patchKey) {
|
|
764
|
+
throw new PatchValidationError(message, file, patchKey);
|
|
765
|
+
}
|
|
766
|
+
function checkMigrationKey(value, context, file, patchKey) {
|
|
767
|
+
if (value !== value.trim()) fail(`${context}: migration key has leading/trailing whitespace`, file, patchKey);
|
|
768
|
+
if (/[/\\]/.test(value)) fail(`${context}: migration key contains a path separator`, file, patchKey);
|
|
769
|
+
if (/[\x00-\x1f\x7f]/.test(value)) fail(`${context}: migration key contains control characters`, file, patchKey);
|
|
770
|
+
if (value.length === 0 || value.length > MAX_MIGRATION_KEY_LENGTH) {
|
|
771
|
+
fail(`${context}: migration key length out of bounds`, file, patchKey);
|
|
772
|
+
}
|
|
773
|
+
return canonicalMigrationKey(value);
|
|
774
|
+
}
|
|
775
|
+
function assertStrictYaml(doc, file, patchKey) {
|
|
776
|
+
if (doc.errors.length > 0) {
|
|
777
|
+
fail(`YAML parse error: ${doc.errors[0].message}`, file, patchKey);
|
|
778
|
+
}
|
|
779
|
+
if (doc.warnings.length > 0) {
|
|
780
|
+
fail(`YAML warning treated as error: ${doc.warnings[0].message}`, file, patchKey);
|
|
781
|
+
}
|
|
782
|
+
const visit = (node) => {
|
|
783
|
+
var _a, _b;
|
|
784
|
+
if (node == null || typeof node !== "object") return;
|
|
785
|
+
if ("source" in node && ((_a = node.constructor) == null ? void 0 : _a.name) === "Alias") {
|
|
786
|
+
fail("YAML aliases are not permitted in patch files", file, patchKey);
|
|
787
|
+
}
|
|
788
|
+
if (node.anchor) {
|
|
789
|
+
fail("YAML anchors are not permitted in patch files", file, patchKey);
|
|
790
|
+
}
|
|
791
|
+
if (node.tag && ![
|
|
792
|
+
"tag:yaml.org,2002:str",
|
|
793
|
+
"tag:yaml.org,2002:int",
|
|
794
|
+
"tag:yaml.org,2002:bool",
|
|
795
|
+
"tag:yaml.org,2002:null",
|
|
796
|
+
"tag:yaml.org,2002:map",
|
|
797
|
+
"tag:yaml.org,2002:seq"
|
|
798
|
+
].includes(node.tag)) {
|
|
799
|
+
fail(`YAML tag '${node.tag}' is not permitted in patch files`, file, patchKey);
|
|
800
|
+
}
|
|
801
|
+
if (Array.isArray(node.items)) {
|
|
802
|
+
for (const item of node.items) {
|
|
803
|
+
if (item && typeof item === "object" && "key" in item) {
|
|
804
|
+
const keyValue = (_b = item.key) == null ? void 0 : _b.value;
|
|
805
|
+
if (keyValue === "<<") fail("YAML merge keys are not permitted in patch files", file, patchKey);
|
|
806
|
+
visit(item.key);
|
|
807
|
+
visit(item.value);
|
|
808
|
+
} else {
|
|
809
|
+
visit(item);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
visit(doc.contents);
|
|
815
|
+
}
|
|
816
|
+
function parsePatchContent(content, fileName, patchKey) {
|
|
817
|
+
var _a;
|
|
818
|
+
const doc = (0, import_yaml.parseDocument)(content, {
|
|
819
|
+
uniqueKeys: true,
|
|
820
|
+
// duplicate mapping keys become errors
|
|
821
|
+
merge: false,
|
|
822
|
+
schema: "core",
|
|
823
|
+
version: "1.2"
|
|
824
|
+
});
|
|
825
|
+
assertStrictYaml(doc, fileName, patchKey);
|
|
826
|
+
const raw = doc.toJS({ mapAsMap: false });
|
|
827
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
828
|
+
fail("Patch document must be a YAML mapping", fileName, patchKey);
|
|
829
|
+
}
|
|
830
|
+
if (!validateSchema(raw)) {
|
|
831
|
+
const detail = ((_a = validateSchema.errors) != null ? _a : []).map((e) => `${e.instancePath || "/"} ${e.message}`).join("; ");
|
|
832
|
+
const anyRaw = raw;
|
|
833
|
+
if (typeof anyRaw.version === "number" && anyRaw.version !== PATCH_FORMAT_VERSION) {
|
|
834
|
+
fail(`Unknown patch format version: ${anyRaw.version} (supported: ${PATCH_FORMAT_VERSION})`, fileName, patchKey);
|
|
835
|
+
}
|
|
836
|
+
fail(`Schema validation failed: ${detail}`, fileName, patchKey);
|
|
837
|
+
}
|
|
838
|
+
const parsed = raw;
|
|
839
|
+
if (parsed.version !== PATCH_FORMAT_VERSION) {
|
|
840
|
+
fail(`Unknown patch format version: ${parsed.version} (supported: ${PATCH_FORMAT_VERSION})`, fileName, patchKey);
|
|
841
|
+
}
|
|
842
|
+
const description = parsed.description.trim();
|
|
843
|
+
if (description.length === 0) fail("description must be non-empty", fileName, patchKey);
|
|
844
|
+
if (description.length > MAX_DESCRIPTION_LENGTH) {
|
|
845
|
+
fail(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters`, fileName, patchKey);
|
|
846
|
+
}
|
|
847
|
+
const operations = parsed.operations.map((op, index) => {
|
|
848
|
+
const verbs = Object.keys(op);
|
|
849
|
+
const verb = verbs[0];
|
|
850
|
+
const context = `operation ${index} (${verb})`;
|
|
851
|
+
switch (verb) {
|
|
852
|
+
case "rename_migration": {
|
|
853
|
+
const from = checkMigrationKey(op.rename_migration.from, context, fileName, patchKey);
|
|
854
|
+
const to = checkMigrationKey(op.rename_migration.to, context, fileName, patchKey);
|
|
855
|
+
if (from === to) {
|
|
856
|
+
fail(`${context}: 'from' and 'to' are identical after canonicalization ('${from}')`, fileName, patchKey);
|
|
857
|
+
}
|
|
858
|
+
return { verb: "rename_migration", from, to };
|
|
859
|
+
}
|
|
860
|
+
case "mark_applied":
|
|
861
|
+
return { verb: "mark_applied", key: checkMigrationKey(op.mark_applied.key, context, fileName, patchKey) };
|
|
862
|
+
case "unmark_applied":
|
|
863
|
+
return { verb: "unmark_applied", key: checkMigrationKey(op.unmark_applied.key, context, fileName, patchKey) };
|
|
864
|
+
default:
|
|
865
|
+
fail(`operation ${index}: unknown verb '${verb}'`, fileName, patchKey);
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
return { version: parsed.version, description, operations };
|
|
869
|
+
}
|
|
870
|
+
function validatePatchPlan(patches, manifest) {
|
|
871
|
+
const renames = [];
|
|
872
|
+
for (const patch of patches) {
|
|
873
|
+
patch.operations.forEach((op, index) => {
|
|
874
|
+
if (op.verb === "rename_migration") {
|
|
875
|
+
renames.push({ from: op.from, to: op.to, file: patch.fileName });
|
|
876
|
+
} else if (op.verb === "mark_applied") {
|
|
877
|
+
if (!manifest.has(op.key)) {
|
|
878
|
+
throw new PatchValidationError(
|
|
879
|
+
`operation ${index} (mark_applied): key '${op.key}' is not present in the current migration manifest`,
|
|
880
|
+
patch.fileName,
|
|
881
|
+
patch.patchKey
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
if (renames.length === 0) return;
|
|
888
|
+
const mentioned = /* @__PURE__ */ new Set();
|
|
889
|
+
renames.forEach((r) => {
|
|
890
|
+
mentioned.add(r.from);
|
|
891
|
+
mentioned.add(r.to);
|
|
892
|
+
});
|
|
893
|
+
const finalKey = (start) => {
|
|
894
|
+
let current = start;
|
|
895
|
+
for (const r of renames) {
|
|
896
|
+
if (current === r.from) current = r.to;
|
|
897
|
+
}
|
|
898
|
+
return current;
|
|
899
|
+
};
|
|
900
|
+
for (const key of mentioned) {
|
|
901
|
+
const finish = finalKey(key);
|
|
902
|
+
if (manifest.has(key)) {
|
|
903
|
+
if (finish !== key) {
|
|
904
|
+
throw new PatchValidationError(
|
|
905
|
+
`rename plan moves current migration '${key}' to '${finish}', which would make its file incorrectly pending`
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
} else {
|
|
909
|
+
if (!manifest.has(finish)) {
|
|
910
|
+
throw new PatchValidationError(
|
|
911
|
+
`rename plan leaves historical key '${key}' at '${finish}', which is not present in the current migration manifest`
|
|
912
|
+
);
|
|
913
|
+
}
|
|
578
914
|
}
|
|
579
915
|
}
|
|
580
|
-
return result;
|
|
581
916
|
}
|
|
582
917
|
|
|
918
|
+
// framework/PatchDirectoryReader.ts
|
|
919
|
+
var PatchDirectoryReader = class {
|
|
920
|
+
constructor(directory) {
|
|
921
|
+
this.directory = directory;
|
|
922
|
+
}
|
|
923
|
+
loadPatches() {
|
|
924
|
+
if (!import_fs4.default.existsSync(this.directory)) return [];
|
|
925
|
+
const entries = import_fs4.default.readdirSync(this.directory, { withFileTypes: true });
|
|
926
|
+
const patchFiles = [];
|
|
927
|
+
for (const entry of entries) {
|
|
928
|
+
if (!entry.name.endsWith(".yaml")) continue;
|
|
929
|
+
if (!entry.isFile() || entry.isSymbolicLink()) {
|
|
930
|
+
if (entry.isSymbolicLink()) {
|
|
931
|
+
throw new PatchValidationError(`patch file must be a regular file, not a symlink`, entry.name);
|
|
932
|
+
}
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
patchFiles.push(entry.name);
|
|
936
|
+
}
|
|
937
|
+
patchFiles.sort((a, b) => {
|
|
938
|
+
const stampA = parseInt(a.slice(0, 13), 10);
|
|
939
|
+
const stampB = parseInt(b.slice(0, 13), 10);
|
|
940
|
+
if (!Number.isNaN(stampA) && !Number.isNaN(stampB) && stampA !== stampB) {
|
|
941
|
+
return stampA - stampB;
|
|
942
|
+
}
|
|
943
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
944
|
+
});
|
|
945
|
+
return patchFiles.map((fileName) => {
|
|
946
|
+
const match = PATCH_FILENAME_REGEX.exec(fileName);
|
|
947
|
+
if (!match) {
|
|
948
|
+
throw new PatchValidationError(
|
|
949
|
+
`invalid patch filename (expected <13-digit-stamp>_<name>.yaml with name matching [a-z0-9][a-z0-9_-]{0,119})`,
|
|
950
|
+
fileName
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
const patchKey = fileName.slice(0, -".yaml".length);
|
|
954
|
+
const filePath = import_path3.default.join(this.directory, fileName);
|
|
955
|
+
const bytes = import_fs4.default.readFileSync(filePath);
|
|
956
|
+
const checksum = import_crypto.default.createHash("sha256").update(bytes).digest("hex");
|
|
957
|
+
const content = bytes.toString("utf8");
|
|
958
|
+
const { version, description, operations } = parsePatchContent(content, fileName, patchKey);
|
|
959
|
+
return { patchKey, fileName, filePath, checksum, version, description, operations };
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
// framework/PatchRunner.ts
|
|
965
|
+
function toError(error) {
|
|
966
|
+
if (error instanceof Error) return error;
|
|
967
|
+
return new Error(String(error));
|
|
968
|
+
}
|
|
969
|
+
function extractRows(result) {
|
|
970
|
+
if (!Array.isArray(result)) return [];
|
|
971
|
+
if (Array.isArray(result[0])) return result[0];
|
|
972
|
+
if (result.length === 2 && result[0] && typeof result[0] === "object" && result[1] && typeof result[1] === "object" && !("rows" in result[1])) {
|
|
973
|
+
return result;
|
|
974
|
+
}
|
|
975
|
+
if (result[0] == null) return [];
|
|
976
|
+
return [result[0]];
|
|
977
|
+
}
|
|
978
|
+
var PatchRunner = class {
|
|
979
|
+
constructor(sqlrunner, config) {
|
|
980
|
+
this.sqlrunner = sqlrunner;
|
|
981
|
+
this.config = config;
|
|
982
|
+
this.patchTable = resolvePatchTable(config);
|
|
983
|
+
this.migrationTable = config.migration_table;
|
|
984
|
+
this.dialect = config.database;
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Discovers, validates, and applies every unapplied patch in order.
|
|
988
|
+
* Each unapplied patch is its own transaction; earlier committed patches
|
|
989
|
+
* remain committed if a later patch fails.
|
|
990
|
+
*/
|
|
991
|
+
applyPending() {
|
|
992
|
+
return __async(this, null, function* () {
|
|
993
|
+
const reader = new PatchDirectoryReader(resolvePatchFolder(this.config));
|
|
994
|
+
const patches = reader.loadPatches();
|
|
995
|
+
const history = yield this.loadHistory();
|
|
996
|
+
if (patches.length === 0 && history.length === 0) {
|
|
997
|
+
return [];
|
|
998
|
+
}
|
|
999
|
+
const byKey = new Map(patches.map((p) => [p.patchKey, p]));
|
|
1000
|
+
for (const row of history) {
|
|
1001
|
+
const file = byKey.get(row.patch_key);
|
|
1002
|
+
if (!file) {
|
|
1003
|
+
throw new PatchIntegrityError(
|
|
1004
|
+
`applied patch '${row.patch_key}' has no corresponding file in the patch folder; patch files are permanent and must never be renamed or deleted`,
|
|
1005
|
+
void 0,
|
|
1006
|
+
row.patch_key
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
if (file.checksum !== row.checksum) {
|
|
1010
|
+
throw new PatchIntegrityError(
|
|
1011
|
+
`applied patch '${row.patch_key}' content changed after application; patch files are immutable once recorded`,
|
|
1012
|
+
file.fileName,
|
|
1013
|
+
row.patch_key,
|
|
1014
|
+
row.checksum,
|
|
1015
|
+
file.checksum
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
const manifest = loadMigrationManifest(this.config.migration_folder, this.dialect);
|
|
1020
|
+
validatePatchPlan(patches, manifest);
|
|
1021
|
+
const appliedKeys = new Set(history.map((r) => r.patch_key));
|
|
1022
|
+
const results = [];
|
|
1023
|
+
for (const patch of patches) {
|
|
1024
|
+
if (appliedKeys.has(patch.patchKey)) {
|
|
1025
|
+
results.push({
|
|
1026
|
+
patchKey: patch.patchKey,
|
|
1027
|
+
fileName: patch.fileName,
|
|
1028
|
+
status: "already_applied",
|
|
1029
|
+
operations: []
|
|
1030
|
+
});
|
|
1031
|
+
continue;
|
|
1032
|
+
}
|
|
1033
|
+
results.push(yield this.applyOne(patch, manifest));
|
|
1034
|
+
}
|
|
1035
|
+
return results;
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
loadHistory() {
|
|
1039
|
+
return __async(this, null, function* () {
|
|
1040
|
+
try {
|
|
1041
|
+
const result = yield this.sqlrunner.query(
|
|
1042
|
+
`SELECT patch_key, checksum FROM ${this.patchTable} WHERE migration_table = ?`,
|
|
1043
|
+
[this.migrationTable]
|
|
1044
|
+
);
|
|
1045
|
+
return extractRows(result);
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
throw new PatchExecutionError(
|
|
1048
|
+
`failed to read patch history from '${this.patchTable}'`,
|
|
1049
|
+
void 0,
|
|
1050
|
+
void 0,
|
|
1051
|
+
void 0,
|
|
1052
|
+
void 0,
|
|
1053
|
+
toError(error)
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
beginSql() {
|
|
1059
|
+
switch (this.dialect) {
|
|
1060
|
+
case "sqlite":
|
|
1061
|
+
return "BEGIN IMMEDIATE";
|
|
1062
|
+
case "pg":
|
|
1063
|
+
return "BEGIN";
|
|
1064
|
+
default:
|
|
1065
|
+
return "START TRANSACTION";
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
begin(patch) {
|
|
1069
|
+
return __async(this, null, function* () {
|
|
1070
|
+
const deadline = Date.now() + 1e4;
|
|
1071
|
+
while (true) {
|
|
1072
|
+
try {
|
|
1073
|
+
yield this.sqlrunner.execute(this.beginSql());
|
|
1074
|
+
return;
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
const message = toError(error).message;
|
|
1077
|
+
if (/SQLITE_BUSY|database is locked/i.test(message) && Date.now() < deadline) {
|
|
1078
|
+
yield new Promise((resolve) => setTimeout(resolve, 50));
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
throw new PatchExecutionError(
|
|
1082
|
+
"failed to start patch transaction",
|
|
1083
|
+
patch.fileName,
|
|
1084
|
+
patch.patchKey,
|
|
1085
|
+
void 0,
|
|
1086
|
+
void 0,
|
|
1087
|
+
toError(error)
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
rollbackQuietly() {
|
|
1094
|
+
return __async(this, null, function* () {
|
|
1095
|
+
try {
|
|
1096
|
+
yield this.sqlrunner.execute("ROLLBACK");
|
|
1097
|
+
} catch (e) {
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
applyOne(patch, manifest) {
|
|
1102
|
+
return __async(this, null, function* () {
|
|
1103
|
+
yield this.begin(patch);
|
|
1104
|
+
try {
|
|
1105
|
+
yield this.sqlrunner.execute(
|
|
1106
|
+
`INSERT INTO ${this.patchTable} (migration_table, patch_key, checksum, format_version, description)
|
|
1107
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
1108
|
+
[this.migrationTable, patch.patchKey, patch.checksum, patch.version, patch.description]
|
|
1109
|
+
);
|
|
1110
|
+
} catch (claimError) {
|
|
1111
|
+
yield this.rollbackQuietly();
|
|
1112
|
+
const committed = yield this.findCommittedRow(patch.patchKey);
|
|
1113
|
+
if (committed) {
|
|
1114
|
+
if (committed.checksum === patch.checksum) {
|
|
1115
|
+
return {
|
|
1116
|
+
patchKey: patch.patchKey,
|
|
1117
|
+
fileName: patch.fileName,
|
|
1118
|
+
status: "already_applied",
|
|
1119
|
+
operations: []
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
throw new PatchIntegrityError(
|
|
1123
|
+
`patch '${patch.patchKey}' was applied elsewhere with a different checksum`,
|
|
1124
|
+
patch.fileName,
|
|
1125
|
+
patch.patchKey,
|
|
1126
|
+
committed.checksum,
|
|
1127
|
+
patch.checksum
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
throw new PatchExecutionError(
|
|
1131
|
+
"failed to claim patch-history row",
|
|
1132
|
+
patch.fileName,
|
|
1133
|
+
patch.patchKey,
|
|
1134
|
+
void 0,
|
|
1135
|
+
void 0,
|
|
1136
|
+
toError(claimError)
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
const operationResults = [];
|
|
1140
|
+
try {
|
|
1141
|
+
for (let index = 0; index < patch.operations.length; index++) {
|
|
1142
|
+
operationResults.push(
|
|
1143
|
+
yield this.applyOperation(patch, patch.operations[index], index, manifest)
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
yield this.sqlrunner.execute("COMMIT");
|
|
1147
|
+
} catch (error) {
|
|
1148
|
+
yield this.rollbackQuietly();
|
|
1149
|
+
if (error instanceof PatchConflictError || error instanceof PatchExecutionError || error instanceof PatchIntegrityError) {
|
|
1150
|
+
throw error;
|
|
1151
|
+
}
|
|
1152
|
+
throw new PatchExecutionError(
|
|
1153
|
+
"patch application failed",
|
|
1154
|
+
patch.fileName,
|
|
1155
|
+
patch.patchKey,
|
|
1156
|
+
void 0,
|
|
1157
|
+
void 0,
|
|
1158
|
+
toError(error)
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
return {
|
|
1162
|
+
patchKey: patch.patchKey,
|
|
1163
|
+
fileName: patch.fileName,
|
|
1164
|
+
status: "applied",
|
|
1165
|
+
operations: operationResults
|
|
1166
|
+
};
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
findCommittedRow(patchKey) {
|
|
1170
|
+
return __async(this, null, function* () {
|
|
1171
|
+
const result = yield this.sqlrunner.query(
|
|
1172
|
+
`SELECT patch_key, checksum FROM ${this.patchTable} WHERE migration_table = ? AND patch_key = ?`,
|
|
1173
|
+
[this.migrationTable, patchKey]
|
|
1174
|
+
);
|
|
1175
|
+
const list = extractRows(result);
|
|
1176
|
+
return list.length > 0 ? list[0] : null;
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
countRows(key) {
|
|
1180
|
+
return __async(this, null, function* () {
|
|
1181
|
+
var _a, _b, _c;
|
|
1182
|
+
const result = yield this.sqlrunner.query(
|
|
1183
|
+
`SELECT COUNT(*) AS row_count FROM ${this.migrationTable} WHERE migration_key = ?`,
|
|
1184
|
+
[key]
|
|
1185
|
+
);
|
|
1186
|
+
const rows = extractRows(result);
|
|
1187
|
+
const value = (_c = (_a = rows[0]) == null ? void 0 : _a.row_count) != null ? _c : Object.values((_b = rows[0]) != null ? _b : {})[0];
|
|
1188
|
+
return Number(value != null ? value : 0);
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
conflict(patch, index, verb, message, keys, counts) {
|
|
1192
|
+
throw new PatchConflictError(
|
|
1193
|
+
`operation ${index} (${verb}): ${message}`,
|
|
1194
|
+
patch.fileName,
|
|
1195
|
+
patch.patchKey,
|
|
1196
|
+
index,
|
|
1197
|
+
verb,
|
|
1198
|
+
keys,
|
|
1199
|
+
counts
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
applyOperation(patch, op, index, manifest) {
|
|
1203
|
+
return __async(this, null, function* () {
|
|
1204
|
+
var _a, _b, _c, _d;
|
|
1205
|
+
try {
|
|
1206
|
+
switch (op.verb) {
|
|
1207
|
+
case "rename_migration": {
|
|
1208
|
+
const fromCount = yield this.countRows(op.from);
|
|
1209
|
+
const toCount = yield this.countRows(op.to);
|
|
1210
|
+
const counts = { [op.from]: fromCount, [op.to]: toCount };
|
|
1211
|
+
if (fromCount > 1 || toCount > 1) {
|
|
1212
|
+
this.conflict(
|
|
1213
|
+
patch,
|
|
1214
|
+
index,
|
|
1215
|
+
op.verb,
|
|
1216
|
+
`ledger corruption: duplicate rows for a migration key`,
|
|
1217
|
+
[op.from, op.to],
|
|
1218
|
+
counts
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
if (fromCount === 1 && toCount === 1) {
|
|
1222
|
+
this.conflict(
|
|
1223
|
+
patch,
|
|
1224
|
+
index,
|
|
1225
|
+
op.verb,
|
|
1226
|
+
`both '${op.from}' and '${op.to}' exist in the ledger`,
|
|
1227
|
+
[op.from, op.to],
|
|
1228
|
+
counts
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
if (fromCount === 0) {
|
|
1232
|
+
return { verb: op.verb, changed: false };
|
|
1233
|
+
}
|
|
1234
|
+
const target = manifest.get(op.to);
|
|
1235
|
+
if (target) {
|
|
1236
|
+
yield this.sqlrunner.execute(
|
|
1237
|
+
`UPDATE ${this.migrationTable} SET migration_key = ?, up = ?, down = ? WHERE migration_key = ?`,
|
|
1238
|
+
[op.to, (_a = target.upFile) != null ? _a : "", (_b = target.downFile) != null ? _b : "", op.from]
|
|
1239
|
+
);
|
|
1240
|
+
} else {
|
|
1241
|
+
yield this.sqlrunner.execute(
|
|
1242
|
+
`UPDATE ${this.migrationTable} SET migration_key = ? WHERE migration_key = ?`,
|
|
1243
|
+
[op.to, op.from]
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
return { verb: op.verb, changed: true };
|
|
1247
|
+
}
|
|
1248
|
+
case "mark_applied": {
|
|
1249
|
+
const count = yield this.countRows(op.key);
|
|
1250
|
+
if (count > 1) {
|
|
1251
|
+
this.conflict(
|
|
1252
|
+
patch,
|
|
1253
|
+
index,
|
|
1254
|
+
op.verb,
|
|
1255
|
+
`ledger corruption: duplicate rows for '${op.key}'`,
|
|
1256
|
+
[op.key],
|
|
1257
|
+
{ [op.key]: count }
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
if (count === 1) {
|
|
1261
|
+
return { verb: op.verb, changed: false };
|
|
1262
|
+
}
|
|
1263
|
+
const entry = manifest.get(op.key);
|
|
1264
|
+
yield this.sqlrunner.execute(
|
|
1265
|
+
`INSERT INTO ${this.migrationTable} (migration_key, up, down) VALUES (?, ?, ?)`,
|
|
1266
|
+
[op.key, (_c = entry == null ? void 0 : entry.upFile) != null ? _c : "", (_d = entry == null ? void 0 : entry.downFile) != null ? _d : ""]
|
|
1267
|
+
);
|
|
1268
|
+
return { verb: op.verb, changed: true };
|
|
1269
|
+
}
|
|
1270
|
+
case "unmark_applied": {
|
|
1271
|
+
const count = yield this.countRows(op.key);
|
|
1272
|
+
if (count > 1) {
|
|
1273
|
+
this.conflict(
|
|
1274
|
+
patch,
|
|
1275
|
+
index,
|
|
1276
|
+
op.verb,
|
|
1277
|
+
`ledger corruption: duplicate rows for '${op.key}'`,
|
|
1278
|
+
[op.key],
|
|
1279
|
+
{ [op.key]: count }
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
if (count === 0) {
|
|
1283
|
+
return { verb: op.verb, changed: false };
|
|
1284
|
+
}
|
|
1285
|
+
yield this.sqlrunner.execute(
|
|
1286
|
+
`DELETE FROM ${this.migrationTable} WHERE migration_key = ?`,
|
|
1287
|
+
[op.key]
|
|
1288
|
+
);
|
|
1289
|
+
return { verb: op.verb, changed: true };
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
if (error instanceof PatchConflictError) throw error;
|
|
1294
|
+
throw new PatchExecutionError(
|
|
1295
|
+
`operation failed`,
|
|
1296
|
+
patch.fileName,
|
|
1297
|
+
patch.patchKey,
|
|
1298
|
+
index,
|
|
1299
|
+
op.verb,
|
|
1300
|
+
toError(error)
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
// framework/PatchCreator.ts
|
|
1308
|
+
var import_fs5 = __toESM(require("fs"));
|
|
1309
|
+
var import_path4 = __toESM(require("path"));
|
|
1310
|
+
var MAX_NAME_LENGTH = 120;
|
|
1311
|
+
var SCAFFOLD = `version: 1
|
|
1312
|
+
description: TODO
|
|
1313
|
+
operations: []
|
|
1314
|
+
`;
|
|
1315
|
+
var PatchCreator = class _PatchCreator {
|
|
1316
|
+
constructor(patchFolder) {
|
|
1317
|
+
this.patchFolder = patchFolder;
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* Normalizes a patch name: trim, whitespace runs -> `_`, lowercase.
|
|
1321
|
+
* Rejects empty results, path separators, `..`, control characters,
|
|
1322
|
+
* characters outside [a-z0-9_-], and names longer than 120 characters.
|
|
1323
|
+
*/
|
|
1324
|
+
static normalizeName(name) {
|
|
1325
|
+
const normalized = (name != null ? name : "").trim().replace(/\s+/g, "_").toLowerCase();
|
|
1326
|
+
if (normalized.length === 0) {
|
|
1327
|
+
throw new CLIError("Patch name is required");
|
|
1328
|
+
}
|
|
1329
|
+
if (normalized.includes("/") || normalized.includes("\\")) {
|
|
1330
|
+
throw new CLIError("Patch name must not contain path separators");
|
|
1331
|
+
}
|
|
1332
|
+
if (normalized.includes("..")) {
|
|
1333
|
+
throw new CLIError("Patch name must not contain '..'");
|
|
1334
|
+
}
|
|
1335
|
+
if (/[\x00-\x1f\x7f]/.test(normalized)) {
|
|
1336
|
+
throw new CLIError("Patch name must not contain control characters");
|
|
1337
|
+
}
|
|
1338
|
+
if (!/^[a-z0-9_-]+$/.test(normalized)) {
|
|
1339
|
+
throw new CLIError("Patch name may only contain characters [a-z0-9_-]");
|
|
1340
|
+
}
|
|
1341
|
+
if (normalized.length > MAX_NAME_LENGTH) {
|
|
1342
|
+
throw new CLIError(`Patch name exceeds ${MAX_NAME_LENGTH} characters after normalization`);
|
|
1343
|
+
}
|
|
1344
|
+
return normalized;
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Creates `<patch_folder>/<stamp>_<normalized_name>.yaml` with exclusive
|
|
1348
|
+
* file creation. On a millisecond-stamp collision, mints a later stamp
|
|
1349
|
+
* and retries. Returns the created path.
|
|
1350
|
+
*/
|
|
1351
|
+
create(name) {
|
|
1352
|
+
const normalized = _PatchCreator.normalizeName(name);
|
|
1353
|
+
if (!import_fs5.default.existsSync(this.patchFolder)) {
|
|
1354
|
+
import_fs5.default.mkdirSync(this.patchFolder, { recursive: true });
|
|
1355
|
+
}
|
|
1356
|
+
let stamp = Date.now();
|
|
1357
|
+
for (let attempt = 0; attempt < 1e3; attempt++) {
|
|
1358
|
+
const filePath = import_path4.default.join(this.patchFolder, `${stamp}_${normalized}.yaml`);
|
|
1359
|
+
try {
|
|
1360
|
+
import_fs5.default.writeFileSync(filePath, SCAFFOLD, { flag: "wx" });
|
|
1361
|
+
return filePath;
|
|
1362
|
+
} catch (error) {
|
|
1363
|
+
if (error && error.code === "EEXIST") {
|
|
1364
|
+
stamp += 1;
|
|
1365
|
+
continue;
|
|
1366
|
+
}
|
|
1367
|
+
throw error;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
throw new CLIError("Unable to create patch file: too many filename collisions");
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
|
|
583
1374
|
// framework/SQLRunner.ts
|
|
584
1375
|
var BaseSQLRunner = class {
|
|
585
1376
|
/**
|
|
@@ -794,12 +1585,122 @@ ${sql}
|
|
|
794
1585
|
});
|
|
795
1586
|
}
|
|
796
1587
|
};
|
|
1588
|
+
var PgRunner = class _PgRunner extends BaseSQLRunner {
|
|
1589
|
+
constructor(connection) {
|
|
1590
|
+
super();
|
|
1591
|
+
this.connection = connection;
|
|
1592
|
+
}
|
|
1593
|
+
/** `?` -> `$1`, `$2`, ... outside of string literals / comments. */
|
|
1594
|
+
static toPositional(sql) {
|
|
1595
|
+
let out = "";
|
|
1596
|
+
let n = 0;
|
|
1597
|
+
let inSingle = false;
|
|
1598
|
+
let inDouble = false;
|
|
1599
|
+
let inLineComment = false;
|
|
1600
|
+
let inBlockComment = false;
|
|
1601
|
+
for (let i = 0; i < sql.length; i++) {
|
|
1602
|
+
const ch = sql[i];
|
|
1603
|
+
const next = sql[i + 1];
|
|
1604
|
+
if (inLineComment) {
|
|
1605
|
+
out += ch;
|
|
1606
|
+
if (ch === "\n") inLineComment = false;
|
|
1607
|
+
continue;
|
|
1608
|
+
}
|
|
1609
|
+
if (inBlockComment) {
|
|
1610
|
+
out += ch;
|
|
1611
|
+
if (ch === "*" && next === "/") {
|
|
1612
|
+
out += next;
|
|
1613
|
+
i++;
|
|
1614
|
+
inBlockComment = false;
|
|
1615
|
+
}
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1618
|
+
if (inSingle) {
|
|
1619
|
+
out += ch;
|
|
1620
|
+
if (ch === "'") inSingle = false;
|
|
1621
|
+
continue;
|
|
1622
|
+
}
|
|
1623
|
+
if (inDouble) {
|
|
1624
|
+
out += ch;
|
|
1625
|
+
if (ch === '"') inDouble = false;
|
|
1626
|
+
continue;
|
|
1627
|
+
}
|
|
1628
|
+
if (ch === "-" && next === "-") {
|
|
1629
|
+
out += ch;
|
|
1630
|
+
inLineComment = true;
|
|
1631
|
+
continue;
|
|
1632
|
+
}
|
|
1633
|
+
if (ch === "/" && next === "*") {
|
|
1634
|
+
out += ch + next;
|
|
1635
|
+
i++;
|
|
1636
|
+
inBlockComment = true;
|
|
1637
|
+
continue;
|
|
1638
|
+
}
|
|
1639
|
+
if (ch === "'") {
|
|
1640
|
+
out += ch;
|
|
1641
|
+
inSingle = true;
|
|
1642
|
+
continue;
|
|
1643
|
+
}
|
|
1644
|
+
if (ch === '"') {
|
|
1645
|
+
out += ch;
|
|
1646
|
+
inDouble = true;
|
|
1647
|
+
continue;
|
|
1648
|
+
}
|
|
1649
|
+
if (ch === "?") {
|
|
1650
|
+
out += `$${++n}`;
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
out += ch;
|
|
1654
|
+
}
|
|
1655
|
+
return out;
|
|
1656
|
+
}
|
|
1657
|
+
run(sql, params) {
|
|
1658
|
+
return __async(this, null, function* () {
|
|
1659
|
+
if (params.length > 0) {
|
|
1660
|
+
return yield this.connection.query(_PgRunner.toPositional(sql), params);
|
|
1661
|
+
}
|
|
1662
|
+
return yield this.connection.query(sql);
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
_query(_0) {
|
|
1666
|
+
return __async(this, arguments, function* (sql, params = []) {
|
|
1667
|
+
const result = yield this.run(sql, params);
|
|
1668
|
+
return [result.rows, result];
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
_execute(_0) {
|
|
1672
|
+
return __async(this, arguments, function* (sql, params = []) {
|
|
1673
|
+
var _a;
|
|
1674
|
+
const result = yield this.run(sql, params);
|
|
1675
|
+
return [{ changes: (_a = result.rowCount) != null ? _a : 0, lastID: void 0 }, result];
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
_end() {
|
|
1679
|
+
return __async(this, null, function* () {
|
|
1680
|
+
if (this.connection && typeof this.connection.end === "function") {
|
|
1681
|
+
yield this.connection.end();
|
|
1682
|
+
}
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
797
1686
|
|
|
798
1687
|
// framework/MigrationRunner.ts
|
|
799
|
-
function
|
|
1688
|
+
function toError2(error) {
|
|
800
1689
|
if (error instanceof Error) return error;
|
|
801
1690
|
return new Error(String(error));
|
|
802
1691
|
}
|
|
1692
|
+
function makeRunner(database, conn) {
|
|
1693
|
+
switch (database) {
|
|
1694
|
+
case "sql":
|
|
1695
|
+
return new SQLRunner(conn);
|
|
1696
|
+
case "sqlite":
|
|
1697
|
+
return new SQLiteRunner(conn);
|
|
1698
|
+
case "pg":
|
|
1699
|
+
return new PgRunner(conn);
|
|
1700
|
+
default:
|
|
1701
|
+
throw ConfigurationError.unknownDatabaseType(database);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
803
1704
|
var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
804
1705
|
static isSQLRunner(conn) {
|
|
805
1706
|
return !!conn && typeof conn.query === "function" && typeof conn.execute === "function" && typeof conn.end === "function";
|
|
@@ -808,14 +1709,17 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
808
1709
|
return __async(this, null, function* () {
|
|
809
1710
|
const configReader = new FileMigrationConfigReader(configFile);
|
|
810
1711
|
const config = configReader.loadFile();
|
|
1712
|
+
let factoryOwnsConnection = false;
|
|
811
1713
|
if (!conn) {
|
|
812
1714
|
conn = yield this.createConnection(config);
|
|
1715
|
+
factoryOwnsConnection = true;
|
|
813
1716
|
}
|
|
814
|
-
return new _MigrationRunnerFactory().create(config, conn);
|
|
1717
|
+
return new _MigrationRunnerFactory().create(config, conn, factoryOwnsConnection);
|
|
815
1718
|
});
|
|
816
1719
|
}
|
|
817
1720
|
static createConnection(config) {
|
|
818
1721
|
return __async(this, null, function* () {
|
|
1722
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
|
|
819
1723
|
let conn = null;
|
|
820
1724
|
switch (config.database) {
|
|
821
1725
|
case "sql":
|
|
@@ -826,23 +1730,42 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
826
1730
|
password: process.env.SQL_PASSWORD
|
|
827
1731
|
}, config.sql);
|
|
828
1732
|
try {
|
|
829
|
-
|
|
1733
|
+
const mysql = yield import("mysql2/promise");
|
|
1734
|
+
conn = yield ((_b = (_a = mysql.default) == null ? void 0 : _a.createConnection) != null ? _b : mysql.createConnection)(settings);
|
|
830
1735
|
return conn;
|
|
831
1736
|
} catch (error) {
|
|
832
|
-
throw DatabaseConnectionError.connectionFailed("sql",
|
|
1737
|
+
throw DatabaseConnectionError.connectionFailed("sql", toError2(error).message);
|
|
833
1738
|
}
|
|
834
1739
|
case "sqlite":
|
|
835
1740
|
if (!config.sqlite) {
|
|
836
1741
|
throw ConfigurationError.missingDatabaseConfiguration("sqlite");
|
|
837
1742
|
}
|
|
838
1743
|
try {
|
|
839
|
-
|
|
1744
|
+
const sqlite = yield import("sqlite");
|
|
1745
|
+
const sqlite3 = yield import("sqlite3");
|
|
1746
|
+
conn = yield ((_d = (_c = sqlite.default) == null ? void 0 : _c.open) != null ? _d : sqlite.open)({
|
|
840
1747
|
filename: config.sqlite.database,
|
|
841
|
-
driver: sqlite3.Database
|
|
1748
|
+
driver: (_f = (_e = sqlite3.default) == null ? void 0 : _e.Database) != null ? _f : sqlite3.Database
|
|
842
1749
|
});
|
|
843
1750
|
return conn;
|
|
844
1751
|
} catch (error) {
|
|
845
|
-
throw DatabaseConnectionError.connectionFailed("sqlite",
|
|
1752
|
+
throw DatabaseConnectionError.connectionFailed("sqlite", toError2(error).message);
|
|
1753
|
+
}
|
|
1754
|
+
case "pg":
|
|
1755
|
+
if (!config.pg && !process.env.DATABASE_URL) {
|
|
1756
|
+
throw ConfigurationError.missingDatabaseConfiguration("pg");
|
|
1757
|
+
}
|
|
1758
|
+
try {
|
|
1759
|
+
const pgcfg = (_g = config.pg) != null ? _g : {};
|
|
1760
|
+
const connectionString = (_h = pgcfg.connectionString) != null ? _h : process.env.DATABASE_URL;
|
|
1761
|
+
const settings2 = connectionString ? { connectionString, ssl: pgcfg.ssl } : __spreadProps(__spreadValues({}, pgcfg), { password: (_i = pgcfg.password) != null ? _i : process.env.PG_PASSWORD });
|
|
1762
|
+
const pg = yield import("pg");
|
|
1763
|
+
const Client = (_k = (_j = pg.default) == null ? void 0 : _j.Client) != null ? _k : pg.Client;
|
|
1764
|
+
conn = new Client(settings2);
|
|
1765
|
+
yield conn.connect();
|
|
1766
|
+
return conn;
|
|
1767
|
+
} catch (error) {
|
|
1768
|
+
throw DatabaseConnectionError.connectionFailed("pg", toError2(error).message);
|
|
846
1769
|
}
|
|
847
1770
|
default:
|
|
848
1771
|
throw ConfigurationError.unknownDatabaseType(config.database);
|
|
@@ -856,7 +1779,7 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
856
1779
|
return new _MigrationRunnerFactory().createEmpty(config);
|
|
857
1780
|
});
|
|
858
1781
|
}
|
|
859
|
-
create(config, conn) {
|
|
1782
|
+
create(config, conn, factoryOwnsConnection = false) {
|
|
860
1783
|
return __async(this, null, function* () {
|
|
861
1784
|
let sqlrunner;
|
|
862
1785
|
let driverConnection = conn;
|
|
@@ -864,42 +1787,34 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
864
1787
|
sqlrunner = conn;
|
|
865
1788
|
driverConnection = null;
|
|
866
1789
|
} else {
|
|
867
|
-
|
|
868
|
-
case "sql":
|
|
869
|
-
sqlrunner = new SQLRunner(conn);
|
|
870
|
-
break;
|
|
871
|
-
case "sqlite":
|
|
872
|
-
sqlrunner = new SQLiteRunner(conn);
|
|
873
|
-
break;
|
|
874
|
-
default:
|
|
875
|
-
throw ConfigurationError.unknownDatabaseType(config.database);
|
|
876
|
-
}
|
|
1790
|
+
sqlrunner = makeRunner(config.database, conn);
|
|
877
1791
|
}
|
|
878
1792
|
const setup = new MigrationSetup(sqlrunner, config);
|
|
879
1793
|
const read_strategy = this.getReadStategy(config);
|
|
880
1794
|
const migration_files = new MigrationDirectoryReader(config.migration_folder, read_strategy, sqlrunner, config.database);
|
|
881
|
-
|
|
882
|
-
|
|
1795
|
+
const runner = new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, driverConnection);
|
|
1796
|
+
try {
|
|
1797
|
+
yield runner.setup();
|
|
1798
|
+
} catch (error) {
|
|
1799
|
+
if (factoryOwnsConnection) {
|
|
1800
|
+
try {
|
|
1801
|
+
yield sqlrunner.end();
|
|
1802
|
+
} catch (e) {
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
throw error;
|
|
1806
|
+
}
|
|
1807
|
+
return runner;
|
|
883
1808
|
});
|
|
884
1809
|
}
|
|
885
1810
|
createEmpty(config) {
|
|
886
1811
|
return __async(this, null, function* () {
|
|
887
1812
|
const conn = null;
|
|
888
|
-
|
|
889
|
-
switch (config.database) {
|
|
890
|
-
case "sql":
|
|
891
|
-
sqlrunner = new SQLRunner(conn);
|
|
892
|
-
break;
|
|
893
|
-
case "sqlite":
|
|
894
|
-
sqlrunner = new SQLiteRunner(conn);
|
|
895
|
-
break;
|
|
896
|
-
default:
|
|
897
|
-
throw ConfigurationError.unknownDatabaseType(config.database);
|
|
898
|
-
}
|
|
1813
|
+
const sqlrunner = makeRunner(config.database, conn);
|
|
899
1814
|
const setup = new MigrationSetup(sqlrunner, config);
|
|
900
1815
|
const read_strategy = this.getReadStategy(config);
|
|
901
1816
|
const migration_files = new MigrationDirectoryReader(config.migration_folder, read_strategy, sqlrunner, config.database);
|
|
902
|
-
return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, conn);
|
|
1817
|
+
return new MySQLMigrationRunner(config, migration_files, setup, sqlrunner, conn, false);
|
|
903
1818
|
});
|
|
904
1819
|
}
|
|
905
1820
|
getReadStategy(config) {
|
|
@@ -908,39 +1823,85 @@ var MigrationRunnerFactory = class _MigrationRunnerFactory {
|
|
|
908
1823
|
return MySqlDialectParser;
|
|
909
1824
|
case "sqlite":
|
|
910
1825
|
return SqliteDialectParser;
|
|
1826
|
+
case "pg":
|
|
1827
|
+
return PgDialectParser;
|
|
911
1828
|
default:
|
|
912
1829
|
throw ConfigurationError.unknownDatabaseType(config.database);
|
|
913
1830
|
}
|
|
914
1831
|
}
|
|
915
1832
|
};
|
|
916
1833
|
var MySQLMigrationRunner = class {
|
|
917
|
-
constructor(config, directory, setupRunner, sqlrunner, connection) {
|
|
1834
|
+
constructor(config, directory, setupRunner, sqlrunner, connection, preflightEnabled = true) {
|
|
918
1835
|
this.config = config;
|
|
919
1836
|
this.directory = directory;
|
|
920
1837
|
this.setupRunner = setupRunner;
|
|
921
1838
|
this.sqlrunner = sqlrunner;
|
|
922
1839
|
this.connection = connection;
|
|
1840
|
+
this.preflightEnabled = preflightEnabled;
|
|
1841
|
+
/**
|
|
1842
|
+
* Memoized in-flight preflight promise. Simultaneous or repeated calls
|
|
1843
|
+
* to setup() on one runner execute the preflight (migration table setup
|
|
1844
|
+
* + patch application) exactly once. Cleared after rejection so a caller
|
|
1845
|
+
* may retry after fixing the cause.
|
|
1846
|
+
*/
|
|
1847
|
+
this.preflightPromise = null;
|
|
1848
|
+
this.lastPatchResults = [];
|
|
923
1849
|
}
|
|
924
1850
|
setup() {
|
|
1851
|
+
return __async(this, null, function* () {
|
|
1852
|
+
if (!this.preflightEnabled) {
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
if (!this.preflightPromise) {
|
|
1856
|
+
this.preflightPromise = this.runPreflight();
|
|
1857
|
+
this.preflightPromise.catch(() => {
|
|
1858
|
+
this.preflightPromise = null;
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
return this.preflightPromise;
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
runPreflight() {
|
|
925
1865
|
return __async(this, null, function* () {
|
|
926
1866
|
try {
|
|
927
1867
|
yield this.setupRunner.setup();
|
|
928
1868
|
} catch (error) {
|
|
929
|
-
throw new MigrationExecutionError("Failed to set up migration database", void 0, void 0,
|
|
1869
|
+
throw new MigrationExecutionError("Failed to set up migration database", void 0, void 0, toError2(error));
|
|
930
1870
|
}
|
|
1871
|
+
const patchRunner = new PatchRunner(this.sqlrunner, this.config);
|
|
1872
|
+
this.lastPatchResults = yield patchRunner.applyPending();
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Delegates to the same idempotent preflight; returns the results of the
|
|
1877
|
+
* patch pass that ran (or is running) for this runner.
|
|
1878
|
+
*/
|
|
1879
|
+
applyPendingPatches() {
|
|
1880
|
+
return __async(this, null, function* () {
|
|
1881
|
+
yield this.setup();
|
|
1882
|
+
return this.lastPatchResults;
|
|
931
1883
|
});
|
|
932
1884
|
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Scaffolds a new ledger patch file and returns the created path.
|
|
1887
|
+
* Never connects to a database.
|
|
1888
|
+
*/
|
|
1889
|
+
createPatch(name) {
|
|
1890
|
+
const creator = new PatchCreator(resolvePatchFolder(this.config));
|
|
1891
|
+
return creator.create(name);
|
|
1892
|
+
}
|
|
933
1893
|
terminate() {
|
|
934
1894
|
return __async(this, null, function* () {
|
|
935
1895
|
try {
|
|
936
1896
|
yield this.setupRunner.teardown();
|
|
937
1897
|
} catch (error) {
|
|
938
|
-
throw new MigrationExecutionError("Failed to tear down migration database", void 0, void 0,
|
|
1898
|
+
throw new MigrationExecutionError("Failed to tear down migration database", void 0, void 0, toError2(error));
|
|
939
1899
|
}
|
|
940
1900
|
});
|
|
941
1901
|
}
|
|
942
1902
|
getMigrationsHistory() {
|
|
943
1903
|
return __async(this, null, function* () {
|
|
1904
|
+
yield this.setup();
|
|
944
1905
|
try {
|
|
945
1906
|
const results = yield this.sqlrunner.query(`
|
|
946
1907
|
select *
|
|
@@ -948,16 +1909,17 @@ var MySQLMigrationRunner = class {
|
|
|
948
1909
|
`);
|
|
949
1910
|
return results[0];
|
|
950
1911
|
} catch (error) {
|
|
951
|
-
throw new MigrationExecutionError("Failed to get migration history", void 0, void 0,
|
|
1912
|
+
throw new MigrationExecutionError("Failed to get migration history", void 0, void 0, toError2(error));
|
|
952
1913
|
}
|
|
953
1914
|
});
|
|
954
1915
|
}
|
|
955
1916
|
getMigrations() {
|
|
956
1917
|
return __async(this, null, function* () {
|
|
1918
|
+
yield this.setup();
|
|
957
1919
|
try {
|
|
958
1920
|
return this.directory.loadMigrations(this.config.migration_table, this.connection);
|
|
959
1921
|
} catch (error) {
|
|
960
|
-
throw new MigrationExecutionError("Failed to load migrations", void 0, void 0,
|
|
1922
|
+
throw new MigrationExecutionError("Failed to load migrations", void 0, void 0, toError2(error));
|
|
961
1923
|
}
|
|
962
1924
|
});
|
|
963
1925
|
}
|
|
@@ -970,7 +1932,7 @@ var MySQLMigrationRunner = class {
|
|
|
970
1932
|
if (error instanceof MigrationExecutionError) {
|
|
971
1933
|
throw error;
|
|
972
1934
|
}
|
|
973
|
-
throw new MigrationExecutionError("Failed to get pending migrations", void 0, void 0,
|
|
1935
|
+
throw new MigrationExecutionError("Failed to get pending migrations", void 0, void 0, toError2(error));
|
|
974
1936
|
}
|
|
975
1937
|
});
|
|
976
1938
|
}
|
|
@@ -983,12 +1945,13 @@ var MySQLMigrationRunner = class {
|
|
|
983
1945
|
if (error instanceof MigrationExecutionError) {
|
|
984
1946
|
throw error;
|
|
985
1947
|
}
|
|
986
|
-
throw new MigrationExecutionError("Failed to get completed migrations", void 0, void 0,
|
|
1948
|
+
throw new MigrationExecutionError("Failed to get completed migrations", void 0, void 0, toError2(error));
|
|
987
1949
|
}
|
|
988
1950
|
});
|
|
989
1951
|
}
|
|
990
1952
|
migrate(migrationNodes, forward) {
|
|
991
1953
|
return __async(this, null, function* () {
|
|
1954
|
+
yield this.setup();
|
|
992
1955
|
for (let node of migrationNodes) {
|
|
993
1956
|
try {
|
|
994
1957
|
if (forward) {
|
|
@@ -1001,7 +1964,7 @@ var MySQLMigrationRunner = class {
|
|
|
1001
1964
|
`Failed to ${forward ? "apply" : "rollback"} migration`,
|
|
1002
1965
|
node.name || String(node),
|
|
1003
1966
|
forward ? node.up_sql() : node.down_sql(),
|
|
1004
|
-
|
|
1967
|
+
toError2(error)
|
|
1005
1968
|
);
|
|
1006
1969
|
}
|
|
1007
1970
|
}
|
|
@@ -1009,6 +1972,7 @@ var MySQLMigrationRunner = class {
|
|
|
1009
1972
|
}
|
|
1010
1973
|
reset() {
|
|
1011
1974
|
return __async(this, null, function* () {
|
|
1975
|
+
yield this.setup();
|
|
1012
1976
|
try {
|
|
1013
1977
|
let migrations = yield this.getMigrations();
|
|
1014
1978
|
const rollback = yield migration_filter(migrations, true);
|
|
@@ -1020,7 +1984,7 @@ var MySQLMigrationRunner = class {
|
|
|
1020
1984
|
if (error instanceof MigrationExecutionError) {
|
|
1021
1985
|
throw error;
|
|
1022
1986
|
}
|
|
1023
|
-
throw new MigrationExecutionError("Failed to reset migrations", void 0, void 0,
|
|
1987
|
+
throw new MigrationExecutionError("Failed to reset migrations", void 0, void 0, toError2(error));
|
|
1024
1988
|
}
|
|
1025
1989
|
});
|
|
1026
1990
|
}
|
|
@@ -1029,7 +1993,7 @@ var MySQLMigrationRunner = class {
|
|
|
1029
1993
|
const creator = new MigrationCreator(this.config);
|
|
1030
1994
|
creator.create(name);
|
|
1031
1995
|
} catch (error) {
|
|
1032
|
-
throw new MigrationExecutionError(`Failed to create migration: ${name}`, void 0, void 0,
|
|
1996
|
+
throw new MigrationExecutionError(`Failed to create migration: ${name}`, void 0, void 0, toError2(error));
|
|
1033
1997
|
}
|
|
1034
1998
|
}
|
|
1035
1999
|
close() {
|
|
@@ -1038,7 +2002,7 @@ var MySQLMigrationRunner = class {
|
|
|
1038
2002
|
try {
|
|
1039
2003
|
yield this.sqlrunner.end();
|
|
1040
2004
|
} catch (error) {
|
|
1041
|
-
throw new DatabaseConnectionError(`Failed to close database connection: ${
|
|
2005
|
+
throw new DatabaseConnectionError(`Failed to close database connection: ${toError2(error).message}`);
|
|
1042
2006
|
}
|
|
1043
2007
|
}
|
|
1044
2008
|
});
|
|
@@ -1052,7 +2016,7 @@ var MySQLMigrationRunner = class {
|
|
|
1052
2016
|
return __async(this, null, function* () {
|
|
1053
2017
|
try {
|
|
1054
2018
|
console.log(`Checking for ${config_file2}`);
|
|
1055
|
-
const config_exist =
|
|
2019
|
+
const config_exist = import_fs6.default.existsSync(config_file2);
|
|
1056
2020
|
if (!config_exist) {
|
|
1057
2021
|
console.log(`Creating ${config_file2}`);
|
|
1058
2022
|
const default_config = {
|
|
@@ -1066,11 +2030,11 @@ var MySQLMigrationRunner = class {
|
|
|
1066
2030
|
"password": ""
|
|
1067
2031
|
}
|
|
1068
2032
|
};
|
|
1069
|
-
|
|
2033
|
+
import_fs6.default.writeFileSync(config_file2, JSON.stringify(default_config, null, 2));
|
|
1070
2034
|
console.log(`Created ${config_file2}`);
|
|
1071
2035
|
}
|
|
1072
2036
|
} catch (error) {
|
|
1073
|
-
throw new ConfigurationError(`Failed to initialize config file: ${
|
|
2037
|
+
throw new ConfigurationError(`Failed to initialize config file: ${toError2(error).message}`);
|
|
1074
2038
|
}
|
|
1075
2039
|
});
|
|
1076
2040
|
}
|
|
@@ -1081,7 +2045,7 @@ var FileMigrationConfigReader = class {
|
|
|
1081
2045
|
}
|
|
1082
2046
|
loadFile() {
|
|
1083
2047
|
try {
|
|
1084
|
-
const fileContent =
|
|
2048
|
+
const fileContent = import_fs6.default.readFileSync(this.configFile);
|
|
1085
2049
|
const config = JSON.parse(fileContent.toString());
|
|
1086
2050
|
if (!config.migration_folder) {
|
|
1087
2051
|
throw ConfigurationError.missingRequiredProperty("migration_folder");
|
|
@@ -1094,6 +2058,8 @@ var FileMigrationConfigReader = class {
|
|
|
1094
2058
|
config.database = "sql";
|
|
1095
2059
|
} else if (config.sqlite) {
|
|
1096
2060
|
config.database = "sqlite";
|
|
2061
|
+
} else if (config.pg) {
|
|
2062
|
+
config.database = "pg";
|
|
1097
2063
|
} else {
|
|
1098
2064
|
throw ConfigurationError.missingRequiredProperty("database");
|
|
1099
2065
|
}
|
|
@@ -1103,7 +2069,7 @@ var FileMigrationConfigReader = class {
|
|
|
1103
2069
|
if (error instanceof ConfigurationError) {
|
|
1104
2070
|
throw error;
|
|
1105
2071
|
}
|
|
1106
|
-
const err =
|
|
2072
|
+
const err = toError2(error);
|
|
1107
2073
|
if (err.message.includes("ENOENT")) {
|
|
1108
2074
|
throw new ConfigurationError(`Config file not found: ${this.configFile}`);
|
|
1109
2075
|
}
|
|
@@ -1120,16 +2086,16 @@ var MigrationCreator = class {
|
|
|
1120
2086
|
throw new CLIError("Migration name is required");
|
|
1121
2087
|
}
|
|
1122
2088
|
try {
|
|
1123
|
-
if (!
|
|
1124
|
-
|
|
2089
|
+
if (!import_fs6.default.existsSync(this.config.migration_folder)) {
|
|
2090
|
+
import_fs6.default.mkdirSync(this.config.migration_folder, { recursive: true });
|
|
1125
2091
|
}
|
|
1126
2092
|
const now_timestamp = Date.now();
|
|
1127
2093
|
const filename_up = `${now_timestamp}_${name}.up.sql`;
|
|
1128
2094
|
const filename_down = `${now_timestamp}_${name}.down.sql`;
|
|
1129
|
-
|
|
2095
|
+
import_fs6.default.writeFileSync(`${this.config.migration_folder}/${filename_up}`, `
|
|
1130
2096
|
-- Write your up migration here
|
|
1131
2097
|
`.trim());
|
|
1132
|
-
|
|
2098
|
+
import_fs6.default.writeFileSync(`${this.config.migration_folder}/${filename_down}`, `
|
|
1133
2099
|
-- Write your down migration here
|
|
1134
2100
|
`.trim());
|
|
1135
2101
|
console.log(`Created migration files:`);
|
|
@@ -1139,16 +2105,16 @@ var MigrationCreator = class {
|
|
|
1139
2105
|
if (error instanceof CLIError) {
|
|
1140
2106
|
throw error;
|
|
1141
2107
|
}
|
|
1142
|
-
throw new MigrationExecutionError(`Failed to create migration files: ${
|
|
2108
|
+
throw new MigrationExecutionError(`Failed to create migration files: ${toError2(error).message}`);
|
|
1143
2109
|
}
|
|
1144
2110
|
}
|
|
1145
2111
|
};
|
|
1146
2112
|
|
|
1147
2113
|
// framework/SeedRunner.ts
|
|
1148
|
-
var
|
|
1149
|
-
var
|
|
2114
|
+
var import_fs7 = __toESM(require("fs"));
|
|
2115
|
+
var import_path5 = __toESM(require("path"));
|
|
1150
2116
|
var import_url = require("url");
|
|
1151
|
-
var
|
|
2117
|
+
var import_ajv2 = __toESM(require("ajv"));
|
|
1152
2118
|
var import_ajv_formats = __toESM(require("ajv-formats"));
|
|
1153
2119
|
var import_api = require("tsx/esm/api");
|
|
1154
2120
|
function resolveAlias(name, aliasMap) {
|
|
@@ -1157,10 +2123,10 @@ function resolveAlias(name, aliasMap) {
|
|
|
1157
2123
|
return (_a = aliasMap[name]) != null ? _a : name;
|
|
1158
2124
|
}
|
|
1159
2125
|
function walkForFile(rootDir, fileName) {
|
|
1160
|
-
if (!
|
|
1161
|
-
const entries =
|
|
2126
|
+
if (!import_fs7.default.existsSync(rootDir)) return null;
|
|
2127
|
+
const entries = import_fs7.default.readdirSync(rootDir, { withFileTypes: true });
|
|
1162
2128
|
for (const entry of entries) {
|
|
1163
|
-
const full =
|
|
2129
|
+
const full = import_path5.default.join(rootDir, entry.name);
|
|
1164
2130
|
if (entry.isDirectory()) {
|
|
1165
2131
|
const found = walkForFile(full, fileName);
|
|
1166
2132
|
if (found) return found;
|
|
@@ -1254,39 +2220,39 @@ function resolveSeed(name, migrationConfig, options) {
|
|
|
1254
2220
|
}
|
|
1255
2221
|
function loadJson(filePath) {
|
|
1256
2222
|
return __async(this, null, function* () {
|
|
1257
|
-
const content = yield
|
|
2223
|
+
const content = yield import_fs7.default.promises.readFile(filePath, "utf8");
|
|
1258
2224
|
return JSON.parse(content);
|
|
1259
2225
|
});
|
|
1260
2226
|
}
|
|
1261
2227
|
function createValidator() {
|
|
1262
|
-
const
|
|
1263
|
-
(0, import_ajv_formats.default)(
|
|
1264
|
-
return
|
|
2228
|
+
const ajv2 = new import_ajv2.default({ allErrors: true, strict: false });
|
|
2229
|
+
(0, import_ajv_formats.default)(ajv2);
|
|
2230
|
+
return ajv2;
|
|
1265
2231
|
}
|
|
1266
2232
|
function validateData(schemaPath, data, validate, log) {
|
|
1267
2233
|
return __async(this, null, function* () {
|
|
1268
2234
|
if (!validate || !schemaPath) return;
|
|
1269
|
-
const content = yield
|
|
2235
|
+
const content = yield import_fs7.default.promises.readFile(schemaPath, "utf8");
|
|
1270
2236
|
const schema = JSON.parse(content);
|
|
1271
|
-
const
|
|
1272
|
-
const validateFn =
|
|
2237
|
+
const ajv2 = createValidator();
|
|
2238
|
+
const validateFn = ajv2.compile(schema);
|
|
1273
2239
|
const ok = validateFn(data);
|
|
1274
2240
|
if (!ok) {
|
|
1275
2241
|
log == null ? void 0 : log(`Validation failed for seed data (${schemaPath})`);
|
|
1276
|
-
throw new Error(`Seed data validation failed: ${
|
|
2242
|
+
throw new Error(`Seed data validation failed: ${ajv2.errorsText(validateFn.errors || [])}`);
|
|
1277
2243
|
}
|
|
1278
2244
|
});
|
|
1279
2245
|
}
|
|
1280
2246
|
function runSqlSeed(runner, resolved, direction) {
|
|
1281
2247
|
return __async(this, null, function* () {
|
|
1282
2248
|
const sqlPath = direction === "up" ? resolved.upPath : resolved.downPath;
|
|
1283
|
-
const sql = yield
|
|
2249
|
+
const sql = yield import_fs7.default.promises.readFile(sqlPath, "utf8");
|
|
1284
2250
|
yield runner.query(sql);
|
|
1285
2251
|
});
|
|
1286
2252
|
}
|
|
1287
2253
|
function loadSeedModule(modulePath) {
|
|
1288
2254
|
return __async(this, null, function* () {
|
|
1289
|
-
const resolved =
|
|
2255
|
+
const resolved = import_path5.default.resolve(modulePath);
|
|
1290
2256
|
if (resolved.endsWith(".ts")) {
|
|
1291
2257
|
const fileUrl = (0, import_url.pathToFileURL)(resolved).href;
|
|
1292
2258
|
return (0, import_api.tsImport)(fileUrl, fileUrl);
|
|
@@ -1376,6 +2342,10 @@ function runSeedsWithRunner(runner, migrationConfig, direction, options) {
|
|
|
1376
2342
|
|
|
1377
2343
|
// cli.ts
|
|
1378
2344
|
var args = MigrationCLIFactory.setup(process.argv);
|
|
2345
|
+
if (args.flags.help) {
|
|
2346
|
+
printUsage();
|
|
2347
|
+
process.exit(0);
|
|
2348
|
+
}
|
|
1379
2349
|
if (!args.commands || args.commands.length === 0) {
|
|
1380
2350
|
console.error("Error: No command specified");
|
|
1381
2351
|
printUsage();
|
|
@@ -1383,7 +2353,7 @@ if (!args.commands || args.commands.length === 0) {
|
|
|
1383
2353
|
}
|
|
1384
2354
|
var commands = args.commands;
|
|
1385
2355
|
var command = commands[0];
|
|
1386
|
-
var load_database = !["init", "create", "help"].includes(command.toLowerCase());
|
|
2356
|
+
var load_database = !["init", "create", "help", "patch"].includes(command.toLowerCase());
|
|
1387
2357
|
var config_file = args.flags.config || "proper.json";
|
|
1388
2358
|
if (command.toLowerCase() === "help") {
|
|
1389
2359
|
printUsage();
|
|
@@ -1392,9 +2362,17 @@ if (command.toLowerCase() === "help") {
|
|
|
1392
2362
|
console.log(`Loading database: ${load_database}`);
|
|
1393
2363
|
var pending_runner = load_database ? MigrationRunnerFactory.create(config_file) : MigrationRunnerFactory.createEmpty(config_file);
|
|
1394
2364
|
pending_runner.then((runner) => __async(null, null, function* () {
|
|
2365
|
+
let failed = false;
|
|
1395
2366
|
try {
|
|
1396
2367
|
if (load_database) {
|
|
1397
|
-
yield runner.
|
|
2368
|
+
const patch_results = yield runner.applyPendingPatches();
|
|
2369
|
+
for (const patch of patch_results) {
|
|
2370
|
+
if (patch.status === "applied") {
|
|
2371
|
+
const changed = patch.operations.filter((op) => op.changed).length;
|
|
2372
|
+
const noop = patch.operations.length - changed;
|
|
2373
|
+
console.log(`Applied patch ${patch.fileName}: ${changed} changed, ${noop} no-op operation(s)`);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
1398
2376
|
}
|
|
1399
2377
|
switch (command.toLowerCase()) {
|
|
1400
2378
|
case "up":
|
|
@@ -1414,7 +2392,6 @@ pending_runner.then((runner) => __async(null, null, function* () {
|
|
|
1414
2392
|
} else {
|
|
1415
2393
|
console.log("No pending migrations");
|
|
1416
2394
|
}
|
|
1417
|
-
yield runner.close();
|
|
1418
2395
|
break;
|
|
1419
2396
|
case "down":
|
|
1420
2397
|
const migrations_rollback = yield runner.getMigrations();
|
|
@@ -1435,13 +2412,11 @@ pending_runner.then((runner) => __async(null, null, function* () {
|
|
|
1435
2412
|
} else {
|
|
1436
2413
|
console.log("No migrations to roll back");
|
|
1437
2414
|
}
|
|
1438
|
-
yield runner.close();
|
|
1439
2415
|
break;
|
|
1440
2416
|
case "reset":
|
|
1441
2417
|
console.log("Resetting all migrations...");
|
|
1442
2418
|
yield runner.reset();
|
|
1443
2419
|
console.log("Reset completed successfully");
|
|
1444
|
-
yield runner.close();
|
|
1445
2420
|
break;
|
|
1446
2421
|
case "create":
|
|
1447
2422
|
let filename = args.flags.name || commands[1];
|
|
@@ -1450,11 +2425,22 @@ pending_runner.then((runner) => __async(null, null, function* () {
|
|
|
1450
2425
|
}
|
|
1451
2426
|
filename = filename.replace(/\s/g, "_");
|
|
1452
2427
|
runner.createMigration(filename);
|
|
1453
|
-
runner.close();
|
|
1454
2428
|
break;
|
|
2429
|
+
case "patch": {
|
|
2430
|
+
const patch_name = args.flags.name || commands[1];
|
|
2431
|
+
if (!patch_name) {
|
|
2432
|
+
throw new CLIError("Patch name is required. Usage: proper patch <name>");
|
|
2433
|
+
}
|
|
2434
|
+
const created_path = runner.createPatch(patch_name);
|
|
2435
|
+
console.log(`Created patch file:`);
|
|
2436
|
+
console.log(` ${created_path}`);
|
|
2437
|
+
console.log("");
|
|
2438
|
+
console.log("Complete this file before running another database-backed");
|
|
2439
|
+
console.log("Proper command: 'operations: []' is intentionally not runnable.");
|
|
2440
|
+
break;
|
|
2441
|
+
}
|
|
1455
2442
|
case "init":
|
|
1456
2443
|
yield runner.init(config_file);
|
|
1457
|
-
runner.close();
|
|
1458
2444
|
break;
|
|
1459
2445
|
case "status":
|
|
1460
2446
|
const { printTable } = require("console-table-printer");
|
|
@@ -1468,7 +2454,6 @@ pending_runner.then((runner) => __async(null, null, function* () {
|
|
|
1468
2454
|
};
|
|
1469
2455
|
}));
|
|
1470
2456
|
printTable(yield Promise.all(table));
|
|
1471
|
-
runner.close();
|
|
1472
2457
|
break;
|
|
1473
2458
|
case "query":
|
|
1474
2459
|
const sql_query = args.flags.query || commands[1];
|
|
@@ -1512,7 +2497,6 @@ Returned ${results.length} row(s)`);
|
|
|
1512
2497
|
} catch (error) {
|
|
1513
2498
|
throw new CLIError(`Query execution failed: ${error.message}`);
|
|
1514
2499
|
}
|
|
1515
|
-
yield runner.close();
|
|
1516
2500
|
break;
|
|
1517
2501
|
case "seed": {
|
|
1518
2502
|
const subCommands = commands.slice(1);
|
|
@@ -1545,7 +2529,6 @@ Returned ${results.length} row(s)`);
|
|
|
1545
2529
|
const reader = new FileMigrationConfigReader(config_file);
|
|
1546
2530
|
const migrationConfig = reader.loadFile();
|
|
1547
2531
|
yield runSeedsWithRunner(runner, migrationConfig, action, seedOptions);
|
|
1548
|
-
yield runner.close();
|
|
1549
2532
|
break;
|
|
1550
2533
|
}
|
|
1551
2534
|
default:
|
|
@@ -1556,9 +2539,25 @@ Returned ${results.length} row(s)`);
|
|
|
1556
2539
|
if (error.stack && process.env.DEBUG) {
|
|
1557
2540
|
console.error(error.stack);
|
|
1558
2541
|
}
|
|
2542
|
+
failed = true;
|
|
2543
|
+
} finally {
|
|
2544
|
+
try {
|
|
2545
|
+
yield runner.close();
|
|
2546
|
+
} catch (closeError) {
|
|
2547
|
+
console.error(`Error closing connection: ${closeError.message}`);
|
|
2548
|
+
failed = true;
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
if (failed) {
|
|
1559
2552
|
process.exit(1);
|
|
1560
2553
|
}
|
|
1561
|
-
}))
|
|
2554
|
+
})).catch((error) => {
|
|
2555
|
+
console.error(`Error: ${error.message}`);
|
|
2556
|
+
if (error.stack && process.env.DEBUG) {
|
|
2557
|
+
console.error(error.stack);
|
|
2558
|
+
}
|
|
2559
|
+
process.exit(1);
|
|
2560
|
+
});
|
|
1562
2561
|
function printUsage() {
|
|
1563
2562
|
console.log(`
|
|
1564
2563
|
SQL Proper - Database migration tool
|
|
@@ -1571,6 +2570,7 @@ Commands:
|
|
|
1571
2570
|
down Roll back completed migrations
|
|
1572
2571
|
reset Roll back all migrations and reapply them
|
|
1573
2572
|
create Create a new migration
|
|
2573
|
+
patch Create a new ledger patch file (repairs migration history; no database access)
|
|
1574
2574
|
init Initialize a new config file
|
|
1575
2575
|
status Show migration status
|
|
1576
2576
|
query Execute a SQL query and display results
|
|
@@ -1589,6 +2589,7 @@ Examples:
|
|
|
1589
2589
|
proper down --increment 3 Roll back the last 3 applied migrations
|
|
1590
2590
|
proper down --all Roll back all completed migrations
|
|
1591
2591
|
proper create my_migration Create a new migration named "my_migration"
|
|
2592
|
+
proper patch fix_renamed_keys Scaffold a ledger patch file in the patch folder
|
|
1592
2593
|
proper init Create a new config file
|
|
1593
2594
|
proper status Show the status of all migrations
|
|
1594
2595
|
proper query "select * from users" Execute a SQL query
|