Skip to content

WIP: feat(postgres): carry Postgres column DEFAULT through to ClickHouse on ADD COLUMN - #4633

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

WIP: feat(postgres): carry Postgres column DEFAULT through to ClickHouse on ADD COLUMN#4633
dtunikov wants to merge 1 commit into
feat/dbi-963/handle-default-in-mysql-cdc-schema-changesfrom
feat/handle-default-pg-ddl

Conversation

@dtunikov

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>
@dtunikov dtunikov changed the title feat(postgres): carry Postgres column DEFAULT through to ClickHouse on ADD COLUMN WIP: feat(postgres): carry Postgres column DEFAULT through to ClickHouse on ADD COLUMN Jul 28, 2026
@dtunikov
dtunikov marked this pull request as ready for review July 28, 2026 10:38
@dtunikov
dtunikov requested a review from a team as a code owner July 28, 2026 10:38
@dtunikov dtunikov closed this Jul 28, 2026
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