@zincapp/znvault-migrate 1.0.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/README.md +74 -0
- package/dist/adapters/engine-adapter.d.ts +91 -0
- package/dist/adapters/engine-adapter.d.ts.map +1 -0
- package/dist/adapters/engine-adapter.js +1 -0
- package/dist/adapters/engine-adapter.js.map +1 -0
- package/dist/adapters/mysql/connection.d.ts +30 -0
- package/dist/adapters/mysql/connection.d.ts.map +1 -0
- package/dist/adapters/mysql/connection.js +132 -0
- package/dist/adapters/mysql/connection.js.map +1 -0
- package/dist/adapters/mysql/lock.d.ts +41 -0
- package/dist/adapters/mysql/lock.d.ts.map +1 -0
- package/dist/adapters/mysql/lock.js +77 -0
- package/dist/adapters/mysql/lock.js.map +1 -0
- package/dist/adapters/mysql/mysql-adapter.d.ts +11 -0
- package/dist/adapters/mysql/mysql-adapter.d.ts.map +1 -0
- package/dist/adapters/mysql/mysql-adapter.js +130 -0
- package/dist/adapters/mysql/mysql-adapter.js.map +1 -0
- package/dist/adapters/mysql/mysql-conn.d.ts +22 -0
- package/dist/adapters/mysql/mysql-conn.d.ts.map +1 -0
- package/dist/adapters/mysql/mysql-conn.js +1 -0
- package/dist/adapters/mysql/mysql-conn.js.map +1 -0
- package/dist/adapters/mysql/scaffolding.d.ts +49 -0
- package/dist/adapters/mysql/scaffolding.d.ts.map +1 -0
- package/dist/adapters/mysql/scaffolding.js +70 -0
- package/dist/adapters/mysql/scaffolding.js.map +1 -0
- package/dist/adapters/mysql/schema-migrations-repo.d.ts +42 -0
- package/dist/adapters/mysql/schema-migrations-repo.d.ts.map +1 -0
- package/dist/adapters/mysql/schema-migrations-repo.js +77 -0
- package/dist/adapters/mysql/schema-migrations-repo.js.map +1 -0
- package/dist/adapters/mysql/sql-splitter.d.ts +15 -0
- package/dist/adapters/mysql/sql-splitter.d.ts.map +1 -0
- package/dist/adapters/mysql/sql-splitter.js +160 -0
- package/dist/adapters/mysql/sql-splitter.js.map +1 -0
- package/dist/config.d.ts +35 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +36 -0
- package/dist/config.js.map +1 -0
- package/dist/core/baseline-marker.d.ts +2 -0
- package/dist/core/baseline-marker.d.ts.map +1 -0
- package/dist/core/baseline-marker.js +10 -0
- package/dist/core/baseline-marker.js.map +1 -0
- package/dist/core/checksum.d.ts +18 -0
- package/dist/core/checksum.d.ts.map +1 -0
- package/dist/core/checksum.js +30 -0
- package/dist/core/checksum.js.map +1 -0
- package/dist/core/migration-files.d.ts +8 -0
- package/dist/core/migration-files.d.ts.map +1 -0
- package/dist/core/migration-files.js +32 -0
- package/dist/core/migration-files.js.map +1 -0
- package/dist/core/planner.d.ts +36 -0
- package/dist/core/planner.d.ts.map +1 -0
- package/dist/core/planner.js +81 -0
- package/dist/core/planner.js.map +1 -0
- package/dist/core/runner.d.ts +159 -0
- package/dist/core/runner.d.ts.map +1 -0
- package/dist/core/runner.js +301 -0
- package/dist/core/runner.js.map +1 -0
- package/dist/core/types.d.ts +24 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +1 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/lease/dynamic-secrets-client.d.ts +43 -0
- package/dist/lease/dynamic-secrets-client.d.ts.map +1 -0
- package/dist/lease/dynamic-secrets-client.js +110 -0
- package/dist/lease/dynamic-secrets-client.js.map +1 -0
- package/dist/lease/run-migrations.d.ts +169 -0
- package/dist/lease/run-migrations.d.ts.map +1 -0
- package/dist/lease/run-migrations.js +302 -0
- package/dist/lease/run-migrations.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
export const CHECKSUM_ALGO = 'sha256-lf-v1';
|
|
4
|
+
/**
|
|
5
|
+
* Compute the canonical sha256-lf-v1 checksum of a migration file buffer.
|
|
6
|
+
*
|
|
7
|
+
* Algorithm (byte-level — never decode to string):
|
|
8
|
+
* 1. Strip UTF-8 BOM (EF BB BF) at offset 0, if present.
|
|
9
|
+
* 2. Remove ALL 0x0D bytes (bare CR and CR in CRLF pairs).
|
|
10
|
+
* 3. SHA-256 the resulting bytes.
|
|
11
|
+
* 4. Return lowercase hex.
|
|
12
|
+
*
|
|
13
|
+
* This matches the Kotlin MigrationChecksummer implementation exactly.
|
|
14
|
+
*/
|
|
15
|
+
export function canonicalChecksum(buf) {
|
|
16
|
+
let b = buf;
|
|
17
|
+
// Strip UTF-8 BOM at offset 0
|
|
18
|
+
if (b.length >= 3 && b[0] === 0xef && b[1] === 0xbb && b[2] === 0xbf) {
|
|
19
|
+
b = b.subarray(3);
|
|
20
|
+
}
|
|
21
|
+
// Drop every 0x0D byte (CR), producing a pure-LF buffer
|
|
22
|
+
const lf = Buffer.from(b.filter((byte) => byte !== 0x0d));
|
|
23
|
+
return createHash('sha256').update(lf).digest('hex');
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Read a migration file from disk and return its canonical sha256-lf-v1 checksum.
|
|
27
|
+
*/
|
|
28
|
+
export function canonicalChecksumFile(path) {
|
|
29
|
+
return canonicalChecksum(readFileSync(path));
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checksum.js","sourceRoot":"","sources":["../../src/core/checksum.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,MAAM,CAAC,MAAM,aAAa,GAAG,cAAc,CAAC;AAE5C;;;;;;;;;;GAUG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,IAAI,CAAC,GAAG,GAAG,CAAC;IACZ,8BAA8B;IAC9B,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACrE,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IACD,wDAAwD;IACxD,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC;IAC1D,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACvD,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,OAAO,iBAAiB,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;AAC/C,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MigrationFile } from './types.js';
|
|
2
|
+
export type { MigrationFile };
|
|
3
|
+
export declare class DuplicatePrefixError extends Error {
|
|
4
|
+
constructor(prefix: string, files: string[]);
|
|
5
|
+
}
|
|
6
|
+
export declare function parsePrefix(filename: string): string;
|
|
7
|
+
export declare function discover(dir: string): MigrationFile[];
|
|
8
|
+
//# sourceMappingURL=migration-files.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migration-files.d.ts","sourceRoot":"","sources":["../../src/core/migration-files.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAKhD,YAAY,EAAE,aAAa,EAAE,CAAC;AAC9B,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE;CAI5C;AACD,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIpD;AACD,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,EAAE,CASrD"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const HELPER_RE = /^(0000)_[^/]*\.sql$/;
|
|
4
|
+
const PREFIX_RE = /^(\d{4}-\d{2}-\d{2}_\d{3})_[^/]*\.sql$/;
|
|
5
|
+
export class DuplicatePrefixError extends Error {
|
|
6
|
+
constructor(prefix, files) {
|
|
7
|
+
super(`Duplicate migration prefix '${prefix}': ${files.join(', ')}. Rename to unique monotonic prefixes.`);
|
|
8
|
+
this.name = 'DuplicatePrefixError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function parsePrefix(filename) {
|
|
12
|
+
const h = HELPER_RE.exec(filename);
|
|
13
|
+
if (h)
|
|
14
|
+
return h[1];
|
|
15
|
+
const p = PREFIX_RE.exec(filename);
|
|
16
|
+
if (p)
|
|
17
|
+
return p[1];
|
|
18
|
+
throw new Error(`Migration filename must match 0000_*.sql or YYYY-MM-DD_NNN_*.sql: ${filename}`);
|
|
19
|
+
}
|
|
20
|
+
export function discover(dir) {
|
|
21
|
+
const files = readdirSync(dir) // shallow — subdirs (baseline/, archive/) not descended
|
|
22
|
+
.filter((name) => name.endsWith('.sql') && statSync(join(dir, name)).isFile())
|
|
23
|
+
.map((name) => ({ version: name, prefix: parsePrefix(name), path: join(dir, name) }))
|
|
24
|
+
.sort((a, b) => (a.prefix < b.prefix ? -1 : a.prefix > b.prefix ? 1 : 0));
|
|
25
|
+
const byPrefix = new Map();
|
|
26
|
+
for (const f of files)
|
|
27
|
+
(byPrefix.get(f.prefix) ?? byPrefix.set(f.prefix, []).get(f.prefix)).push(f.version);
|
|
28
|
+
for (const [prefix, dups] of byPrefix)
|
|
29
|
+
if (dups.length > 1)
|
|
30
|
+
throw new DuplicatePrefixError(prefix, dups);
|
|
31
|
+
return files;
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migration-files.js","sourceRoot":"","sources":["../../src/core/migration-files.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC,MAAM,SAAS,GAAG,qBAAqB,CAAC;AACxC,MAAM,SAAS,GAAG,wCAAwC,CAAC;AAG3D,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAC7C,YAAY,MAAc,EAAE,KAAe;QACzC,KAAK,CAAC,+BAA+B,MAAM,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,wCAAwC,CAAC,CAAC;QAC3G,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACrC,CAAC;CACF;AACD,MAAM,UAAU,WAAW,CAAC,QAAgB;IAC1C,MAAM,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAAC,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC,CAAC,CAAE,CAAC;IACxD,MAAM,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAAC,IAAI,CAAC;QAAE,OAAO,CAAC,CAAC,CAAC,CAAE,CAAC;IACxD,MAAM,IAAI,KAAK,CAAC,qEAAqE,QAAQ,EAAE,CAAC,CAAC;AACnG,CAAC;AACD,MAAM,UAAU,QAAQ,CAAC,GAAW;IAClC,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,CAAkC,wDAAwD;SACrH,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;SAC7E,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;SACpF,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5E,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAoB,CAAC;IAC7C,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;IAC7G,KAAK,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,QAAQ;QAAE,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;YAAE,MAAM,IAAI,oBAAoB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACzG,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { MigrationFile, MigrationRow, MigrationPlan } from './types.js';
|
|
2
|
+
export type { MigrationPlan };
|
|
3
|
+
export declare class ChecksumMismatchError extends Error {
|
|
4
|
+
constructor(version: string);
|
|
5
|
+
}
|
|
6
|
+
export declare class OrphanTrackedRowError extends Error {
|
|
7
|
+
constructor(version: string);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Build a migration plan by comparing discovered files against tracked DB rows.
|
|
11
|
+
*
|
|
12
|
+
* Ports MigrationPlanner.kt exactly:
|
|
13
|
+
* - 0000_ helpers are skipped (applied unconditionally by the runner before planning).
|
|
14
|
+
* - For each tracked row: if no matching file exists on disk → throw OrphanTrackedRowError
|
|
15
|
+
* (migration history is immutable — a renamed/deleted file is a fatal integrity violation).
|
|
16
|
+
* - For each tracked row: if the file's checksum differs from the stored checksum → throw
|
|
17
|
+
* ChecksumMismatchError (migration history is immutable).
|
|
18
|
+
* - File without a row → pending.
|
|
19
|
+
* - Row with success=1 or baselined=1 → applied.
|
|
20
|
+
* - Row with success=0 and baselined=0 → reconcile (crashed mid-run).
|
|
21
|
+
*
|
|
22
|
+
* @param files Migration files for the CURRENT phase's directory (from
|
|
23
|
+
* discover()) — the set that gets classified into apply/reconcile/
|
|
24
|
+
* pending buckets.
|
|
25
|
+
* @param rows All rows from schema_migrations (from repo.all()).
|
|
26
|
+
* @param checksumOf Checksum function — injectable for unit tests (defaults to canonicalChecksumFile).
|
|
27
|
+
* @param allTrackedFiles The UNION of every migration directory that shares this
|
|
28
|
+
* schema_migrations table (pre ∪ post). Used ONLY for the
|
|
29
|
+
* orphan/checksum integrity lookup, so a row applied in a sibling
|
|
30
|
+
* phase's directory is not mistaken for a renamed/deleted file.
|
|
31
|
+
* Defaults to `files` (single-directory configs are byte-identical
|
|
32
|
+
* to the pre-split behavior). Classification still uses `files`
|
|
33
|
+
* alone — the current phase only ever applies its own directory.
|
|
34
|
+
*/
|
|
35
|
+
export declare function plan(files: MigrationFile[], rows: MigrationRow[], checksumOf?: (path: string) => string, allTrackedFiles?: MigrationFile[]): MigrationPlan;
|
|
36
|
+
//# sourceMappingURL=planner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"planner.d.ts","sourceRoot":"","sources":["../../src/core/planner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAG7E,YAAY,EAAE,aAAa,EAAE,CAAC;AAE9B,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAM5B;AAED,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAM5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,IAAI,CAClB,KAAK,EAAE,aAAa,EAAE,EACtB,IAAI,EAAE,YAAY,EAAE,EACpB,UAAU,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAA8B,EAC5D,eAAe,GAAE,aAAa,EAAU,GACvC,aAAa,CA4Cf"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { canonicalChecksumFile } from './checksum.js';
|
|
2
|
+
export class ChecksumMismatchError extends Error {
|
|
3
|
+
constructor(version) {
|
|
4
|
+
super(`Checksum mismatch for already-applied migration '${version}' (immutable history)`);
|
|
5
|
+
this.name = 'ChecksumMismatchError';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export class OrphanTrackedRowError extends Error {
|
|
9
|
+
constructor(version) {
|
|
10
|
+
super(`Tracked migration '${version}' has no file on disk (renamed/deleted?). Refusing to proceed.`);
|
|
11
|
+
this.name = 'OrphanTrackedRowError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Build a migration plan by comparing discovered files against tracked DB rows.
|
|
16
|
+
*
|
|
17
|
+
* Ports MigrationPlanner.kt exactly:
|
|
18
|
+
* - 0000_ helpers are skipped (applied unconditionally by the runner before planning).
|
|
19
|
+
* - For each tracked row: if no matching file exists on disk → throw OrphanTrackedRowError
|
|
20
|
+
* (migration history is immutable — a renamed/deleted file is a fatal integrity violation).
|
|
21
|
+
* - For each tracked row: if the file's checksum differs from the stored checksum → throw
|
|
22
|
+
* ChecksumMismatchError (migration history is immutable).
|
|
23
|
+
* - File without a row → pending.
|
|
24
|
+
* - Row with success=1 or baselined=1 → applied.
|
|
25
|
+
* - Row with success=0 and baselined=0 → reconcile (crashed mid-run).
|
|
26
|
+
*
|
|
27
|
+
* @param files Migration files for the CURRENT phase's directory (from
|
|
28
|
+
* discover()) — the set that gets classified into apply/reconcile/
|
|
29
|
+
* pending buckets.
|
|
30
|
+
* @param rows All rows from schema_migrations (from repo.all()).
|
|
31
|
+
* @param checksumOf Checksum function — injectable for unit tests (defaults to canonicalChecksumFile).
|
|
32
|
+
* @param allTrackedFiles The UNION of every migration directory that shares this
|
|
33
|
+
* schema_migrations table (pre ∪ post). Used ONLY for the
|
|
34
|
+
* orphan/checksum integrity lookup, so a row applied in a sibling
|
|
35
|
+
* phase's directory is not mistaken for a renamed/deleted file.
|
|
36
|
+
* Defaults to `files` (single-directory configs are byte-identical
|
|
37
|
+
* to the pre-split behavior). Classification still uses `files`
|
|
38
|
+
* alone — the current phase only ever applies its own directory.
|
|
39
|
+
*/
|
|
40
|
+
export function plan(files, rows, checksumOf = canonicalChecksumFile, allTrackedFiles = files) {
|
|
41
|
+
// Integrity lookup spans the UNION of all phase dirs that share the history table.
|
|
42
|
+
// A shared schema_migrations table (pre/ then post/ against one DB) means the
|
|
43
|
+
// post phase legitimately sees rows for pre/ migrations whose files live in the
|
|
44
|
+
// sibling directory — those must NOT be flagged as orphans.
|
|
45
|
+
const integrityByVersion = new Map(allTrackedFiles.filter((f) => !f.version.startsWith('0000_')).map((f) => [f.version, f]));
|
|
46
|
+
// Integrity pass (matches Kotlin MigrationPlanner.plan exactly, now union-scoped):
|
|
47
|
+
// Iterate every tracked row (non-0000_); throw on the first orphan or mismatch.
|
|
48
|
+
// This runs BEFORE bucket classification so no partially-classified plan is returned.
|
|
49
|
+
for (const row of rows) {
|
|
50
|
+
if (row.version.startsWith('0000_'))
|
|
51
|
+
continue; // helpers are never tracked
|
|
52
|
+
const f = integrityByVersion.get(row.version);
|
|
53
|
+
if (!f) {
|
|
54
|
+
throw new OrphanTrackedRowError(row.version);
|
|
55
|
+
}
|
|
56
|
+
if (checksumOf(f.path) !== row.checksum) {
|
|
57
|
+
throw new ChecksumMismatchError(row.version);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Bucket classification — now safe: all tracked rows have a matching file.
|
|
61
|
+
const rowByVersion = new Map(rows.map((r) => [r.version, r]));
|
|
62
|
+
const applied = [];
|
|
63
|
+
const reconcile = [];
|
|
64
|
+
const pending = [];
|
|
65
|
+
for (const f of files) {
|
|
66
|
+
// 0000_ helpers are re-applied every run and are never tracked/planned.
|
|
67
|
+
if (f.version.startsWith('0000_'))
|
|
68
|
+
continue;
|
|
69
|
+
const row = rowByVersion.get(f.version);
|
|
70
|
+
if (!row) {
|
|
71
|
+
pending.push(f);
|
|
72
|
+
}
|
|
73
|
+
else if (row.success || row.baselined) {
|
|
74
|
+
applied.push(f);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
reconcile.push(f);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { applied, reconcile, pending };
|
|
81
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"planner.js","sourceRoot":"","sources":["../../src/core/planner.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAItD,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IAC9C,YAAY,OAAe;QACzB,KAAK,CACH,oDAAoD,OAAO,uBAAuB,CACnF,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;IACtC,CAAC;CACF;AAED,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IAC9C,YAAY,OAAe;QACzB,KAAK,CACH,sBAAsB,OAAO,gEAAgE,CAC9F,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;IACtC,CAAC;CACF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,IAAI,CAClB,KAAsB,EACtB,IAAoB,EACpB,aAAuC,qBAAqB,EAC5D,kBAAmC,KAAK;IAExC,mFAAmF;IACnF,8EAA8E;IAC9E,gFAAgF;IAChF,4DAA4D;IAC5D,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAChC,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CACzF,CAAC;IAEF,mFAAmF;IACnF,gFAAgF;IAChF,sFAAsF;IACtF,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC;YAAE,SAAS,CAAC,4BAA4B;QAC3E,MAAM,CAAC,GAAG,kBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC9C,IAAI,CAAC,CAAC,EAAE,CAAC;YACP,MAAM,IAAI,qBAAqB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC/C,CAAC;QACD,IAAI,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,QAAQ,EAAE,CAAC;YACxC,MAAM,IAAI,qBAAqB,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC;IAED,2EAA2E;IAC3E,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9D,MAAM,OAAO,GAAoB,EAAE,CAAC;IACpC,MAAM,SAAS,GAAoB,EAAE,CAAC;IACtC,MAAM,OAAO,GAAoB,EAAE,CAAC;IAEpC,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,wEAAwE;QACxE,IAAI,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC;YAAE,SAAS;QAE5C,MAAM,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QACxC,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;aAAM,IAAI,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,SAAS,EAAE,CAAC;YACxC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;aAAM,CAAC;YACN,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,CAAC;IACH,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC;AACzC,CAAC"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { EngineAdapter, Conn } from '../adapters/engine-adapter.js';
|
|
2
|
+
import type { RunResult } from './types.js';
|
|
3
|
+
export type { RunResult };
|
|
4
|
+
/**
|
|
5
|
+
* Orchestrates migration discovery, planning, locking, and execution.
|
|
6
|
+
*
|
|
7
|
+
* Ports MigrationRunner.kt + MigrateMain.kt orchestration EXACTLY:
|
|
8
|
+
* - status(): read-only — ensureTable + plan; preflight(version-check only).
|
|
9
|
+
* - run(): write path — preflight(requireWritePrimary=true) → ensureTable (before lock,
|
|
10
|
+
* Kotlin parity) → acquire → seedIfVirgin → plan → reconcile → pending → release.
|
|
11
|
+
*
|
|
12
|
+
* `zn_*` helper procedures (0000_ files) are NOT created by this engine — see the
|
|
13
|
+
* doc comment on run() for details.
|
|
14
|
+
*
|
|
15
|
+
* Engine-agnostic core: the six MySQL-specific collaborators (preflight, lock
|
|
16
|
+
* acquire/isHeld/release, migration-file apply, reconcile, definer-object cleanup,
|
|
17
|
+
* schema_migrations repo) are delegated to an injected `EngineAdapter` operating
|
|
18
|
+
* on a live `Conn`. The runner's own control flow — ordering, guards, error
|
|
19
|
+
* strings — is unchanged from the MySQL-only source.
|
|
20
|
+
*/
|
|
21
|
+
export declare class MigrationRunner {
|
|
22
|
+
private readonly adapter;
|
|
23
|
+
private readonly conn;
|
|
24
|
+
private readonly migrationsDir;
|
|
25
|
+
private readonly appliedBy;
|
|
26
|
+
private readonly integrityDirs;
|
|
27
|
+
private readonly scaffolding?;
|
|
28
|
+
private repo;
|
|
29
|
+
/**
|
|
30
|
+
* @param adapter The engine adapter — implements the six MySQL-specific
|
|
31
|
+
* collaborators (preflight, lock, migration-file apply,
|
|
32
|
+
* reconcile, definer-object cleanup, schema_migrations repo).
|
|
33
|
+
* @param conn The live connection the adapter's methods operate on.
|
|
34
|
+
* @param migrationsDir The CURRENT phase's migrations directory — the one whose
|
|
35
|
+
* files are classified and applied.
|
|
36
|
+
* @param appliedBy The applied-by identity recorded on each row.
|
|
37
|
+
* @param integrityDirs Additional directories that share this DB's
|
|
38
|
+
* schema_migrations history (the OTHER migration phase's dir,
|
|
39
|
+
* e.g. the pre/ dir when this runner is running post/). Used
|
|
40
|
+
* only to widen the orphan/checksum integrity lookup so a row
|
|
41
|
+
* applied by a sibling phase is not mistaken for a
|
|
42
|
+
* renamed/deleted file. Defaults to none (single-dir configs).
|
|
43
|
+
* @param scaffolding Optional migration-helper scaffolding config for THIS phase.
|
|
44
|
+
* When set, `run()` applies `<migrationsDir>/<filename>` at the
|
|
45
|
+
* start of the phase (after discover(), before seeding/reconcile/
|
|
46
|
+
* pending) and, unconditionally, drops every object the given
|
|
47
|
+
* `leaseUser` defined once the reconcile+pending work concludes
|
|
48
|
+
* (success or failure) — see the ordering note on run(). Undefined
|
|
49
|
+
* (the default) means no scaffolding: byte-identical to the
|
|
50
|
+
* pre-scaffolding runner.
|
|
51
|
+
*/
|
|
52
|
+
constructor(adapter: EngineAdapter, conn: Conn, migrationsDir: string, appliedBy: string, integrityDirs?: string[], scaffolding?: {
|
|
53
|
+
filename: string;
|
|
54
|
+
leaseUser: string;
|
|
55
|
+
} | undefined);
|
|
56
|
+
/**
|
|
57
|
+
* Build the union of migration files across this phase's dir and any sibling
|
|
58
|
+
* integrity dirs (pre ∪ post), for the planner's integrity lookup. Missing dirs
|
|
59
|
+
* are skipped defensively (discover() throws on a non-existent path). If two dirs
|
|
60
|
+
* ever declare the same version prefix, the current phase's file wins the lookup.
|
|
61
|
+
*/
|
|
62
|
+
private allTrackedFiles;
|
|
63
|
+
/**
|
|
64
|
+
* Read-only status: how many migrations are in each state.
|
|
65
|
+
* Does NOT acquire the lock or apply any helpers.
|
|
66
|
+
* Mirrors MigrateMain: preflight(requireWritePrimary=false) for 'status'.
|
|
67
|
+
*/
|
|
68
|
+
status(): Promise<{
|
|
69
|
+
applied: number;
|
|
70
|
+
reconcile: number;
|
|
71
|
+
pending: number;
|
|
72
|
+
}>;
|
|
73
|
+
/**
|
|
74
|
+
* Apply all pending and reconcile migrations.
|
|
75
|
+
*
|
|
76
|
+
* Order (load-bearing — matches Kotlin MigrationRunner.run() exactly):
|
|
77
|
+
* 1. preflight(requireWritePrimary=true) — refuse read-only replicas.
|
|
78
|
+
* 2. ensureTable() BEFORE the lock (the only pre-lock mutation, per Kotlin parity).
|
|
79
|
+
* 3. acquire(db) — GET_LOCK.
|
|
80
|
+
* 3a. scaffolding (if configured) — apply THIS phase's helper objects (migration_utils.sql
|
|
81
|
+
* or equivalent) before any seeding/reconcile/pending work touches the DB.
|
|
82
|
+
* 4. seedBaselineIfVirgin — seed baselined rows when schema_migrations is empty.
|
|
83
|
+
* 5. plan() — classify remaining files.
|
|
84
|
+
* 6. reconcile — asserts-first; re-run body only when unmet or no asserts.
|
|
85
|
+
* 7. pending — claim(success=0) → exec → markSuccess(success=1).
|
|
86
|
+
* 7a. finally (nested, scoped to 6+7): scaffolding cleanup — if configured and the
|
|
87
|
+
* lock is still held, drop every object the lease user defined
|
|
88
|
+
* (dropDefinerObjects), unconditionally — runs whether 6/7 succeeded or threw.
|
|
89
|
+
* A cleanup failure is logged, never rethrown (must not mask a primary error).
|
|
90
|
+
* The lock-held check itself (lockHeld()) is also non-throwing — a dead
|
|
91
|
+
* connection can make the underlying IS_USED_LOCK query reject rather than
|
|
92
|
+
* resolve false, and this guard must not mask a primary error either.
|
|
93
|
+
* 8. finally: release lock; if release was not clean and no primary error, throw tripwire.
|
|
94
|
+
*
|
|
95
|
+
* The `zn_*` helper procedures (0000_ files) are NO LONGER created here. They are
|
|
96
|
+
* now provisioned ahead of the migration phase by vault's routines-apply step,
|
|
97
|
+
* owned by a persistent routines DB account — see the vault-side dynsec-routines-
|
|
98
|
+
* provisioning feature (docs/superpowers/specs/2026-07-01-dynsec-routines-
|
|
99
|
+
* provisioning-design.md). Root cause: DROP+CREATE'ing them here made the
|
|
100
|
+
* ephemeral migrate user their DEFINER, and MySQL 8.4 refuses `DROP USER` for an
|
|
101
|
+
* account referenced as a stored-routine DEFINER (ER 4006) — which broke lease
|
|
102
|
+
* revocation on every migration run. Migrations now only `CALL zn_*`; they never
|
|
103
|
+
* (re)create the procedures themselves.
|
|
104
|
+
*/
|
|
105
|
+
run(): Promise<RunResult>;
|
|
106
|
+
/**
|
|
107
|
+
* Abort immediately if this session no longer holds the GET_LOCK.
|
|
108
|
+
* A killed session or proxy reconnect would silently drop the lock server-side.
|
|
109
|
+
*/
|
|
110
|
+
private requireLockHeld;
|
|
111
|
+
/**
|
|
112
|
+
* Non-throwing counterpart to requireLockHeld() — used by cleanup paths (e.g.
|
|
113
|
+
* scaffolding's finally block) that must NOT touch the DB once the lock is lost,
|
|
114
|
+
* but also must not themselves throw while already unwinding another error.
|
|
115
|
+
*
|
|
116
|
+
* `lock.isHeld()` runs `SELECT IS_USED_LOCK(...)`, which can itself REJECT on a
|
|
117
|
+
* dead connection (killed session / proxy reconnect — exactly the scenario this
|
|
118
|
+
* guard exists for), not just resolve to false. Since callers await this from a
|
|
119
|
+
* `finally` block, an unswallowed rejection here would replace/mask whatever
|
|
120
|
+
* primary error is already in flight (a throwing `finally` overrides the pending
|
|
121
|
+
* exception). Treat any error the same as "lock not held": skip the guarded
|
|
122
|
+
* cleanup rather than risk touching a possibly-dead connection or masking the
|
|
123
|
+
* real error.
|
|
124
|
+
*/
|
|
125
|
+
private lockHeld;
|
|
126
|
+
/**
|
|
127
|
+
* Execute every SQL statement in a file (text protocol, one at a time).
|
|
128
|
+
* A failed CALL zn_assert_* signals SIGNAL '45000', which propagates as an
|
|
129
|
+
* Error and leaves any pending claim row at success=0.
|
|
130
|
+
*
|
|
131
|
+
* The split+per-statement-execute logic lives on the adapter (`applyMigrationFile`)
|
|
132
|
+
* — core only reads the file and hands the SQL text over.
|
|
133
|
+
*/
|
|
134
|
+
private executeStatements;
|
|
135
|
+
/**
|
|
136
|
+
* Reconcile a success=0 row per spec step 7a.
|
|
137
|
+
*
|
|
138
|
+
* Run the file's trailing CALL zn_assert_* postconditions FIRST:
|
|
139
|
+
* - If they exist and all pass → the migration is already fully applied;
|
|
140
|
+
* markSuccess WITHOUT re-running the body (avoids double-apply hazard for
|
|
141
|
+
* non-idempotent bodies).
|
|
142
|
+
* - If there are no asserts, OR any assert throws → re-run the ENTIRE
|
|
143
|
+
* (idempotent) body, then markSuccess.
|
|
144
|
+
*
|
|
145
|
+
* The asserts-first logic (and the requireLockHeld() guard before markSuccess)
|
|
146
|
+
* lives on the adapter (`reconcile`) — core only reads the file, derives the
|
|
147
|
+
* version, and delegates.
|
|
148
|
+
*/
|
|
149
|
+
private reconcile;
|
|
150
|
+
/**
|
|
151
|
+
* Seed baselined rows for a virgin DB (schema_migrations has no rows yet).
|
|
152
|
+
*
|
|
153
|
+
* Reads the BASELINE_MARKER from baseline/00-baseline-schema.sql and seeds
|
|
154
|
+
* a baselined row for every non-0000_ file whose prefix <= the marker.
|
|
155
|
+
* Returns the number of rows seeded.
|
|
156
|
+
*/
|
|
157
|
+
private seedBaselineIfVirgin;
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/core/runner.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,IAAI,EAAwB,MAAM,+BAA+B,CAAC;AAK/F,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,YAAY,EAAE,SAAS,EAAE,CAAC;AAE1B;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,eAAe;IA2BxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IA/B/B,OAAO,CAAC,IAAI,CAAuB;IAEnC;;;;;;;;;;;;;;;;;;;;;;OAsBG;gBAEgB,OAAO,EAAE,aAAa,EACtB,IAAI,EAAE,IAAI,EACV,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,MAAM,EACjB,aAAa,GAAE,MAAM,EAAO,EAC5B,WAAW,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,YAAA;IAKxE;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IAWvB;;;;OAIG;IACG,MAAM,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAQhF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;IACG,GAAG,IAAI,OAAO,CAAC,SAAS,CAAC;IAoG/B;;;OAGG;YACW,eAAe;IAQ7B;;;;;;;;;;;;;OAaG;YACW,QAAQ;IAatB;;;;;;;OAOG;YACW,iBAAiB;IAK/B;;;;;;;;;;;;;OAaG;YACW,SAAS;IAMvB;;;;;;OAMG;YACW,oBAAoB;CAcnC"}
|