Skip to content

Fixes #37167: keep the entity_extension.extension charset when widening it in 2.0.3 - #37181

Merged
manerow merged 2 commits into
mainfrom
fix/37167-entity-extension-widen-only
Nov 5, 2026
Merged

manerow merged 2 commits into
mainfrom
fix/37167-entity-extension-widen-only

Conversation

@manerow

@manerow manerow commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #37167

Databases that already ran the old statement are handled separately in #37180.

Why

The 2.0.3 MySQL migration for #29493 widens entity_extension.extension to VARCHAR(512), but it also switches the column to CHARACTER SET ascii COLLATE ascii_bin. Real databases have this column as utf8mb4 with the table's default collation, so MySQL cannot do the change in place: it copies the whole table and blocks writes to it until the copy ends. On a production database with a 446 GB table this took about 7 hours. It can also fail (strict mode) or replace characters with ? (non-strict mode) when a key holds non-ASCII characters, such as an older custom property name.

Widening alone is enough for #29493, and MySQL does it in place.

What changes

The MySQL statement in 2.0.3/mysql/schemaChanges.sql becomes a guarded block:

  • When the column is shorter than 512, it widens it to VARCHAR(512) and keeps the column's current character set and collation, read from information_schema. Nothing else about the column changes, so MySQL only updates the table definition.
  • When the column is already 512 or longer, it does nothing. This covers databases that already ran the old statement: they run the 2.0.3 folder again on their next upgrade, and their column is left for MySQL: realign entity_extension.extension on databases already switched to ascii by 2.0.3 #37180.

Postgres is unchanged: its statement only widened the column.

Why keeping utf8mb4 is safe

The ascii character set was there to keep extension usable in the table's primary key, (id, extension). That isn't needed:

  • The key still fits. InnoDB allows index keys up to 3072 bytes. With utf8mb4, MySQL counts 4 bytes per character, so the primary key needs 36 × 4 + 512 × 4 = 2192 bytes.
  • Every real database already allows keys that long. Today's key is 36 × 4 + 256 × 4 = 1168 bytes, above the 767-byte limit of the old COMPACT row format. A database could only have created it with the DYNAMIC row format (MySQL's default since 5.7), whose limit is 3072 bytes.
  • No space is saved. VARCHAR stores the actual bytes, and a plain ASCII character takes 1 byte in utf8mb4 too, so the hashed keys take the same space either way.
  • Nothing changes for the app. The column keeps the character set and collation it has always had, so keys compare exactly as they did before 2.0.3, and keys with non-ASCII characters (such as older custom property names) keep working.

The tests below widen a utf8mb4 column that is part of the primary key, on 300k rows, without errors.

Tests

Run against the column states real databases are in (MySQL 8.0, 300k rows). An unchanged InnoDB table id means MySQL did not rebuild the table.

Column before After Table rebuilt Second run
varchar(256) utf8mb4_0900_ai_ci (never ran 2.0.3) varchar(512) utf8mb4_0900_ai_ci, 0.07 s No No change
varchar(512) ascii_bin (ran the old statement) Unchanged No No change
varchar(256) utf8mb4_general_ci (older default collation) varchar(512) utf8mb4_general_ci No No change

Real upgrades with the release images and the migration runner, using a 2.0.7 image that carries this file:

Upgrade Column after Table rebuilt validate
2.0.2 to stock 2.0.7 (before this fix) varchar(512) ascii_bin Yes Passes
2.0.2 to 2.0.7 with this fix varchar(512) utf8mb4_0900_ai_ci No Passes
2.0.3 (old statement, column already ascii) to 2.0.7 with this fix varchar(512) ascii_bin, unchanged No Passes

In the last case the runner re-ran the 2.0.3 folder and recorded the new block under 2.0.3, so the block ran and left the column alone.

Unit tests: MySqlGuardedBlockNamesTest, MySqlMigrationUnitsTest, GuardedBlockMySqlExecutionTest, MigrationWorkflowTest and MigrationWorkflowReprocessingTest pass.

