@metaobjectsdev/migrate-ts 0.8.1-rc.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.
Files changed (115) hide show
  1. package/README.md +1 -3
  2. package/dist/apply/apply.d.ts +61 -0
  3. package/dist/apply/apply.d.ts.map +1 -0
  4. package/dist/apply/apply.js +241 -0
  5. package/dist/apply/apply.js.map +1 -0
  6. package/dist/apply/ledger.d.ts +78 -0
  7. package/dist/apply/ledger.d.ts.map +1 -0
  8. package/dist/apply/ledger.js +146 -0
  9. package/dist/apply/ledger.js.map +1 -0
  10. package/dist/check-expr-compare.d.ts +13 -0
  11. package/dist/check-expr-compare.d.ts.map +1 -0
  12. package/dist/check-expr-compare.js +48 -0
  13. package/dist/check-expr-compare.js.map +1 -0
  14. package/dist/diff/index.d.ts +3 -1
  15. package/dist/diff/index.d.ts.map +1 -1
  16. package/dist/diff/index.js +57 -14
  17. package/dist/diff/index.js.map +1 -1
  18. package/dist/diff/status.js +3 -0
  19. package/dist/diff/status.js.map +1 -1
  20. package/dist/drift/classify.d.ts +16 -0
  21. package/dist/drift/classify.d.ts.map +1 -0
  22. package/dist/drift/classify.js +44 -0
  23. package/dist/drift/classify.js.map +1 -0
  24. package/dist/drift/drift.d.ts +32 -0
  25. package/dist/drift/drift.d.ts.map +1 -0
  26. package/dist/drift/drift.js +36 -0
  27. package/dist/drift/drift.js.map +1 -0
  28. package/dist/emit/d1-safety-pass.d.ts.map +1 -1
  29. package/dist/emit/d1-safety-pass.js +15 -45
  30. package/dist/emit/d1-safety-pass.js.map +1 -1
  31. package/dist/emit/postgres.d.ts.map +1 -1
  32. package/dist/emit/postgres.js +47 -4
  33. package/dist/emit/postgres.js.map +1 -1
  34. package/dist/emit/sqlite.d.ts.map +1 -1
  35. package/dist/emit/sqlite.js +22 -0
  36. package/dist/emit/sqlite.js.map +1 -1
  37. package/dist/errors.d.ts.map +1 -1
  38. package/dist/errors.js +4 -0
  39. package/dist/errors.js.map +1 -1
  40. package/dist/expected-schema.d.ts.map +1 -1
  41. package/dist/expected-schema.js +114 -5
  42. package/dist/expected-schema.js.map +1 -1
  43. package/dist/index.d.ts +13 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +13 -0
  46. package/dist/index.js.map +1 -1
  47. package/dist/introspect/d1.d.ts.map +1 -1
  48. package/dist/introspect/d1.js +1 -0
  49. package/dist/introspect/d1.js.map +1 -1
  50. package/dist/introspect/postgres.d.ts.map +1 -1
  51. package/dist/introspect/postgres.js +38 -2
  52. package/dist/introspect/postgres.js.map +1 -1
  53. package/dist/introspect/sqlite.d.ts.map +1 -1
  54. package/dist/introspect/sqlite.js +13 -2
  55. package/dist/introspect/sqlite.js.map +1 -1
  56. package/dist/snapshot/checksum.d.ts +10 -0
  57. package/dist/snapshot/checksum.d.ts.map +1 -0
  58. package/dist/snapshot/checksum.js +14 -0
  59. package/dist/snapshot/checksum.js.map +1 -0
  60. package/dist/snapshot/plan.d.ts +25 -0
  61. package/dist/snapshot/plan.d.ts.map +1 -0
  62. package/dist/snapshot/plan.js +30 -0
  63. package/dist/snapshot/plan.js.map +1 -0
  64. package/dist/snapshot/serialize.d.ts +10 -0
  65. package/dist/snapshot/serialize.d.ts.map +1 -0
  66. package/dist/snapshot/serialize.js +63 -0
  67. package/dist/snapshot/serialize.js.map +1 -0
  68. package/dist/snapshot/store.d.ts +12 -0
  69. package/dist/snapshot/store.d.ts.map +1 -0
  70. package/dist/snapshot/store.js +32 -0
  71. package/dist/snapshot/store.js.map +1 -0
  72. package/dist/sql/split-statements.d.ts +12 -0
  73. package/dist/sql/split-statements.d.ts.map +1 -0
  74. package/dist/sql/split-statements.js +112 -0
  75. package/dist/sql/split-statements.js.map +1 -0
  76. package/dist/sql-type.d.ts +2 -0
  77. package/dist/sql-type.d.ts.map +1 -1
  78. package/dist/sql-type.js +2 -0
  79. package/dist/sql-type.js.map +1 -1
  80. package/dist/types.d.ts +36 -5
  81. package/dist/types.d.ts.map +1 -1
  82. package/dist/verify/replay.d.ts +25 -0
  83. package/dist/verify/replay.d.ts.map +1 -0
  84. package/dist/verify/replay.js +25 -0
  85. package/dist/verify/replay.js.map +1 -0
  86. package/dist/view-sql-compare.d.ts +8 -0
  87. package/dist/view-sql-compare.d.ts.map +1 -0
  88. package/dist/view-sql-compare.js +44 -0
  89. package/dist/view-sql-compare.js.map +1 -0
  90. package/package.json +2 -2
  91. package/src/apply/apply.ts +340 -0
  92. package/src/apply/ledger.ts +241 -0
  93. package/src/check-expr-compare.ts +49 -0
  94. package/src/diff/index.ts +59 -15
  95. package/src/diff/status.ts +3 -0
  96. package/src/drift/classify.ts +56 -0
  97. package/src/drift/drift.ts +66 -0
  98. package/src/emit/d1-safety-pass.ts +16 -45
  99. package/src/emit/postgres.ts +47 -4
  100. package/src/emit/sqlite.ts +22 -0
  101. package/src/errors.ts +4 -0
  102. package/src/expected-schema.ts +124 -4
  103. package/src/index.ts +44 -0
  104. package/src/introspect/d1.ts +1 -0
  105. package/src/introspect/postgres.ts +38 -4
  106. package/src/introspect/sqlite.ts +13 -3
  107. package/src/snapshot/checksum.ts +15 -0
  108. package/src/snapshot/plan.ts +53 -0
  109. package/src/snapshot/serialize.ts +81 -0
  110. package/src/snapshot/store.ts +33 -0
  111. package/src/sql/split-statements.ts +115 -0
  112. package/src/sql-type.ts +3 -0
  113. package/src/types.ts +26 -9
  114. package/src/verify/replay.ts +43 -0
  115. 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; emits migration SQL but does not yet apply against the DB.
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