Skip to content

migrate-ts: (col)::text cast folded on bpchar columns hides real regex CHECK drift on Postgres #470

Open

Description

@dmealing

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

  1. bpchar has its own regex operator. ~(bpchar, text) resolves to bpcharregexeq, which sees the blank padding. Casting to text trims the trailing blanks first, so the cast changes the result.
  2. Postgres never adds the cast on bpchar. For a varchar column 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 a char(n) column it stores (key ~ '…'::text) with no column cast. Folding the cast on bpchar therefore 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 varchar

With 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 bpchar from PG_TEXT_FAMILY_UDTS so only text and varchar fold the cast.
  • In test/check-evolution/diff-offline.test.ts, drop bpchar from the test.each(["text", "varchar", "bpchar"]) no-drift case.
  • Add a test that an explicit (key)::text ~ '…' on a bpchar column still reads as drift against key ~ '…'.
  • Add a check that the uncast form Postgres stores on bpchar ((key ~ '…'::text)) is still clean.
  • Update the CHANGELOG line and the ColumnDescriptor.udtName doc comment, which name bpchar as 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions