@everystack/cli 0.4.67 → 0.4.68
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/package.json +1 -1
- package/src/cli/deparse-normal.ts +72 -5
package/package.json
CHANGED
|
@@ -24,13 +24,33 @@
|
|
|
24
24
|
* for n ≥ 1. PostgreSQL defines an array-to-array cast element-wise (parse_coerce), so
|
|
25
25
|
* the two expressions denote the same value for every input; wrapping an element in
|
|
26
26
|
* parentheses is parse-neutral. The empty array is EXCLUDED: `ARRAY[]` without a cast has
|
|
27
|
-
* no type, so its cast is load-bearing and stays.
|
|
27
|
+
* no type, so its cast is load-bearing and stays. (The measured CROSS-VERSION case: two PG
|
|
28
|
+
* majors const-fold the same DDL differently.)
|
|
29
|
+
*
|
|
30
|
+
* LITERAL-TEXT-CAST STRIP: `'lit'::text == 'lit'`, but ONLY the bare `text` cast applied
|
|
31
|
+
* directly to a single-quoted string literal. This is a CROSS-PRODUCER class, present on
|
|
32
|
+
* every version: PostgreSQL coerces an unknown-type string literal compared to a text
|
|
33
|
+
* column and RECORDS `'lit'::text` in the stored tree, while the author's model source
|
|
34
|
+
* writes the un-cast `'lit'`. The two producers' texts never converge without this rule, so
|
|
35
|
+
* a `text_col = 'lit'` partial index / CHECK / policy re-diffs DROP+CREATE forever and the
|
|
36
|
+
* fingerprint gate cannot MATCH. Safe because `text` takes no typmod and its input function
|
|
37
|
+
* is the identity on the literal's bytes — the cast cannot change the value. The rule is
|
|
38
|
+
* `text`-ONLY for exactly that reason: `bpchar`/`character(n)` blank-pad or blank-trim,
|
|
39
|
+
* `varchar(n)`/`"char"` truncate — those casts CHANGE the value, so stripping one would
|
|
40
|
+
* collapse two opposite-truth predicates to equal text (a false match, the disaster). The
|
|
41
|
+
* array-type cast `::text[]` is likewise excluded — it carries the element type. See the
|
|
42
|
+
* `char(2)` false-match test for why the family is not widened.
|
|
28
43
|
*
|
|
29
44
|
* Anything the scanner does not positively recognize is left byte-for-byte unchanged — an
|
|
30
45
|
* unrecognized spelling is a MISS (drop + create, the pre-existing behavior), never a guess.
|
|
31
|
-
* Escalation is named, not implied: the SECOND cross-
|
|
46
|
+
* Escalation is named, not implied: the SECOND cross-VERSION variant found in the field is
|
|
32
47
|
* the trigger to adopt libpg_query and move identity onto normalized ASTs (Ty, 2026-08-09) —
|
|
33
|
-
* a growing rule table over raw text is where scanning stops being defensible.
|
|
48
|
+
* a growing rule table over raw text is where scanning stops being defensible. One correction
|
|
49
|
+
* to that trigger (2026-08-24): libpg_query closes cross-VERSION SYNTAX drift (whitespace,
|
|
50
|
+
* parens, keyword case), NOT cross-PRODUCER COERCION drift — a raw parse tree carries no type
|
|
51
|
+
* resolution, so `'lit'` and `'lit'::text` still differ structurally without a catalog, which
|
|
52
|
+
* the CI-pure `db:diff` lane does not have. That class earns a provable text rewrite here,
|
|
53
|
+
* verified end-to-end by `db:check`'s from-scratch build on a real database.
|
|
34
54
|
*
|
|
35
55
|
* Applied at the PRODUCERS (live introspection's `predicate()`, the compiler's policy
|
|
36
56
|
* assembly), so every downstream comparison — the identity matcher, the canonical hash, the
|
|
@@ -121,6 +141,53 @@ function distributeOnce(s: string): string | null {
|
|
|
121
141
|
return null;
|
|
122
142
|
}
|
|
123
143
|
|
|
144
|
+
/**
|
|
145
|
+
* A bare `text` cast token — `::text` or `::pg_catalog.text` — from just past a literal's
|
|
146
|
+
* closing quote. Whitespace around `::` is tolerated (pg emits none; an author might). The
|
|
147
|
+
* trailing `\b` rejects `::textfoo`; the caller additionally rejects a following `[` so the
|
|
148
|
+
* array-type cast `::text[]` is never stripped.
|
|
149
|
+
*/
|
|
150
|
+
const LITERAL_TEXT_CAST = /^\s*::\s*(?:pg_catalog\.)?text\b/;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Strip a bare `text` cast applied directly to a single-quoted string literal, wherever one
|
|
154
|
+
* appears. One left-to-right pass is complete: removing a cast token cannot create a new
|
|
155
|
+
* `'literal'::text` pair to its left, and it never forms an `(ARRAY[…])::T[]` for the other
|
|
156
|
+
* rule. Quoted regions are skipped so a `::text` inside a literal is content, not structure.
|
|
157
|
+
*/
|
|
158
|
+
function stripLiteralTextCast(s: string): string {
|
|
159
|
+
let out = '';
|
|
160
|
+
let i = 0;
|
|
161
|
+
while (i < s.length) {
|
|
162
|
+
const c = s[i];
|
|
163
|
+
if (c === "'") {
|
|
164
|
+
const close = skipQuoted(s, i);
|
|
165
|
+
if (close < 0) return out + s.slice(i); // unterminated literal — leave the rest alone
|
|
166
|
+
out += s.slice(i, close + 1); // the literal itself, verbatim
|
|
167
|
+
i = close + 1;
|
|
168
|
+
// Drop a bare text cast on the literal, and any stacked `::text::text`, so the result
|
|
169
|
+
// is a true fixpoint. `::text[]` is the array-type cast (element type, load-bearing),
|
|
170
|
+
// not a scalar text cast — the look-ahead skips whitespace so `::text []` is kept too.
|
|
171
|
+
let m = LITERAL_TEXT_CAST.exec(s.slice(i));
|
|
172
|
+
while (m && !/^\s*\[/.test(s.slice(i + m[0].length))) {
|
|
173
|
+
i += m[0].length; // drop the cast token
|
|
174
|
+
m = LITERAL_TEXT_CAST.exec(s.slice(i));
|
|
175
|
+
}
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (c === '"') {
|
|
179
|
+
const close = skipQuoted(s, i);
|
|
180
|
+
if (close < 0) return out + s.slice(i);
|
|
181
|
+
out += s.slice(i, close + 1); // a quoted identifier — never a string literal, skip whole
|
|
182
|
+
i = close + 1;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
out += c;
|
|
186
|
+
i++;
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
|
|
124
191
|
/**
|
|
125
192
|
* Normalize one deparsed expression. Idempotent; unrecognized text returns unchanged.
|
|
126
193
|
* Fixpoint-bounded: nesting deeper than the cap returns its progress, which is still
|
|
@@ -130,8 +197,8 @@ export function normalizeDeparsedExpr(text: string): string {
|
|
|
130
197
|
let s = text;
|
|
131
198
|
for (let n = 0; n < 64; n++) {
|
|
132
199
|
const next = distributeOnce(s);
|
|
133
|
-
if (next === null)
|
|
200
|
+
if (next === null) break;
|
|
134
201
|
s = next;
|
|
135
202
|
}
|
|
136
|
-
return s;
|
|
203
|
+
return stripLiteralTextCast(s);
|
|
137
204
|
}
|