@@ -41,12 +41,17 @@ function unwrapBacktickIdentifiers(expr: string): string {
4141 . join ( "" ) ;
4242}
4343
44+ const NO_COLUMNS : ReadonlySet < string > = new Set ( ) ;
45+
4446/**
4547 * Canonical form: drop casts/brackets/parens, fold `= ANY (ARRAY[…])` back to `IN`,
4648 * lower-case, collapse whitespace, canonicalize cast target types, and collapse spacing
4749 * around commas, casts and SYMBOLIC operators (outside single-quoted literals).
50+ *
51+ * `textColumns` (lower-cased names of text-family columns) lets a `(col)::text` cast before
52+ * a regex operator fold away; see isElidableCast.
4853 */
49- export function normalizeCheckExpr ( expr : string ) : string {
54+ export function normalizeCheckExpr ( expr : string , textColumns : ReadonlySet < string > = NO_COLUMNS ) : string {
5055 const stripped = unwrapBacktickIdentifiers ( expr )
5156 . toLowerCase ( )
5257 // Drop `::text` / `::"MyType"` type casts PG adds to literals. The lookbehind
@@ -95,7 +100,7 @@ export function normalizeCheckExpr(expr: string): string {
95100 // meaningful, so two checks differing only there must stay distinct — else a real
96101 // pattern change reads as clean drift, which is this file's failure in the other
97102 // direction and the worse of the two.
98- return collapseSeparatorSpacingOutsideQuotes ( folded ) ;
103+ return collapseSeparatorSpacingOutsideQuotes ( folded , textColumns ) ;
99104}
100105
101106/**
@@ -192,7 +197,17 @@ function readCastTarget(s: string, i: number): { type: string; consumed: number
192197 * cast on anything that is not a literal or a text-returning operator is a REAL cast and
193198 * survives, because erasing it would let a genuine change read as clean drift.
194199 */
195- function isElidableCast ( type : string , lhs : string ) : boolean {
200+ function isElidableCast ( type : string , lhs : string , rest : string , textColumns : ReadonlySet < string > ) : boolean {
201+ // `::text` on a COLUMN directly before a regex-match operator (`~`, `~*`, `!~`, `!~*`),
202+ // and only when the caller names that column as text-family (text, character varying,
203+ // character). On such a column PG adds the cast itself (`"key" ~ '…'` comes back
204+ // `((key)::text ~ '…'::text)`). On any other column, citext included, the cast changes
205+ // the operator (`col::text ~ 'A'` is case-sensitive, `col ~ 'A'` on citext is not), so it
206+ // survives. With no set supplied nothing is elided.
207+ if ( type === "text" && / ^ \s * ! ? ~ \* ? (? ! [ - + * \/ < > = ~ ! @ # % ^ & | ? ] ) / . test ( rest ) ) {
208+ const column = / (?: ^ | [ ^ \w . ] ) ( [ a - z _ ] \w * ) $ / . exec ( lhs ) ?. [ 1 ] ;
209+ if ( column !== undefined && textColumns . has ( column ) ) return true ;
210+ }
196211 // `->>` / `#>>` yield text; a following `::text` cannot change anything.
197212 if ( type === "text" && / (?: - > > | # > > ) \s * (?: ' (?: [ ^ ' ] | ' ' ) * ' | [ \w . " ] + ) $ / . test ( lhs ) ) return true ;
198213 // A numeric literal cast to a numeric type.
@@ -210,7 +225,7 @@ function isElidableCast(type: string, lhs: string): boolean {
210225 * distinct regex CHECKs would compare equal — a real change reading as clean drift, which
211226 * is this file's failure in the worse direction.
212227 */
213- function collapseSeparatorSpacingOutsideQuotes ( s : string ) : string {
228+ function collapseSeparatorSpacingOutsideQuotes ( s : string , textColumns : ReadonlySet < string > ) : string {
214229 let out = "" ;
215230 let i = 0 ;
216231 const n = s . length ;
@@ -246,7 +261,7 @@ function collapseSeparatorSpacingOutsideQuotes(s: string): string {
246261 const target = readCastTarget ( s , i ) ;
247262 i += target . consumed ;
248263 const lhs = out . replace ( / \s + $ / , "" ) ;
249- token = isElidableCast ( target . type , lhs ) ? "" : `::${ target . type } ` ;
264+ token = isElidableCast ( target . type , lhs , s . slice ( i ) , textColumns ) ? "" : `::${ target . type } ` ;
250265 out = lhs ;
251266 // Whitespace AFTER the target type is load-bearing — `x::integer is not null` must
252267 // not become `x::integeris not null` — so the cast, unlike a comma or an operator,
@@ -272,9 +287,13 @@ function collapseSeparatorSpacingOutsideQuotes(s: string): string {
272287}
273288
274289/** True when two CHECK expressions are equivalent after normalization. */
275- export function checkExprEquals ( a : string | undefined , b : string | undefined ) : boolean {
290+ export function checkExprEquals (
291+ a : string | undefined ,
292+ b : string | undefined ,
293+ textColumns ?: ReadonlySet < string > ,
294+ ) : boolean {
276295 if ( a === undefined || b === undefined ) return false ;
277- return normalizeCheckExpr ( a ) === normalizeCheckExpr ( b ) ;
296+ return normalizeCheckExpr ( a , textColumns ) === normalizeCheckExpr ( b , textColumns ) ;
278297}
279298
280299/**
0 commit comments