@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
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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"}
|