Repository navigation
Description
Summary
meta verify --db on Postgres treats a hand-authored (col)::text ~ '…' CHECK on a char(n) (bpchar) column as equal to the uncast col ~ '…'. The two expressions match different values, so this is a false-clean result: a real constraint difference is reported as no drift.
The cause is that #463 put bpchar in the text-family set whose (col)::text cast is folded away before ~, ~*, !~ and !~*:
server/typescript/packages/migrate-ts/src/diff/index.ts:const PG_TEXT_FAMILY_UDTS = new Set(["text", "varchar", "bpchar"]);
For text and varchar that fold is correct. For bpchar it is not.
Why bpchar is different
bpcharhas its own regex operator.~(bpchar, text)resolves tobpcharregexeq, which sees the blank padding. Casting totexttrims the trailing blanks first, so the cast changes the result.- Postgres never adds the cast on
bpchar. For avarcharcolumn it stores((v)::text ~ '…'::text), which is the drift fix(migrate-ts): regex CHECK on a varchar/text column no longer reads as drift on Postgres #463 fixed. For achar(n)column it stores(key ~ '…'::text)with no column cast. Folding the cast onbpchartherefore fixes no server-generated drift. It only affects hand-authored casts, and there it hides a real difference.
Reproduction (Postgres 16)
select ('ab'::char(8)) ~ 'b$'; -- f (bpcharregexeq sees 'ab ')
select (('ab'::char(8))::text) ~ 'b$'; -- t (cast trims the padding)
select oprcode from pg_operator
where oprname = '~' and oprleft = 'bpchar'::regtype; -- bpcharregexeq
create table t (key char(8) check (key ~ '^a'), v varchar(8) check (v ~ '^a'));
select conname, pg_get_constraintdef(oid) from pg_constraint where conrelid = 't'::regclass;
-- t_key_check | CHECK ((key ~ '^a'::text)) <- no column cast on bpchar
-- t_v_check | CHECK (((v)::text ~ '^a'::text)) <- cast added on varcharWith the current code, an expected key ~ 'b$' and an actual (key)::text ~ 'b$' on a char(8) column compare equal, although the first rejects 'ab' and the second accepts it.
Proposed fix
- Remove
bpcharfromPG_TEXT_FAMILY_UDTSso onlytextandvarcharfold the cast. - In
test/check-evolution/diff-offline.test.ts, dropbpcharfrom thetest.each(["text", "varchar", "bpchar"])no-drift case. - Add a test that an explicit
(key)::text ~ '…'on abpcharcolumn still reads as drift againstkey ~ '…'. - Add a check that the uncast form Postgres stores on
bpchar((key ~ '…'::text)) is still clean. - Update the CHANGELOG line and the
ColumnDescriptor.udtNamedoc comment, which namebpcharas part of the text family.
The char(n) scenario in the #463 PR description (uncast check, no drift) keeps passing, because Postgres stores no column cast there.
Activity
dmealing commentedon Nov 13, 2026
Resolved by #498 (shipped in 1.2.0):
bpcharis no longer inPG_TEXT_FAMILY_UDTS(migrate-ts/src/diff/index.ts), andtest/check-evolution/diff-offline.test.tscovers both cases asked for here — an explicit(key)::text ~ '…'on abpcharcolumn reads as drift, and the uncast form Postgres stores stays clean (plus the same forlength((col)::text)). Closing as part of the 1.3.0 docs truth pass.