@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/dist/sql-type.d.ts
CHANGED
package/dist/sql-type.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sql-type.d.ts","sourceRoot":"","sources":["../src/sql-type.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,MAAM,OAAO,GACf;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,EAAE,GAAG,EAAE,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACvD;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,YAAY,EAAE,OAAO,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAErB,sCAAsC;AACtC,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"sql-type.d.ts","sourceRoot":"","sources":["../src/sql-type.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,MAAM,OAAO,GACf;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,EAAE,GAAG,EAAE,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACvD;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,YAAY,EAAE,OAAO,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAErB,sCAAsC;AACtC,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAuB7D;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,GAAG,OAAO,CAyC9D"}
|
package/dist/sql-type.js
CHANGED
|
@@ -17,6 +17,7 @@ export function sqlTypeEquals(a, b) {
|
|
|
17
17
|
case "real4":
|
|
18
18
|
case "boolean":
|
|
19
19
|
case "date":
|
|
20
|
+
case "time":
|
|
20
21
|
case "json":
|
|
21
22
|
case "blob":
|
|
22
23
|
case "uuid":
|
|
@@ -68,6 +69,7 @@ export function isWidening(from, to) {
|
|
|
68
69
|
case "real4":
|
|
69
70
|
case "boolean":
|
|
70
71
|
case "date":
|
|
72
|
+
case "time":
|
|
71
73
|
case "json":
|
|
72
74
|
case "blob":
|
|
73
75
|
case "uuid":
|
package/dist/sql-type.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sql-type.js","sourceRoot":"","sources":["../src/sql-type.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sql-type.js","sourceRoot":"","sources":["../src/sql-type.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export interface TableDescriptor {
|
|
|
23
23
|
columns: ColumnDescriptor[];
|
|
24
24
|
indexes: IndexDescriptor[];
|
|
25
25
|
foreignKeys: FkDescriptor[];
|
|
26
|
+
checks: CheckDescriptor[];
|
|
26
27
|
primaryKey: string[];
|
|
27
28
|
/**
|
|
28
29
|
* Human-readable description threaded from entity `@description`.
|
|
@@ -55,6 +56,12 @@ export interface IndexDescriptor {
|
|
|
55
56
|
columns: string[];
|
|
56
57
|
unique: boolean;
|
|
57
58
|
}
|
|
59
|
+
export interface CheckDescriptor {
|
|
60
|
+
/** Constraint name, e.g. `<table>_<column>_chk`. Diff/identity key. */
|
|
61
|
+
name: string;
|
|
62
|
+
/** The boolean SQL expression, e.g. `status IN ('OPEN','CLOSED')`. */
|
|
63
|
+
expression: string;
|
|
64
|
+
}
|
|
58
65
|
export interface FkDescriptor {
|
|
59
66
|
name: string;
|
|
60
67
|
columns: string[];
|
|
@@ -69,11 +76,17 @@ export interface ViewDescriptor {
|
|
|
69
76
|
/** Same semantics as TableDescriptor.schema. */
|
|
70
77
|
schema?: string;
|
|
71
78
|
/**
|
|
72
|
-
* View
|
|
73
|
-
*
|
|
74
|
-
* `buildExpectedSchema`
|
|
75
|
-
*
|
|
76
|
-
*
|
|
79
|
+
* View definition SQL.
|
|
80
|
+
*
|
|
81
|
+
* On the EXPECTED side (`buildExpectedSchema` / `buildExpectedViews`) this is
|
|
82
|
+
* the view body — the SELECT clause through the FROM/WHERE/GROUP-BY tail.
|
|
83
|
+
*
|
|
84
|
+
* On the ACTUAL side (`introspect`) this is whatever the DB catalog stores:
|
|
85
|
+
* sqlite's `sqlite_master.sql` is the full `CREATE VIEW <name> AS <body>`
|
|
86
|
+
* statement, while Postgres' `information_schema.views.view_definition` is the
|
|
87
|
+
* body only. `diff`'s view-body comparator normalizes both sides (strips any
|
|
88
|
+
* leading `CREATE VIEW ... AS`, collapses whitespace) before comparing, so a
|
|
89
|
+
* body change triggers a `replace-view`.
|
|
77
90
|
*/
|
|
78
91
|
sql?: string;
|
|
79
92
|
}
|
|
@@ -97,6 +110,7 @@ export type Change = {
|
|
|
97
110
|
kind: "drop-table";
|
|
98
111
|
table: string;
|
|
99
112
|
schema?: string;
|
|
113
|
+
restore?: TableDescriptor;
|
|
100
114
|
status: ChangeStatus;
|
|
101
115
|
} | {
|
|
102
116
|
kind: "rename-table";
|
|
@@ -115,6 +129,7 @@ export type Change = {
|
|
|
115
129
|
table: string;
|
|
116
130
|
schema?: string;
|
|
117
131
|
column: string;
|
|
132
|
+
restore?: ColumnDescriptor;
|
|
118
133
|
status: ChangeStatus;
|
|
119
134
|
} | {
|
|
120
135
|
kind: "rename-column";
|
|
@@ -158,6 +173,7 @@ export type Change = {
|
|
|
158
173
|
table: string;
|
|
159
174
|
schema?: string;
|
|
160
175
|
index: string;
|
|
176
|
+
restore?: IndexDescriptor;
|
|
161
177
|
status: ChangeStatus;
|
|
162
178
|
} | {
|
|
163
179
|
kind: "add-fk";
|
|
@@ -170,6 +186,20 @@ export type Change = {
|
|
|
170
186
|
table: string;
|
|
171
187
|
schema?: string;
|
|
172
188
|
fk: string;
|
|
189
|
+
restore?: FkDescriptor;
|
|
190
|
+
status: ChangeStatus;
|
|
191
|
+
} | {
|
|
192
|
+
kind: "add-check";
|
|
193
|
+
table: string;
|
|
194
|
+
schema?: string;
|
|
195
|
+
check: CheckDescriptor;
|
|
196
|
+
status: ChangeStatus;
|
|
197
|
+
} | {
|
|
198
|
+
kind: "drop-check";
|
|
199
|
+
table: string;
|
|
200
|
+
schema?: string;
|
|
201
|
+
check: string;
|
|
202
|
+
restore?: CheckDescriptor;
|
|
173
203
|
status: ChangeStatus;
|
|
174
204
|
} | {
|
|
175
205
|
kind: "create-view";
|
|
@@ -199,6 +229,7 @@ export interface AllowOptions {
|
|
|
199
229
|
typeChange?: boolean;
|
|
200
230
|
dropIndex?: boolean;
|
|
201
231
|
dropFk?: boolean;
|
|
232
|
+
dropCheck?: boolean;
|
|
202
233
|
/** Existing data must satisfy NOT NULL; diff cannot verify this. */
|
|
203
234
|
nullableToNotNull?: boolean;
|
|
204
235
|
}
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAO7C,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,8EAA8E;IAC9E,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB;;;OAGG;IACH,IAAI,CAAC,EAAE,YAAY,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,WAAW,EAAE,YAAY,EAAE,CAAC;IAC5B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,QAAQ,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC;IAChC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAED,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,WAAW,CAAC;AAEzE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Kysely } from "kysely";
|
|
2
|
+
import { type DriftClassification } from "../drift/classify.js";
|
|
3
|
+
import type { Dialect, SchemaSnapshot } from "../types.js";
|
|
4
|
+
export interface VerifyReplayArgs {
|
|
5
|
+
/** A FRESH, throwaway database. Replay applies every migration into it from empty. */
|
|
6
|
+
db: Kysely<Record<string, unknown>>;
|
|
7
|
+
dialect: Extract<Dialect, "postgres" | "sqlite">;
|
|
8
|
+
/** Directory holding the committed `<timestamp>-<slug>/up.sql` migrations. */
|
|
9
|
+
migrationsDir: string;
|
|
10
|
+
/** The committed snapshot the migrations are expected to reproduce. */
|
|
11
|
+
snapshot: SchemaSnapshot;
|
|
12
|
+
}
|
|
13
|
+
export interface VerifyReplayResult extends DriftClassification {
|
|
14
|
+
/** True when the replayed schema matches the snapshot (no drift, no unmanaged). */
|
|
15
|
+
ok: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Replay all committed migrations into a fresh database, introspect the result,
|
|
19
|
+
* and compare it to the committed snapshot. A non-empty `drift`/`unmanaged` means
|
|
20
|
+
* the migrations-as-applied diverge from the snapshot — e.g. a hand-edited up.sql
|
|
21
|
+
* that changed structure the metadata-derived snapshot doesn't know about. The
|
|
22
|
+
* ledger sidecar table is excluded from the comparison.
|
|
23
|
+
*/
|
|
24
|
+
export declare function verifyReplay(args: VerifyReplayArgs): Promise<VerifyReplayResult>;
|
|
25
|
+
//# sourceMappingURL=replay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../../src/verify/replay.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAIrC,OAAO,EAAwB,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AACtF,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE3D,MAAM,WAAW,gBAAgB;IAC/B,sFAAsF;IACtF,EAAE,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACpC,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,UAAU,GAAG,QAAQ,CAAC,CAAC;IACjD,8EAA8E;IAC9E,aAAa,EAAE,MAAM,CAAC;IACtB,uEAAuE;IACvE,QAAQ,EAAE,cAAc,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAmB,SAAQ,mBAAmB;IAC7D,mFAAmF;IACnF,EAAE,EAAE,OAAO,CAAC;CACb;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAYtF"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { applyPending } from "../apply/apply.js";
|
|
2
|
+
import { MIGRATIONS_TABLE } from "../apply/ledger.js";
|
|
3
|
+
import { introspect } from "../introspect/index.js";
|
|
4
|
+
import { driftAgainstSnapshot } from "../drift/classify.js";
|
|
5
|
+
/**
|
|
6
|
+
* Replay all committed migrations into a fresh database, introspect the result,
|
|
7
|
+
* and compare it to the committed snapshot. A non-empty `drift`/`unmanaged` means
|
|
8
|
+
* the migrations-as-applied diverge from the snapshot — e.g. a hand-edited up.sql
|
|
9
|
+
* that changed structure the metadata-derived snapshot doesn't know about. The
|
|
10
|
+
* ledger sidecar table is excluded from the comparison.
|
|
11
|
+
*/
|
|
12
|
+
export async function verifyReplay(args) {
|
|
13
|
+
await applyPending(args.db, args.migrationsDir, { dryRun: false, dialect: args.dialect });
|
|
14
|
+
const introspected = await introspect(args.db, args.dialect);
|
|
15
|
+
const actual = {
|
|
16
|
+
...introspected,
|
|
17
|
+
tables: introspected.tables.filter((t) => t.name !== MIGRATIONS_TABLE),
|
|
18
|
+
};
|
|
19
|
+
const classification = await driftAgainstSnapshot(args.snapshot, actual, args.dialect);
|
|
20
|
+
return {
|
|
21
|
+
...classification,
|
|
22
|
+
ok: classification.drift.length === 0 && classification.unmanaged.length === 0,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=replay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"replay.js","sourceRoot":"","sources":["../../src/verify/replay.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,oBAAoB,EAA4B,MAAM,sBAAsB,CAAC;AAkBtF;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAsB;IACvD,MAAM,YAAY,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,aAAa,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;IAC1F,MAAM,YAAY,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAC7D,MAAM,MAAM,GAAmB;QAC7B,GAAG,YAAY;QACf,MAAM,EAAE,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,gBAAgB,CAAC;KACvE,CAAC;IACF,MAAM,cAAc,GAAG,MAAM,oBAAoB,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IACvF,OAAO;QACL,GAAG,cAAc;QACjB,EAAE,EAAE,cAAc,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,cAAc,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC;KAC/E,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collapse a view definition (full `CREATE VIEW ... AS body` OR a bare body)
|
|
3
|
+
* to a canonical, comparable string. Whitespace-, case-, and wrapper-insensitive.
|
|
4
|
+
*/
|
|
5
|
+
export declare function normalizeViewSql(sql: string): string;
|
|
6
|
+
/** True when two view definitions are equivalent after normalization. */
|
|
7
|
+
export declare function viewSqlEquals(a: string | undefined, b: string | undefined): boolean;
|
|
8
|
+
//# sourceMappingURL=view-sql-compare.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view-sql-compare.d.ts","sourceRoot":"","sources":["../src/view-sql-compare.ts"],"names":[],"mappings":"AA4BA;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAOpD;AAED,yEAAyE;AACzE,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAGnF"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// view-sql-compare.ts — the single shared comparator for view definition SQL.
|
|
2
|
+
//
|
|
3
|
+
// View definition SQL arrives in two shapes across the pipeline:
|
|
4
|
+
// - EXPECTED (buildExpectedViews): the body only — `SELECT ... FROM ...`.
|
|
5
|
+
// - ACTUAL (introspect): sqlite's sqlite_master.sql is the full
|
|
6
|
+
// `CREATE VIEW <name> AS <body>`; Postgres' view_definition is body-only.
|
|
7
|
+
// - The CLI's readExistingViewSql synthesizes `CREATE VIEW <name> AS <body>`
|
|
8
|
+
// for Postgres so it matches what emitViewDdl produces.
|
|
9
|
+
//
|
|
10
|
+
// normalizeViewSql reduces any of these to a comparable canonical form so the
|
|
11
|
+
// diff (expected vs introspected) and the CLI (emitted DDL vs existing DDL) use
|
|
12
|
+
// ONE comparison. It strips a leading `CREATE [OR REPLACE] VIEW <name> AS`,
|
|
13
|
+
// collapses runs of whitespace to a single space, drops a trailing `;`, and
|
|
14
|
+
// lower-cases — view-body drift should be classified by structure, not by
|
|
15
|
+
// incidental whitespace/case/wrapper differences.
|
|
16
|
+
//
|
|
17
|
+
// CAVEAT (accepted tradeoff): lower-casing makes keyword/identifier comparison
|
|
18
|
+
// case-insensitive but can mask a difference that lives ONLY in a case-sensitive
|
|
19
|
+
// string literal in the body (e.g. `WHERE status = 'Active'` vs `'active'`) —
|
|
20
|
+
// such a change would NOT be flagged as drift. Acceptable for generated
|
|
21
|
+
// aggregate/passthrough projections (no literals); revisit if hand-authored
|
|
22
|
+
// views with case-sensitive literals become a drift concern. The name regex
|
|
23
|
+
// matches one whitespace/`(`-free token, so a quoted view name containing a
|
|
24
|
+
// space would not strip cleanly — also a non-issue for generated identifiers.
|
|
25
|
+
const CREATE_VIEW_PREFIX = /^\s*create\s+(?:or\s+replace\s+)?(?:temp(?:orary)?\s+)?view\s+(?:if\s+not\s+exists\s+)?[^\s(]+(?:\s*\([^)]*\))?\s+as\s+/i;
|
|
26
|
+
/**
|
|
27
|
+
* Collapse a view definition (full `CREATE VIEW ... AS body` OR a bare body)
|
|
28
|
+
* to a canonical, comparable string. Whitespace-, case-, and wrapper-insensitive.
|
|
29
|
+
*/
|
|
30
|
+
export function normalizeViewSql(sql) {
|
|
31
|
+
return sql
|
|
32
|
+
.replace(CREATE_VIEW_PREFIX, "")
|
|
33
|
+
.replace(/\s+/g, " ")
|
|
34
|
+
.replace(/;\s*$/, "")
|
|
35
|
+
.trim()
|
|
36
|
+
.toLowerCase();
|
|
37
|
+
}
|
|
38
|
+
/** True when two view definitions are equivalent after normalization. */
|
|
39
|
+
export function viewSqlEquals(a, b) {
|
|
40
|
+
if (a === undefined || b === undefined)
|
|
41
|
+
return false;
|
|
42
|
+
return normalizeViewSql(a) === normalizeViewSql(b);
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=view-sql-compare.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view-sql-compare.js","sourceRoot":"","sources":["../src/view-sql-compare.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,EAAE;AACF,iEAAiE;AACjE,4EAA4E;AAC5E,kEAAkE;AAClE,8EAA8E;AAC9E,+EAA+E;AAC/E,4DAA4D;AAC5D,EAAE;AACF,8EAA8E;AAC9E,gFAAgF;AAChF,4EAA4E;AAC5E,4EAA4E;AAC5E,0EAA0E;AAC1E,kDAAkD;AAClD,EAAE;AACF,+EAA+E;AAC/E,iFAAiF;AACjF,8EAA8E;AAC9E,wEAAwE;AACxE,4EAA4E;AAC5E,4EAA4E;AAC5E,4EAA4E;AAC5E,8EAA8E;AAE9E,MAAM,kBAAkB,GACtB,0HAA0H,CAAC;AAE7H;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,OAAO,GAAG;SACP,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC;SAC/B,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;SACpB,IAAI,EAAE;SACN,WAAW,EAAE,CAAC;AACnB,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,aAAa,CAAC,CAAqB,EAAE,CAAqB;IACxE,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACrD,OAAO,gBAAgB,CAAC,CAAC,CAAC,KAAK,gBAAgB,CAAC,CAAC,CAAC,CAAC;AACrD,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metaobjectsdev/migrate-ts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0-rc.1",
|
|
4
4
|
"description": "Schema migration tooling for MetaObjects: diff metadata vs DB and emit SQL for Postgres and SQLite.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@iarna/toml": "^2.2.5",
|
|
37
|
-
"@metaobjectsdev/metadata": "0.
|
|
37
|
+
"@metaobjectsdev/metadata": "0.9.0-rc.1"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"kysely": ">=0.27.0"
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { type Kysely, type Transaction, sql } from "kysely";
|
|
5
|
+
import {
|
|
6
|
+
appliedRecords,
|
|
7
|
+
DEFAULT_LEDGER_SCHEMA,
|
|
8
|
+
deleteApplied,
|
|
9
|
+
ensureLedger,
|
|
10
|
+
type LedgerDialect,
|
|
11
|
+
type LedgerOptions,
|
|
12
|
+
MIGRATIONS_TABLE,
|
|
13
|
+
recordApplied,
|
|
14
|
+
} from "./ledger.js";
|
|
15
|
+
import { splitSqlStatements } from "../sql/split-statements.js";
|
|
16
|
+
|
|
17
|
+
// Re-exported here for back-compat: `splitSqlStatements` historically lived in
|
|
18
|
+
// this module. Its canonical home is now ../sql/split-statements.js (shared with
|
|
19
|
+
// the D1 safety pass), but consumers importing from apply.js keep working.
|
|
20
|
+
export { splitSqlStatements };
|
|
21
|
+
|
|
22
|
+
/** The per-migration up-SQL filename, shared with writeMigration's layout. */
|
|
23
|
+
const UP_SQL = "up.sql";
|
|
24
|
+
/** The per-migration down-SQL filename, shared with writeMigration's layout. */
|
|
25
|
+
const DOWN_SQL = "down.sql";
|
|
26
|
+
|
|
27
|
+
export interface ApplyPendingOptions {
|
|
28
|
+
/** When true, compute + return the plan but apply nothing. */
|
|
29
|
+
dryRun: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Target dialect. Decides ledger schema-qualification (pg) and whether the
|
|
32
|
+
* Postgres advisory lock is taken. Defaults to `sqlite` (no schema, no lock)
|
|
33
|
+
* to preserve the original single-DB behavior for callers that omit it.
|
|
34
|
+
*/
|
|
35
|
+
dialect?: LedgerDialect;
|
|
36
|
+
/** Multi-tenant ledger location + advisory-lock name. Defaults preserve current behavior. */
|
|
37
|
+
ledger?: LedgerOptions;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ApplyPendingResult {
|
|
41
|
+
/** Migration names that were pending (not yet in the ledger), in order. */
|
|
42
|
+
pending: string[];
|
|
43
|
+
/** Migration names that were applied this run, in order. Empty on dryRun. */
|
|
44
|
+
applied: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface DiscoveredMigration {
|
|
48
|
+
/** `<timestamp>-<slug>` directory name — stable id + sort key. */
|
|
49
|
+
name: string;
|
|
50
|
+
/** Absolute path to the up.sql file. */
|
|
51
|
+
upPath: string;
|
|
52
|
+
/** Absolute path to the down.sql file (may not exist on disk). */
|
|
53
|
+
downPath: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Apply pending committed migration files in order, tracked by the
|
|
58
|
+
* migration-history ledger, transactionally.
|
|
59
|
+
*
|
|
60
|
+
* Idempotency comes from the LEDGER (skip names already recorded), NOT from
|
|
61
|
+
* re-diffing — so hand-authored files + data steps replay exactly once.
|
|
62
|
+
*
|
|
63
|
+
* For each pending migration (sorted by directory name), the file's SQL and a
|
|
64
|
+
* `recordApplied` row are run in the SAME Kysely transaction; any failure rolls
|
|
65
|
+
* back that file's tx, leaving it unrecorded (so a re-run retries it), and
|
|
66
|
+
* stops the run. Previously-applied files are checksum-compared against the
|
|
67
|
+
* ledger — a changed file errors (tamper guard).
|
|
68
|
+
*/
|
|
69
|
+
export async function applyPending(
|
|
70
|
+
db: Kysely<Record<string, unknown>>,
|
|
71
|
+
dir: string,
|
|
72
|
+
opts: ApplyPendingOptions,
|
|
73
|
+
): Promise<ApplyPendingResult> {
|
|
74
|
+
const dialect = opts.dialect ?? "sqlite";
|
|
75
|
+
const ledger = opts.ledger;
|
|
76
|
+
|
|
77
|
+
// Serialize concurrent applies against the same ledger with a Postgres
|
|
78
|
+
// session advisory lock (no-op on SQLite). Held for the whole apply duration.
|
|
79
|
+
return withAdvisoryLock(db, dialect, ledger, async () => {
|
|
80
|
+
await ensureLedger(db, dialect, ledger);
|
|
81
|
+
const recorded = await appliedRecords(db, dialect, ledger);
|
|
82
|
+
|
|
83
|
+
const discovered = await discoverMigrations(dir);
|
|
84
|
+
|
|
85
|
+
// Tamper guard: any already-applied migration whose current up.sql checksum
|
|
86
|
+
// differs from the recorded one is a hard error.
|
|
87
|
+
for (const m of discovered) {
|
|
88
|
+
const recordedChecksum = recorded.get(m.name);
|
|
89
|
+
if (recordedChecksum === undefined) continue;
|
|
90
|
+
const current = checksumOf(await readFile(m.upPath, "utf8"));
|
|
91
|
+
if (current !== recordedChecksum) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`migration '${m.name}' was already applied but its up.sql checksum changed ` +
|
|
94
|
+
`(recorded ${recordedChecksum.slice(0, 12)}…, current ${current.slice(0, 12)}…). ` +
|
|
95
|
+
`Applied migrations are immutable; revert the edit or author a new migration.`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const pending = discovered.filter((m) => !recorded.has(m.name));
|
|
101
|
+
const pendingNames = pending.map((m) => m.name);
|
|
102
|
+
|
|
103
|
+
if (opts.dryRun) {
|
|
104
|
+
return { pending: pendingNames, applied: [] };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const applied: string[] = [];
|
|
108
|
+
for (const m of pending) {
|
|
109
|
+
const text = await readFile(m.upPath, "utf8");
|
|
110
|
+
const checksum = checksumOf(text);
|
|
111
|
+
// Run the file's SQL + the ledger insert in ONE transaction. A failure
|
|
112
|
+
// rolls the whole file back (unrecorded) and propagates — stopping the run.
|
|
113
|
+
await runSqlFileWithLedgerMutation(db, text, (trx) =>
|
|
114
|
+
recordApplied(trx, m.name, checksum, dialect, ledger),
|
|
115
|
+
);
|
|
116
|
+
applied.push(m.name);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { pending: pendingNames, applied };
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface RollbackToOptions {
|
|
124
|
+
/** Target dialect. Decides ledger schema-qualification + advisory lock. Defaults to `sqlite`. */
|
|
125
|
+
dialect?: LedgerDialect;
|
|
126
|
+
/** Multi-tenant ledger location + advisory-lock name. Defaults preserve current behavior. */
|
|
127
|
+
ledger?: LedgerOptions;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface RollbackToResult {
|
|
131
|
+
/** Migration names rolled back, in execution (reverse-chronological) order. */
|
|
132
|
+
rolledBack: string[];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Roll back applied migrations newer than `target` (or all, when `target` is
|
|
137
|
+
* `null`), in REVERSE lexical order — running each migration's `down.sql` then
|
|
138
|
+
* deleting its ledger row, in ONE transaction per migration. `target` is itself
|
|
139
|
+
* retained (only ledger names strictly-greater than it are rolled back; lexical
|
|
140
|
+
* = chronological given the zero-padded timestamp prefix).
|
|
141
|
+
*
|
|
142
|
+
* An empty / whitespace-only `down.sql` THROWS before that migration is
|
|
143
|
+
* unrecorded — data-migration downs are hand-authored and must never be
|
|
144
|
+
* silently skipped. `down.sql` is split with the same {@link splitSqlStatements}
|
|
145
|
+
* the up-path uses. Wrapped in the same Postgres session advisory lock as
|
|
146
|
+
* {@link applyPending} (no-op on SQLite).
|
|
147
|
+
*/
|
|
148
|
+
export async function rollbackTo(
|
|
149
|
+
db: Kysely<Record<string, unknown>>,
|
|
150
|
+
dir: string,
|
|
151
|
+
target: string | null,
|
|
152
|
+
opts: RollbackToOptions = {},
|
|
153
|
+
): Promise<RollbackToResult> {
|
|
154
|
+
const dialect = opts.dialect ?? "sqlite";
|
|
155
|
+
const ledger = opts.ledger;
|
|
156
|
+
|
|
157
|
+
return withAdvisoryLock(db, dialect, ledger, async () => {
|
|
158
|
+
await ensureLedger(db, dialect, ledger);
|
|
159
|
+
const recorded = await appliedRecords(db, dialect, ledger);
|
|
160
|
+
|
|
161
|
+
const discovered = await discoverMigrations(dir);
|
|
162
|
+
const byName = new Map(discovered.map((m) => [m.name, m]));
|
|
163
|
+
|
|
164
|
+
// Applied names strictly-greater than target (or all when target is null),
|
|
165
|
+
// newest-first.
|
|
166
|
+
const toRollback = [...recorded.keys()]
|
|
167
|
+
.filter((name) => target === null || compareLexical(name, target) > 0)
|
|
168
|
+
.sort((a, b) => compareLexical(b, a));
|
|
169
|
+
|
|
170
|
+
const rolledBack: string[] = [];
|
|
171
|
+
for (const name of toRollback) {
|
|
172
|
+
const m = byName.get(name);
|
|
173
|
+
if (m === undefined) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`rollback '${name}': migration directory is missing (cannot read its down.sql)`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
const downText = await readDownSql(m.downPath, name);
|
|
179
|
+
if (downText.trim().length === 0) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`rollback '${name}': down.sql is empty — data-migration downs must be ` +
|
|
182
|
+
`hand-authored, never silently skipped.`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
// Run the down SQL + the ledger delete in ONE transaction.
|
|
186
|
+
await runSqlFileWithLedgerMutation(db, downText, (trx) =>
|
|
187
|
+
deleteApplied(trx, name, dialect, ledger),
|
|
188
|
+
);
|
|
189
|
+
rolledBack.push(name);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { rolledBack };
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Read a migration's `down.sql`. Distinguishes a MISSING file (never authored —
|
|
198
|
+
* ENOENT) from a present-but-empty one: a missing down throws a "not found"
|
|
199
|
+
* error (a never-written down has a different cause than a deliberately-blank
|
|
200
|
+
* one), while genuinely-empty content falls through to the caller's empty-down
|
|
201
|
+
* check. Both remain hard errors; this only reports the right cause.
|
|
202
|
+
*/
|
|
203
|
+
async function readDownSql(path: string, name: string): Promise<string> {
|
|
204
|
+
try {
|
|
205
|
+
return await readFile(path, "utf8");
|
|
206
|
+
} catch (err) {
|
|
207
|
+
if (isErrnoException(err) && err.code === "ENOENT") {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`rollback '${name}': down.sql not found for migration '${name}' ` +
|
|
210
|
+
`(expected at ${path}) — data-migration downs must be hand-authored.`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
throw err;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Narrow an unknown caught value to a Node errno exception (has a string `code`). */
|
|
218
|
+
function isErrnoException(err: unknown): err is NodeJS.ErrnoException {
|
|
219
|
+
return (
|
|
220
|
+
err instanceof Error &&
|
|
221
|
+
typeof (err as NodeJS.ErrnoException).code === "string"
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Run `body` while holding a Postgres SESSION-level advisory lock for mutual
|
|
227
|
+
* exclusion across concurrent applies/rollbacks against the same ledger.
|
|
228
|
+
*
|
|
229
|
+
* pg session advisory locks are per-connection, so the lock is taken on a single
|
|
230
|
+
* dedicated connection (`db.connection()`) held for the entire `body` duration;
|
|
231
|
+
* `body` still runs its own migrations via `db.transaction()` on the pool — the
|
|
232
|
+
* lock only needs to be held by some session for mutual exclusion. SESSION (not
|
|
233
|
+
* transaction) level so a `CREATE INDEX CONCURRENTLY` in a migration cannot
|
|
234
|
+
* deadlock against it. On SQLite (single-writer; no advisory locks) it is a
|
|
235
|
+
* pass-through.
|
|
236
|
+
*/
|
|
237
|
+
async function withAdvisoryLock<T>(
|
|
238
|
+
db: Kysely<Record<string, unknown>>,
|
|
239
|
+
dialect: LedgerDialect,
|
|
240
|
+
ledger: LedgerOptions | undefined,
|
|
241
|
+
body: () => Promise<T>,
|
|
242
|
+
): Promise<T> {
|
|
243
|
+
if (dialect !== "postgres") {
|
|
244
|
+
return body();
|
|
245
|
+
}
|
|
246
|
+
const key = advisoryKey(lockNameFor(ledger));
|
|
247
|
+
return db.connection().execute(async (lockConn) => {
|
|
248
|
+
// Bind the key as a parameter cast to bigint (a signed 64-bit int as a
|
|
249
|
+
// decimal string, possibly negative) — matching the runner's
|
|
250
|
+
// `pg_advisory_lock($1::bigint)`.
|
|
251
|
+
await sql`SELECT pg_advisory_lock(${key}::bigint)`.execute(lockConn);
|
|
252
|
+
try {
|
|
253
|
+
return await body();
|
|
254
|
+
} finally {
|
|
255
|
+
// Releasing the lock must NOT mask an in-flight body error: a throw out of
|
|
256
|
+
// `finally` would replace any pending body rejection. Log-and-swallow the
|
|
257
|
+
// unlock failure so the body's error (if any) propagates intact. The lock
|
|
258
|
+
// is session-scoped, so it is released anyway when the connection closes.
|
|
259
|
+
try {
|
|
260
|
+
await sql`SELECT pg_advisory_unlock(${key}::bigint)`.execute(lockConn);
|
|
261
|
+
} catch (unlockErr) {
|
|
262
|
+
console.warn(
|
|
263
|
+
`migrate-ts: failed to release advisory lock (it will be freed when the ` +
|
|
264
|
+
`session ends): ${String(unlockErr)}`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Default advisory-lock name: explicit `lockName`, else `<schema>.<table>`. */
|
|
272
|
+
function lockNameFor(ledger: LedgerOptions | undefined): string {
|
|
273
|
+
if (ledger?.lockName !== undefined) return ledger.lockName;
|
|
274
|
+
const schema = ledger?.schema ?? DEFAULT_LEDGER_SCHEMA;
|
|
275
|
+
const table = ledger?.table ?? MIGRATIONS_TABLE;
|
|
276
|
+
return `${schema}.${table}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Stable 64-bit signed advisory-lock key (decimal string) from a lock name. */
|
|
280
|
+
function advisoryKey(name: string): string {
|
|
281
|
+
const hash = createHash("sha256").update(name).digest();
|
|
282
|
+
return hash.readBigInt64BE(0).toString();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Run a migration SQL file's statements followed by a ledger mutation, all in
|
|
287
|
+
* ONE Kysely transaction on the pool. The file is split with
|
|
288
|
+
* {@link splitSqlStatements} and each statement executed in order, then
|
|
289
|
+
* `mutateLedger` records/unrecords the migration — so the data change and its
|
|
290
|
+
* ledger row commit or roll back together. Any failure rolls the whole
|
|
291
|
+
* transaction back (leaving the ledger untouched) and propagates to the caller.
|
|
292
|
+
*
|
|
293
|
+
* Shared by both the apply (up.sql + recordApplied) and rollback
|
|
294
|
+
* (down.sql + deleteApplied) paths.
|
|
295
|
+
*/
|
|
296
|
+
async function runSqlFileWithLedgerMutation(
|
|
297
|
+
db: Kysely<Record<string, unknown>>,
|
|
298
|
+
sqlText: string,
|
|
299
|
+
mutateLedger: (trx: Transaction<Record<string, unknown>>) => Promise<void>,
|
|
300
|
+
): Promise<void> {
|
|
301
|
+
await db.transaction().execute(async (trx) => {
|
|
302
|
+
for (const stmt of splitSqlStatements(sqlText)) {
|
|
303
|
+
await sql.raw(stmt).execute(trx);
|
|
304
|
+
}
|
|
305
|
+
await mutateLedger(trx);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function discoverMigrations(dir: string): Promise<DiscoveredMigration[]> {
|
|
310
|
+
let entries: { name: string; isDirectory: () => boolean }[];
|
|
311
|
+
try {
|
|
312
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
313
|
+
} catch {
|
|
314
|
+
return [];
|
|
315
|
+
}
|
|
316
|
+
const migrations: DiscoveredMigration[] = [];
|
|
317
|
+
for (const e of entries) {
|
|
318
|
+
if (!e.isDirectory()) continue;
|
|
319
|
+
migrations.push({
|
|
320
|
+
name: e.name,
|
|
321
|
+
upPath: join(dir, e.name, UP_SQL),
|
|
322
|
+
downPath: join(dir, e.name, DOWN_SQL),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
// Directory names are timestamp-prefixed (`<YYYYMMDDHHMMSS>-<slug>`), so a
|
|
326
|
+
// plain lexical (code-unit) sort is the apply order.
|
|
327
|
+
migrations.sort((a, b) => compareLexical(a.name, b.name));
|
|
328
|
+
return migrations;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function checksumOf(text: string): string {
|
|
332
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Stable lexical (code-unit) comparison; the same ordering as `a < b`/`a > b`. */
|
|
336
|
+
function compareLexical(a: string, b: string): number {
|
|
337
|
+
if (a < b) return -1;
|
|
338
|
+
if (a > b) return 1;
|
|
339
|
+
return 0;
|
|
340
|
+
}
|