fix(github): classify and format plan DDL under its own dialect - #1194
Conversation
The plan summary classified every statement with the MySQL family's parser, so PostgreSQL plans undercounted (statements the parser rejected were silently dropped) and the DDL block reformatted statements under the MySQL grammar's quoting rules. Classification and display formatting now resolve the plan's own dialect parser, and unclassifiable statements are logged instead of dropped.
There was a problem hiding this comment.
Pull request overview
This PR fixes plan comment rendering so DDL classification and formatting are performed using the target database’s own SQL dialect (instead of always using the MySQL grammar), improving correctness for PostgreSQL plans and keeping MySQL-family output unchanged.
Changes:
- Add
ddl.FormatDDLForDialectand use it when rendering plan DDL blocks so statements are formatted under the plan’s dialect. - Classify plan statements for summary counting using the dialect-specific parser derived from
DatabaseType, with a raw-statement fallback when classification isn’t available. - Extend/adjust tests and preview fixtures to include
DatabaseTypeand validate PostgreSQL plan rendering/counting behavior.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/webhook/templates/plan.go | Switch plan summary classification to dialect-specific parser and format DDL blocks via FormatDDLForDialect. |
| pkg/ddl/format.go | Introduce FormatDDLForDialect to canonicalize statements via the dialect’s parser (best-effort). |
| pkg/ddl/format_test.go | Add coverage for MySQL vs Postgres formatting behavior and best-effort fallbacks. |
| pkg/webhook/templates/plan_dialect_test.go | New test asserting PostgreSQL plans classify/render without MySQL backtick quoting. |
| pkg/webhook/templates/summarize_changes_test.go | Update tests to provide DatabaseType and verify PostgreSQL classification affects summary counts. |
| pkg/webhook/templates/preview.go | Populate DatabaseType in preview plan data so previews exercise dialect-aware logic. |
| pkg/webhook/plan_test.go | Update multi-env plan tests to use the canonical DatabaseType values (e.g. vitess). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Review follow-ups: non-MySQL dialects get the same per-clause line-break layout as MySQL, a dialect with no registered display parser is logged instead of silently rendering unformatted, PostgreSQL plans use the schema-based heading treatment instead of the keyspace vocabulary, TEMPLATES.md gains a Postgres Plan preview so future formatting regressions surface in review diffs, and the plan summary reports the raw DDL statement count alongside a vschema update instead of hiding it, matching SummarizeChanges.
…t-dialect-classify * origin/main: fix(github): keep the PR progress comment updating between operation dispatch waves (#1104) fix(tern): classify materialized change DDL with the target dialect parser (#1187) fix(engine): resolve a cancel or stop that arrives before remote dispatch (#1184) fix: default connect and write timeouts on managed database connections (#1182) fix(storage): index the apply-operation claim ordering (#1180) fix(tern): generalize control resume state and complete cancels with no live engine work (#1179) fix(github): name each table's outcome in unsuccessful apply summaries (#1186) ci: peel tern and webhook into a dedicated integration shard (#1166) fix(engine): report a drained schema change's terminal outcome instead of pending (#1114) feat(serve): contain gRPC handler panics with recovery interceptors (#1164) feat(observability): tell operators when a log window hides older entries (#1185) fix(tern): settle sequential tasks when the engine loses in-flight work (#1113) fix(github): align PR comment severity glyphs with the shared vocabulary (#1135) fix(tern): release a database held by a stopped schema change (#1175) fix(plan): canonicalize drift DDL with the target's dialect parser (#1177) fix(e2e): stop injecting connection kills once the k8s pause is observed (#1178) # Conflicts: # pkg/webhook/templates/plan.go
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
aparajon
left a comment
There was a problem hiding this comment.
🤖 Adversarial correctness review, requested by @aparajon and performed by their agent. Reviewed at head 1a86fa89.
Verdict: this is the right fix in the right place — routing classification and formatting through the target's own parser, with a raw-statement-total fallback so an unclassifiable plan never reads as "no changes". Two findings, neither blocking: the fallback closes a narrower window than its comment claims, and the layout pass this PR newly points at Postgres DDL mis-splits quoted string literals.
Threading DatabaseType all the way to countStatementTypes and writeKeyspaceChanges is the correct seam, and adding a Postgres Plan scenario to TEMPLATES.md means the rendering is reviewable without a checkout.
Findings
1. The raw-total fallback only fires when every typed count is zero, so a mixed plan still undercounts. writePlanSummary adds the total only under len(parts) == 0, so any non-zero create/alter/drop suppresses it. I rendered a greenfield Postgres plan — CREATE TABLE sessions (...) plus CREATE INDEX sessions_user_id_idx and CREATE INDEX sessions_created_at_idx — and got 📋 **Plan**: **1** table to create, with SummarizeChanges (the check-run summary) agreeing at 1 create. The two index statements vanish from both surfaces. The comment directly above the new fallback says statements that classify as none of create/alter/drop "still count", which is only true when nothing else counted. This is pre-existing and also reproduces on MySQL (verified), but it becomes the common Postgres case rather than an edge one, since a greenfield Postgres table is idiomatically a CREATE TABLE plus its index statements — exactly the shape block/pg-sprite#63 makes reachable.
2. The layout pass splits inside single-quoted literals, so the rendered DDL is not the DDL that runs. layoutDDL → formatCreateTable respects parentheses but not quotes. CREATE TABLE t (..., note text DEFAULT 'x, y') renders as:
note text DEFAULT 'x,
y'
The literal now contains a newline and four spaces — an operator copying the block out of the PR comment gets a different default value. numeric(10, 2) and CHECK (kind IN ('a', 'b')) both survive, so it is specifically the quote case. Pre-existing on the MySQL path (verified identically there), but this PR is what routes Postgres canonical form into the same function, and text defaults and CHECK ... IN (...) lists are ordinary Postgres shapes.
Action items
- (Finding 1) Make the raw-statement total additive rather than a last-resort substitute — or count every statement type the vocabulary knows, so index and rename statements are not silently dropped from a mixed plan.
- (Finding 1) Either way, fix the fallback's comment: as written it promises coverage the code only provides when all three typed counts are zero.
- (Finding 2) Teach the comma splitter to skip over single-quoted literals (it already tracks parenthesis depth), so a string default or an
IN (...)list is never broken across lines.
Verified (tried to break, couldn't)
Every non-test PlanCommentData construction site sets DatabaseType — plan.go, rollback.go, preview.go, preview_sharded.go — so no existing plan silently degrades to the raw-total fallback; DialectForDatabaseType covers all four storage.DatabaseType* values and lowercases its input, so "PostgreSQL", "Vitess" and friends all resolve; DatabaseType is the canonical engine string with the display label derived through schemaChangePlanDatabaseTypeLabel, which is why the "PlanetScale" → "vitess" test-data correction was needed and it is complete; the plan comment's summary and SummarizeChanges still derive from the same helper and agree statement for statement; both unclassifiable branches log with the database type and keyspace, satisfying the no-silent-fallback rule; the Postgres ALTER rendering keeps bare identifiers where the MySQL formatter would backtick them, which is what pins the routing; no test functions were deleted and no assertions weakened anywhere in the diff; go build ./... plus ./pkg/webhook/... ./pkg/ddl/... pass locally at head, and CI is green across all 37 checks.
This review was generated by Claude Code (claude-opus-5).
…omment The raw-statement total only renders when every typed count is zero, so a mixed plan shows typed counts alone. Say that plainly instead of promising unclassified statements always count.
|
Review response from Kiran's (@Kiran01bm) code review assessment agent (Amp / Claude Opus 4.5) Both findings are valid; the fallback comment is corrected in this PR, and the two behavioural fixes (additive statement totals, quote-aware comma splitting) are tracked as internal follow-ups since both predate this PR and touch surfaces beyond it.
|
…t-dialect-classify * origin/main: chore(postgres): remove unreachable blocked apply guard (#1203) feat(postgres): enforce the table size ceiling at plan time (#1199) fix(github): give multi-keyspace Strata applies the keyspace-grouped shard layout (#1189) feat(cli): name each remote handle in the deployment-filtered status list (#1062) fix(github): lead wide shard groups with coverage instead of walling the plan comment (#1188) feat(engine): cap how many drivers one apply may occupy (#1183) # Conflicts: # pkg/webhook/templates/plan.go # pkg/webhook/templates/sharded_plan_test.go
* fix(github): render apply comment DDL under its own dialect
Apply progress and terminal summary comments formatted every DDL
block through the MySQL display formatter, mangling PostgreSQL
statements. The apply's engine now selects the formatting dialect.
* fix(github): accept the postgresql spelling in apply dialect routing
The engine field also carries the long-form spelling the progress API
produces ("PostgreSQL"), which previously fell through to the MySQL
fallback. The fallback's doc comment now states the real rationale
(preserving the established rendering for legacy engine values) instead
of a false unchanged-statement guarantee.
* fix(github): warn when an unrecognized engine value falls back to MySQL rendering
dialectForEngine silently rendered any unknown engine string under the
MySQL grammar. Known MySQL-family values still resolve silently; an
unrecognized value now logs a warning with the engine and apply ID so
the fallback is triageable.
…ogress-dialect * origin/main: fix(github): redact paths and connection identities in comments (#1200) fix(github): classify and format plan DDL under its own dialect (#1194) chore(postgres): remove unreachable blocked apply guard (#1203) feat(postgres): enforce the table size ceiling at plan time (#1199) fix(github): give multi-keyspace Strata applies the keyspace-grouped shard layout (#1189) feat(cli): name each remote handle in the deployment-filtered status list (#1062) fix(github): lead wide shard groups with coverage instead of walling the plan comment (#1188) feat(engine): cap how many drivers one apply may occupy (#1183) # Conflicts: # pkg/ddl/format_test.go # pkg/webhook/templates/plan.go # pkg/webhook/templates/sharded_plan_test.go
Plan comments now classify and format each DDL statement under the target database's own SQL dialect instead of always the MySQL grammar.
Why
The plan comment's summary counts (tables to create/alter/drop) and its DDL blocks both ran every statement through the MySQL parser. Valid PostgreSQL statements (
uuid,jsonb,byteacolumns, and other Postgres-only constructs) were unparseable under that grammar, so the summary miscounted them and the DDL block reformatted them under the wrong dialect's quoting rules.What
ddl.FormatDDLForDialect(dialect, stmt): the MySQL family keeps the existingFormatDDLtreatment; any other dialect renders its own parser's canonical form with the same line-break layout (each column/clause on its own line), skipping only the MySQL-specific lowercasing pass. LikeFormatDDL, it is a best-effort display formatter — an unparseable statement renders as-is, and a dialect with no registered parser is logged.FormatDDLForDialect.MySQL-family plans render byte-identically; TEMPLATES.md gains a new Postgres Plan preview section and is otherwise unchanged.
Before / after
Rendered: PostgreSQL plan comment
Schema Change Plan — Staging
Database:
testapp| Type:PostgreSQLRequested by @jackjackbits at 2026-01-01 00:00:00 UTC · planned from
abcdef1📋 Plan: 1 table to create, 1 table to alter