@metaobjectsdev/migrate-ts 1.0.0-rc.5 → 1.0.0-rc.7
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/dist/apply/apply.d.ts +24 -0
- package/dist/apply/apply.d.ts.map +1 -1
- package/dist/apply/apply.js +41 -2
- package/dist/apply/apply.js.map +1 -1
- package/dist/check-expr-compare.d.ts +5 -1
- package/dist/check-expr-compare.d.ts.map +1 -1
- package/dist/check-expr-compare.js +193 -18
- package/dist/check-expr-compare.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/apply/apply.ts +37 -4
- package/src/check-expr-compare.ts +195 -18
- package/src/index.ts +1 -0
package/dist/apply/apply.d.ts
CHANGED
|
@@ -20,6 +20,30 @@ export interface ApplyPendingResult {
|
|
|
20
20
|
/** Migration names that were applied this run, in order. Empty on dryRun. */
|
|
21
21
|
applied: string[];
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* A committed migration's SQL failed. Carries WHICH one, because the driver's error says
|
|
25
|
+
* only what the database refused ("no such table: purchases") and never which file asked.
|
|
26
|
+
*
|
|
27
|
+
* The position matters to callers, not just the name: `--replay` prescribes a different
|
|
28
|
+
* remedy at the HEAD of the chain, where nothing earlier in this chain could have created
|
|
29
|
+
* what the migration needs, so the base schema must come from outside it.
|
|
30
|
+
*/
|
|
31
|
+
export declare class MigrationApplyError extends Error {
|
|
32
|
+
/** `<timestamp>-<slug>` directory name of the migration that failed. */
|
|
33
|
+
readonly migration: string;
|
|
34
|
+
/** 0-based position among the PENDING migrations this run attempted. */
|
|
35
|
+
readonly index: number;
|
|
36
|
+
/** How many migrations were pending when the run started. */
|
|
37
|
+
readonly pendingCount: number;
|
|
38
|
+
readonly cause: unknown;
|
|
39
|
+
constructor(
|
|
40
|
+
/** `<timestamp>-<slug>` directory name of the migration that failed. */
|
|
41
|
+
migration: string,
|
|
42
|
+
/** 0-based position among the PENDING migrations this run attempted. */
|
|
43
|
+
index: number,
|
|
44
|
+
/** How many migrations were pending when the run started. */
|
|
45
|
+
pendingCount: number, cause: unknown);
|
|
46
|
+
}
|
|
23
47
|
/**
|
|
24
48
|
* Apply pending committed migration files in order, tracked by the
|
|
25
49
|
* migration-history ledger, transactionally.
|
|
@@ -1 +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;AAMhE,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,
|
|
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;AAMhE,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;AAED;;;;;;;GAOG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAE1C,wEAAwE;IACxE,QAAQ,CAAC,SAAS,EAAE,MAAM;IAC1B,wEAAwE;IACxE,QAAQ,CAAC,KAAK,EAAE,MAAM;IACtB,6DAA6D;IAC7D,QAAQ,CAAC,YAAY,EAAE,MAAM;aACX,KAAK,EAAE,OAAO;;IANhC,wEAAwE;IAC/D,SAAS,EAAE,MAAM;IAC1B,wEAAwE;IAC/D,KAAK,EAAE,MAAM;IACtB,6DAA6D;IACpD,YAAY,EAAE,MAAM,EACX,KAAK,EAAE,OAAO;CAKnC;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,CA0D7B;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"}
|
package/dist/apply/apply.js
CHANGED
|
@@ -13,6 +13,34 @@ export { splitSqlStatements };
|
|
|
13
13
|
const UP_SQL = "up.sql";
|
|
14
14
|
/** The per-migration down-SQL filename, shared with writeMigration's layout. */
|
|
15
15
|
const DOWN_SQL = "down.sql";
|
|
16
|
+
/**
|
|
17
|
+
* A committed migration's SQL failed. Carries WHICH one, because the driver's error says
|
|
18
|
+
* only what the database refused ("no such table: purchases") and never which file asked.
|
|
19
|
+
*
|
|
20
|
+
* The position matters to callers, not just the name: `--replay` prescribes a different
|
|
21
|
+
* remedy at the HEAD of the chain, where nothing earlier in this chain could have created
|
|
22
|
+
* what the migration needs, so the base schema must come from outside it.
|
|
23
|
+
*/
|
|
24
|
+
export class MigrationApplyError extends Error {
|
|
25
|
+
migration;
|
|
26
|
+
index;
|
|
27
|
+
pendingCount;
|
|
28
|
+
cause;
|
|
29
|
+
constructor(
|
|
30
|
+
/** `<timestamp>-<slug>` directory name of the migration that failed. */
|
|
31
|
+
migration,
|
|
32
|
+
/** 0-based position among the PENDING migrations this run attempted. */
|
|
33
|
+
index,
|
|
34
|
+
/** How many migrations were pending when the run started. */
|
|
35
|
+
pendingCount, cause) {
|
|
36
|
+
super(`migration '${migration}' failed to apply: ${cause?.message ?? String(cause)}`);
|
|
37
|
+
this.migration = migration;
|
|
38
|
+
this.index = index;
|
|
39
|
+
this.pendingCount = pendingCount;
|
|
40
|
+
this.cause = cause;
|
|
41
|
+
this.name = "MigrationApplyError";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
16
44
|
/**
|
|
17
45
|
* Apply pending committed migration files in order, tracked by the
|
|
18
46
|
* migration-history ledger, transactionally.
|
|
@@ -54,12 +82,23 @@ export async function applyPending(db, dir, opts) {
|
|
|
54
82
|
return { pending: pendingNames, applied: [] };
|
|
55
83
|
}
|
|
56
84
|
const applied = [];
|
|
57
|
-
for (const m of pending) {
|
|
85
|
+
for (const [i, m] of pending.entries()) {
|
|
58
86
|
const text = await readFile(m.upPath, "utf8");
|
|
59
87
|
const checksum = checksumOf(text);
|
|
60
88
|
// Run the file's SQL + the ledger insert in ONE transaction. A failure
|
|
61
89
|
// rolls the whole file back (unrecorded) and propagates — stopping the run.
|
|
62
|
-
|
|
90
|
+
//
|
|
91
|
+
// Wrapped so the failure names the FILE. The driver reports what the database
|
|
92
|
+
// refused and nothing about who asked, which left every caller — `apply-pending`
|
|
93
|
+
// and `verify --replay` alike — printing "no such table: purchases" over a
|
|
94
|
+
// directory of migrations, and `--replay` guessing at a remedy it could not
|
|
95
|
+
// have chosen correctly without knowing the position.
|
|
96
|
+
try {
|
|
97
|
+
await runSqlFileWithLedgerMutation(db, text, (trx) => recordApplied(trx, m.name, checksum, dialect, ledger));
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
throw new MigrationApplyError(m.name, i, pending.length, err);
|
|
101
|
+
}
|
|
63
102
|
applied.push(m.name);
|
|
64
103
|
}
|
|
65
104
|
return { pending: pendingNames, applied };
|
package/dist/apply/apply.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apply.js","sourceRoot":"","sources":["../../src/apply/apply.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAiC,GAAG,EAAE,MAAM,QAAQ,CAAC;AAC5D,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,aAAa,EACb,YAAY,EAGZ,gBAAgB,EAChB,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,2BAA2B,EAAE,MAAM,8BAA8B,CAAC;AAE3E,+EAA+E;AAC/E,iFAAiF;AACjF,2EAA2E;AAC3E,OAAO,EAAE,kBAAkB,EAAE,CAAC;AAE9B,8EAA8E;AAC9E,MAAM,MAAM,GAAG,QAAQ,CAAC;AACxB,gFAAgF;AAChF,MAAM,QAAQ,GAAG,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"apply.js","sourceRoot":"","sources":["../../src/apply/apply.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAiC,GAAG,EAAE,MAAM,QAAQ,CAAC;AAC5D,OAAO,EACL,cAAc,EACd,qBAAqB,EACrB,aAAa,EACb,YAAY,EAGZ,gBAAgB,EAChB,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,2BAA2B,EAAE,MAAM,8BAA8B,CAAC;AAE3E,+EAA+E;AAC/E,iFAAiF;AACjF,2EAA2E;AAC3E,OAAO,EAAE,kBAAkB,EAAE,CAAC;AAE9B,8EAA8E;AAC9E,MAAM,MAAM,GAAG,QAAQ,CAAC;AACxB,gFAAgF;AAChF,MAAM,QAAQ,GAAG,UAAU,CAAC;AAsB5B;;;;;;;GAOG;AACH,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAGjC;IAEA;IAEA;IACS;IAPpB;IACE,wEAAwE;IAC/D,SAAiB;IAC1B,wEAAwE;IAC/D,KAAa;IACtB,6DAA6D;IACpD,YAAoB,EACX,KAAc;QAEhC,KAAK,CAAC,cAAc,SAAS,sBAAuB,KAAe,EAAE,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAPxF,cAAS,GAAT,SAAS,CAAQ;QAEjB,UAAK,GAAL,KAAK,CAAQ;QAEb,iBAAY,GAAZ,YAAY,CAAQ;QACX,UAAK,GAAL,KAAK,CAAS;QAGhC,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACpC,CAAC;CACF;AAWD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,EAAmC,EACnC,GAAW,EACX,IAAyB;IAEzB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,QAAQ,CAAC;IACzC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAE3B,uEAAuE;IACvE,8EAA8E;IAC9E,OAAO,gBAAgB,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,IAAI,EAAE;QACtD,MAAM,YAAY,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QACxC,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QAE3D,MAAM,UAAU,GAAG,MAAM,kBAAkB,CAAC,GAAG,CAAC,CAAC;QAEjD,4EAA4E;QAC5E,iDAAiD;QACjD,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;YAC3B,MAAM,gBAAgB,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;YAC9C,IAAI,gBAAgB,KAAK,SAAS;gBAAE,SAAS;YAC7C,MAAM,OAAO,GAAG,UAAU,CAAC,MAAM,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;YAC7D,IAAI,OAAO,KAAK,gBAAgB,EAAE,CAAC;gBACjC,MAAM,IAAI,KAAK,CACb,cAAc,CAAC,CAAC,IAAI,wDAAwD;oBAC1E,aAAa,gBAAgB,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM;oBAClF,8EAA8E,CACjF,CAAC;YACJ,CAAC;QACH,CAAC;QAED,MAAM,OAAO,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QAChE,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAEhD,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;QAChD,CAAC;QAED,MAAM,OAAO,GAAa,EAAE,CAAC;QAC7B,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;YACvC,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YAC9C,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;YAClC,uEAAuE;YACvE,4EAA4E;YAC5E,EAAE;YACF,8EAA8E;YAC9E,iFAAiF;YACjF,2EAA2E;YAC3E,4EAA4E;YAC5E,sDAAsD;YACtD,IAAI,CAAC;gBACH,MAAM,4BAA4B,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,GAAG,EAAE,EAAE,CACnD,aAAa,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,CAAC,CACtD,CAAC;YACJ,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,IAAI,mBAAmB,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;YAChE,CAAC;YACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACvB,CAAC;QAED,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC;IAC5C,CAAC,CAAC,CAAC;AACL,CAAC;AAcD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,EAAmC,EACnC,GAAW,EACX,MAAqB,EACrB,OAA0B,EAAE;IAE5B,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,QAAQ,CAAC;IACzC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAE3B,OAAO,gBAAgB,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,IAAI,EAAE;QACtD,MAAM,YAAY,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QACxC,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QAE3D,MAAM,UAAU,GAAG,MAAM,kBAAkB,CAAC,GAAG,CAAC,CAAC;QACjD,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAE3D,2EAA2E;QAC3E,gBAAgB;QAChB,MAAM,UAAU,GAAG,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC;aACpC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,KAAK,IAAI,IAAI,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;aACrE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAExC,MAAM,UAAU,GAAa,EAAE,CAAC;QAChC,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;YAC9B,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC3B,IAAI,CAAC,KAAK,SAAS,EAAE,CAAC;gBACpB,MAAM,IAAI,KAAK,CACb,aAAa,IAAI,8DAA8D,CAChF,CAAC;YACJ,CAAC;YACD,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;YACrD,IAAI,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACjC,MAAM,IAAI,KAAK,CACb,aAAa,IAAI,sDAAsD;oBACrE,wCAAwC,CAC3C,CAAC;YACJ,CAAC;YACD,2DAA2D;YAC3D,MAAM,4BAA4B,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,EAAE,CACvD,aAAa,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,CAC1C,CAAC;YACF,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxB,CAAC;QAED,OAAO,EAAE,UAAU,EAAE,CAAC;IACxB,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;GAMG;AACH,KAAK,UAAU,WAAW,CAAC,IAAY,EAAE,IAAY;IACnD,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACtC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,gBAAgB,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACnD,MAAM,IAAI,KAAK,CACb,aAAa,IAAI,wCAAwC,IAAI,IAAI;gBAC/D,gBAAgB,IAAI,iDAAiD,CACxE,CAAC;QACJ,CAAC;QACD,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC;AAED,sFAAsF;AACtF,SAAS,gBAAgB,CAAC,GAAY;IACpC,OAAO,CACL,GAAG,YAAY,KAAK;QACpB,OAAQ,GAA6B,CAAC,IAAI,KAAK,QAAQ,CACxD,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,KAAK,UAAU,gBAAgB,CAC7B,EAAmC,EACnC,OAAsB,EACtB,MAAiC,EACjC,IAAsB;IAEtB,IAAI,OAAO,KAAK,UAAU,EAAE,CAAC;QAC3B,OAAO,IAAI,EAAE,CAAC;IAChB,CAAC;IACD,MAAM,GAAG,GAAG,WAAW,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;IAC7C,OAAO,EAAE,CAAC,UAAU,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE;QAChD,uEAAuE;QACvE,6DAA6D;QAC7D,kCAAkC;QAClC,MAAM,GAAG,CAAA,2BAA2B,GAAG,WAAW,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACrE,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,EAAE,CAAC;QACtB,CAAC;gBAAS,CAAC;YACT,2EAA2E;YAC3E,0EAA0E;YAC1E,0EAA0E;YAC1E,0EAA0E;YAC1E,IAAI,CAAC;gBACH,MAAM,GAAG,CAAA,6BAA6B,GAAG,WAAW,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;YACzE,CAAC;YAAC,OAAO,SAAS,EAAE,CAAC;gBACnB,OAAO,CAAC,IAAI,CACV,yEAAyE;oBACvE,kBAAkB,MAAM,CAAC,SAAS,CAAC,EAAE,CACxC,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED,gFAAgF;AAChF,SAAS,WAAW,CAAC,MAAiC;IACpD,IAAI,MAAM,EAAE,QAAQ,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,QAAQ,CAAC;IAC3D,MAAM,MAAM,GAAG,MAAM,EAAE,MAAM,IAAI,qBAAqB,CAAC;IACvD,MAAM,KAAK,GAAG,MAAM,EAAE,KAAK,IAAI,gBAAgB,CAAC;IAChD,OAAO,GAAG,MAAM,IAAI,KAAK,EAAE,CAAC;AAC9B,CAAC;AAED,gFAAgF;AAChF,SAAS,WAAW,CAAC,IAAY;IAC/B,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC;IACxD,OAAO,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;AAC3C,CAAC;AAED;;;;;;;;;;GAUG;AACH,KAAK,UAAU,4BAA4B,CACzC,EAAmC,EACnC,OAAe,EACf,YAA0E;IAE1E,kFAAkF;IAClF,qFAAqF;IACrF,kFAAkF;IAClF,kFAAkF;IAClF,qFAAqF;IACrF,MAAM,EAAE,UAAU,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,GACjE,2BAA2B,CAAC,OAAO,CAAC,CAAC;IAEvC,sFAAsF;IACtF,wFAAwF;IACxF,mFAAmF;IACnF,+DAA+D;IAC/D,IAAI,sBAAsB,EAAE,CAAC;QAC3B,MAAM,GAAG,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACzD,CAAC;IACD,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;YAC3C,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;gBAC9B,MAAM,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YACnC,CAAC;YACD,MAAM,YAAY,CAAC,GAAG,CAAC,CAAC;QAC1B,CAAC,CAAC,CAAC;IACL,CAAC;YAAS,CAAC;QACT,qFAAqF;QACrF,IAAI,sBAAsB,EAAE,CAAC;YAC3B,MAAM,GAAG,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACxD,CAAC;IACH,CAAC;IAED,uFAAuF;IACvF,gFAAgF;IAChF,KAAK,MAAM,KAAK,IAAI,qBAAqB,EAAE,CAAC;QAC1C,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAChD,MAAM,IAAI,GAAI,MAA+B,CAAC,IAAI,IAAI,EAAE,CAAC;QACzD,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CACb,kBAAkB,IAAI,CAAC,MAAM,uCAAuC,KAAK,YAAY;gBACnF,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,kDAAkD;gBACrF,gCAAgC,CACnC,CAAC;QACJ,CAAC;IACH,CAAC;AACH,CAAC;AAED,KAAK,UAAU,kBAAkB,CAAC,GAAW;IAC3C,IAAI,OAAuD,CAAC;IAC5D,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IACxD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,MAAM,UAAU,GAA0B,EAAE,CAAC;IAC7C,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,IAAI,CAAC,CAAC,CAAC,WAAW,EAAE;YAAE,SAAS;QAC/B,UAAU,CAAC,IAAI,CAAC;YACd,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,MAAM,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC;YACjC,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC;SACtC,CAAC,CAAC;IACL,CAAC;IACD,2EAA2E;IAC3E,qDAAqD;IACrD,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IAC1D,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,SAAS,UAAU,CAAC,IAAY;IAC9B,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACjE,CAAC;AAED,mFAAmF;AACnF,SAAS,cAAc,CAAC,CAAS,EAAE,CAAS;IAC1C,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,CAAC,CAAC,CAAC;IACrB,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,CAAC,CAAC;IACpB,OAAO,CAAC,CAAC;AACX,CAAC"}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Canonical form: drop casts/brackets/parens, fold `= ANY (ARRAY[…])` back to `IN`,
|
|
3
|
+
* lower-case, collapse whitespace, canonicalize cast target types, and collapse spacing
|
|
4
|
+
* around commas, casts and SYMBOLIC operators (outside single-quoted literals).
|
|
5
|
+
*/
|
|
2
6
|
export declare function normalizeCheckExpr(expr: string): string;
|
|
3
7
|
/** True when two CHECK expressions are equivalent after normalization. */
|
|
4
8
|
export declare function checkExprEquals(a: string | undefined, b: string | undefined): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check-expr-compare.d.ts","sourceRoot":"","sources":["../src/check-expr-compare.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"check-expr-compare.d.ts","sourceRoot":"","sources":["../src/check-expr-compare.ts"],"names":[],"mappings":"AA8BA;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAkDvD;AA+KD,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"}
|
|
@@ -2,16 +2,36 @@
|
|
|
2
2
|
//
|
|
3
3
|
// CHECK-expression comparison. Postgres rewrites a stored CHECK body so the raw
|
|
4
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.
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
5
|
+
// thing. This reduces both to ONE canonical form for comparison.
|
|
6
|
+
//
|
|
7
|
+
// It was written when every expression reaching it was machine-derived with a simple,
|
|
8
|
+
// known shape (comparison / IN / length / regex), and the header said there was "no
|
|
9
|
+
// arbitrary author SQL to mis-normalize". That stopped being true when `index.lookup`'s
|
|
10
|
+
// `@expr` and `@where` began using the same canonicalizer: those are AUTHOR-written SQL.
|
|
11
|
+
// Each rewrite below is therefore justified on the stronger ground it now needs — that
|
|
12
|
+
// the difference it erases is not semantic in ANY valid SQL, not merely in the shapes we
|
|
13
|
+
// emit. The rewrites PG applies (verified against a live server):
|
|
9
14
|
// - parenthesizes terms: `col >= 0 AND col <= 100` → `(col >= 0) AND (col <= 100)`
|
|
10
15
|
// - rewrites IN-lists: `status IN ('A','B')` → `status = ANY (ARRAY['A'::text, 'B'::text])`
|
|
11
16
|
// - appends type casts: string literals gain `::text`
|
|
12
|
-
//
|
|
17
|
+
// - parenthesizes a cast OPERAND: `x::integer` → `(x)::integer`
|
|
18
|
+
// - respells a cast TARGET canonically: `x::int` → `(x)::integer`
|
|
19
|
+
// All five are canonicalized below so an enum/range CHECK introspected from PG
|
|
13
20
|
// compares equal to the one we generate (idempotency on the --from-db / verify paths).
|
|
14
|
-
|
|
21
|
+
//
|
|
22
|
+
// The last two are why a cast-bearing `@expr` used to drift FOREVER. The paren-strip
|
|
23
|
+
// turns PG's added parens into a space, so `(x)::integer` reduced to `x ::integer` while
|
|
24
|
+
// the authored `x::integer` stayed tight — and `:` is not one of PG's operator characters,
|
|
25
|
+
// so the operator-run collapse never reached it. On top of that, PG re-emits the target
|
|
26
|
+
// type in its canonical spelling, so an authored `int`/`varchar`/`timestamptz` could not
|
|
27
|
+
// match the `integer`/`character varying`/`timestamp with time zone` it got back. Both
|
|
28
|
+
// are fixed in the quote-aware walker below, which is the only place that can tell a real
|
|
29
|
+
// cast from a `::` inside a regex literal.
|
|
30
|
+
/**
|
|
31
|
+
* Canonical form: drop casts/brackets/parens, fold `= ANY (ARRAY[…])` back to `IN`,
|
|
32
|
+
* lower-case, collapse whitespace, canonicalize cast target types, and collapse spacing
|
|
33
|
+
* around commas, casts and SYMBOLIC operators (outside single-quoted literals).
|
|
34
|
+
*/
|
|
15
35
|
export function normalizeCheckExpr(expr) {
|
|
16
36
|
const stripped = expr
|
|
17
37
|
.toLowerCase()
|
|
@@ -36,16 +56,142 @@ export function normalizeCheckExpr(expr) {
|
|
|
36
56
|
// PG stores `col IN (…)` as `col = ANY (ARRAY[…])`; after the bracket strip above
|
|
37
57
|
// that reads `col = any array …`. Fold it back to the `col in …` form we emit.
|
|
38
58
|
const folded = stripped.replace(/=\s*any\s+array/g, "in").replace(/\s+/g, " ").trim();
|
|
39
|
-
// Normalize spacing
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
59
|
+
// Normalize spacing around COMMAS and SYMBOLIC OPERATORS, so the spelling a human
|
|
60
|
+
// writes compares equal to the one Postgres hands back.
|
|
61
|
+
//
|
|
62
|
+
// Commas: a generated IN-list (`'a', 'b'`) must equal a hand-written one (`'a','b'`) —
|
|
63
|
+
// a hand SQLite migration omits the space.
|
|
64
|
+
//
|
|
65
|
+
// Operators: `pg_get_expr` re-emits every operator SPACED, and a person authoring an
|
|
66
|
+
// `index.lookup @expr` writes it tight. So `(request_context->>'device_id')` — the
|
|
67
|
+
// natural spelling — normalized to `request_context->>'device_id'` while the live index
|
|
68
|
+
// introspected to `request_context ->> 'device_id'`, and `verify --db` proposed DROP +
|
|
69
|
+
// CREATE against an index the database already held, exactly as declared. Casts and
|
|
70
|
+
// redundant parens were ALREADY canonicalized above, so the one difference left was the
|
|
71
|
+
// spacing — and it was the one difference a human actually produces.
|
|
72
|
+
//
|
|
73
|
+
// Safe because whitespace around a symbolic operator is not semantic in ANY valid SQL:
|
|
74
|
+
// where two spellings differ only there, at most one of them parses. Postgres lexes a
|
|
75
|
+
// maximal run of operator characters as a SINGLE token, so `a =- 1` is not `a = -1`
|
|
76
|
+
// spelled differently — it is a syntax error. Only SYMBOLIC operators are touched;
|
|
77
|
+
// collapsing around an alphabetic one would turn `a AND b` into `aANDb`.
|
|
78
|
+
//
|
|
79
|
+
// Both are done OUTSIDE single-quoted literals: a comma or an operator character inside
|
|
80
|
+
// a string/regex literal (`'a, b'`, a quantifier `'{1, 3}'`, a pattern `'a ->> b'`) IS
|
|
81
|
+
// meaningful, so two checks differing only there must stay distinct — else a real
|
|
82
|
+
// pattern change reads as clean drift, which is this file's failure in the other
|
|
83
|
+
// direction and the worse of the two.
|
|
84
|
+
return collapseSeparatorSpacingOutsideQuotes(folded);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Postgres' operator characters. A maximal run of these is ONE operator token — which is
|
|
88
|
+
* why `->>` and `!~*` survive intact rather than being split apart. Derived as the
|
|
89
|
+
* character SET rather than a list of known operators, so a spelling this file has never
|
|
90
|
+
* seen (`@>`, `?|`, a user-defined operator) normalizes by the same rule instead of
|
|
91
|
+
* needing an edit here.
|
|
92
|
+
*/
|
|
93
|
+
const OPERATOR_CHARS = new Set([..."+-*/<>=~!@#%^&|?"]);
|
|
94
|
+
/**
|
|
95
|
+
* Cast target types PG respells, mapped to the spelling `pg_get_constraintdef` /
|
|
96
|
+
* `pg_get_expr` hand back. MEASURED against PostgreSQL 16.15 by creating one CHECK per
|
|
97
|
+
* alias and reading the definition back — not derived from the docs, because the two
|
|
98
|
+
* that would most easily have been missed (`timestamp` → `timestamp WITHOUT time zone`,
|
|
99
|
+
* `float` → `double precision`) only show up when you ask the server.
|
|
100
|
+
*
|
|
101
|
+
* The four `… time zone` forms map to THEMSELVES on purpose: they are already canonical,
|
|
102
|
+
* and they exist here to be matched BEFORE the shorter `timestamp` / `time` aliases would
|
|
103
|
+
* otherwise rewrite their leading word into `timestamp without time zone with time zone`.
|
|
104
|
+
* No other canonical form needs that guard — `\b` already stops `int` matching inside
|
|
105
|
+
* `integer`, `char` inside `character`, and `bool` inside `boolean`.
|
|
106
|
+
*/
|
|
107
|
+
const CAST_TYPE_ALIASES = {
|
|
108
|
+
"timestamp with time zone": "timestamp with time zone",
|
|
109
|
+
"timestamp without time zone": "timestamp without time zone",
|
|
110
|
+
"time with time zone": "time with time zone",
|
|
111
|
+
"time without time zone": "time without time zone",
|
|
112
|
+
timestamptz: "timestamp with time zone",
|
|
113
|
+
timestamp: "timestamp without time zone",
|
|
114
|
+
timetz: "time with time zone",
|
|
115
|
+
time: "time without time zone",
|
|
116
|
+
varchar: "character varying",
|
|
117
|
+
varbit: "bit varying",
|
|
118
|
+
bpchar: "character",
|
|
119
|
+
char: "character",
|
|
120
|
+
int8: "bigint",
|
|
121
|
+
int4: "integer",
|
|
122
|
+
int2: "smallint",
|
|
123
|
+
int: "integer",
|
|
124
|
+
float8: "double precision",
|
|
125
|
+
float4: "real",
|
|
126
|
+
float: "double precision",
|
|
127
|
+
bool: "boolean",
|
|
128
|
+
decimal: "numeric",
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Anchored alternation over the alias keys, LONGEST FIRST — JS alternation is
|
|
132
|
+
* first-match-wins, not longest-match, so the order is load-bearing and is derived here
|
|
133
|
+
* rather than trusted to the literal order above.
|
|
134
|
+
*/
|
|
135
|
+
const CAST_TYPE_RE = new RegExp(`^(${Object.keys(CAST_TYPE_ALIASES)
|
|
136
|
+
.sort((a, b) => b.length - a.length)
|
|
137
|
+
.join("|")})\\b`);
|
|
138
|
+
/** Numeric target types, for the literal-cast rule in isElidableCast. */
|
|
139
|
+
const NUMERIC_CAST_TYPES = new Set([
|
|
140
|
+
"numeric", "integer", "bigint", "smallint", "real", "double precision",
|
|
141
|
+
]);
|
|
142
|
+
/**
|
|
143
|
+
* Read the target type of a cast at `i`, canonicalized. Returns the canonical spelling and
|
|
144
|
+
* how many characters it consumed (0 for an unrecognizable target — `::` before something
|
|
145
|
+
* this walker should just copy through).
|
|
146
|
+
*/
|
|
147
|
+
function readCastTarget(s, i) {
|
|
148
|
+
const alias = CAST_TYPE_RE.exec(s.slice(i));
|
|
149
|
+
if (alias)
|
|
150
|
+
return { type: CAST_TYPE_ALIASES[alias[1]], consumed: alias[1].length };
|
|
151
|
+
// Any other type name — `uuid`, `jsonb`, `text`, `public.mytype`. PG's spelling and ours
|
|
152
|
+
// already agree for these, so it passes through as written.
|
|
153
|
+
const plain = /^[a-z_][\w.]*/.exec(s.slice(i));
|
|
154
|
+
if (plain)
|
|
155
|
+
return { type: plain[0], consumed: plain[0].length };
|
|
156
|
+
return { type: "", consumed: 0 };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* True when a cast is one Postgres ADDS or ELIDES on its own, so its presence on one side
|
|
160
|
+
* and absence on the other is not a difference in meaning. Two cases, both found by a
|
|
161
|
+
* differential against live PG 16.15 rather than by reading the SQL:
|
|
162
|
+
*
|
|
163
|
+
* 1. `::text` on a `->>` / `#>>` result. Those operators are DEFINED to return text, so
|
|
164
|
+
* PG drops the author's explicit cast; `(payload->>'k')::text` comes back as
|
|
165
|
+
* `(payload ->> 'k'::text)`, whose only cast belongs to the KEY literal.
|
|
166
|
+
* 2. A numeric cast on a NUMERIC LITERAL. PG coerces the literal to the column's type, so
|
|
167
|
+
* `amt > 0` on a numeric column comes back as `amt > (0)::numeric`.
|
|
168
|
+
*
|
|
169
|
+
* Case 2 is the rule this file has always applied to STRING literals — the post-quote
|
|
170
|
+
* `'open'::text` strip up in normalizeCheckExpr — extended to the numeric ones, where the
|
|
171
|
+
* same PG behaviour produces the same permanent drift. Both are deliberately narrow: a
|
|
172
|
+
* cast on anything that is not a literal or a text-returning operator is a REAL cast and
|
|
173
|
+
* survives, because erasing it would let a genuine change read as clean drift.
|
|
174
|
+
*/
|
|
175
|
+
function isElidableCast(type, lhs) {
|
|
176
|
+
// `->>` / `#>>` yield text; a following `::text` cannot change anything.
|
|
177
|
+
if (type === "text" && /(?:->>|#>>)\s*(?:'(?:[^']|'')*'|[\w."]+)$/.test(lhs))
|
|
178
|
+
return true;
|
|
179
|
+
// A numeric literal cast to a numeric type.
|
|
180
|
+
if (NUMERIC_CAST_TYPES.has(type) && /(?:^|[^\w."'])\d+(?:\.\d+)?$/.test(lhs))
|
|
181
|
+
return true;
|
|
182
|
+
return false;
|
|
46
183
|
}
|
|
47
|
-
/**
|
|
48
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Collapse whitespace around commas, casts and symbolic-operator runs, and canonicalize
|
|
186
|
+
* cast target types — all OUTSIDE single-quoted literals (see normalizeCheckExpr).
|
|
187
|
+
*
|
|
188
|
+
* The cast work lives in this walker rather than in a `.replace()` up in the pipeline
|
|
189
|
+
* because only a quote-aware pass can tell a real `x::int` cast from the `::int` inside
|
|
190
|
+
* a regex literal (`slug ~ 'a::int'`). A blind replace would rewrite the pattern, and two
|
|
191
|
+
* distinct regex CHECKs would compare equal — a real change reading as clean drift, which
|
|
192
|
+
* is this file's failure in the worse direction.
|
|
193
|
+
*/
|
|
194
|
+
function collapseSeparatorSpacingOutsideQuotes(s) {
|
|
49
195
|
let out = "";
|
|
50
196
|
let i = 0;
|
|
51
197
|
const n = s.length;
|
|
@@ -69,12 +215,41 @@ function collapseCommaSpacingOutsideQuotes(s) {
|
|
|
69
215
|
out += s.slice(start, i);
|
|
70
216
|
continue;
|
|
71
217
|
}
|
|
218
|
+
// A separator token: a comma, a `::` cast, or a symbolic-operator run. All three bind
|
|
219
|
+
// tight to what PRECEDES them, so any whitespace already emitted before them is
|
|
220
|
+
// dropped. They differ only in whether what FOLLOWS binds tight too.
|
|
221
|
+
let token;
|
|
222
|
+
let tightAfter = true;
|
|
72
223
|
if (ch === ",") {
|
|
73
|
-
|
|
74
|
-
out += ",";
|
|
224
|
+
token = ",";
|
|
75
225
|
i++;
|
|
226
|
+
}
|
|
227
|
+
else if (ch === ":" && s[i + 1] === ":") {
|
|
228
|
+
i += 2;
|
|
76
229
|
while (i < n && /\s/.test(s[i]))
|
|
77
|
-
i++; //
|
|
230
|
+
i++; // `:: integer` → `::integer`
|
|
231
|
+
const target = readCastTarget(s, i);
|
|
232
|
+
i += target.consumed;
|
|
233
|
+
const lhs = out.replace(/\s+$/, "");
|
|
234
|
+
token = isElidableCast(target.type, lhs) ? "" : `::${target.type}`;
|
|
235
|
+
out = lhs;
|
|
236
|
+
// Whitespace AFTER the target type is load-bearing — `x::integer is not null` must
|
|
237
|
+
// not become `x::integeris not null` — so the cast, unlike a comma or an operator,
|
|
238
|
+
// binds tight on its left only.
|
|
239
|
+
tightAfter = false;
|
|
240
|
+
}
|
|
241
|
+
else if (OPERATOR_CHARS.has(ch)) {
|
|
242
|
+
const start = i;
|
|
243
|
+
// Take the WHOLE run, so `->>` stays one token rather than three.
|
|
244
|
+
while (i < n && OPERATOR_CHARS.has(s[i]))
|
|
245
|
+
i++;
|
|
246
|
+
token = s.slice(start, i);
|
|
247
|
+
}
|
|
248
|
+
if (token !== undefined) {
|
|
249
|
+
out = out.replace(/\s+$/, "") + token;
|
|
250
|
+
if (tightAfter)
|
|
251
|
+
while (i < n && /\s/.test(s[i]))
|
|
252
|
+
i++;
|
|
78
253
|
continue;
|
|
79
254
|
}
|
|
80
255
|
out += ch;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check-expr-compare.js","sourceRoot":"","sources":["../src/check-expr-compare.ts"],"names":[],"mappings":"AAAA,4BAA4B;AAC5B,EAAE;AACF,gFAAgF;AAChF,gFAAgF;AAChF,+EAA+E;AAC/E,
|
|
1
|
+
{"version":3,"file":"check-expr-compare.js","sourceRoot":"","sources":["../src/check-expr-compare.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export { findReferencedRebuilds, D1ReferencedTableRebuildError, D1CyclicForeignK
|
|
|
35
35
|
export type { D1RebuildRefusal } from "./emit/d1-fk-refuse.js";
|
|
36
36
|
export { introspectD1, type D1Runner, type IntrospectD1Options } from "./introspect/d1.js";
|
|
37
37
|
export { ensureLedger, recordApplied, deleteApplied, appliedNames, appliedRecords, recordBaseline, baselineRecord, BASELINE_NAME, MIGRATIONS_TABLE, DEFAULT_LEDGER_SCHEMA, type LedgerRow, type LedgerOptions, type LedgerDialect, } from "./apply/ledger.js";
|
|
38
|
-
export { applyPending, rollbackTo, type ApplyPendingOptions, type ApplyPendingResult, type RollbackToOptions, type RollbackToResult, } from "./apply/apply.js";
|
|
38
|
+
export { applyPending, MigrationApplyError, rollbackTo, type ApplyPendingOptions, type ApplyPendingResult, type RollbackToOptions, type RollbackToResult, } from "./apply/apply.js";
|
|
39
39
|
export { verifyReplay } from "./verify/replay.js";
|
|
40
40
|
export type { VerifyReplayArgs, VerifyReplayResult } from "./verify/replay.js";
|
|
41
41
|
export { openReplayEngine } from "./verify/replay-engine.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -49,7 +49,7 @@ export { findReferencedRebuilds, D1ReferencedTableRebuildError, D1CyclicForeignK
|
|
|
49
49
|
export { introspectD1 } from "./introspect/d1.js";
|
|
50
50
|
// Migration-history ledger + ordered transactional apply (postgres/sqlite)
|
|
51
51
|
export { ensureLedger, recordApplied, deleteApplied, appliedNames, appliedRecords, recordBaseline, baselineRecord, BASELINE_NAME, MIGRATIONS_TABLE, DEFAULT_LEDGER_SCHEMA, } from "./apply/ledger.js";
|
|
52
|
-
export { applyPending, rollbackTo, } from "./apply/apply.js";
|
|
52
|
+
export { applyPending, MigrationApplyError, rollbackTo, } from "./apply/apply.js";
|
|
53
53
|
// Snapshot-integrity: replay migrations and assert == committed snapshot.
|
|
54
54
|
export { verifyReplay } from "./verify/replay.js";
|
|
55
55
|
// An empty in-process database to replay a committed chain into (#313).
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metaobjectsdev/migrate-ts",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.7",
|
|
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",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@iarna/toml": "^2.2.5",
|
|
48
|
-
"@metaobjectsdev/metadata": "1.0.0-rc.
|
|
48
|
+
"@metaobjectsdev/metadata": "1.0.0-rc.7"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"kysely": ">=0.27.0 <0.30.0",
|
package/src/apply/apply.ts
CHANGED
|
@@ -45,6 +45,29 @@ export interface ApplyPendingResult {
|
|
|
45
45
|
applied: string[];
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* A committed migration's SQL failed. Carries WHICH one, because the driver's error says
|
|
50
|
+
* only what the database refused ("no such table: purchases") and never which file asked.
|
|
51
|
+
*
|
|
52
|
+
* The position matters to callers, not just the name: `--replay` prescribes a different
|
|
53
|
+
* remedy at the HEAD of the chain, where nothing earlier in this chain could have created
|
|
54
|
+
* what the migration needs, so the base schema must come from outside it.
|
|
55
|
+
*/
|
|
56
|
+
export class MigrationApplyError extends Error {
|
|
57
|
+
constructor(
|
|
58
|
+
/** `<timestamp>-<slug>` directory name of the migration that failed. */
|
|
59
|
+
readonly migration: string,
|
|
60
|
+
/** 0-based position among the PENDING migrations this run attempted. */
|
|
61
|
+
readonly index: number,
|
|
62
|
+
/** How many migrations were pending when the run started. */
|
|
63
|
+
readonly pendingCount: number,
|
|
64
|
+
override readonly cause: unknown,
|
|
65
|
+
) {
|
|
66
|
+
super(`migration '${migration}' failed to apply: ${(cause as Error)?.message ?? String(cause)}`);
|
|
67
|
+
this.name = "MigrationApplyError";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
48
71
|
interface DiscoveredMigration {
|
|
49
72
|
/** `<timestamp>-<slug>` directory name — stable id + sort key. */
|
|
50
73
|
name: string;
|
|
@@ -106,14 +129,24 @@ export async function applyPending(
|
|
|
106
129
|
}
|
|
107
130
|
|
|
108
131
|
const applied: string[] = [];
|
|
109
|
-
for (const m of pending) {
|
|
132
|
+
for (const [i, m] of pending.entries()) {
|
|
110
133
|
const text = await readFile(m.upPath, "utf8");
|
|
111
134
|
const checksum = checksumOf(text);
|
|
112
135
|
// Run the file's SQL + the ledger insert in ONE transaction. A failure
|
|
113
136
|
// rolls the whole file back (unrecorded) and propagates — stopping the run.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
137
|
+
//
|
|
138
|
+
// Wrapped so the failure names the FILE. The driver reports what the database
|
|
139
|
+
// refused and nothing about who asked, which left every caller — `apply-pending`
|
|
140
|
+
// and `verify --replay` alike — printing "no such table: purchases" over a
|
|
141
|
+
// directory of migrations, and `--replay` guessing at a remedy it could not
|
|
142
|
+
// have chosen correctly without knowing the position.
|
|
143
|
+
try {
|
|
144
|
+
await runSqlFileWithLedgerMutation(db, text, (trx) =>
|
|
145
|
+
recordApplied(trx, m.name, checksum, dialect, ledger),
|
|
146
|
+
);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
throw new MigrationApplyError(m.name, i, pending.length, err);
|
|
149
|
+
}
|
|
117
150
|
applied.push(m.name);
|
|
118
151
|
}
|
|
119
152
|
|
|
@@ -2,17 +2,37 @@
|
|
|
2
2
|
//
|
|
3
3
|
// CHECK-expression comparison. Postgres rewrites a stored CHECK body so the raw
|
|
4
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.
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
5
|
+
// thing. This reduces both to ONE canonical form for comparison.
|
|
6
|
+
//
|
|
7
|
+
// It was written when every expression reaching it was machine-derived with a simple,
|
|
8
|
+
// known shape (comparison / IN / length / regex), and the header said there was "no
|
|
9
|
+
// arbitrary author SQL to mis-normalize". That stopped being true when `index.lookup`'s
|
|
10
|
+
// `@expr` and `@where` began using the same canonicalizer: those are AUTHOR-written SQL.
|
|
11
|
+
// Each rewrite below is therefore justified on the stronger ground it now needs — that
|
|
12
|
+
// the difference it erases is not semantic in ANY valid SQL, not merely in the shapes we
|
|
13
|
+
// emit. The rewrites PG applies (verified against a live server):
|
|
9
14
|
// - parenthesizes terms: `col >= 0 AND col <= 100` → `(col >= 0) AND (col <= 100)`
|
|
10
15
|
// - rewrites IN-lists: `status IN ('A','B')` → `status = ANY (ARRAY['A'::text, 'B'::text])`
|
|
11
16
|
// - appends type casts: string literals gain `::text`
|
|
12
|
-
//
|
|
17
|
+
// - parenthesizes a cast OPERAND: `x::integer` → `(x)::integer`
|
|
18
|
+
// - respells a cast TARGET canonically: `x::int` → `(x)::integer`
|
|
19
|
+
// All five are canonicalized below so an enum/range CHECK introspected from PG
|
|
13
20
|
// compares equal to the one we generate (idempotency on the --from-db / verify paths).
|
|
21
|
+
//
|
|
22
|
+
// The last two are why a cast-bearing `@expr` used to drift FOREVER. The paren-strip
|
|
23
|
+
// turns PG's added parens into a space, so `(x)::integer` reduced to `x ::integer` while
|
|
24
|
+
// the authored `x::integer` stayed tight — and `:` is not one of PG's operator characters,
|
|
25
|
+
// so the operator-run collapse never reached it. On top of that, PG re-emits the target
|
|
26
|
+
// type in its canonical spelling, so an authored `int`/`varchar`/`timestamptz` could not
|
|
27
|
+
// match the `integer`/`character varying`/`timestamp with time zone` it got back. Both
|
|
28
|
+
// are fixed in the quote-aware walker below, which is the only place that can tell a real
|
|
29
|
+
// cast from a `::` inside a regex literal.
|
|
14
30
|
|
|
15
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Canonical form: drop casts/brackets/parens, fold `= ANY (ARRAY[…])` back to `IN`,
|
|
33
|
+
* lower-case, collapse whitespace, canonicalize cast target types, and collapse spacing
|
|
34
|
+
* around commas, casts and SYMBOLIC operators (outside single-quoted literals).
|
|
35
|
+
*/
|
|
16
36
|
export function normalizeCheckExpr(expr: string): string {
|
|
17
37
|
const stripped = expr
|
|
18
38
|
.toLowerCase()
|
|
@@ -37,17 +57,147 @@ export function normalizeCheckExpr(expr: string): string {
|
|
|
37
57
|
// PG stores `col IN (…)` as `col = ANY (ARRAY[…])`; after the bracket strip above
|
|
38
58
|
// that reads `col = any array …`. Fold it back to the `col in …` form we emit.
|
|
39
59
|
const folded = stripped.replace(/=\s*any\s+array/g, "in").replace(/\s+/g, " ").trim();
|
|
40
|
-
// Normalize spacing
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
|
|
60
|
+
// Normalize spacing around COMMAS and SYMBOLIC OPERATORS, so the spelling a human
|
|
61
|
+
// writes compares equal to the one Postgres hands back.
|
|
62
|
+
//
|
|
63
|
+
// Commas: a generated IN-list (`'a', 'b'`) must equal a hand-written one (`'a','b'`) —
|
|
64
|
+
// a hand SQLite migration omits the space.
|
|
65
|
+
//
|
|
66
|
+
// Operators: `pg_get_expr` re-emits every operator SPACED, and a person authoring an
|
|
67
|
+
// `index.lookup @expr` writes it tight. So `(request_context->>'device_id')` — the
|
|
68
|
+
// natural spelling — normalized to `request_context->>'device_id'` while the live index
|
|
69
|
+
// introspected to `request_context ->> 'device_id'`, and `verify --db` proposed DROP +
|
|
70
|
+
// CREATE against an index the database already held, exactly as declared. Casts and
|
|
71
|
+
// redundant parens were ALREADY canonicalized above, so the one difference left was the
|
|
72
|
+
// spacing — and it was the one difference a human actually produces.
|
|
73
|
+
//
|
|
74
|
+
// Safe because whitespace around a symbolic operator is not semantic in ANY valid SQL:
|
|
75
|
+
// where two spellings differ only there, at most one of them parses. Postgres lexes a
|
|
76
|
+
// maximal run of operator characters as a SINGLE token, so `a =- 1` is not `a = -1`
|
|
77
|
+
// spelled differently — it is a syntax error. Only SYMBOLIC operators are touched;
|
|
78
|
+
// collapsing around an alphabetic one would turn `a AND b` into `aANDb`.
|
|
79
|
+
//
|
|
80
|
+
// Both are done OUTSIDE single-quoted literals: a comma or an operator character inside
|
|
81
|
+
// a string/regex literal (`'a, b'`, a quantifier `'{1, 3}'`, a pattern `'a ->> b'`) IS
|
|
82
|
+
// meaningful, so two checks differing only there must stay distinct — else a real
|
|
83
|
+
// pattern change reads as clean drift, which is this file's failure in the other
|
|
84
|
+
// direction and the worse of the two.
|
|
85
|
+
return collapseSeparatorSpacingOutsideQuotes(folded);
|
|
47
86
|
}
|
|
48
87
|
|
|
49
|
-
/**
|
|
50
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Postgres' operator characters. A maximal run of these is ONE operator token — which is
|
|
90
|
+
* why `->>` and `!~*` survive intact rather than being split apart. Derived as the
|
|
91
|
+
* character SET rather than a list of known operators, so a spelling this file has never
|
|
92
|
+
* seen (`@>`, `?|`, a user-defined operator) normalizes by the same rule instead of
|
|
93
|
+
* needing an edit here.
|
|
94
|
+
*/
|
|
95
|
+
const OPERATOR_CHARS = new Set([..."+-*/<>=~!@#%^&|?"]);
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Cast target types PG respells, mapped to the spelling `pg_get_constraintdef` /
|
|
99
|
+
* `pg_get_expr` hand back. MEASURED against PostgreSQL 16.15 by creating one CHECK per
|
|
100
|
+
* alias and reading the definition back — not derived from the docs, because the two
|
|
101
|
+
* that would most easily have been missed (`timestamp` → `timestamp WITHOUT time zone`,
|
|
102
|
+
* `float` → `double precision`) only show up when you ask the server.
|
|
103
|
+
*
|
|
104
|
+
* The four `… time zone` forms map to THEMSELVES on purpose: they are already canonical,
|
|
105
|
+
* and they exist here to be matched BEFORE the shorter `timestamp` / `time` aliases would
|
|
106
|
+
* otherwise rewrite their leading word into `timestamp without time zone with time zone`.
|
|
107
|
+
* No other canonical form needs that guard — `\b` already stops `int` matching inside
|
|
108
|
+
* `integer`, `char` inside `character`, and `bool` inside `boolean`.
|
|
109
|
+
*/
|
|
110
|
+
const CAST_TYPE_ALIASES: Record<string, string> = {
|
|
111
|
+
"timestamp with time zone": "timestamp with time zone",
|
|
112
|
+
"timestamp without time zone": "timestamp without time zone",
|
|
113
|
+
"time with time zone": "time with time zone",
|
|
114
|
+
"time without time zone": "time without time zone",
|
|
115
|
+
timestamptz: "timestamp with time zone",
|
|
116
|
+
timestamp: "timestamp without time zone",
|
|
117
|
+
timetz: "time with time zone",
|
|
118
|
+
time: "time without time zone",
|
|
119
|
+
varchar: "character varying",
|
|
120
|
+
varbit: "bit varying",
|
|
121
|
+
bpchar: "character",
|
|
122
|
+
char: "character",
|
|
123
|
+
int8: "bigint",
|
|
124
|
+
int4: "integer",
|
|
125
|
+
int2: "smallint",
|
|
126
|
+
int: "integer",
|
|
127
|
+
float8: "double precision",
|
|
128
|
+
float4: "real",
|
|
129
|
+
float: "double precision",
|
|
130
|
+
bool: "boolean",
|
|
131
|
+
decimal: "numeric",
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Anchored alternation over the alias keys, LONGEST FIRST — JS alternation is
|
|
136
|
+
* first-match-wins, not longest-match, so the order is load-bearing and is derived here
|
|
137
|
+
* rather than trusted to the literal order above.
|
|
138
|
+
*/
|
|
139
|
+
const CAST_TYPE_RE = new RegExp(
|
|
140
|
+
`^(${Object.keys(CAST_TYPE_ALIASES)
|
|
141
|
+
.sort((a, b) => b.length - a.length)
|
|
142
|
+
.join("|")})\\b`,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
/** Numeric target types, for the literal-cast rule in isElidableCast. */
|
|
146
|
+
const NUMERIC_CAST_TYPES = new Set([
|
|
147
|
+
"numeric", "integer", "bigint", "smallint", "real", "double precision",
|
|
148
|
+
]);
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Read the target type of a cast at `i`, canonicalized. Returns the canonical spelling and
|
|
152
|
+
* how many characters it consumed (0 for an unrecognizable target — `::` before something
|
|
153
|
+
* this walker should just copy through).
|
|
154
|
+
*/
|
|
155
|
+
function readCastTarget(s: string, i: number): { type: string; consumed: number } {
|
|
156
|
+
const alias = CAST_TYPE_RE.exec(s.slice(i));
|
|
157
|
+
if (alias) return { type: CAST_TYPE_ALIASES[alias[1]!]!, consumed: alias[1]!.length };
|
|
158
|
+
// Any other type name — `uuid`, `jsonb`, `text`, `public.mytype`. PG's spelling and ours
|
|
159
|
+
// already agree for these, so it passes through as written.
|
|
160
|
+
const plain = /^[a-z_][\w.]*/.exec(s.slice(i));
|
|
161
|
+
if (plain) return { type: plain[0], consumed: plain[0].length };
|
|
162
|
+
return { type: "", consumed: 0 };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* True when a cast is one Postgres ADDS or ELIDES on its own, so its presence on one side
|
|
167
|
+
* and absence on the other is not a difference in meaning. Two cases, both found by a
|
|
168
|
+
* differential against live PG 16.15 rather than by reading the SQL:
|
|
169
|
+
*
|
|
170
|
+
* 1. `::text` on a `->>` / `#>>` result. Those operators are DEFINED to return text, so
|
|
171
|
+
* PG drops the author's explicit cast; `(payload->>'k')::text` comes back as
|
|
172
|
+
* `(payload ->> 'k'::text)`, whose only cast belongs to the KEY literal.
|
|
173
|
+
* 2. A numeric cast on a NUMERIC LITERAL. PG coerces the literal to the column's type, so
|
|
174
|
+
* `amt > 0` on a numeric column comes back as `amt > (0)::numeric`.
|
|
175
|
+
*
|
|
176
|
+
* Case 2 is the rule this file has always applied to STRING literals — the post-quote
|
|
177
|
+
* `'open'::text` strip up in normalizeCheckExpr — extended to the numeric ones, where the
|
|
178
|
+
* same PG behaviour produces the same permanent drift. Both are deliberately narrow: a
|
|
179
|
+
* cast on anything that is not a literal or a text-returning operator is a REAL cast and
|
|
180
|
+
* survives, because erasing it would let a genuine change read as clean drift.
|
|
181
|
+
*/
|
|
182
|
+
function isElidableCast(type: string, lhs: string): boolean {
|
|
183
|
+
// `->>` / `#>>` yield text; a following `::text` cannot change anything.
|
|
184
|
+
if (type === "text" && /(?:->>|#>>)\s*(?:'(?:[^']|'')*'|[\w."]+)$/.test(lhs)) return true;
|
|
185
|
+
// A numeric literal cast to a numeric type.
|
|
186
|
+
if (NUMERIC_CAST_TYPES.has(type) && /(?:^|[^\w."'])\d+(?:\.\d+)?$/.test(lhs)) return true;
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Collapse whitespace around commas, casts and symbolic-operator runs, and canonicalize
|
|
192
|
+
* cast target types — all OUTSIDE single-quoted literals (see normalizeCheckExpr).
|
|
193
|
+
*
|
|
194
|
+
* The cast work lives in this walker rather than in a `.replace()` up in the pipeline
|
|
195
|
+
* because only a quote-aware pass can tell a real `x::int` cast from the `::int` inside
|
|
196
|
+
* a regex literal (`slug ~ 'a::int'`). A blind replace would rewrite the pattern, and two
|
|
197
|
+
* distinct regex CHECKs would compare equal — a real change reading as clean drift, which
|
|
198
|
+
* is this file's failure in the worse direction.
|
|
199
|
+
*/
|
|
200
|
+
function collapseSeparatorSpacingOutsideQuotes(s: string): string {
|
|
51
201
|
let out = "";
|
|
52
202
|
let i = 0;
|
|
53
203
|
const n = s.length;
|
|
@@ -68,13 +218,40 @@ function collapseCommaSpacingOutsideQuotes(s: string): string {
|
|
|
68
218
|
out += s.slice(start, i);
|
|
69
219
|
continue;
|
|
70
220
|
}
|
|
221
|
+
|
|
222
|
+
// A separator token: a comma, a `::` cast, or a symbolic-operator run. All three bind
|
|
223
|
+
// tight to what PRECEDES them, so any whitespace already emitted before them is
|
|
224
|
+
// dropped. They differ only in whether what FOLLOWS binds tight too.
|
|
225
|
+
let token: string | undefined;
|
|
226
|
+
let tightAfter = true;
|
|
71
227
|
if (ch === ",") {
|
|
72
|
-
|
|
73
|
-
out += ",";
|
|
228
|
+
token = ",";
|
|
74
229
|
i++;
|
|
75
|
-
|
|
230
|
+
} else if (ch === ":" && s[i + 1] === ":") {
|
|
231
|
+
i += 2;
|
|
232
|
+
while (i < n && /\s/.test(s[i]!)) i++; // `:: integer` → `::integer`
|
|
233
|
+
const target = readCastTarget(s, i);
|
|
234
|
+
i += target.consumed;
|
|
235
|
+
const lhs = out.replace(/\s+$/, "");
|
|
236
|
+
token = isElidableCast(target.type, lhs) ? "" : `::${target.type}`;
|
|
237
|
+
out = lhs;
|
|
238
|
+
// Whitespace AFTER the target type is load-bearing — `x::integer is not null` must
|
|
239
|
+
// not become `x::integeris not null` — so the cast, unlike a comma or an operator,
|
|
240
|
+
// binds tight on its left only.
|
|
241
|
+
tightAfter = false;
|
|
242
|
+
} else if (OPERATOR_CHARS.has(ch)) {
|
|
243
|
+
const start = i;
|
|
244
|
+
// Take the WHOLE run, so `->>` stays one token rather than three.
|
|
245
|
+
while (i < n && OPERATOR_CHARS.has(s[i]!)) i++;
|
|
246
|
+
token = s.slice(start, i);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (token !== undefined) {
|
|
250
|
+
out = out.replace(/\s+$/, "") + token;
|
|
251
|
+
if (tightAfter) while (i < n && /\s/.test(s[i]!)) i++;
|
|
76
252
|
continue;
|
|
77
253
|
}
|
|
254
|
+
|
|
78
255
|
out += ch;
|
|
79
256
|
i++;
|
|
80
257
|
}
|