@metaobjectsdev/migrate-ts 0.8.1 → 0.9.0-rc.1
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 +1 -3
- package/dist/apply/apply.d.ts +61 -0
- package/dist/apply/apply.d.ts.map +1 -0
- package/dist/apply/apply.js +241 -0
- package/dist/apply/apply.js.map +1 -0
- package/dist/apply/ledger.d.ts +78 -0
- package/dist/apply/ledger.d.ts.map +1 -0
- package/dist/apply/ledger.js +146 -0
- package/dist/apply/ledger.js.map +1 -0
- package/dist/check-expr-compare.d.ts +13 -0
- package/dist/check-expr-compare.d.ts.map +1 -0
- package/dist/check-expr-compare.js +48 -0
- package/dist/check-expr-compare.js.map +1 -0
- package/dist/diff/index.d.ts +3 -1
- package/dist/diff/index.d.ts.map +1 -1
- package/dist/diff/index.js +57 -14
- package/dist/diff/index.js.map +1 -1
- package/dist/diff/status.js +3 -0
- package/dist/diff/status.js.map +1 -1
- package/dist/drift/classify.d.ts +16 -0
- package/dist/drift/classify.d.ts.map +1 -0
- package/dist/drift/classify.js +44 -0
- package/dist/drift/classify.js.map +1 -0
- package/dist/drift/drift.d.ts +32 -0
- package/dist/drift/drift.d.ts.map +1 -0
- package/dist/drift/drift.js +36 -0
- package/dist/drift/drift.js.map +1 -0
- package/dist/emit/d1-safety-pass.d.ts.map +1 -1
- package/dist/emit/d1-safety-pass.js +15 -45
- package/dist/emit/d1-safety-pass.js.map +1 -1
- package/dist/emit/postgres.d.ts.map +1 -1
- package/dist/emit/postgres.js +47 -4
- package/dist/emit/postgres.js.map +1 -1
- package/dist/emit/sqlite.d.ts.map +1 -1
- package/dist/emit/sqlite.js +22 -0
- package/dist/emit/sqlite.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -0
- package/dist/errors.js.map +1 -1
- package/dist/expected-schema.d.ts.map +1 -1
- package/dist/expected-schema.js +114 -5
- package/dist/expected-schema.js.map +1 -1
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -1
- package/dist/introspect/d1.d.ts.map +1 -1
- package/dist/introspect/d1.js +1 -0
- package/dist/introspect/d1.js.map +1 -1
- package/dist/introspect/postgres.d.ts.map +1 -1
- package/dist/introspect/postgres.js +38 -2
- package/dist/introspect/postgres.js.map +1 -1
- package/dist/introspect/sqlite.d.ts.map +1 -1
- package/dist/introspect/sqlite.js +13 -2
- package/dist/introspect/sqlite.js.map +1 -1
- package/dist/snapshot/checksum.d.ts +10 -0
- package/dist/snapshot/checksum.d.ts.map +1 -0
- package/dist/snapshot/checksum.js +14 -0
- package/dist/snapshot/checksum.js.map +1 -0
- package/dist/snapshot/plan.d.ts +25 -0
- package/dist/snapshot/plan.d.ts.map +1 -0
- package/dist/snapshot/plan.js +30 -0
- package/dist/snapshot/plan.js.map +1 -0
- package/dist/snapshot/serialize.d.ts +10 -0
- package/dist/snapshot/serialize.d.ts.map +1 -0
- package/dist/snapshot/serialize.js +63 -0
- package/dist/snapshot/serialize.js.map +1 -0
- package/dist/snapshot/store.d.ts +12 -0
- package/dist/snapshot/store.d.ts.map +1 -0
- package/dist/snapshot/store.js +32 -0
- package/dist/snapshot/store.js.map +1 -0
- package/dist/sql/split-statements.d.ts +12 -0
- package/dist/sql/split-statements.d.ts.map +1 -0
- package/dist/sql/split-statements.js +112 -0
- package/dist/sql/split-statements.js.map +1 -0
- package/dist/sql-type.d.ts +2 -0
- package/dist/sql-type.d.ts.map +1 -1
- package/dist/sql-type.js +2 -0
- package/dist/sql-type.js.map +1 -1
- package/dist/types.d.ts +36 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/verify/replay.d.ts +25 -0
- package/dist/verify/replay.d.ts.map +1 -0
- package/dist/verify/replay.js +25 -0
- package/dist/verify/replay.js.map +1 -0
- package/dist/view-sql-compare.d.ts +8 -0
- package/dist/view-sql-compare.d.ts.map +1 -0
- package/dist/view-sql-compare.js +44 -0
- package/dist/view-sql-compare.js.map +1 -0
- package/package.json +2 -2
- package/src/apply/apply.ts +340 -0
- package/src/apply/ledger.ts +241 -0
- package/src/check-expr-compare.ts +49 -0
- package/src/diff/index.ts +59 -15
- package/src/diff/status.ts +3 -0
- package/src/drift/classify.ts +56 -0
- package/src/drift/drift.ts +66 -0
- package/src/emit/d1-safety-pass.ts +16 -45
- package/src/emit/postgres.ts +47 -4
- package/src/emit/sqlite.ts +22 -0
- package/src/errors.ts +4 -0
- package/src/expected-schema.ts +124 -4
- package/src/index.ts +44 -0
- package/src/introspect/d1.ts +1 -0
- package/src/introspect/postgres.ts +38 -4
- package/src/introspect/sqlite.ts +13 -3
- package/src/snapshot/checksum.ts +15 -0
- package/src/snapshot/plan.ts +53 -0
- package/src/snapshot/serialize.ts +81 -0
- package/src/snapshot/store.ts +33 -0
- package/src/sql/split-statements.ts +115 -0
- package/src/sql-type.ts +3 -0
- package/src/types.ts +26 -9
- package/src/verify/replay.ts +43 -0
- package/src/view-sql-compare.ts +46 -0
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Schema migration tool for MetaObjects-driven projects.
|
|
|
5
5
|
Compares loaded MetaObjects metadata against a live Postgres or SQLite (libsql/Turso) database
|
|
6
6
|
and emits paired `up.sql` + `down.sql` migration files.
|
|
7
7
|
|
|
8
|
-
**Status:** v0.3. TS reference implementation
|
|
8
|
+
**Status:** v0.3. TS reference implementation. Emits migration SQL, applies pending migrations against the DB (`--apply`), and tracks migration history via a ledger table.
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
@@ -97,8 +97,6 @@ Targets Cloudflare D1 via the wrangler CLI. Connection is read from `wrangler.to
|
|
|
97
97
|
|
|
98
98
|
## Not yet shipped
|
|
99
99
|
|
|
100
|
-
- `meta migrate --apply` (apply migrations against the DB).
|
|
101
|
-
- Migration history table.
|
|
102
100
|
- Triggers, generated columns, partial indexes, exclusion constraints, check constraints.
|
|
103
101
|
- MySQL.
|
|
104
102
|
- Data migrations (column-type changes that need data transformation: error with hint).
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type Kysely } from "kysely";
|
|
2
|
+
import { type LedgerDialect, type LedgerOptions } from "./ledger.js";
|
|
3
|
+
import { splitSqlStatements } from "../sql/split-statements.js";
|
|
4
|
+
export { splitSqlStatements };
|
|
5
|
+
export interface ApplyPendingOptions {
|
|
6
|
+
/** When true, compute + return the plan but apply nothing. */
|
|
7
|
+
dryRun: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Target dialect. Decides ledger schema-qualification (pg) and whether the
|
|
10
|
+
* Postgres advisory lock is taken. Defaults to `sqlite` (no schema, no lock)
|
|
11
|
+
* to preserve the original single-DB behavior for callers that omit it.
|
|
12
|
+
*/
|
|
13
|
+
dialect?: LedgerDialect;
|
|
14
|
+
/** Multi-tenant ledger location + advisory-lock name. Defaults preserve current behavior. */
|
|
15
|
+
ledger?: LedgerOptions;
|
|
16
|
+
}
|
|
17
|
+
export interface ApplyPendingResult {
|
|
18
|
+
/** Migration names that were pending (not yet in the ledger), in order. */
|
|
19
|
+
pending: string[];
|
|
20
|
+
/** Migration names that were applied this run, in order. Empty on dryRun. */
|
|
21
|
+
applied: string[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Apply pending committed migration files in order, tracked by the
|
|
25
|
+
* migration-history ledger, transactionally.
|
|
26
|
+
*
|
|
27
|
+
* Idempotency comes from the LEDGER (skip names already recorded), NOT from
|
|
28
|
+
* re-diffing — so hand-authored files + data steps replay exactly once.
|
|
29
|
+
*
|
|
30
|
+
* For each pending migration (sorted by directory name), the file's SQL and a
|
|
31
|
+
* `recordApplied` row are run in the SAME Kysely transaction; any failure rolls
|
|
32
|
+
* back that file's tx, leaving it unrecorded (so a re-run retries it), and
|
|
33
|
+
* stops the run. Previously-applied files are checksum-compared against the
|
|
34
|
+
* ledger — a changed file errors (tamper guard).
|
|
35
|
+
*/
|
|
36
|
+
export declare function applyPending(db: Kysely<Record<string, unknown>>, dir: string, opts: ApplyPendingOptions): Promise<ApplyPendingResult>;
|
|
37
|
+
export interface RollbackToOptions {
|
|
38
|
+
/** Target dialect. Decides ledger schema-qualification + advisory lock. Defaults to `sqlite`. */
|
|
39
|
+
dialect?: LedgerDialect;
|
|
40
|
+
/** Multi-tenant ledger location + advisory-lock name. Defaults preserve current behavior. */
|
|
41
|
+
ledger?: LedgerOptions;
|
|
42
|
+
}
|
|
43
|
+
export interface RollbackToResult {
|
|
44
|
+
/** Migration names rolled back, in execution (reverse-chronological) order. */
|
|
45
|
+
rolledBack: string[];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Roll back applied migrations newer than `target` (or all, when `target` is
|
|
49
|
+
* `null`), in REVERSE lexical order — running each migration's `down.sql` then
|
|
50
|
+
* deleting its ledger row, in ONE transaction per migration. `target` is itself
|
|
51
|
+
* retained (only ledger names strictly-greater than it are rolled back; lexical
|
|
52
|
+
* = chronological given the zero-padded timestamp prefix).
|
|
53
|
+
*
|
|
54
|
+
* An empty / whitespace-only `down.sql` THROWS before that migration is
|
|
55
|
+
* unrecorded — data-migration downs are hand-authored and must never be
|
|
56
|
+
* silently skipped. `down.sql` is split with the same {@link splitSqlStatements}
|
|
57
|
+
* the up-path uses. Wrapped in the same Postgres session advisory lock as
|
|
58
|
+
* {@link applyPending} (no-op on SQLite).
|
|
59
|
+
*/
|
|
60
|
+
export declare function rollbackTo(db: Kysely<Record<string, unknown>>, dir: string, target: string | null, opts?: RollbackToOptions): Promise<RollbackToResult>;
|
|
61
|
+
//# sourceMappingURL=apply.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../src/apply/apply.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,MAAM,EAAyB,MAAM,QAAQ,CAAC;AAC5D,OAAO,EAKL,KAAK,aAAa,EAClB,KAAK,aAAa,EAGnB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAKhE,OAAO,EAAE,kBAAkB,EAAE,CAAC;AAO9B,MAAM,WAAW,mBAAmB;IAClC,8DAA8D;IAC9D,MAAM,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,6FAA6F;IAC7F,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,2EAA2E;IAC3E,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,6EAA6E;IAC7E,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAWD;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAChC,EAAE,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACnC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC,kBAAkB,CAAC,CAgD7B;AAED,MAAM,WAAW,iBAAiB;IAChC,iGAAiG;IACjG,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,6FAA6F;IAC7F,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,+EAA+E;IAC/E,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,UAAU,CAC9B,EAAE,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACnC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,IAAI,GAAE,iBAAsB,GAC3B,OAAO,CAAC,gBAAgB,CAAC,CAyC3B"}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { sql } from "kysely";
|
|
5
|
+
import { appliedRecords, DEFAULT_LEDGER_SCHEMA, deleteApplied, ensureLedger, MIGRATIONS_TABLE, recordApplied, } from "./ledger.js";
|
|
6
|
+
import { splitSqlStatements } from "../sql/split-statements.js";
|
|
7
|
+
// Re-exported here for back-compat: `splitSqlStatements` historically lived in
|
|
8
|
+
// this module. Its canonical home is now ../sql/split-statements.js (shared with
|
|
9
|
+
// the D1 safety pass), but consumers importing from apply.js keep working.
|
|
10
|
+
export { splitSqlStatements };
|
|
11
|
+
/** The per-migration up-SQL filename, shared with writeMigration's layout. */
|
|
12
|
+
const UP_SQL = "up.sql";
|
|
13
|
+
/** The per-migration down-SQL filename, shared with writeMigration's layout. */
|
|
14
|
+
const DOWN_SQL = "down.sql";
|
|
15
|
+
/**
|
|
16
|
+
* Apply pending committed migration files in order, tracked by the
|
|
17
|
+
* migration-history ledger, transactionally.
|
|
18
|
+
*
|
|
19
|
+
* Idempotency comes from the LEDGER (skip names already recorded), NOT from
|
|
20
|
+
* re-diffing — so hand-authored files + data steps replay exactly once.
|
|
21
|
+
*
|
|
22
|
+
* For each pending migration (sorted by directory name), the file's SQL and a
|
|
23
|
+
* `recordApplied` row are run in the SAME Kysely transaction; any failure rolls
|
|
24
|
+
* back that file's tx, leaving it unrecorded (so a re-run retries it), and
|
|
25
|
+
* stops the run. Previously-applied files are checksum-compared against the
|
|
26
|
+
* ledger — a changed file errors (tamper guard).
|
|
27
|
+
*/
|
|
28
|
+
export async function applyPending(db, dir, opts) {
|
|
29
|
+
const dialect = opts.dialect ?? "sqlite";
|
|
30
|
+
const ledger = opts.ledger;
|
|
31
|
+
// Serialize concurrent applies against the same ledger with a Postgres
|
|
32
|
+
// session advisory lock (no-op on SQLite). Held for the whole apply duration.
|
|
33
|
+
return withAdvisoryLock(db, dialect, ledger, async () => {
|
|
34
|
+
await ensureLedger(db, dialect, ledger);
|
|
35
|
+
const recorded = await appliedRecords(db, dialect, ledger);
|
|
36
|
+
const discovered = await discoverMigrations(dir);
|
|
37
|
+
// Tamper guard: any already-applied migration whose current up.sql checksum
|
|
38
|
+
// differs from the recorded one is a hard error.
|
|
39
|
+
for (const m of discovered) {
|
|
40
|
+
const recordedChecksum = recorded.get(m.name);
|
|
41
|
+
if (recordedChecksum === undefined)
|
|
42
|
+
continue;
|
|
43
|
+
const current = checksumOf(await readFile(m.upPath, "utf8"));
|
|
44
|
+
if (current !== recordedChecksum) {
|
|
45
|
+
throw new Error(`migration '${m.name}' was already applied but its up.sql checksum changed ` +
|
|
46
|
+
`(recorded ${recordedChecksum.slice(0, 12)}…, current ${current.slice(0, 12)}…). ` +
|
|
47
|
+
`Applied migrations are immutable; revert the edit or author a new migration.`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const pending = discovered.filter((m) => !recorded.has(m.name));
|
|
51
|
+
const pendingNames = pending.map((m) => m.name);
|
|
52
|
+
if (opts.dryRun) {
|
|
53
|
+
return { pending: pendingNames, applied: [] };
|
|
54
|
+
}
|
|
55
|
+
const applied = [];
|
|
56
|
+
for (const m of pending) {
|
|
57
|
+
const text = await readFile(m.upPath, "utf8");
|
|
58
|
+
const checksum = checksumOf(text);
|
|
59
|
+
// Run the file's SQL + the ledger insert in ONE transaction. A failure
|
|
60
|
+
// rolls the whole file back (unrecorded) and propagates — stopping the run.
|
|
61
|
+
await runSqlFileWithLedgerMutation(db, text, (trx) => recordApplied(trx, m.name, checksum, dialect, ledger));
|
|
62
|
+
applied.push(m.name);
|
|
63
|
+
}
|
|
64
|
+
return { pending: pendingNames, applied };
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Roll back applied migrations newer than `target` (or all, when `target` is
|
|
69
|
+
* `null`), in REVERSE lexical order — running each migration's `down.sql` then
|
|
70
|
+
* deleting its ledger row, in ONE transaction per migration. `target` is itself
|
|
71
|
+
* retained (only ledger names strictly-greater than it are rolled back; lexical
|
|
72
|
+
* = chronological given the zero-padded timestamp prefix).
|
|
73
|
+
*
|
|
74
|
+
* An empty / whitespace-only `down.sql` THROWS before that migration is
|
|
75
|
+
* unrecorded — data-migration downs are hand-authored and must never be
|
|
76
|
+
* silently skipped. `down.sql` is split with the same {@link splitSqlStatements}
|
|
77
|
+
* the up-path uses. Wrapped in the same Postgres session advisory lock as
|
|
78
|
+
* {@link applyPending} (no-op on SQLite).
|
|
79
|
+
*/
|
|
80
|
+
export async function rollbackTo(db, dir, target, opts = {}) {
|
|
81
|
+
const dialect = opts.dialect ?? "sqlite";
|
|
82
|
+
const ledger = opts.ledger;
|
|
83
|
+
return withAdvisoryLock(db, dialect, ledger, async () => {
|
|
84
|
+
await ensureLedger(db, dialect, ledger);
|
|
85
|
+
const recorded = await appliedRecords(db, dialect, ledger);
|
|
86
|
+
const discovered = await discoverMigrations(dir);
|
|
87
|
+
const byName = new Map(discovered.map((m) => [m.name, m]));
|
|
88
|
+
// Applied names strictly-greater than target (or all when target is null),
|
|
89
|
+
// newest-first.
|
|
90
|
+
const toRollback = [...recorded.keys()]
|
|
91
|
+
.filter((name) => target === null || compareLexical(name, target) > 0)
|
|
92
|
+
.sort((a, b) => compareLexical(b, a));
|
|
93
|
+
const rolledBack = [];
|
|
94
|
+
for (const name of toRollback) {
|
|
95
|
+
const m = byName.get(name);
|
|
96
|
+
if (m === undefined) {
|
|
97
|
+
throw new Error(`rollback '${name}': migration directory is missing (cannot read its down.sql)`);
|
|
98
|
+
}
|
|
99
|
+
const downText = await readDownSql(m.downPath, name);
|
|
100
|
+
if (downText.trim().length === 0) {
|
|
101
|
+
throw new Error(`rollback '${name}': down.sql is empty — data-migration downs must be ` +
|
|
102
|
+
`hand-authored, never silently skipped.`);
|
|
103
|
+
}
|
|
104
|
+
// Run the down SQL + the ledger delete in ONE transaction.
|
|
105
|
+
await runSqlFileWithLedgerMutation(db, downText, (trx) => deleteApplied(trx, name, dialect, ledger));
|
|
106
|
+
rolledBack.push(name);
|
|
107
|
+
}
|
|
108
|
+
return { rolledBack };
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Read a migration's `down.sql`. Distinguishes a MISSING file (never authored —
|
|
113
|
+
* ENOENT) from a present-but-empty one: a missing down throws a "not found"
|
|
114
|
+
* error (a never-written down has a different cause than a deliberately-blank
|
|
115
|
+
* one), while genuinely-empty content falls through to the caller's empty-down
|
|
116
|
+
* check. Both remain hard errors; this only reports the right cause.
|
|
117
|
+
*/
|
|
118
|
+
async function readDownSql(path, name) {
|
|
119
|
+
try {
|
|
120
|
+
return await readFile(path, "utf8");
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
if (isErrnoException(err) && err.code === "ENOENT") {
|
|
124
|
+
throw new Error(`rollback '${name}': down.sql not found for migration '${name}' ` +
|
|
125
|
+
`(expected at ${path}) — data-migration downs must be hand-authored.`);
|
|
126
|
+
}
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Narrow an unknown caught value to a Node errno exception (has a string `code`). */
|
|
131
|
+
function isErrnoException(err) {
|
|
132
|
+
return (err instanceof Error &&
|
|
133
|
+
typeof err.code === "string");
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Run `body` while holding a Postgres SESSION-level advisory lock for mutual
|
|
137
|
+
* exclusion across concurrent applies/rollbacks against the same ledger.
|
|
138
|
+
*
|
|
139
|
+
* pg session advisory locks are per-connection, so the lock is taken on a single
|
|
140
|
+
* dedicated connection (`db.connection()`) held for the entire `body` duration;
|
|
141
|
+
* `body` still runs its own migrations via `db.transaction()` on the pool — the
|
|
142
|
+
* lock only needs to be held by some session for mutual exclusion. SESSION (not
|
|
143
|
+
* transaction) level so a `CREATE INDEX CONCURRENTLY` in a migration cannot
|
|
144
|
+
* deadlock against it. On SQLite (single-writer; no advisory locks) it is a
|
|
145
|
+
* pass-through.
|
|
146
|
+
*/
|
|
147
|
+
async function withAdvisoryLock(db, dialect, ledger, body) {
|
|
148
|
+
if (dialect !== "postgres") {
|
|
149
|
+
return body();
|
|
150
|
+
}
|
|
151
|
+
const key = advisoryKey(lockNameFor(ledger));
|
|
152
|
+
return db.connection().execute(async (lockConn) => {
|
|
153
|
+
// Bind the key as a parameter cast to bigint (a signed 64-bit int as a
|
|
154
|
+
// decimal string, possibly negative) — matching the runner's
|
|
155
|
+
// `pg_advisory_lock($1::bigint)`.
|
|
156
|
+
await sql `SELECT pg_advisory_lock(${key}::bigint)`.execute(lockConn);
|
|
157
|
+
try {
|
|
158
|
+
return await body();
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
// Releasing the lock must NOT mask an in-flight body error: a throw out of
|
|
162
|
+
// `finally` would replace any pending body rejection. Log-and-swallow the
|
|
163
|
+
// unlock failure so the body's error (if any) propagates intact. The lock
|
|
164
|
+
// is session-scoped, so it is released anyway when the connection closes.
|
|
165
|
+
try {
|
|
166
|
+
await sql `SELECT pg_advisory_unlock(${key}::bigint)`.execute(lockConn);
|
|
167
|
+
}
|
|
168
|
+
catch (unlockErr) {
|
|
169
|
+
console.warn(`migrate-ts: failed to release advisory lock (it will be freed when the ` +
|
|
170
|
+
`session ends): ${String(unlockErr)}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/** Default advisory-lock name: explicit `lockName`, else `<schema>.<table>`. */
|
|
176
|
+
function lockNameFor(ledger) {
|
|
177
|
+
if (ledger?.lockName !== undefined)
|
|
178
|
+
return ledger.lockName;
|
|
179
|
+
const schema = ledger?.schema ?? DEFAULT_LEDGER_SCHEMA;
|
|
180
|
+
const table = ledger?.table ?? MIGRATIONS_TABLE;
|
|
181
|
+
return `${schema}.${table}`;
|
|
182
|
+
}
|
|
183
|
+
/** Stable 64-bit signed advisory-lock key (decimal string) from a lock name. */
|
|
184
|
+
function advisoryKey(name) {
|
|
185
|
+
const hash = createHash("sha256").update(name).digest();
|
|
186
|
+
return hash.readBigInt64BE(0).toString();
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Run a migration SQL file's statements followed by a ledger mutation, all in
|
|
190
|
+
* ONE Kysely transaction on the pool. The file is split with
|
|
191
|
+
* {@link splitSqlStatements} and each statement executed in order, then
|
|
192
|
+
* `mutateLedger` records/unrecords the migration — so the data change and its
|
|
193
|
+
* ledger row commit or roll back together. Any failure rolls the whole
|
|
194
|
+
* transaction back (leaving the ledger untouched) and propagates to the caller.
|
|
195
|
+
*
|
|
196
|
+
* Shared by both the apply (up.sql + recordApplied) and rollback
|
|
197
|
+
* (down.sql + deleteApplied) paths.
|
|
198
|
+
*/
|
|
199
|
+
async function runSqlFileWithLedgerMutation(db, sqlText, mutateLedger) {
|
|
200
|
+
await db.transaction().execute(async (trx) => {
|
|
201
|
+
for (const stmt of splitSqlStatements(sqlText)) {
|
|
202
|
+
await sql.raw(stmt).execute(trx);
|
|
203
|
+
}
|
|
204
|
+
await mutateLedger(trx);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
async function discoverMigrations(dir) {
|
|
208
|
+
let entries;
|
|
209
|
+
try {
|
|
210
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
const migrations = [];
|
|
216
|
+
for (const e of entries) {
|
|
217
|
+
if (!e.isDirectory())
|
|
218
|
+
continue;
|
|
219
|
+
migrations.push({
|
|
220
|
+
name: e.name,
|
|
221
|
+
upPath: join(dir, e.name, UP_SQL),
|
|
222
|
+
downPath: join(dir, e.name, DOWN_SQL),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
// Directory names are timestamp-prefixed (`<YYYYMMDDHHMMSS>-<slug>`), so a
|
|
226
|
+
// plain lexical (code-unit) sort is the apply order.
|
|
227
|
+
migrations.sort((a, b) => compareLexical(a.name, b.name));
|
|
228
|
+
return migrations;
|
|
229
|
+
}
|
|
230
|
+
function checksumOf(text) {
|
|
231
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
232
|
+
}
|
|
233
|
+
/** Stable lexical (code-unit) comparison; the same ordering as `a < b`/`a > b`. */
|
|
234
|
+
function compareLexical(a, b) {
|
|
235
|
+
if (a < b)
|
|
236
|
+
return -1;
|
|
237
|
+
if (a > b)
|
|
238
|
+
return 1;
|
|
239
|
+
return 0;
|
|
240
|
+
}
|
|
241
|
+
//# sourceMappingURL=apply.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apply.js","sourceRoot":"","sources":["../../src/apply/apply.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { type Kysely } from "kysely";
|
|
2
|
+
/**
|
|
3
|
+
* Default migration-history ledger table name. A single source of truth tracked
|
|
4
|
+
* across postgres + sqlite so `meta migrate --apply` can skip already-applied
|
|
5
|
+
* files (idempotency from the LEDGER, not from re-diffing).
|
|
6
|
+
*/
|
|
7
|
+
export declare const MIGRATIONS_TABLE = "_metaobjects_migrations";
|
|
8
|
+
/** Default Postgres schema holding the ledger (and, by default, the lock scope). */
|
|
9
|
+
export declare const DEFAULT_LEDGER_SCHEMA = "public";
|
|
10
|
+
/** The dialect signal threaded through the ledger fns (schema-qualification only applies to pg). */
|
|
11
|
+
export type LedgerDialect = "postgres" | "sqlite";
|
|
12
|
+
/**
|
|
13
|
+
* Multi-tenant ledger configuration. Generalizes the fixed
|
|
14
|
+
* `public._metaobjects_migrations` ledger so multiple apps/tenants can track
|
|
15
|
+
* independently in one physical database.
|
|
16
|
+
*
|
|
17
|
+
* Defaults preserve the original single-tenant behavior exactly: `schema`
|
|
18
|
+
* defaults to `public`, `table` to `_metaobjects_migrations`. Postgres uses
|
|
19
|
+
* `schema`; SQLite has no schemas and ignores it. `lockName` is consumed by the
|
|
20
|
+
* advisory-lock path (apply/rollback) — see {@link applyPending}.
|
|
21
|
+
*/
|
|
22
|
+
export interface LedgerOptions {
|
|
23
|
+
/** Postgres schema holding the ledger table. Default `public`. Ignored on SQLite. */
|
|
24
|
+
schema?: string;
|
|
25
|
+
/** Ledger table name. Default `_metaobjects_migrations`. */
|
|
26
|
+
table?: string;
|
|
27
|
+
/** Advisory-lock name. Default derived from `<schema>.<table>`. Postgres-only. */
|
|
28
|
+
lockName?: string;
|
|
29
|
+
}
|
|
30
|
+
/** A single ledger row. */
|
|
31
|
+
export interface LedgerRow {
|
|
32
|
+
/** Migration name = the `<timestamp>-<slug>` directory name (sort key + id). */
|
|
33
|
+
name: string;
|
|
34
|
+
/** sha-256 of the up.sql contents at apply time (tamper guard). */
|
|
35
|
+
checksum: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Create the migration-history table if it does not already exist. Idempotent:
|
|
39
|
+
* re-running is a no-op and preserves existing rows. Dialect-portable DDL
|
|
40
|
+
* (TEXT columns work on both sqlite and postgres; `applied_at` is stored as
|
|
41
|
+
* text so we don't depend on a dialect-specific timestamp type).
|
|
42
|
+
*
|
|
43
|
+
* On Postgres a multi-tenant `schema` is created first (`CREATE SCHEMA IF NOT
|
|
44
|
+
* EXISTS`) so the ledger can live outside `public`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function ensureLedger(db: Kysely<Record<string, unknown>>, dialect?: LedgerDialect, opts?: LedgerOptions): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Record a migration as applied. Inserts a row with the current UTC timestamp.
|
|
49
|
+
* Intended to run inside the SAME transaction that applied the migration SQL.
|
|
50
|
+
*/
|
|
51
|
+
export declare function recordApplied(db: Kysely<Record<string, unknown>>, name: string, checksum: string, dialect?: LedgerDialect, opts?: LedgerOptions): Promise<void>;
|
|
52
|
+
/** Delete a migration's ledger row (rollback unrecord). */
|
|
53
|
+
export declare function deleteApplied(db: Kysely<Record<string, unknown>>, name: string, dialect?: LedgerDialect, opts?: LedgerOptions): Promise<void>;
|
|
54
|
+
/** Return the set of applied migration names. */
|
|
55
|
+
export declare function appliedNames(db: Kysely<Record<string, unknown>>, dialect?: LedgerDialect, opts?: LedgerOptions): Promise<Set<string>>;
|
|
56
|
+
/**
|
|
57
|
+
* Return a name→checksum map for all applied migrations (tamper-guard input).
|
|
58
|
+
*
|
|
59
|
+
* The {@link BASELINE_NAME} marker row is excluded at the SQL level: it is a
|
|
60
|
+
* marker, NOT a migration, so no migration-listing consumer (e.g. rollback-all,
|
|
61
|
+
* which derives its work list from these names) should ever see it. The baseline
|
|
62
|
+
* is read independently via {@link baselineRecord}.
|
|
63
|
+
*/
|
|
64
|
+
export declare function appliedRecords(db: Kysely<Record<string, unknown>>, dialect?: LedgerDialect, opts?: LedgerOptions): Promise<Map<string, string>>;
|
|
65
|
+
/** Reserved ledger name for the baseline marker (sorts before any timestamped migration). */
|
|
66
|
+
export declare const BASELINE_NAME = "0000-baseline";
|
|
67
|
+
/**
|
|
68
|
+
* Record (or overwrite) the baseline marker — the snapshot checksum captured when
|
|
69
|
+
* `migrate baseline` seeded the reference snapshot. Lets a later check detect a
|
|
70
|
+
* snapshot that was hand-edited out of sync with the migration chain.
|
|
71
|
+
*/
|
|
72
|
+
export declare function recordBaseline(db: Kysely<Record<string, unknown>>, dialect: LedgerDialect, checksum: string, opts?: LedgerOptions): Promise<void>;
|
|
73
|
+
/** Read the baseline marker, or null if none recorded. */
|
|
74
|
+
export declare function baselineRecord(db: Kysely<Record<string, unknown>>, dialect?: LedgerDialect, opts?: LedgerOptions): Promise<{
|
|
75
|
+
name: string;
|
|
76
|
+
checksum: string;
|
|
77
|
+
} | null>;
|
|
78
|
+
//# sourceMappingURL=ledger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ledger.d.ts","sourceRoot":"","sources":["../../src/apply/ledger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAO,MAAM,QAAQ,CAAC;AAE1C;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,4BAA4B,CAAC;AAE1D,oFAAoF;AACpF,eAAO,MAAM,qBAAqB,WAAW,CAAC;AA2B9C,oGAAoG;AACpG,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,QAAQ,CAAC;AAElD;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAa;IAC5B,qFAAqF;IACrF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,2BAA2B;AAC3B,MAAM,WAAW,SAAS;IACxB,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAC;CAClB;AA4CD;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAChC,EAAE,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACnC,OAAO,GAAE,aAAwB,EACjC,IAAI,CAAC,EAAE,aAAa,GACnB,OAAO,CAAC,IAAI,CAAC,CAYf;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,EAAE,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACnC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,aAAwB,EACjC,IAAI,CAAC,EAAE,aAAa,GACnB,OAAO,CAAC,IAAI,CAAC,CAOf;AAED,2DAA2D;AAC3D,wBAAsB,aAAa,CACjC,EAAE,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACnC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,aAAwB,EACjC,IAAI,CAAC,EAAE,aAAa,GACnB,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,iDAAiD;AACjD,wBAAsB,YAAY,CAChC,EAAE,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACnC,OAAO,GAAE,aAAwB,EACjC,IAAI,CAAC,EAAE,aAAa,GACnB,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAEtB;AAED;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACnC,OAAO,GAAE,aAAwB,EACjC,IAAI,CAAC,EAAE,aAAa,GACnB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAY9B;AAED,6FAA6F;AAC7F,eAAO,MAAM,aAAa,kBAAkB,CAAC;AAE7C;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACnC,OAAO,EAAE,aAAa,EACtB,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,aAAa,GACnB,OAAO,CAAC,IAAI,CAAC,CAUf;AAED,0DAA0D;AAC1D,wBAAsB,cAAc,CAClC,EAAE,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACnC,OAAO,GAAE,aAAwB,EACjC,IAAI,CAAC,EAAE,aAAa,GACnB,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAOpD"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { sql } from "kysely";
|
|
2
|
+
/**
|
|
3
|
+
* Default migration-history ledger table name. A single source of truth tracked
|
|
4
|
+
* across postgres + sqlite so `meta migrate --apply` can skip already-applied
|
|
5
|
+
* files (idempotency from the LEDGER, not from re-diffing).
|
|
6
|
+
*/
|
|
7
|
+
export const MIGRATIONS_TABLE = "_metaobjects_migrations";
|
|
8
|
+
/** Default Postgres schema holding the ledger (and, by default, the lock scope). */
|
|
9
|
+
export const DEFAULT_LEDGER_SCHEMA = "public";
|
|
10
|
+
/**
|
|
11
|
+
* Safe SQL identifier pattern for caller-supplied `schema` / `table` names. These
|
|
12
|
+
* are interpolated as SQL identifiers (one `sql.ref` per part), so they MUST be a
|
|
13
|
+
* single, unquoted-style identifier — no dots, spaces, or quotes — otherwise
|
|
14
|
+
* Kysely's `sql.ref` would split a value like `"v1.2_migrations"` on every `.`
|
|
15
|
+
* into a wrong multi-part name. Validated at resolve time; a violation throws.
|
|
16
|
+
*/
|
|
17
|
+
const SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
18
|
+
/**
|
|
19
|
+
* Throw a clear, actionable error if a caller-supplied SQL-identifier option
|
|
20
|
+
* (`schema` / `table`) is not a single safe identifier. Names the offending
|
|
21
|
+
* option + value so the caller can fix the misconfiguration directly.
|
|
22
|
+
*/
|
|
23
|
+
function assertSafeIdentifier(option, value) {
|
|
24
|
+
if (!SAFE_IDENTIFIER.test(value)) {
|
|
25
|
+
throw new Error(`LedgerOptions.${option} must be a single SQL identifier matching ` +
|
|
26
|
+
`${SAFE_IDENTIFIER.source} (letters, digits, underscore; not starting with a digit). ` +
|
|
27
|
+
`Got ${JSON.stringify(value)} — a dotted/quoted/spaced value would be mis-parsed as a ` +
|
|
28
|
+
`multi-part identifier. Use a plain name (e.g. a per-tenant prefix) instead.`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a {@link LedgerOptions} (+ dialect) to a concrete ledger location.
|
|
33
|
+
* On Postgres the table is schema-qualified (`"<schema>"."<table>"`); on SQLite
|
|
34
|
+
* (no schema concept) the schema is ignored and the bare `"<table>"` is used.
|
|
35
|
+
*
|
|
36
|
+
* Caller-supplied `schema` / `table` are validated against {@link SAFE_IDENTIFIER}
|
|
37
|
+
* (a violation throws here, naming the option + value), then assembled from two
|
|
38
|
+
* separate `sql.ref` parts so identifier quoting stays dialect-portable AND no
|
|
39
|
+
* `.` can be mis-parsed as a multi-part separator.
|
|
40
|
+
*/
|
|
41
|
+
function resolveLedger(dialect, opts) {
|
|
42
|
+
const schema = opts?.schema ?? DEFAULT_LEDGER_SCHEMA;
|
|
43
|
+
const table = opts?.table ?? MIGRATIONS_TABLE;
|
|
44
|
+
assertSafeIdentifier("table", table);
|
|
45
|
+
if (dialect === "postgres") {
|
|
46
|
+
assertSafeIdentifier("schema", schema);
|
|
47
|
+
}
|
|
48
|
+
const ref = dialect === "postgres"
|
|
49
|
+
? sql `${sql.ref(schema)}.${sql.ref(table)}`
|
|
50
|
+
: sql `${sql.ref(table)}`;
|
|
51
|
+
return { dialect, schema, table, ref };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Create the migration-history table if it does not already exist. Idempotent:
|
|
55
|
+
* re-running is a no-op and preserves existing rows. Dialect-portable DDL
|
|
56
|
+
* (TEXT columns work on both sqlite and postgres; `applied_at` is stored as
|
|
57
|
+
* text so we don't depend on a dialect-specific timestamp type).
|
|
58
|
+
*
|
|
59
|
+
* On Postgres a multi-tenant `schema` is created first (`CREATE SCHEMA IF NOT
|
|
60
|
+
* EXISTS`) so the ledger can live outside `public`.
|
|
61
|
+
*/
|
|
62
|
+
export async function ensureLedger(db, dialect = "sqlite", opts) {
|
|
63
|
+
const ledger = resolveLedger(dialect, opts);
|
|
64
|
+
if (ledger.dialect === "postgres") {
|
|
65
|
+
await sql `CREATE SCHEMA IF NOT EXISTS ${sql.ref(ledger.schema)}`.execute(db);
|
|
66
|
+
}
|
|
67
|
+
await sql `
|
|
68
|
+
CREATE TABLE IF NOT EXISTS ${ledger.ref} (
|
|
69
|
+
name TEXT PRIMARY KEY,
|
|
70
|
+
applied_at TEXT NOT NULL,
|
|
71
|
+
checksum TEXT NOT NULL
|
|
72
|
+
)
|
|
73
|
+
`.execute(db);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Record a migration as applied. Inserts a row with the current UTC timestamp.
|
|
77
|
+
* Intended to run inside the SAME transaction that applied the migration SQL.
|
|
78
|
+
*/
|
|
79
|
+
export async function recordApplied(db, name, checksum, dialect = "sqlite", opts) {
|
|
80
|
+
const ledger = resolveLedger(dialect, opts);
|
|
81
|
+
const appliedAt = new Date().toISOString();
|
|
82
|
+
await sql `
|
|
83
|
+
INSERT INTO ${ledger.ref} (name, applied_at, checksum)
|
|
84
|
+
VALUES (${name}, ${appliedAt}, ${checksum})
|
|
85
|
+
`.execute(db);
|
|
86
|
+
}
|
|
87
|
+
/** Delete a migration's ledger row (rollback unrecord). */
|
|
88
|
+
export async function deleteApplied(db, name, dialect = "sqlite", opts) {
|
|
89
|
+
const ledger = resolveLedger(dialect, opts);
|
|
90
|
+
await sql `
|
|
91
|
+
DELETE FROM ${ledger.ref} WHERE name = ${name}
|
|
92
|
+
`.execute(db);
|
|
93
|
+
}
|
|
94
|
+
/** Return the set of applied migration names. */
|
|
95
|
+
export async function appliedNames(db, dialect = "sqlite", opts) {
|
|
96
|
+
return new Set((await appliedRecords(db, dialect, opts)).keys());
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Return a name→checksum map for all applied migrations (tamper-guard input).
|
|
100
|
+
*
|
|
101
|
+
* The {@link BASELINE_NAME} marker row is excluded at the SQL level: it is a
|
|
102
|
+
* marker, NOT a migration, so no migration-listing consumer (e.g. rollback-all,
|
|
103
|
+
* which derives its work list from these names) should ever see it. The baseline
|
|
104
|
+
* is read independently via {@link baselineRecord}.
|
|
105
|
+
*/
|
|
106
|
+
export async function appliedRecords(db, dialect = "sqlite", opts) {
|
|
107
|
+
const ledger = resolveLedger(dialect, opts);
|
|
108
|
+
// Raw select keeps this dialect-portable and sidesteps typing the dynamic
|
|
109
|
+
// table name against the untyped Kysely<Record<string, unknown>> schema.
|
|
110
|
+
const result = await sql `
|
|
111
|
+
SELECT name, checksum FROM ${ledger.ref} WHERE name != ${BASELINE_NAME}
|
|
112
|
+
`.execute(db);
|
|
113
|
+
const map = new Map();
|
|
114
|
+
for (const row of result.rows) {
|
|
115
|
+
map.set(row.name, row.checksum);
|
|
116
|
+
}
|
|
117
|
+
return map;
|
|
118
|
+
}
|
|
119
|
+
/** Reserved ledger name for the baseline marker (sorts before any timestamped migration). */
|
|
120
|
+
export const BASELINE_NAME = "0000-baseline";
|
|
121
|
+
/**
|
|
122
|
+
* Record (or overwrite) the baseline marker — the snapshot checksum captured when
|
|
123
|
+
* `migrate baseline` seeded the reference snapshot. Lets a later check detect a
|
|
124
|
+
* snapshot that was hand-edited out of sync with the migration chain.
|
|
125
|
+
*/
|
|
126
|
+
export async function recordBaseline(db, dialect, checksum, opts) {
|
|
127
|
+
const ledger = resolveLedger(dialect, opts);
|
|
128
|
+
await ensureLedger(db, dialect, opts);
|
|
129
|
+
const appliedAt = new Date().toISOString();
|
|
130
|
+
// Upsert: delete any prior baseline, then insert (portable across sqlite/pg).
|
|
131
|
+
await sql `DELETE FROM ${ledger.ref} WHERE name = ${BASELINE_NAME}`.execute(db);
|
|
132
|
+
await sql `
|
|
133
|
+
INSERT INTO ${ledger.ref} (name, applied_at, checksum)
|
|
134
|
+
VALUES (${BASELINE_NAME}, ${appliedAt}, ${checksum})
|
|
135
|
+
`.execute(db);
|
|
136
|
+
}
|
|
137
|
+
/** Read the baseline marker, or null if none recorded. */
|
|
138
|
+
export async function baselineRecord(db, dialect = "sqlite", opts) {
|
|
139
|
+
const ledger = resolveLedger(dialect, opts);
|
|
140
|
+
const result = await sql `
|
|
141
|
+
SELECT name, checksum FROM ${ledger.ref} WHERE name = ${BASELINE_NAME}
|
|
142
|
+
`.execute(db);
|
|
143
|
+
const row = result.rows[0];
|
|
144
|
+
return row ? { name: row.name, checksum: row.checksum } : null;
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=ledger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ledger.js","sourceRoot":"","sources":["../../src/apply/ledger.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Canonical form: drop casts/brackets/parens, fold `= ANY (ARRAY[…])` back to `IN`, lower-case, collapse whitespace. */
|
|
2
|
+
export declare function normalizeCheckExpr(expr: string): string;
|
|
3
|
+
/** True when two CHECK expressions are equivalent after normalization. */
|
|
4
|
+
export declare function checkExprEquals(a: string | undefined, b: string | undefined): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* `CHECK (<expr>)` → `<expr>` (balanced outer wrapper); returns input unchanged
|
|
7
|
+
* if there is no CHECK wrapper. Tolerates a trailing constraint modifier suffix
|
|
8
|
+
* (`pg_get_constraintdef` can return `CHECK (<expr>) NOT VALID`) so the wrapper
|
|
9
|
+
* still strips cleanly to the inner expression instead of falling through to the
|
|
10
|
+
* unchanged-input fallback (which would cause spurious drop+add churn).
|
|
11
|
+
*/
|
|
12
|
+
export declare function stripCheckWrapper(def: string): string;
|
|
13
|
+
//# sourceMappingURL=check-expr-compare.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check-expr-compare.d.ts","sourceRoot":"","sources":["../src/check-expr-compare.ts"],"names":[],"mappings":"AAcA,yHAAyH;AACzH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAevD;AAED,0EAA0E;AAC1E,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAGrF;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAGrD"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// src/check-expr-compare.ts
|
|
2
|
+
//
|
|
3
|
+
// CHECK-expression comparison. Postgres rewrites a stored CHECK body so the raw
|
|
4
|
+
// text we generate and the introspected text differ textually but mean the same
|
|
5
|
+
// thing. This reduces both to ONE canonical form for comparison. Reliable here
|
|
6
|
+
// because every check expression we emit is machine-derived with a simple, known
|
|
7
|
+
// shape (comparison / IN / length / regex) — there is no arbitrary author SQL to
|
|
8
|
+
// mis-normalize. The rewrites PG applies (verified against a live server):
|
|
9
|
+
// - parenthesizes terms: `col >= 0 AND col <= 100` → `(col >= 0) AND (col <= 100)`
|
|
10
|
+
// - rewrites IN-lists: `status IN ('A','B')` → `status = ANY (ARRAY['A'::text, 'B'::text])`
|
|
11
|
+
// - appends type casts: string literals gain `::text`
|
|
12
|
+
// All three are canonicalized below so an enum/range CHECK introspected from PG
|
|
13
|
+
// compares equal to the one we generate (idempotency on the --from-db / verify paths).
|
|
14
|
+
/** Canonical form: drop casts/brackets/parens, fold `= ANY (ARRAY[…])` back to `IN`, lower-case, collapse whitespace. */
|
|
15
|
+
export function normalizeCheckExpr(expr) {
|
|
16
|
+
const stripped = expr
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
// Drop `::text` / `::"MyType"` type casts PG adds to literals. The lookbehind
|
|
19
|
+
// restricts the strip to a cast that immediately follows a CLOSING single
|
|
20
|
+
// quote (`'open'::text`), so a `::` appearing INSIDE a regex pattern literal
|
|
21
|
+
// (`slug ~ 'a::foo'`) is preserved — otherwise two distinct regex CHECKs would
|
|
22
|
+
// normalize equal and a pattern change would be silently missed.
|
|
23
|
+
.replace(/(?<=')::\s*"?\w+"?/g, "")
|
|
24
|
+
.replace(/[()[\]]/g, " ") // drop parens AND square brackets (ARRAY[…])
|
|
25
|
+
.replace(/\s+/g, " ")
|
|
26
|
+
.trim();
|
|
27
|
+
// PG stores `col IN (…)` as `col = ANY (ARRAY[…])`; after the bracket strip above
|
|
28
|
+
// that reads `col = any array …`. Fold it back to the `col in …` form we emit.
|
|
29
|
+
return stripped.replace(/=\s*any\s+array/g, "in").replace(/\s+/g, " ").trim();
|
|
30
|
+
}
|
|
31
|
+
/** True when two CHECK expressions are equivalent after normalization. */
|
|
32
|
+
export function checkExprEquals(a, b) {
|
|
33
|
+
if (a === undefined || b === undefined)
|
|
34
|
+
return false;
|
|
35
|
+
return normalizeCheckExpr(a) === normalizeCheckExpr(b);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* `CHECK (<expr>)` → `<expr>` (balanced outer wrapper); returns input unchanged
|
|
39
|
+
* if there is no CHECK wrapper. Tolerates a trailing constraint modifier suffix
|
|
40
|
+
* (`pg_get_constraintdef` can return `CHECK (<expr>) NOT VALID`) so the wrapper
|
|
41
|
+
* still strips cleanly to the inner expression instead of falling through to the
|
|
42
|
+
* unchanged-input fallback (which would cause spurious drop+add churn).
|
|
43
|
+
*/
|
|
44
|
+
export function stripCheckWrapper(def) {
|
|
45
|
+
const m = /^\s*CHECK\s*\((.*)\)(?:\s+NOT\s+VALID)?\s*$/is.exec(def);
|
|
46
|
+
return m ? m[1].trim() : def.trim();
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=check-expr-compare.js.map
|