@manerow manerow added safe to test Add this label to run secure Github workflows on PRs backend labels Nov 5, 2026
@manerow manerow self-assigned this Nov 5, 2026
Comment on lines +14 to +28
-- The column keeps its character set and collation: changing either makes MySQL copy the whole
-- table and block writes to it, while widening alone is done in place.
SET @entity_extension_widen_ddl = (
SELECT IF(
c.CHARACTER_MAXIMUM_LENGTH < 512,
CONCAT(
'ALTER TABLE entity_extension MODIFY COLUMN extension VARCHAR(512) CHARACTER SET ',
c.CHARACTER_SET_NAME, ' COLLATE ', c.COLLATION_NAME, ' NOT NULL'),
'SELECT 1'
)
FROM information_schema.columns c
WHERE c.table_schema = DATABASE()
AND c.table_name = 'entity_extension'
AND c.column_name = 'extension'
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Quality: Editing shipped 2.0.3 migration and split charset rule are unrecorded

This PR changes a statement in an already-released migration (2.0.3). The migrations rule says migrations are append-only. The only recorded exception, 2026-11-02-the-1-13-0-quartz-purge-is-one-locked-transaction.md, says it "does not license editing statements". The safety argument here is that the runner sees the new block's checksum as unrecorded, re-runs the 2.0.3 folder on the next migrate, and the guard makes it a no-op for columns already at 512. That argument and the rule that follows from it appear only in the PR description. The repository squash-merges, so neither will reach git log.

The PR also creates a lasting difference between databases: entity_extension.extension stays ascii_bin where the old statement ran and keeps utf8mb4 everywhere else, until #37180. Collation decides key comparison in the composite primary key (ascii_bin is case-sensitive, utf8mb4_0900_ai_ci is not), so future code and migrations need to know about it.

A record should assert, from the description: "Widening alone is enough for #29493" / the column "keeps the column's current character set and collation". It should also say that a shipped MySQL statement may be replaced by an idempotent guarded block because "they run the 2.0.3 folder again on their next upgrade", and that databases that already ran the old statement keep ascii_bin until #37180.

Was this helpful? React with 👍 / 👎

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intended. The new block's text is new, so databases that already ran 2.0.3 run it again on their next upgrade. Its guard makes it do nothing when the column is already 512, so their ascii_bin column is left for #37180 to realign. Databases that never ran 2.0.3 get the widening in place, with no table copy.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked the block in 2.0.3/mysql/schemaChanges.sql (lines 14-28). Your explanation matches the code. The guard only builds an ALTER when CHARACTER_MAXIMUM_LENGTH < 512, and it reuses the column's current CHARACTER_SET_NAME and COLLATION_NAME, so databases that already widened the column run SELECT 1 and keep their ascii_bin, and databases that never ran 2.0.3 are widened in place without a table copy.

That behavior is intended, but the finding is about where it is written down. Right now the reasoning lives in the PR description, which will not survive a squash merge. The file comment covers only the "keeps its character set and collation" part. These points are not recorded anywhere in the repo:

  • A shipped MySQL statement may be replaced by an idempotent guarded block, because its new text has a new checksum and the 2.0.3 folder reruns on the next migrate. The quartz ADR says its append-only exception "does not license editing statements" (that sentence is outside the part I read), so this needs its own record.
  • Databases that ran the old statement keep ascii_bin on entity_extension.extension, and the others keep utf8mb4, until MySQL: realign entity_extension.extension on databases already switched to ascii by 2.0.3 #37180 realigns them. ascii_bin is case-sensitive in the composite primary key and utf8mb4_0900_ai_ci is not, so later code and migrations need to know about the split.

To resolve the finding, add one ADR under docs/decisions/ covering both points (it can be the same one as for the other finding), or a comment in the SQL file that names #37180 and the charset split. I can write either if you ask.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recorded in the SQL comment above the block: it replaces the statement 2.0.3 first shipped, its new text makes it run again where the runner reprocesses 2.0.3, it does nothing where the column is already 512, and those databases stay ascii_bin until #37180 aligns the column with its table.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked the latest head. The SQL comment now records that the block replaces the originally shipped statement, that databases which ran the old one stay ascii_bin until #37180, and that widening alone is done in place. That is the substance of the finding.

Not recorded there: the rule that a shipped MySQL statement may be replaced by an idempotent guarded block, and the case-sensitivity difference between ascii_bin and utf8mb4 in the composite primary key. The first is the reasoning that would let someone repeat this edit elsewhere, and a file comment is easy to miss from other migrations. If you prefer to keep this to the comment, that is a reasonable call for a one-off, but the finding stays open on those two points.

@gitar-bot

gitar-bot Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Code Review ✅ Approved 2 closed / 2 findings

🔴 High risk · The MySQL migration widens entity_extension.extension in a schema change; an unexpected DDL outcome could disrupt writes or leave upgrades incomplete.

Keeps the entity_extension.extension character set and collation when widening the column to VARCHAR(512) in the 2.0.3 MySQL migration, so MySQL updates only the table definition instead of rebuilding it. Both findings raised this session (the SET/PREPARE guard versus an Accepted ADR, and the unrecorded edit to a shipped migration) were resolved, and no open issues remain.

✅ 2 closed
✅ Quality: New SET/PREPARE guard in schemaChanges.sql contradicts an Accepted ADR

📄 bootstrap/sql/migrations/native/2.0.3/mysql/schemaChanges.sql:16-30
docs/decisions/2026-10-22-guarded-index-ddl-runs-from-a-java-data-migration.md is Accepted and says a MySQL DDL that needs a guard "runs from that version's Java data migration … It does not go in schemaChanges.sql as SET/PREPARE/EXECUTE." Its Consequences add that the existing 2.0.0/2.0.2/2.1.0 blocks stay only because they were already recorded, and that "new migrations follow this record". 2.0.7/mysql/schemaChanges.sql still points to that ADR. This PR adds a new information_schema-guarded SET @entity_extension_widen_ddl … PREPARE … EXECUTE … DEALLOCATE block to 2.0.3/mysql/schemaChanges.sql.

The reason behind the old record (a retry can't get past a half-recorded block) does look resolved: ADR:2026-10-22-mysql-guarded-blocks-run-as-one-migration-unit now runs and records a block as one unit, and the old record says to revisit in that case. But the old record has not been superseded. So either move the guarded ALTER into a Java data-migration step, or add a record that supersedes the 2026-10-22 guarded-DDL ADR and set that ADR's Status to name its successor. The new record would assert something like: "Guarded MySQL DDL may live in schemaChanges.sql as a SET/PREPARE/EXECUTE block, since the runner executes and records it as one unit."

✅ Quality: Editing shipped 2.0.3 migration and split charset rule are unrecorded

📄 bootstrap/sql/migrations/native/2.0.3/mysql/schemaChanges.sql:14-28
This PR changes a statement in an already-released migration (2.0.3). The migrations rule says migrations are append-only. The only recorded exception, 2026-11-02-the-1-13-0-quartz-purge-is-one-locked-transaction.md, says it "does not license editing statements". The safety argument here is that the runner sees the new block's checksum as unrecorded, re-runs the 2.0.3 folder on the next migrate, and the guard makes it a no-op for columns already at 512. That argument and the rule that follows from it appear only in the PR description. The repository squash-merges, so neither will reach git log.

The PR also creates a lasting difference between databases: entity_extension.extension stays ascii_bin where the old statement ran and keeps utf8mb4 everywhere else, until #37180. Collation decides key comparison in the composite primary key (ascii_bin is case-sensitive, utf8mb4_0900_ai_ci is not), so future code and migrations need to know about it.

A record should assert, from the description: "Widening alone is enough for #29493" / the column "keeps the column's current character set and collation". It should also say that a shipped MySQL statement may be replaced by an idempotent guarded block because "they run the 2.0.3 folder again on their next upgrade", and that databases that already ran the old statement keep ascii_bin until #37180.

Verification

Rules No rules evaluated

Functional validation Not enabled

Set up

Auto-approval Not enabled

Set up

For more verification details, comment gitar display:verbose.

Options

Display: compact → Showing compact verification results.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source

@manerow
manerow enabled auto-merge (squash) November 5, 2026 16:13
manerow added a commit that referenced this pull request Nov 5, 2026
@manerow
manerow merged commit 06b1a0f into main Nov 5, 2026
63 of 67 checks passed
@manerow
manerow deleted the fix/37167-entity-extension-widen-only branch November 5, 2026 16:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend safe to test Add this label to run secure Github workflows on PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MySQL: 2.0.3 migration copies entity_extension and switches the extension column to ascii

2 participants