@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.
Files changed (74) hide show
  1. package/README.md +74 -0
  2. package/dist/adapters/engine-adapter.d.ts +91 -0
  3. package/dist/adapters/engine-adapter.d.ts.map +1 -0
  4. package/dist/adapters/engine-adapter.js +1 -0
  5. package/dist/adapters/engine-adapter.js.map +1 -0
  6. package/dist/adapters/mysql/connection.d.ts +30 -0
  7. package/dist/adapters/mysql/connection.d.ts.map +1 -0
  8. package/dist/adapters/mysql/connection.js +132 -0
  9. package/dist/adapters/mysql/connection.js.map +1 -0
  10. package/dist/adapters/mysql/lock.d.ts +41 -0
  11. package/dist/adapters/mysql/lock.d.ts.map +1 -0
  12. package/dist/adapters/mysql/lock.js +77 -0
  13. package/dist/adapters/mysql/lock.js.map +1 -0
  14. package/dist/adapters/mysql/mysql-adapter.d.ts +11 -0
  15. package/dist/adapters/mysql/mysql-adapter.d.ts.map +1 -0
  16. package/dist/adapters/mysql/mysql-adapter.js +130 -0
  17. package/dist/adapters/mysql/mysql-adapter.js.map +1 -0
  18. package/dist/adapters/mysql/mysql-conn.d.ts +22 -0
  19. package/dist/adapters/mysql/mysql-conn.d.ts.map +1 -0
  20. package/dist/adapters/mysql/mysql-conn.js +1 -0
  21. package/dist/adapters/mysql/mysql-conn.js.map +1 -0
  22. package/dist/adapters/mysql/scaffolding.d.ts +49 -0
  23. package/dist/adapters/mysql/scaffolding.d.ts.map +1 -0
  24. package/dist/adapters/mysql/scaffolding.js +70 -0
  25. package/dist/adapters/mysql/scaffolding.js.map +1 -0
  26. package/dist/adapters/mysql/schema-migrations-repo.d.ts +42 -0
  27. package/dist/adapters/mysql/schema-migrations-repo.d.ts.map +1 -0
  28. package/dist/adapters/mysql/schema-migrations-repo.js +77 -0
  29. package/dist/adapters/mysql/schema-migrations-repo.js.map +1 -0
  30. package/dist/adapters/mysql/sql-splitter.d.ts +15 -0
  31. package/dist/adapters/mysql/sql-splitter.d.ts.map +1 -0
  32. package/dist/adapters/mysql/sql-splitter.js +160 -0
  33. package/dist/adapters/mysql/sql-splitter.js.map +1 -0
  34. package/dist/config.d.ts +35 -0
  35. package/dist/config.d.ts.map +1 -0
  36. package/dist/config.js +36 -0
  37. package/dist/config.js.map +1 -0
  38. package/dist/core/baseline-marker.d.ts +2 -0
  39. package/dist/core/baseline-marker.d.ts.map +1 -0
  40. package/dist/core/baseline-marker.js +10 -0
  41. package/dist/core/baseline-marker.js.map +1 -0
  42. package/dist/core/checksum.d.ts +18 -0
  43. package/dist/core/checksum.d.ts.map +1 -0
  44. package/dist/core/checksum.js +30 -0
  45. package/dist/core/checksum.js.map +1 -0
  46. package/dist/core/migration-files.d.ts +8 -0
  47. package/dist/core/migration-files.d.ts.map +1 -0
  48. package/dist/core/migration-files.js +32 -0
  49. package/dist/core/migration-files.js.map +1 -0
  50. package/dist/core/planner.d.ts +36 -0
  51. package/dist/core/planner.d.ts.map +1 -0
  52. package/dist/core/planner.js +81 -0
  53. package/dist/core/planner.js.map +1 -0
  54. package/dist/core/runner.d.ts +159 -0
  55. package/dist/core/runner.d.ts.map +1 -0
  56. package/dist/core/runner.js +301 -0
  57. package/dist/core/runner.js.map +1 -0
  58. package/dist/core/types.d.ts +24 -0
  59. package/dist/core/types.d.ts.map +1 -0
  60. package/dist/core/types.js +1 -0
  61. package/dist/core/types.js.map +1 -0
  62. package/dist/index.d.ts +13 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +13 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/lease/dynamic-secrets-client.d.ts +43 -0
  67. package/dist/lease/dynamic-secrets-client.d.ts.map +1 -0
  68. package/dist/lease/dynamic-secrets-client.js +110 -0
  69. package/dist/lease/dynamic-secrets-client.js.map +1 -0
  70. package/dist/lease/run-migrations.d.ts +169 -0
  71. package/dist/lease/run-migrations.d.ts.map +1 -0
  72. package/dist/lease/run-migrations.js +302 -0
  73. package/dist/lease/run-migrations.js.map +1 -0
  74. package/package.json +65 -0
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # @zincapp/znvault-migrate
2
+
3
+ Shared **MySQL** schema-migration library for ZnVault deployer tooling.
4
+
5
+ Extracts the migration engine (discover `.sql` files → plan pending/reconcile →
6
+ acquire lock → apply scaffolding helpers → run → reconcile → drop scaffolding →
7
+ track in `schema_migrations`) behind an engine-agnostic `EngineAdapter`
8
+ interface, so multiple deployer plugins and the `znvault` CLI can share one
9
+ proven migration runner.
10
+
11
+ > **Scope: MySQL only.** The `EngineAdapter` interface is engine-agnostic so a
12
+ > PostgreSQL adapter can be added later, but **no PostgreSQL adapter is built
13
+ > here** — `engine: 'postgres'` errors at validation. (PostgreSQL has an
14
+ > unsolved ownership-transfer-on-revoke problem that gets its own design.)
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install @zincapp/znvault-migrate
20
+ ```
21
+
22
+ ## What it provides
23
+
24
+ - **`runMigrations(ctx, opts, deps)`** — the top-level entrypoint: mints a
25
+ dynamic-secrets lease, opens a dedicated connection via the engine adapter,
26
+ runs the migration engine, and revokes the lease (with a settle delay,
27
+ revoke-retry, and SIGINT/SIGTERM handlers).
28
+ - **`MigrationRunner`** — the engine-agnostic orchestrator (`run()` / `status()`).
29
+ - **`mysqlAdapter`** — the MySQL `EngineAdapter` implementation.
30
+ - **`validateMigrationConfig(cfg)`** / **`MigrationConfig`** — config schema +
31
+ validation (`engine` is required; `engine: 'postgres'` is rejected).
32
+
33
+ ## Credential model
34
+
35
+ Credentials come **only** from a ZnVault dynamic-secrets lease (config carries a
36
+ `roleId`; there are no raw connection-string options). Every run mints an
37
+ ephemeral, audited lease and revokes it on exit — preserving the clean-revoke
38
+ posture the scaffolding design depends on.
39
+
40
+ ## MySQL scaffolding (ER-4006)
41
+
42
+ MySQL's `DROP USER` fails (ER-4006 on 8.4) if the user is the `DEFINER` of any
43
+ stored routine. When a `scaffoldingFile` is configured, the lease user creates
44
+ the helper procedures at phase start and the runner drops every object that user
45
+ defines at phase end — so the ephemeral migrate user owns nothing on revoke and
46
+ `DROP USER` is always clean.
47
+
48
+ ## Development
49
+
50
+ ```bash
51
+ npm install
52
+ npm run build # tsc
53
+ npm test # vitest run (real-MySQL e2e self-skips without MYSQL_TEST_HOST)
54
+ npm run typecheck
55
+ npm run lint
56
+
57
+ # Run the real-MySQL-8.4 e2e + integration tests against a local MySQL:
58
+ MYSQL_TEST_HOST=127.0.0.1 MYSQL_TEST_PORT=33306 MYSQL_TEST_DB=zincdb \
59
+ MYSQL_TEST_USER=root MYSQL_TEST_PASSWORD=root npm test
60
+ ```
61
+
62
+ ## Release
63
+
64
+ Publishing is handled by GitHub Actions on a version tag (OIDC Trusted
65
+ Publishing — no npm token):
66
+
67
+ ```bash
68
+ npm version patch # or minor/major
69
+ git push origin main --tags
70
+ ```
71
+
72
+ ## License
73
+
74
+ MIT
@@ -0,0 +1,91 @@
1
+ import type { MigrationRow } from '../core/types.js';
2
+ /** A live DB connection handle. */
3
+ export interface Conn {
4
+ /**
5
+ * Returns result ROWS DIRECTLY (unwrapped) — NOT a driver `[rows, meta]` tuple.
6
+ *
7
+ * DELIBERATE DIVERGENCE from dynamic-secrets `DatabaseClient.query()` (which returns a
8
+ * `[rows, meta]` tuple, `types.ts:35`). ROWS-DIRECTLY is chosen because the existing plugin
9
+ * runner already assumes it (`migrate/db.ts:85-88` unwraps the mysql2 tuple before returning)
10
+ * and every core-runner call site expects rows. The adapter MUST unwrap its driver's result
11
+ * to rows before returning. This exact tuple-vs-rows mismatch caused a real Critical bug before
12
+ * (`scaffolding.ts:25-34`), so it is pinned. We mirror db-clients' STRUCTURE, NOT its query shape.
13
+ */
14
+ query(sql: string, params?: unknown[]): Promise<unknown[]>;
15
+ end(): Promise<void>;
16
+ }
17
+ export interface SchemaMigrationsRepo {
18
+ ensureTable(): Promise<void>;
19
+ all(): Promise<MigrationRow[]>;
20
+ claim(version: string, checksum: string, appliedBy: string): Promise<void>;
21
+ markSuccess(version: string, executionMs: number): Promise<void>;
22
+ seedBaseline(version: string, checksum: string, appliedBy: string): Promise<void>;
23
+ }
24
+ export interface EngineCredentials {
25
+ host: string;
26
+ port: number;
27
+ database: string;
28
+ user: string;
29
+ password: string;
30
+ ssl?: boolean;
31
+ }
32
+ /**
33
+ * The engine seam. Only 'mysql' is implemented in this project; 'postgres' is a valid literal
34
+ * so the interface is future-ready, but building a PG adapter is DEFERRED (§11).
35
+ */
36
+ export interface EngineAdapter {
37
+ readonly engine: 'mysql' | 'postgres';
38
+ /** Open a DEDICATED (non-pool) connection — session-scoped locks require it. */
39
+ openConnection(creds: EngineCredentials): Promise<Conn>;
40
+ /**
41
+ * Version check + optionally refuse a read-only/replica target. Engine SQL. Throws on unsafe target.
42
+ * `requireWritePrimary`: true for a migrate run (reject a read-only replica), false for `status()`
43
+ * (allow read-only). MUST preserve the existing two-mode behavior — `db.ts:113-115`,
44
+ * called `preflight(db, true)` for migrate and `preflight(db, false)` for status
45
+ * (`migration-runner.ts:87-89`). Dropping this flag would make `status()` wrongly reject replicas.
46
+ */
47
+ preflight(conn: Conn, requireWritePrimary: boolean): Promise<void>;
48
+ /** Session-scoped migration lock. acquire throws on busy/timeout. */
49
+ acquireLock(conn: Conn): Promise<void>;
50
+ lockHeld(conn: Conn): Promise<boolean>;
51
+ /**
52
+ * Release the lock. Returns TRUE if the lock was cleanly held-and-released, FALSE if it was NOT
53
+ * held at release time (a lost-lock signal). MUST NOT throw. The core runner uses the boolean:
54
+ * if release returns false AND no primary error occurred, the runner throws
55
+ * "Lock was not held at release — a concurrent runner may have run" (the lost-lock tripwire —
56
+ * `migration-lock.ts:70-86`, `migration-runner.ts:218-223`). Returning `Promise<void>` would
57
+ * erase this concurrency-corruption signal — do NOT simplify it away.
58
+ */
59
+ releaseLock(conn: Conn): Promise<boolean>;
60
+ /** Apply one migration file. mysql: split into statements + execute each. */
61
+ applyMigrationFile(conn: Conn, sql: string): Promise<void>;
62
+ /**
63
+ * Reconcile a file whose tracking row is success=0 (claimed-but-not-confirmed — a crash between
64
+ * claim and confirm). Takes the file's `version` (derived by the core runner from the path) and
65
+ * the `SchemaMigrationsRepo` so the adapter can `markSuccess(version, 0)` when the postconditions
66
+ * pass — this matches the existing `reconcile(path)` at `migration-runner.ts:321-348` which derives
67
+ * the version and calls `repo.markSuccess(version, 0)`. mysql: re-run only the trailing
68
+ * `CALL zn_assert_*` postconditions; if they pass, the migration already applied → `markSuccess`
69
+ * without re-executing.
70
+ */
71
+ reconcile(conn: Conn, version: string, fileSql: string, repo: SchemaMigrationsRepo): Promise<void>;
72
+ schemaMigrationsRepo(conn: Conn): SchemaMigrationsRepo;
73
+ /**
74
+ * REQUIRED — the "leave the migrate user droppable" teardown (§2.3). Runs at phase end (in the
75
+ * finally, if the lock is still held) — but ONLY WHEN SCAFFOLDING IS CONFIGURED (see the runner
76
+ * contract below; the current runner only drops definer objects inside the scaffolding bracket —
77
+ * `migration-runner.ts:194-196`, pinned by `test/migrate/migration-runner.scaffolding.test.ts:327-339`).
78
+ * - mysql: drop every object the lease user is the DEFINER of (dropDefinerObjects sweep); returns count.
79
+ * (A future PG adapter would implement its OWN teardown here — §11 — NOT built in this project.)
80
+ */
81
+ dropOwnedObjects(conn: Conn, leaseUser: string): Promise<number>;
82
+ /**
83
+ * Read + split the scaffolding file into helper statements, and apply them as the lease user
84
+ * (MySQL). Takes the RESOLVED scaffolding file path (the core runner resolves `scaffoldingFile`
85
+ * against `migrationsDir` — the bare-vs-absolute rule — but the READING+SPLITTING is MySQL-specific,
86
+ * `readScaffoldingSql`/`scaffolding.ts:13-22`, so it lives on the adapter, not core). Absent on a
87
+ * PG adapter → the scaffolding bracket is a no-op.
88
+ */
89
+ applyScaffolding?(conn: Conn, scaffoldingFilePath: string): Promise<void>;
90
+ }
91
+ //# sourceMappingURL=engine-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/engine-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAErD,mCAAmC;AACnC,MAAM,WAAW,IAAI;IACnB;;;;;;;;;OASG;IACH,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IAC3D,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,GAAG,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IAC/B,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3E,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnF;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAC;CAC7F;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,UAAU,CAAC;IAEtC,gFAAgF;IAChF,cAAc,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAExD;;;;;;OAMG;IACH,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,mBAAmB,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnE,qEAAqE;IACrE,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACvC;;;;;;;OAOG;IACH,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE1C,6EAA6E;IAC7E,kBAAkB,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3D;;;;;;;;OAQG;IACH,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnG,oBAAoB,CAAC,IAAI,EAAE,IAAI,GAAG,oBAAoB,CAAC;IAEvD;;;;;;;OAOG;IACH,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAGjE;;;;;;OAMG;IACH,gBAAgB,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,mBAAmB,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3E"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine-adapter.js","sourceRoot":"","sources":["../../src/adapters/engine-adapter.ts"],"names":[],"mappings":""}
@@ -0,0 +1,30 @@
1
+ import type { Conn, EngineCredentials } from '../engine-adapter.js';
2
+ /**
3
+ * Open a single MySQL connection (NOT a pool — pools can silently reconnect
4
+ * and would drop the session-scoped GET_LOCK).
5
+ *
6
+ * Security / integrity contracts:
7
+ * - Asserts that the LIVE session has @@autocommit = 1 (not just a config flag).
8
+ * - Captures CONNECTION_ID() and exposes it as db.connectionId so callers can
9
+ * verify lock ownership via IS_USED_LOCK.
10
+ * - Sets time_zone = '+00:00' so applied_at timestamps are UTC.
11
+ * - Redacts the password from any connection error before re-throwing.
12
+ * - multipleStatements: false to prevent SQL-injection via DDL concatenation.
13
+ *
14
+ * Ported VERBATIM from znvault-plugin-payara/src/migrate/db.ts's `openDb`. The only
15
+ * change is the input type: `EngineCredentials.ssl` is `boolean | undefined` (vs. the
16
+ * source `DbConfig.ssl: boolean`) — `undefined` is treated as falsy, same as `ssl: false`.
17
+ */
18
+ export declare function openConnection(creds: EngineCredentials): Promise<Conn>;
19
+ /**
20
+ * Pre-migration safety checks against the LIVE connection. Ports
21
+ * ConnectionFactory.preflight: requires MySQL 8+, and (when requireWritePrimary)
22
+ * refuses to run against a read-only replica (SELECT @@read_only === 1).
23
+ *
24
+ * Call with requireWritePrimary=true for `migrate`, false for `status`.
25
+ *
26
+ * Ported VERBATIM from znvault-plugin-payara/src/migrate/db.ts's `preflight`. Only
27
+ * uses `conn.query`, so it works against the plain engine-agnostic `Conn`.
28
+ */
29
+ export declare function preflight(conn: Conn, requireWritePrimary: boolean): Promise<void>;
30
+ //# sourceMappingURL=connection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../../../src/adapters/mysql/connection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAGpE;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwE5E;AAED;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,mBAAmB,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CA4BvF"}
@@ -0,0 +1,132 @@
1
+ import mysql from 'mysql2/promise';
2
+ /**
3
+ * Open a single MySQL connection (NOT a pool — pools can silently reconnect
4
+ * and would drop the session-scoped GET_LOCK).
5
+ *
6
+ * Security / integrity contracts:
7
+ * - Asserts that the LIVE session has @@autocommit = 1 (not just a config flag).
8
+ * - Captures CONNECTION_ID() and exposes it as db.connectionId so callers can
9
+ * verify lock ownership via IS_USED_LOCK.
10
+ * - Sets time_zone = '+00:00' so applied_at timestamps are UTC.
11
+ * - Redacts the password from any connection error before re-throwing.
12
+ * - multipleStatements: false to prevent SQL-injection via DDL concatenation.
13
+ *
14
+ * Ported VERBATIM from znvault-plugin-payara/src/migrate/db.ts's `openDb`. The only
15
+ * change is the input type: `EngineCredentials.ssl` is `boolean | undefined` (vs. the
16
+ * source `DbConfig.ssl: boolean`) — `undefined` is treated as falsy, same as `ssl: false`.
17
+ */
18
+ export async function openConnection(creds) {
19
+ let conn;
20
+ try {
21
+ conn = await mysql.createConnection({
22
+ host: creds.host,
23
+ port: creds.port,
24
+ database: creds.database,
25
+ user: creds.user,
26
+ password: creds.password,
27
+ // Kotlin's ConnectionFactory uses sslMode=REQUIRED — encrypt but do NOT verify the
28
+ // server certificate (REQUIRED ≠ VERIFY_CA). mysql2's `ssl: {}` defaults to
29
+ // rejectUnauthorized:true (verify against the system CA store), which FAILS against
30
+ // the prod MySQL's internal CA ("unable to verify the first certificate"). Match
31
+ // Kotlin: encrypt without cert verification. (Local/e2e uses ssl:false/undefined.)
32
+ ssl: creds.ssl ? { rejectUnauthorized: false } : undefined,
33
+ multipleStatements: false,
34
+ // connector-j 9.x note: on the SSL-disabled path (dev/e2e) allowPublicKeyRetrieval
35
+ // is needed for caching_sha2_password auth. The mysql2 driver handles this
36
+ // transparently when ssl is not set.
37
+ supportBigNumbers: true,
38
+ bigNumberStrings: false, // BIGINT execution_ms comes back as JS number
39
+ timezone: 'Z',
40
+ });
41
+ }
42
+ catch (e) {
43
+ throw redact(e);
44
+ }
45
+ try {
46
+ // 1. Assert LIVE autocommit — a config flag is NOT sufficient; a MySQL Router
47
+ // or session carryover could leave it 0.
48
+ const [acRows] = (await conn.query('SELECT @@autocommit AS ac'));
49
+ const acValue = acRows[0]?.ac;
50
+ if (Number(acValue) !== 1) {
51
+ throw new Error('Target session does not have autocommit=1 — refusing to open a migration connection');
52
+ }
53
+ // 2. Set time_zone so TS-written applied_at values match the convention.
54
+ await conn.query("SET time_zone = '+00:00'");
55
+ // 3. Capture CONNECTION_ID immediately; expose it for lock ownership checks.
56
+ const [cidRows] = (await conn.query('SELECT CONNECTION_ID() AS id'));
57
+ const connectionId = Number(cidRows[0]?.id);
58
+ const db = {
59
+ connectionId,
60
+ // Text protocol — required for the migration DDL path (multipleStatements guard above).
61
+ query: async (sql, params) => {
62
+ const [rows] = await conn.query(sql, params);
63
+ return rows;
64
+ },
65
+ // Prepared statement path — used for parameterized metadata queries in repo.
66
+ // Returns raw tuple [OkPacket|rows, fields] so callers can inspect affectedRows
67
+ // for DML statements (UPDATE/INSERT) as well as row data for SELECT statements.
68
+ execute: async (sql, params) => {
69
+ // Cast to the driver's stricter `ExecuteValues[]` param type — mysql2 3.22.x
70
+ // narrowed this overload vs. the 3.16.x pinned in the source plugin; the
71
+ // interface contract here (unknown[]) is intentionally broader, and mysql2
72
+ // itself just serializes each value at runtime, so this cast changes no
73
+ // behavior, only satisfies stricter compile-time types.
74
+ const result = await conn.execute(sql, params);
75
+ return result;
76
+ },
77
+ end: () => conn.end(),
78
+ };
79
+ return db;
80
+ }
81
+ catch (e) {
82
+ await conn.end().catch(() => {
83
+ // best-effort close; swallow error
84
+ });
85
+ throw redact(e);
86
+ }
87
+ }
88
+ /**
89
+ * Pre-migration safety checks against the LIVE connection. Ports
90
+ * ConnectionFactory.preflight: requires MySQL 8+, and (when requireWritePrimary)
91
+ * refuses to run against a read-only replica (SELECT @@read_only === 1).
92
+ *
93
+ * Call with requireWritePrimary=true for `migrate`, false for `status`.
94
+ *
95
+ * Ported VERBATIM from znvault-plugin-payara/src/migrate/db.ts's `preflight`. Only
96
+ * uses `conn.query`, so it works against the plain engine-agnostic `Conn`.
97
+ */
98
+ export async function preflight(conn, requireWritePrimary) {
99
+ // 1. MySQL >= 8.0.0
100
+ // SELECT VERSION() → e.g. "8.4.10" or "8.4.10-mysql": take the part before '-',
101
+ // split on '.', compare major/minor/patch as the Kotlin does.
102
+ const versionRows = await conn.query('SELECT VERSION() AS v');
103
+ const versionValue = versionRows[0]?.v;
104
+ const versionStr = String(versionValue).split('-')[0] ?? '';
105
+ const parts = versionStr.split('.').map((p) => Number(p) || 0);
106
+ const major = parts[0] ?? 0;
107
+ const minor = parts[1] ?? 0;
108
+ const patch = parts[2] ?? 0;
109
+ // Replicate the Kotlin three-way compare: major>8 || (major==8 && minor>0) || (major==8 && minor==0 && patch>=0)
110
+ // which simplifies to: major > 8 || major === 8 (i.e. major >= 8).
111
+ const atLeast8 = major > 8 || (major === 8 && (minor > 0 || (minor === 0 && patch >= 0)));
112
+ if (!atLeast8) {
113
+ throw new Error('MySQL 8+ required');
114
+ }
115
+ // 2. Refuse to run DDL against a read-only replica.
116
+ if (requireWritePrimary) {
117
+ const roRows = await conn.query('SELECT @@read_only AS ro');
118
+ const roValue = roRows[0]?.ro;
119
+ if (Number(roValue) === 1) {
120
+ throw new Error('Target is read-only (not the write primary). Refusing to migrate.');
121
+ }
122
+ }
123
+ }
124
+ /**
125
+ * Redact the password from any error message so it never leaks in logs/traces.
126
+ */
127
+ function redact(e) {
128
+ const err = e instanceof Error ? e : new Error(String(e));
129
+ const safe = new Error(err.message.replace(/password\s*[:=]\s*'[^']*'/gi, "password:'***'"));
130
+ safe.stack = err.stack?.replace(/password\s*[:=]\s*'[^']*'/gi, "password:'***'");
131
+ return safe;
132
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.js","sourceRoot":"","sources":["../../../src/adapters/mysql/connection.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ import type { MysqlConn } from './mysql-conn.js';
2
+ export declare const LOCK_KEY = "zincapp_migration";
3
+ export declare class MigrationLockBusyError extends Error {
4
+ constructor(msg: string);
5
+ }
6
+ export declare class MigrationLockError extends Error {
7
+ constructor(msg: string);
8
+ }
9
+ /**
10
+ * Acquire the named session-scoped migration lock via GET_LOCK.
11
+ *
12
+ * GET_LOCK return values:
13
+ * 1 → lock acquired successfully
14
+ * 0 → timed out waiting (another session holds it) → throws MigrationLockBusyError
15
+ * NULL → error (OOM, connection error, etc.) → throws MigrationLockError
16
+ *
17
+ * @param db An open Db connection.
18
+ * @param timeoutSeconds How long to wait for the lock (default 30s, matching Kotlin).
19
+ */
20
+ export declare function acquire(db: MysqlConn, timeoutSeconds?: number): Promise<void>;
21
+ /**
22
+ * Check whether THIS connection's session still holds the migration lock.
23
+ *
24
+ * Uses `IS_USED_LOCK(?) = ?` with the captured CONNECTION_ID so we verify
25
+ * that the lock is held by THIS specific session — not just by any session.
26
+ * A MySQL Router / wait_timeout eviction would silently drop the lock server-side
27
+ * and this function would correctly return false even if the same code path
28
+ * reconnected with a new session.
29
+ *
30
+ * @param db An open Db connection (must be the same connection that called acquire).
31
+ */
32
+ export declare function isHeld(db: MysqlConn): Promise<boolean>;
33
+ /**
34
+ * Release the migration lock. NEVER throws — this is designed to run in a
35
+ * finally block and must not mask any in-flight migration exception.
36
+ *
37
+ * @returns true if the lock was cleanly released; false if the lock was not
38
+ * held at release time (possible lost session or concurrent runner).
39
+ */
40
+ export declare function release(db: MysqlConn): Promise<boolean>;
41
+ //# sourceMappingURL=lock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock.d.ts","sourceRoot":"","sources":["../../../src/adapters/mysql/lock.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD,eAAO,MAAM,QAAQ,sBAAsB,CAAC;AAE5C,qBAAa,sBAAuB,SAAQ,KAAK;gBACnC,GAAG,EAAE,MAAM;CAIxB;AAED,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,GAAG,EAAE,MAAM;CAIxB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,cAAc,SAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAU/E;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,MAAM,CAAC,EAAE,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CAQ5D;AAED;;;;;;GAMG;AACH,wBAAsB,OAAO,CAAC,EAAE,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CAiB7D"}
@@ -0,0 +1,77 @@
1
+ export const LOCK_KEY = 'zincapp_migration';
2
+ export class MigrationLockBusyError extends Error {
3
+ constructor(msg) {
4
+ super(msg);
5
+ this.name = 'MigrationLockBusyError';
6
+ }
7
+ }
8
+ export class MigrationLockError extends Error {
9
+ constructor(msg) {
10
+ super(msg);
11
+ this.name = 'MigrationLockError';
12
+ }
13
+ }
14
+ /**
15
+ * Acquire the named session-scoped migration lock via GET_LOCK.
16
+ *
17
+ * GET_LOCK return values:
18
+ * 1 → lock acquired successfully
19
+ * 0 → timed out waiting (another session holds it) → throws MigrationLockBusyError
20
+ * NULL → error (OOM, connection error, etc.) → throws MigrationLockError
21
+ *
22
+ * @param db An open Db connection.
23
+ * @param timeoutSeconds How long to wait for the lock (default 30s, matching Kotlin).
24
+ */
25
+ export async function acquire(db, timeoutSeconds = 30) {
26
+ const rows = await db.query('SELECT GET_LOCK(?, ?)', [LOCK_KEY, timeoutSeconds]);
27
+ // mysql2 returns the result column by its expression text; extract robustly
28
+ const cell = Object.values(rows[0])[0];
29
+ if (cell === null || cell === undefined) {
30
+ throw new MigrationLockError(`GET_LOCK returned NULL (connection/server error)`);
31
+ }
32
+ if (Number(cell) !== 1) {
33
+ throw new MigrationLockBusyError(`Another migration runner holds '${LOCK_KEY}' (GET_LOCK returned 0)`);
34
+ }
35
+ }
36
+ /**
37
+ * Check whether THIS connection's session still holds the migration lock.
38
+ *
39
+ * Uses `IS_USED_LOCK(?) = ?` with the captured CONNECTION_ID so we verify
40
+ * that the lock is held by THIS specific session — not just by any session.
41
+ * A MySQL Router / wait_timeout eviction would silently drop the lock server-side
42
+ * and this function would correctly return false even if the same code path
43
+ * reconnected with a new session.
44
+ *
45
+ * @param db An open Db connection (must be the same connection that called acquire).
46
+ */
47
+ export async function isHeld(db) {
48
+ const rows = await db.query('SELECT IS_USED_LOCK(?) = ? AS held', [LOCK_KEY, db.connectionId]);
49
+ const cell = rows[0]['held'];
50
+ if (cell === null || cell === undefined) {
51
+ // NULL means no session holds the lock at all
52
+ return false;
53
+ }
54
+ return Number(cell) === 1;
55
+ }
56
+ /**
57
+ * Release the migration lock. NEVER throws — this is designed to run in a
58
+ * finally block and must not mask any in-flight migration exception.
59
+ *
60
+ * @returns true if the lock was cleanly released; false if the lock was not
61
+ * held at release time (possible lost session or concurrent runner).
62
+ */
63
+ export async function release(db) {
64
+ try {
65
+ const rows = await db.query('SELECT RELEASE_LOCK(?) AS released', [LOCK_KEY]);
66
+ const cell = rows[0]['released'];
67
+ const released = cell !== null && cell !== undefined && Number(cell) === 1;
68
+ if (!released) {
69
+ console.warn(`RELEASE_LOCK('${LOCK_KEY}') returned ${String(cell)} — lock was not held at release (lost session / concurrent runner?)`);
70
+ }
71
+ return released;
72
+ }
73
+ catch (e) {
74
+ console.warn(`RELEASE_LOCK('${LOCK_KEY}') failed (connection likely dead): ${e instanceof Error ? e.message : String(e)}`);
75
+ return false;
76
+ }
77
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock.js","sourceRoot":"","sources":["../../../src/adapters/mysql/lock.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,QAAQ,GAAG,mBAAmB,CAAC;AAE5C,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IAC/C,YAAY,GAAW;QACrB,KAAK,CAAC,GAAG,CAAC,CAAC;QACX,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;IACvC,CAAC;CACF;AAED,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,GAAW;QACrB,KAAK,CAAC,GAAG,CAAC,CAAC;QACX,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,EAAa,EAAE,cAAc,GAAG,EAAE;IAC9D,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,uBAAuB,EAAE,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC,CAAC;IACjF,4EAA4E;IAC5E,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAA4B,CAAC,CAAC,CAAC,CAAC,CAAC;IAClE,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACxC,MAAM,IAAI,kBAAkB,CAAC,kDAAkD,CAAC,CAAC;IACnF,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,sBAAsB,CAAC,mCAAmC,QAAQ,yBAAyB,CAAC,CAAC;IACzG,CAAC;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,EAAa;IACxC,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,oCAAoC,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC;IAC/F,MAAM,IAAI,GAAI,IAAI,CAAC,CAAC,CAA6B,CAAC,MAAM,CAAC,CAAC;IAC1D,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACxC,8CAA8C;QAC9C,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC5B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,EAAa;IACzC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,KAAK,CAAC,oCAAoC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;QAC9E,MAAM,IAAI,GAAI,IAAI,CAAC,CAAC,CAA6B,CAAC,UAAU,CAAC,CAAC;QAC9D,MAAM,QAAQ,GAAG,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,SAAS,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3E,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,OAAO,CAAC,IAAI,CACV,iBAAiB,QAAQ,eAAe,MAAM,CAAC,IAAI,CAAC,qEAAqE,CAC1H,CAAC;QACJ,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,OAAO,CAAC,IAAI,CACV,iBAAiB,QAAQ,uCAAuC,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAC7G,CAAC;QACF,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { EngineAdapter } from '../engine-adapter.js';
2
+ /**
3
+ * The concrete MySQL EngineAdapter. Wires the already-ported leaf modules
4
+ * (lock, sql-splitter, scaffolding, schema-migrations-repo) and this file's
5
+ * connection.ts into the engine-agnostic `EngineAdapter` interface consumed by
6
+ * `core/runner.ts`. No new logic beyond delegation — the split-execute apply
7
+ * logic and the asserts-first reconcile logic are relocated verbatim from the
8
+ * source `migration-runner.ts` (`executeStatements` / `reconcile`).
9
+ */
10
+ export declare const mysqlAdapter: EngineAdapter;
11
+ //# sourceMappingURL=mysql-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mysql-adapter.d.ts","sourceRoot":"","sources":["../../../src/adapters/mysql/mysql-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAQ,aAAa,EAAoE,MAAM,sBAAsB,CAAC;AAiBlI;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,EAAE,aA+H1B,CAAC"}
@@ -0,0 +1,130 @@
1
+ import { openConnection, preflight } from './connection.js';
2
+ import * as lock from './lock.js';
3
+ import { splitStatements } from './sql-splitter.js';
4
+ import { readScaffoldingSql, dropDefinerObjects } from './scaffolding.js';
5
+ import { SchemaMigrationsRepo } from './schema-migrations-repo.js';
6
+ /**
7
+ * True when a statement is a postcondition assertion call (zn_assert_* proc).
8
+ * MySQL-convention detection, relocated verbatim from
9
+ * znvault-plugin-payara/src/migrate/migration-runner.ts's private `isPostcondition`.
10
+ */
11
+ function isPostcondition(stmt) {
12
+ return /^\s*CALL\s+zn_assert_/i.test(stmt);
13
+ }
14
+ /**
15
+ * The concrete MySQL EngineAdapter. Wires the already-ported leaf modules
16
+ * (lock, sql-splitter, scaffolding, schema-migrations-repo) and this file's
17
+ * connection.ts into the engine-agnostic `EngineAdapter` interface consumed by
18
+ * `core/runner.ts`. No new logic beyond delegation — the split-execute apply
19
+ * logic and the asserts-first reconcile logic are relocated verbatim from the
20
+ * source `migration-runner.ts` (`executeStatements` / `reconcile`).
21
+ */
22
+ export const mysqlAdapter = {
23
+ engine: 'mysql',
24
+ openConnection(creds) {
25
+ return openConnection(creds);
26
+ },
27
+ preflight(conn, requireWritePrimary) {
28
+ return preflight(conn, requireWritePrimary);
29
+ },
30
+ acquireLock(conn) {
31
+ return lock.acquire(conn);
32
+ },
33
+ lockHeld(conn) {
34
+ return lock.isHeld(conn);
35
+ },
36
+ releaseLock(conn) {
37
+ return lock.release(conn);
38
+ },
39
+ /**
40
+ * Relocated verbatim from migration-runner.ts's private `executeStatements`:
41
+ * split the file's SQL into individual statements and execute each in order
42
+ * over the text protocol.
43
+ */
44
+ async applyMigrationFile(conn, sql) {
45
+ for (const stmt of splitStatements(sql)) {
46
+ await conn.query(stmt);
47
+ }
48
+ },
49
+ /**
50
+ * Relocated verbatim from migration-runner.ts's private `reconcile(path)`:
51
+ * asserts-first — run the file's trailing `CALL zn_assert_*` postconditions
52
+ * first; if they all pass, the migration is already applied (markSuccess
53
+ * without re-running the idempotent body). If there are no asserts, or any
54
+ * assert throws, re-run the entire body, then markSuccess.
55
+ *
56
+ * FIDELITY NOTE (documented per task brief): the source's `reconcile(path)`
57
+ * called `this.requireLockHeld()` a SECOND time here, immediately before
58
+ * `repo.markSuccess(version, 0)` (a guard against a session kill happening
59
+ * during the (possibly long) body re-execution). That per-reconcile guard is
60
+ * intentionally OMITTED here — the interface's `reconcile` signature has no
61
+ * lock hook, and lock-liveness semantics are owned by the core runner, not
62
+ * this adapter. The core runner (`core/runner.ts`, reconcile loop) already
63
+ * calls `await this.requireLockHeld()` immediately BEFORE invoking
64
+ * `adapter.reconcile(...)` for each file, so the PRE-reconcile guard is
65
+ * preserved. What's dropped is only the narrower POST-body-re-execution,
66
+ * pre-markSuccess check. This is a minimal, deliberate fidelity relocation,
67
+ * not an oversight — flagged here per the task brief's instruction to
68
+ * document rather than silently decide.
69
+ */
70
+ async reconcile(conn, version, fileSql, repo) {
71
+ const stmts = splitStatements(fileSql);
72
+ const asserts = stmts.filter((s) => isPostcondition(s));
73
+ let satisfied = false;
74
+ if (asserts.length > 0) {
75
+ try {
76
+ for (const a of asserts) {
77
+ await conn.query(a);
78
+ }
79
+ satisfied = true; // all asserts passed
80
+ }
81
+ catch {
82
+ satisfied = false; // at least one assert failed → re-run body
83
+ }
84
+ }
85
+ // satisfied = false also when asserts.length === 0 (no-asserts → always re-run)
86
+ if (!satisfied) {
87
+ // Re-run entire body — must be idempotent (spec requirement).
88
+ // A genuinely failed CALL zn_assert_* will throw and propagate.
89
+ for (const s of stmts) {
90
+ await conn.query(s);
91
+ }
92
+ }
93
+ await repo.markSuccess(version, 0);
94
+ },
95
+ schemaMigrationsRepo(conn) {
96
+ return new SchemaMigrationsRepo(conn);
97
+ },
98
+ dropOwnedObjects(conn, leaseUser) {
99
+ return dropDefinerObjects(conn, leaseUser);
100
+ },
101
+ /**
102
+ * Read + split the scaffolding file (already resolved to an absolute path by
103
+ * the core runner) and apply its statements over the text protocol. The core
104
+ * runner passes an already-resolved absolute path in `scaffoldingFilePath`;
105
+ * `readScaffoldingSql`'s `isAbsolute(filename) ? filename : join(migrationsDir,
106
+ * filename)` short-circuits on an absolute filename, so the `migrationsDir`
107
+ * argument is irrelevant here and passed as ''.
108
+ *
109
+ * FIDELITY NOTE: the source runner's scaffolding-apply loop
110
+ * (`migration-runner.ts`) called `this.requireLockHeld()` before EACH
111
+ * statement — a per-statement guard against a killed session / proxy
112
+ * reconnect silently dropping the GET_LOCK server-side mid-apply. That
113
+ * per-statement check is restored HERE, in the adapter, because both the
114
+ * scaffolding split/apply logic and the lock module are MySQL-specific
115
+ * concerns; `core/runner.ts` calls this method as a single unit and stays
116
+ * engine-SQL-free.
117
+ */
118
+ async applyScaffolding(conn, scaffoldingFilePath) {
119
+ const { statements } = readScaffoldingSql('', scaffoldingFilePath);
120
+ for (const stmt of statements) {
121
+ // Per-statement lock-liveness guard (faithful port of the source runner's
122
+ // scaffolding loop): a killed session / proxy reconnect silently drops the
123
+ // GET_LOCK server-side; abort before running further DDL on a lock-less session.
124
+ if (!(await lock.isHeld(conn))) {
125
+ throw new Error('Lost the migration lock mid-run (session killed or reconnected). Aborting to avoid concurrent DDL.');
126
+ }
127
+ await conn.query(stmt);
128
+ }
129
+ },
130
+ };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mysql-adapter.js","sourceRoot":"","sources":["../../../src/adapters/mysql/mysql-adapter.ts"],"names":[],"mappings":"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"}