fix(statement): record the default's literal kind so keyword and bit defaults converge - #1225
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Several updated comments/docs contradict the actual SHOW CREATE TABLE forms asserted in tests and should be corrected to avoid future maintenance confusion.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR improves how pkg/statement preserves and emits column DEFAULT values by recording the default literal’s AST-derived form (Column.DefaultKind) so diffing, normalization, and formatting don’t have to re-guess intent from restored text (notably for boolean keywords vs numeric literals vs bit literals).
Changes:
- Introduces
DefaultKindclassification duringCREATE TABLEparsing and replacesColumn.DefaultIsStringwithColumn.DefaultKind. - Updates default emission (
formatColumnDefinition) and column comparison (columnsEqualWithContext) to useDefaultKindfor quoting / identity decisions. - Expands boolean-keyword default folding to cover additional types (including
bit) and adds/updates unit + integration tests for convergence and round-trips.
File summaries
| File | Description |
|---|---|
| pkg/statement/utils.go | Updates needsQuotes commentary to clarify it’s a fallback now that DefaultKind exists. |
| pkg/statement/README.md | Updates documentation for boolean keyword default folding behavior and scope. |
| pkg/statement/normalize_function_aliases.go | Switches the string-literal guard from DefaultIsString to DefaultKind. |
| pkg/statement/normalize_boolean_keyword_default.go | Extends and refactors keyword folding to set both stored value and DefaultKind. |
| pkg/statement/normalize_boolean_keyword_default_test.go | Expands convergence and “excluded types” coverage for boolean keyword default folding. |
| pkg/statement/literal_roundtrip_test.go | Adds round-trip coverage for bit-literal defaults and updates keyword-default expectations. |
| pkg/statement/format.go | Emits defaults based on DefaultKind (string vs bit literal vs number vs heuristic fallback). |
| pkg/statement/diff.go | Updates default equivalence/identity rules to use DefaultKind (incl. NULL keyword handling). |
| pkg/statement/diff_integration_test.go | Adds integration coverage across folding types and asserts behavior on excluded types. |
| pkg/statement/default_kind.go | New: defines DefaultKind and AST-based classification logic (incl. bit vs hex distinction). |
| pkg/statement/default_kind_test.go | New: tests DefaultKind classification and heuristic fallback emission for unknown kinds. |
| pkg/statement/create_table.go | Records DefaultKind at parse time while the default AST is still available. |
| pkg/statement/create_table_test.go | Updates parenthesized literal default tests to assert DefaultKindString. |
| pkg/statement/columns_equal_guard_test.go | Updates the “all fields compared” guard to include DefaultKind instead of DefaultIsString. |
Review details
- Files reviewed: 14/14 changed files
- Comments generated: 5
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
🤖 Comment from Morgan's AI agent. Heads up on the MySQL 9.7 CI failure here — it's not a flake, and the underlying reason is worth knowing beyond this PR.
MySQL 9.7 changed how a boolean keyword default resolves on
The test hardcodes the 8.4 reading with no version gate, so 9.7 fails on the This looks like a test-expectation defect only: the normalizer in this PR already excludes Two ways out, in my order of preference:
Either way, The other red job here ( |
…defaults converge
A column DEFAULT was reduced to restored text plus a single "was it quoted"
flag, which loses distinctions the characters cannot carry. TRUE, 1 and '1'
all restore to a numeric-looking string, and b'1' restores to text that reads
like a quoted string but has to be emitted bare. The parser knows all of them
apart — the boolean keyword carries IsBooleanFlag, a bit literal is a binary
literal — and consults that knowledge to produce the text before dropping it.
Record the form as Column.DefaultKind at the point the AST is still in hand,
and have emission, comparison and the keyword fold read it instead of
re-deriving it from the value. Two things follow.
A bit literal default is now emitted as a bit literal. Quoting it produced
DEFAULT 'b\'101\'', which MySQL rejects with "Invalid default value", so a bit
column carrying a default could not be applied at all — and the error read as
the author's mistake rather than as ours. MySQL reports a bit literal in its
minimal form independent of the column's width (b'0101' comes back as b'101',
bit(8) DEFAULT b'00000001' as b'1'), which is the form the parser restores, so
the recorded text is already canonical.
The keyword fold now covers every type that stores the keyword as exactly 1/0,
in the form that type reports it as: bare on the integer types, unscaled
decimal, double and float; quoted on varchar, char and varbinary; and as a bit
literal on bit. Setting the kind alongside the value is what makes the two
sides compare equal, since the literal form is part of column identity.
Types that put the keyword through a conversion of their own are still left
alone, each reading taken from a live server: scaled decimal pads to its scale
(decimal(4,2) DEFAULT TRUE stores '1.00'), year reads it as a year ('2001'),
binary pads to the column width with NULs ('1\0\0\0'), and on enum and set the
keyword names a member index rather than a value — enum('0','1') DEFAULT TRUE
stores '0', the member at index 1, and DEFAULT FALSE is rejected outright
because no member sits at index 0. The integration tests assert the leftover
diff alongside each reading, so an exclusion's cost is visible rather than
implied.
A hex literal remains unmodelled: MySQL never reports one back, converting it
to whatever the column's type stores (an integer column reports 0x1A as 26),
so converging it is a per-type conversion rather than a literal form.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…xture
SHOW CREATE TABLE quotes a numeric column's default the same way it quotes
a string column's — `int NOT NULL DEFAULT '1'` — verified on 8.0.28, 8.4.11
and 9.7.2. Four comments said MySQL reports the value bare on a numeric
column, contradicting both the assertions beneath them and the note in
columnsEqualWithContext, which had it right. What the recorded kind decides
is the form Spirit emits, so say that instead.
The bit-literal example in literal_roundtrip_test.go was not a stray smart
quote: gofmt rewrites a pair of single quotes in a doc comment into a
typographic closing quote. Move the escaped form into a code block, which
gofmt leaves alone, and note why.
Drop the enum column from the excluded-types integration fixture. MySQL 9.7
resolves a boolean keyword default on enum/set to a member value where 8.4
and earlier resolve it to a member index, so enum('0','1') DEFAULT TRUE
stores '1' on 9.7 and '0' before it. The fixture asserted the pre-9.7
reading with no version gate and failed the 9.7 job. There is no single
reading to record, and the exclusion itself is already pinned without a
server in TestBooleanKeywordDefaultLeavesOtherTypesAlone; the version split
is now documented on the normalizer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
f7c24de to
76c6b94
Compare
|
🤖 Comment from Armand's AI agent. Thanks — this was a good catch and the diagnosis is right. I reproduced it against Your table matches what I got, with one exception worth recording:
On dropping versus gating, I went with dropping for the reason you gave, plus one more: that the fold skips Confirmed the old fixture fails on 9.7 and the new one passes, and the full Separately, I fixed what Copilot flagged, because it was right and I had it backwards in four places: Thanks also for pinning the replica-job red to #1239. |
A column
DEFAULTwas reduced to restored text plus a single "was it quoted" flag, which loses distinctions the characters cannot carry.TRUE,1and'1'all restore to a numeric-looking string, andb'1'restores to text that reads like a quoted string but has to be emitted bare.The parser knows all of them apart — the boolean keyword carries
IsBooleanFlag, a bit literal is a binary literal — andValueExpr.Restoreconsults that knowledge to produce the text before it is dropped. So the fix is to record the literal form asColumn.DefaultKindwhile the AST is still in hand, and have emission, comparison and the keyword fold read it instead of re-deriving it from the value.Two things follow.
A bit literal default is now emitted as a bit literal. Quoting it produced
DEFAULT 'b\'101\'', which MySQL rejects withInvalid default value, so abitcolumn carrying a default could not be applied at all — and the error read as the author's mistake rather than ours. MySQL reports a bit literal in its minimal form independent of the column's width (b'0101'comes back asb'101',bit(8) DEFAULT b'00000001'asb'1'), which is the form the parser already restores, so the recorded text was canonical and only needed to stop being quoted. A hex literal default has the same shape of bug (int DEFAULT 0x1Aemitted'x\'1a\'') and is deliberately left for separate work — see below.The keyword fold now covers every type that stores the keyword as exactly 1/0, recording the literal form Spirit emits it in. Setting the kind alongside the value is what makes the two sides compare equal, since the literal form is part of column identity — folding the value alone would still diff on the form.
decimal,double,float'1'/'0'1/0varchar,char,varbinary'1'/'0''1'/'0'bitb'1'/b'0'b'1'/b'0'SHOW CREATE TABLEquotes the value on numeric and string columns alike — onlybitreports a literal of its own. The bare numeric emission still converges, because quotedness is not part of column identity on a numeric column (columnsEqualWithContext); on a string column it is, which is what the recorded kind is there for.Types that put the keyword through a conversion of their own are still left alone, each reading taken from a live server:
DEFAULT TRUEstoresdecimal'1.00'year'2001'binary(4)'1\0\0\0'varbinaryhas nothing to pad and does foldenum/set'0'before 9.7,'1'from 9.7enumandsetresolve the keyword differently across the supported matrix. Through 8.4 it resolves numerically, as a member index:enum('0','1') DEFAULT TRUEstores'0'(the member at index 1),enum('a','1') DEFAULT TRUEstores'a', andDEFAULT FALSEis rejected because no member sits at index 0. From 9.7 it resolves as the string'1'/'0'and is matched against the member list, so both of those columns store'1'andDEFAULT FALSEis accepted. The two readings disagree silently. Readings taken from the officialmysql:8.4.11andmysql:9.7.2images.That is why neither type appears in the excluded-types integration fixture: there is no stable reading to assert. That they are skipped is pinned without a server in
TestBooleanKeywordDefaultLeavesOtherTypesAlone.A hex literal stays unmodelled for the same class of reason: MySQL never reports one back, converting it to whatever the column's type stores (an integer column reports
0x1Aas26, avarbinarystores the raw byte), so converging it is a per-type conversion rather than a literal form.Breaking:
Column.DefaultIsStringis replaced byColumn.DefaultKind, so a caller testing quotedness comparesDefaultKind != DefaultKindStringinstead. Nothing in this repo outsidepkg/statementread the field.Tests cover the classification directly (including that a signed default such as
DEFAULT -1is a unary operator over a literal rather than a literal, and that an unmodelled form keeps the emission it had before kinds existed), and the integration tests assert the leftover diff alongside each excluded type's live reading, so an exclusion's cost is visible in the test rather than implied. Run against MySQL 8.4.11 and 9.7.2 locally, in addition to CI.The repository template asks that a PR be associated with an Issue; the behavior and the boundary are written up here so the discussion can happen in one place, and I am glad to open one if you would prefer it tracked separately.
🤖 Generated with Claude Code