Skip to content

wip: feat(postgres): handle postgres DEFAULT in schema changes - #4635

Draft
dtunikov wants to merge 1 commit into
feat/dbi-963/handle-default-in-mysql-cdc-schema-changesfrom
feat/handle-default-pg-ddl
Draft

wip: feat(postgres): handle postgres DEFAULT in schema changes#4635
dtunikov wants to merge 1 commit into
feat/dbi-963/handle-default-in-mysql-cdc-schema-changesfrom
feat/handle-default-pg-ddl

Conversation

@dtunikov

@dtunikov dtunikov commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #4632, which added FieldDescription.default_expr and the ClickHouse-side replay. This fills the field in for the Postgres source, so rows that predate an ADD COLUMN ... DEFAULT read back the same value on both sides instead of ClickHouse's type zero.

How

Relation messages carry neither nullability nor defaults, so both now come from one pg_catalog lookup over the added columns, replacing the nullable-only query (and its replica-identity heuristic — attnotnull is authoritative for every column). Generated columns are excluded, since their expression also lives in pg_attrdef.

pg_get_expr output is not a bare literal: Postgres constant folds and type labels defaults, so smallint DEFAULT -32768 arrives as '-32768'::integer, numeric(10,2) DEFAULT 1.50 as 1.50, char(3) as 'abc'::bpchar. The translator peels one cast label or paren wrapper, then validates the constant against the column's QValueKind:

  • numeric kinds — must parse as a number, emitted unquoted; 'NaN'/'Infinity' declined
  • Booleantrue/false
  • string-ish kinds (text, enum, uuid, json, inet, date, timestamp, ...) — requoted with SQL doubling; declined if the value holds a backslash or a control character, which escape differently across dialects
  • TimestampTZ — the +00 offset Postgres renders under timezone=UTC is stripped, since the destination column carries no zone
  • everything else (bytea, arrays, interval, time, geo) — declined, their text form does not carry over

Anything that is not a constant (now(), nextval(...), (2 + 3), generated expressions) is declined, leaving the column without a default. Defaults are only translated for the Q type system; PG-type-system destinations splice the source type in verbatim.

Tests

  • TestDefaultExprFromPostgresDefault — table-driven over what pg_get_expr renders, including hostile input ('x'::text || 'y'::text, chained casts, unterminated literals)
  • TestAddedColumnCatalogInfo — same shape but against a live Postgres, so the rendering quirks above are pinned down rather than assumed
  • Test_PG_AlterTableAddColumnDefault / ..._Untranslated — pg→ClickHouse e2e; a row lands before the ALTER so it reads through the ClickHouse default, and the untranslated case also covers TIMESTAMP DEFAULT 'infinity', which is translated but rejected by ClickHouse, exercising the retry-without-default fallback

All four pass locally against the Tilt environment.

🤖 Generated with Claude Code

Relation messages carry neither nullability nor defaults, so read both from
pg_catalog for added columns in one lookup, replacing the nullable-only query.
Constant defaults are translated to a dialect neutral SQL literal per the
column's QValueKind; anything else is declined.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant