Stop migrate re-declaring keys and chasing integer widths - #17
Conversation
syncColumns could not run at all against MySQL 8. Two defects, both in the column comparison. A primary key column whose type had drifted was emitted as `MODIFY COLUMN id BIGINT AUTO_INCREMENT PRIMARY KEY`. The key already exists, so the server rejects the statement with "Multiple primary key defined" and the whole migration aborts. Keys belong to the table's index definitions, so the key clause is now dropped on MODIFY and kept on ADD, where there is no existing key to collide with. Since 8.0.17 MySQL discards display widths on integer types, so a schema declaring INT(11) reads back as plain `int`. The comparison treated that as a difference, which marked nearly every integer column in the database as changed. Against one real schema this meant rewriting 43 tables that needed nothing; after the fix, 5, all of them genuine drift. The comparison also used str_contains, so `bigint` contains `int` and a real widening from int to bigint was read as already satisfied and skipped. It now compares normalized types for equality, which catches the widening while still ignoring the discarded width. Verified against a live MySQL 8 database: the widening applied, the primary keys survived, a second run is a no-op, and nothing remains out of sync.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e43468b030
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // MySQL reject the whole statement. | ||
| if ($this->needsColumnModification($currentColumns[$columnName], $newColumn)) { | ||
| $queries[] = "MODIFY COLUMN " . $this->convertColumnToSql($newColumn); | ||
| $queries[] = "MODIFY COLUMN " . $this->convertColumnToSql($newColumn, false); |
There was a problem hiding this comment.
Preserve newly declared keys on existing columns
When an existing non-key column is changed to a primary or unique key while its type also changes, always passing false strips the newly requested key clause. For example, migrating an existing INT id to the BIGINT PRIMARY KEY produced by PrimaryKeyFactory changes the type but never creates the primary key; later runs see the matching type and never retry the constraint. Only suppress a key that is confirmed to exist, or emit the key change separately.
Useful? React with 👍 / 👎.
| $currentType = $this->normalizeType((string) $currentColumnData['COLUMN_TYPE']); | ||
| $newType = $this->normalizeType($this->declaredType($newColumn)); |
There was a problem hiding this comment.
Normalize type modifiers before exact comparison
For schema columns with modifiers such as UNSIGNED, these operands use different representations: COLUMN_TYPE is bigint unsigned, while declaredType() returns only bigint because UNSIGNED lives in the column attributes. The exact comparison therefore emits the same MODIFY COLUMN ... BIGINT UNSIGNED on every synchronization even though the database is already correct; server-expanded defaults such as DECIMAL becoming decimal(10,0) have the same problem. Compare parsed base types and modifiers/defaults, or construct both normalized values from equivalent inputs.
Useful? React with 👍 / 👎.
Why
syncColumnscould not complete against MySQL 8 at all. Any project calling it gotMultiple primary key definedand the migration aborted, which means no column could be added to any existing table on a deployed environment.Found while trying to add a column in Navigator. I had started designing around the failure before realising the tool was the thing that needed fixing.
The three defects
1. The key clause was re-declared on MODIFY. A primary key column whose type had drifted produced:
The key already exists, so the server rejects the whole statement. Keys are owned by the table's index definitions, not by a column modification. The clause is now stripped on
MODIFYand kept onADD, where there is no existing key to collide with.AUTO_INCREMENTstays in both, since that genuinely is a column property.2. Integer display widths were treated as differences. MySQL discards them as of 8.0.17, so a schema declaring
INT(11)reads back as plainint. Every integer column therefore looked permanently out of date. Against one real schema,migratewanted to rewrite 43 tables that needed nothing. After the fix: 5, all genuine drift.3. The comparison used
str_contains.bigintcontainsint, so a real widening frominttoBIGINTwas read as already satisfied and silently skipped. It now compares normalized types for equality, which catches the widening while still ignoring the discarded width.Defects 2 and 3 are the same mistake in both directions: a substring test standing in for a type comparison.
Evidence
Nine new unit tests, each pinning one behaviour. Four of them failed before this change:
Full suite:
OK (12 tests, 31 assertions).Verified against a live MySQL 8 database rather than only in mocks:
migratecompletes where it previously aborted.idcolumns actually widened frominttobigint.SHOW KEYSconfirms the primary keys survived.Note on the last test
test_a_table_in_sync_produces_no_queryis the one that matters most for anyone operating this. Before the change, a fully in-sync table still emitted anALTER, somigratewas never safe to run casually against production.🤖 Generated with Claude Code