feat(postgres): add ADD COLUMN synthesis to the statement parser seam - #1212
Conversation
Pure seam for startup schema convergence: lift the ColumnDef from the desired CREATE TABLE into a deparsed ALTER TABLE ... ADD COLUMN, so the converger never hand-maintains a second copy of column DDL. Nothing calls it yet; the tripwire-to-convergence flip follows separately.
There was a problem hiding this comment.
Pull request overview
This PR extends the pkg/ddl PostgreSQL parser seam with a pure helper that can synthesize an ALTER TABLE … ADD COLUMN statement for a specific column by parsing a desired CREATE TABLE statement and lifting the column’s ColumnDef into an AlterTableStmt. This supports upcoming PostgreSQL startup schema convergence work without duplicating column DDL in code.
Changes:
- Added
SynthesizePostgresAddColumn(createTableDDL, columnName)to synthesizeADD COLUMNfrom a parsed PostgreSQLCREATE TABLEAST and deparse it back to SQL. - Added unit tests for common column forms (defaults, NOT NULL, typmods, quoted/schema-qualified identifiers) and all major error paths.
- Added a corpus-style test that iterates embedded PostgreSQL schema files and verifies every column can be synthesized and re-parsed.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| pkg/ddl/postgres_parser.go | Adds SynthesizePostgresAddColumn which parses a single CREATE TABLE, finds a target ColumnDef, constructs an AlterTableStmt, and deparses to SQL. |
| pkg/ddl/postgres_parser_test.go | Adds unit + embedded-schema corpus tests validating synthesis output and error handling, plus round-trip parsing of synthesized DDL. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
morgo
left a comment
There was a problem hiding this comment.
🤖 Approved on Morgan's behalf by his AI agent.
Pure addition — one new function plus its tests, no call sites anywhere in the tree yet, 37/37 green. Blast radius today is zero, which is what makes this stampable.
The construction is sound: len(stmts) != 1 and the non-CreateStmt type assertion are both guarded with clear errors, a missing column reports by name, and reusing createNode.CreateStmt.GetRelation() for the AlterTableStmt means schema-qualified names carry through rather than being reassembled. Deriving the ADD COLUMN from the embedded desired schema instead of hand-maintaining a parallel copy of every column's DDL is clearly the right call — the parallel copy is the version that silently rots.
One thing to carry into the follow-up PR, not this one. The synthesized Def is the ColumnDef node verbatim from the CREATE TABLE, so it carries that column's constraints with it. Constraints that are trivially valid at table-creation time are not all valid as an ALTER TABLE ... ADD COLUMN against a populated table:
NOT NULLwithout aDEFAULTfails on any non-empty table (column "x" of relation "y" contains null values). With a default it's fine, and cheap since PG 11.PRIMARY KEY/UNIQUEon an existing table with rows can fail on duplicates, and takes a lock while it builds.
Since the stated plan is for a follow-up to "flip the tripwire outcomes to converge under an advisory lock," that follow-up will be feeding this output at live storage tables at startup — where a failure means the pod doesn't come up. Worth deciding there whether to refuse synthesis for the unsafe shapes, or to let it emit and have the converger classify them. Flagging it here mainly so the next reviewer doesn't assume this seam only ever emits DDL that's safe to apply; the function's doc comment currently promises synthesis, not safety, and that's a reasonable contract as long as it's an explicit one.
|
🤖 Adversarial correctness review, requested by @aparajon and performed by their agent. Reviewed at head Verdict: the synthesis itself is solid and genuinely inert — nothing here blocks, but the corpus test proves less than it looks like it proves, and that matters for the converger that lands next. I attacked the AST lift with every column shape I could think of and could not make it silently degrade one; the seam is faithful, which is the right property for it to have. The finding is about where the applicability judgment lives, since this PR is the foundation for flipping a startup tripwire from "fail" to "converge". Findings1. 2. 3. (nit) This is the only dialect-named exported function in 4. (nit) The corpus test reads Action items
Verified (tried to break, couldn't)I could not find a column shape that lifts lossily: identity columns, This review was generated by Claude Code (claude-opus-5). |
aparajon
left a comment
There was a problem hiding this comment.
🤖 Approving on @aparajon's behalf after the adversarial correctness review above. The findings there are yours to pick up as follow-ups — flagging them, not gating on them.
This stamp was left by Claude Code (claude-opus-5).
Dialect-specific parsing behavior must be reachable only through ParserForDialect, matching CreateIndex. Pin the seam's contract in its docs: synthesis is faithful and judges applicability nowhere, and the column name is the parser-folded spelling CreateTableColumns returns. The corpus test iterates every CREATE TABLE in the embedded schema and proves proto-equality round-trips, nothing more.
morgo
left a comment
There was a problem hiding this comment.
🤖 Re-approving on Morgan's behalf at 615b4f1d (automated review).
The branch was force-pushed past my earlier approval — the old SHA is unreachable, so I re-checked rather than assuming a clean rebase.
The new commit moves ADD COLUMN synthesis behind the parser seam (+25/-0 in parser.go, +7/-3 in postgres_parser.go), which is the right placement: it keeps the synthesis reachable through StatementParser rather than as a PostgreSQL-only entry point, consistent with how CostScalesWithTableSize and CreateIndex sit on the same interface. Still +246/-0 overall, confined to pkg/ddl, no production callers changed. CI green.
Nothing further from me.
|
🤖 Review response — created by Kiran's code review agent (Amp, Claude Opus 4.5) — pull/1212, follow-up commit All four findings fixed: the seam's contract (faithful by design, parser-folded column names) is now stated in its docs, the entry point moved behind
|
…dcolumn-ddl-seam * origin/main: (28 commits) docs: document the PostgreSQL support envelope (#1144) fix(engine): report why a Vitess schema change failed (#1242) feat(ddl): detect statements whose cost scales with table size (#1237) fix(operator): keep a multi-table apply running while tables are queued behind a cutover (#1241) fix(storage): index the webhook inbox claim ordering (#1196) fix(github): drop the cutover duration promise from progress surfaces (#1240) fix(github): render row-copy progress percentages at their true precision (#1239) fix(observability): do not report a shutdown as a claim failure (#1233) fix(github): tell an operator why a refused apply's database is busy (#1224) fix(engine): do not mark an apply failed when its driver shuts down (#1234) feat(github): render live row-copy progress on sharded table lines (#1191) feat(ui): add approximate row and byte formatters (#1236) fix(planetscale): delete the branch an apply created when it fails before its deploy request (#963) feat(api): app grouping field on database config (#1226) feat(cli): filter pulled tables with --table (#1235) fix(github): refuse a Vitess foreign key at plan time instead of at apply time (#966) feat(lint): add severityglyphs analyzer to keep the severity vocabulary in pkg/glyph (#1153) feat: remove the volume control operation end to end in favor of autoscaling (#1225) ci: give the k8s e2e job budget room for setup plus go test's timeout (#1232) fix(storage): canonicalize lock and check identity keys (#1216) ... # Conflicts: # pkg/ddl/parser.go # pkg/ddl/parser_test.go
morgo
left a comment
There was a problem hiding this comment.
🤖 Re-approving on Morgan's behalf (automated review) — the head moved past my last approval at 615b4f1d, so re-verifying rather than letting the stale stamp stand.
The move is a main merge (88fd597d) and nothing else: the PR's own diff is still exactly +246/-0 across the same four pkg/ddl files, and the two content commits (9d44c005, 615b4f1d) are unchanged. So this is a re-check of the merge, not of the change.
The merge is worth checking rather than waving through, because #1237 landed on main at 19:59Z and touches the same two files — pkg/ddl/parser.go and pkg/ddl/postgres_parser.go. A merge that quietly dropped one side's additions would produce a green-looking branch that silently lost CostScalesWithTableSize. It didn't:
- Both methods are present in the
StatementParserinterface with their doc comments intact, and both parsers implement both —SynthesizeAddColumnatparser.go:173/postgres_parser.go:125,CostScalesWithTableSizeatparser.go:185/postgres_parser.go:195, with #1237'salterSpecScalesWithTableSize,alterCmdScalesWithTableSize,addColumnScalesWithTableSizeandisConstantExprall still there. - The PR diff shows
-0against a merge base that now includes #1237, which is the mechanical confirmation that no main content was removed. mergeStateStatus: CLEAN, and all 37 checks pass — includingBuildand all fourlintvariants (default, e2e, integration, consumer-module), which is what proves the two additive interface methods didn't strand an implementation under some other build tag.
Re-confirming the safety posture that made this stampable in the first place, since it survives the merge unchanged: the MySQL side is an explicit refusal, not a silent path —
func (tidbStatementParser) SynthesizeAddColumn(string, string) (string, error) {
return "", fmt.Errorf("ADD COLUMN synthesis is not supported by the MySQL statement parser")
}— and the PR still wires no callers, so the live MySQL production path is untouched by construction rather than by argument.
My earlier findings stand as written and none of them are affected by the merge. Nothing new to raise.
…ditive-convergence * origin/main: (33 commits) feat(postgres): add ADD COLUMN synthesis to the statement parser seam (#1212) feat(cli): add storage canonicalize-identity-keys admin subcommand (#1231) fix(storage): canonicalize remaining identity keys (#1218) fix(storage): canonicalize apply and task identity keys (#1217) fix(webhook): canonicalize repository identity at ingress (#1213) docs: document the PostgreSQL support envelope (#1144) fix(engine): report why a Vitess schema change failed (#1242) feat(ddl): detect statements whose cost scales with table size (#1237) fix(operator): keep a multi-table apply running while tables are queued behind a cutover (#1241) fix(storage): index the webhook inbox claim ordering (#1196) fix(github): drop the cutover duration promise from progress surfaces (#1240) fix(github): render row-copy progress percentages at their true precision (#1239) fix(observability): do not report a shutdown as a claim failure (#1233) fix(github): tell an operator why a refused apply's database is busy (#1224) fix(engine): do not mark an apply failed when its driver shuts down (#1234) feat(github): render live row-copy progress on sharded table lines (#1191) feat(ui): add approximate row and byte formatters (#1236) fix(planetscale): delete the branch an apply created when it fails before its deploy request (#963) feat(api): app grouping field on database config (#1226) feat(cli): filter pulled tables with --table (#1235) ... # Conflicts: # docs/configuration.md # pkg/ddl/postgres_parser.go # pkg/ddl/postgres_parser_test.go
Add a
pkg/ddlseam that synthesizesALTER TABLE … ADD COLUMNfrom the desired CREATE TABLE statement, for PostgreSQL startup schema convergence.Why
verifyPostgresSchemaShapecurrently fails startup when a storage table is missing a column — it detects drift but cannot converge it. Converging safely requires deriving the exact ADD COLUMN from the embedded desired schema rather than hand-maintaining a parallel copy of every column's DDL. This PR adds that pure synthesis seam; a follow-up PR flips the tripwire outcomes to converge under an advisory lock.What
SynthesizeAddColumn(createTableDDL, columnName)on theStatementParserinterface, reached throughParserForDialectlike every other dialect-sensitive operation. The PostgreSQL implementation parses the CREATE TABLE with pg_query, lifts the column's ColumnDef — type and column-level constraints — into an AlterTableStmt AST, and deparses back to SQL. Table-level constraints (PRIMARY KEY (…),UNIQUE (…),CHECK (…)) are not part of a column declaration and are not carried; the output is pg_query's normalized rendering, not a textual slice of the input. The MySQL-family parser returns a not-supported error (its bootstrapper diffs schemas with Spirit instead).NOT NULLcolumn without aDEFAULTsynthesizes exactly as declared even though PostgreSQL rejects that ALTER on a populated table. Applicability is the caller's judgment; the follow-up converger PR adds the classifier that makes it.columnNamematches the parser-folded column name (the valuesCreateTableColumnsreturns), documented on the seam.No production caller yet — no behavior change.
Before / after