MDEV-40417 Fix default value for compressed columns - #5484
Conversation
…LT '' Compression is remembered by storing Field::TMYSQL_COMPRESSED in Column_definition::unireg_check, but has_default_function() treated every unireg_check other than Field::NONE as "this column has a default function". A COMPRESSED NOT NULL column without an explicit DEFAULT therefore never got NO_DEFAULT_VALUE_FLAG, neither in mysql_prepare_create_table() nor in Column_definition::check(). As FIELDFLAG_NO_DEFAULT was not set either, the missing flag was written into the FRM and the column silently became optional: CREATE TABLE t (c LONGTEXT COMPRESSED NOT NULL) ENGINE=InnoDB; INSERT INTO t () VALUES (); -- succeeded SHOW CREATE TABLE also displayed a phantom DEFAULT '' for such a column. Exclude TMYSQL_COMPRESSED in has_default_function(), and use that method in Column_definition::check() instead of the open-coded unireg_check comparison, so that both places which set NO_DEFAULT_VALUE_FLAG cannot drift apart again. Note that tables created before this fix keep the wrong pack_flag in their FRM until they are rebuilt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…LT '' Repair pre-existing tables when their FRM is read. The previous commit fixed the write path only, so a table created before it keeps a pack_flag without FIELDFLAG_NO_DEFAULT, and a COMPRESSED NOT NULL column of such a table still behaves as if it had DEFAULT '': 10.11.19 > CREATE TABLE t (c LONGTEXT COMPRESSED NOT NULL); 10.11.19+ > INSERT INTO t (other_column) VALUES (1); -- still succeeded Restore the flag in TABLE_SHARE::init_from_binary_frm_image() for blob columns. An explicit DEFAULT of a blob column is always stored in the FRM as an expression, see Column_definition::has_default_expression(), so a COMPRESSED NOT NULL blob that has no default_value provably had no DEFAULT clause. The check is a no-op for FRMs written after the fix, where the flag is present already, so no version condition is needed. VARCHAR and VARBINARY are deliberately not repaired. A constant DEFAULT of a non-blob column is stored in the default record, exactly like the wrong implicit default, which makes the FRMs of c VARCHAR(100) COMPRESSED NOT NULL c VARCHAR(100) COMPRESSED NOT NULL DEFAULT '' byte for byte identical. Repairing them would be a guess, and a wrong guess would start rejecting INSERTs against tables whose DEFAULT '' was intentional. Such columns can still be corrected explicitly with ALTER TABLE ... MODIFY. std_data/MDEV-40417.* is a MyISAM table created by 10.11.19 before the fix, used by the new test to cover reading an old FRM. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gkodinov
left a comment
There was a problem hiding this comment.
Thank you for your contribution! This is a preliminary review.
Please merge the two commits into one.
Also, please add a design description of the fix:
- approach rationale and summary
- detailed design notes
- what functionality is changed
- what is supposed to work
- what is supposed to fail etc.
I also do not quite subscribe to the premise of the fix: I believe that whether a column has a default or not should not depend on other column attributes. Thus, to me it's weird to always require having a default for compressed columns.
I would consider fixing the bug differently: I'd make the DEFAULT clause independent from the COMPRESSED attribute. And then offer upgrade advice for tables that are binary and have a default value, but do not have the flag on in FRM.
Please at least explain why you've taken the approach you did.
|
I think there is a misunderstanding. autoincrement, and default timestamp functions are stored in MDEV-40417 describes the issue which this PR resolves. |
There was a problem hiding this comment.
Pull request overview
This PR fixes MDEV-40417 where COMPRESSED NOT NULL columns could incorrectly appear to have an implicit DEFAULT '' due to TMYSQL_COMPRESSED being stored in unireg_check and mistakenly treated as a “default function”. It also adds an FRM-load repair path for affected legacy tables (limited to BLOB/TEXT where the FRM can distinguish explicit defaults).
Changes:
- Treat
Field::TMYSQL_COMPRESSEDas not being a default function when determining whether aNOT NULLcolumn lacks a default. - Repair legacy FRMs on load by setting
NO_DEFAULT_VALUE_FLAGforCOMPRESSED NOT NULLBLOB/TEXT columns that provably had no explicitDEFAULT. - Add/extend
mysql-testcoverage, including an upgrade scenario using a pre-fix.frm.
Reviewed changes
Copilot reviewed 5 out of 8 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| sql/table.cc | Adds FRM-load repair for legacy compressed BLOB/TEXT columns missing the no-default flag. |
| sql/field.h | Updates has_default_function() to ignore TMYSQL_COMPRESSED. |
| sql/field.cc | Uses has_default_function() in default/NOT NULL validation to avoid treating compression as a default. |
| mysql-test/main/column_compression.test | Adds regression + upgrade tests for MDEV-40417 behavior. |
| mysql-test/main/column_compression.result | Updates expected outputs for the new/changed test cases. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
gkodinov
left a comment
There was a problem hiding this comment.
Indeed. I was a bit too hasty to speak before throughly understanding the change. Sorry for that. And thank you for your patience.
I've taken the time to study this now to the best of my abilities.
I believe that indeed the changes to field.[cc|h] are OK.
But I have an issue with the change in table.cc: I do not think it's a good idea to keep "garbled" .frms on disk that we can repair and silently just ignore the broken parts at load. I'd do it as follows: if there's a controversy in how the FRM is (as in: it's garbled), I'd report it in the error log and optionally mark the table as crashed.
IMHO the actual repair should happen in e.g. ALTER TABLE ... FORCE and be done once and not on every load. This said, I do not mind the workaround you did completely, but it needs a more permanent ways to fix this situation in the FRM itself IMHO.
However this is all stuff for the final reviewer to decide on it seems.
Hence please just focus on making up your mind on how exactly do you think this should be fixed and merging the two commits to match that.
When the FRM of a table created before the MDEV-40417 fix is read and a COMPRESSED NOT NULL blob column is repaired, say so in the error log: Found COMPRESSED NOT NULL field 'blob_nodef' in `test`.`mdev40417` with a wrong implicit DEFAULT '' (MDEV-40417); the implicit default is ignored. Please do "ALTER TABLE `mdev40417` FORCE" to fix the .frm file! This follows the old DECIMAL 5.0.3 -> 5.0.4 conversion a few lines above, which reports the affected column and asks for ALTER TABLE ... FORCE too. The warning is emitted only when FIELDFLAG_NO_DEFAULT was really missing from the FRM, so tables written after the fix stay silent. Nothing is reported for VARCHAR and VARBINARY: those cannot be recognized as affected at all, as an old FRM of a column with an explicit DEFAULT '' is identical to one with the wrong implicit default. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thank, you. I added a warning to the log when a column with buggy implicit default is encountered. Currently the table is silently patched on every load. Which means some inserts that worked with a previous version may now produce an error when they are not explicitly set in the insert statement. Also it may be advised to mention in the upgrade notes that you need to manually update any compressed varchar / varbinary columns that should not have a default value. This query shows all columns that may have an unwanted default, not automatically detectable by MariaDB. SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, COLUMN_TYPE
FROM information_schema.COLUMNS
WHERE COLUMN_TYPE LIKE 'var%COMPRESSED%'
AND IS_NULLABLE = 'NO'
AND COLUMN_DEFAULT = ''''''
AND TABLE_SCHEMA NOT IN ('mysql','information_schema','performance_schema','sys')
ORDER BY TABLE_SCHEMA, TABLE_NAME, ORDINAL_POSITION;As a side effect this query should cause MariaDB to log all affected tables which can be fixed with grep 'implicit DEFAULT' error.log | sort -u |
The embedded server writes the warning to stderr, not to the error log, so searching mysqld.1.err found nothing there. As include/not_embedded.inc skips the whole test file, the check gets a file of its own instead of skipping all of main.column_compression. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Alternatives and recommendation as per Claude: Route 1:patch the bytes in place — possible, and there's precedentEverything needed is already at hand. TABLE_SHARE::write_frm_image() (table.cc:3630) exists, and init_from_binary_frm_image() itself already writes the image back when its write parameter is true (table.cc:1829) — that's how CREATE TABLE persists an FRM. The normal open path passes write=false (table.cc:737). Even the surgical variant has precedent: update_frm_version() (handler.cc:4975) does a 4-byte pwrite at offset 51 to stamp the version. Ours would be one bit — FIELDFLAG_NO_DEFAULT in the field's 2-byte pack_flag. But doing it from inside init_from_binary_frm_image() would be wrong, because that function runs on a plain SELECT under a shared MDL. It would mean writing to the schema during a read, and it breaks in every context where that isn't allowed: --read-only, replicas, read-only filesystems, a mariadb-backup snapshot. There's also no replication of the change (Galera nodes would silently diverge), and a torn write leaves an unopenable table. Note that update_frm_version() avoids all of this by running only from mysql_admin_table after a successful CHECK TABLE — under an admin statement's locking, not a reader's — and it deliberately refuses when keep_original_mysql_version is set (handler.cc:4993-4995). Route 2:mark it as needing upgrade — the idiomatic oneCHECK TABLE ... FOR UPGRADE calls handler::ha_check_for_upgrade() ([handler.cc:4911](vscode-webview://0hi93marjqtcegb1kl26eoi6khc7dqabi28vb3afi1j0b3k5528d/sql/handler.cc#L4911)), which is already a composition of exactly this kind of check, each returning HA_ADMIN_NEEDS_ALTER: old types, pre-5.0.3 varchar, collation compatibility, and — the direct analogue — check_long_hash_compatibility() ([handler.cc:4880](vscode-webview://0hi93marjqtcegb1kl26eoi6khc7dqabi28vb3afi1j0b3k5528d/sql/handler.cc#L4880)). That last one is MDEV-27653: a wrong value written by an older server that only a rebuild can fix, and its comment even explains choosing HA_ADMIN_NEEDS_ALTER over NEEDS_UPGRADE so the user gets ER_TABLE_NEEDS_REBUILD. Same situation, same shape.Adding a check_compressed_default_compatibility() alongside it means mariadb-check --check-upgrade --all-databases — i.e. what mariadb-upgrade runs, and what operators already run after upgrading — issues the ALTER TABLE ... FORCE itself. The FRM is then rewritten through the normal path: correct locking, crash-safe, replicated, version bookkeeping handled. That is "automatic" in the way that matters, and it also solves the completeness problem from your last question, since --all-databases opens everything rather than waiting for someone to touch each table. One wrinkle: after the in-memory repair the field is indistinguishable from a healthy one, so the check needs a breadcrumb — a bool on TABLE_SHARE set where the repair happens. I'd add a dedicated one rather than reuse share->incompatible_version, because that field is a mask of HA_CREATE_USED_* bits with a heavy side effect: it makes open_table_from_share() fail outright with OPEN_FRM_NEEDS_REBUILD (table.cc:4221-4227) for anything but ALTER/REPAIR/FLUSH. That would turn a working table into an unusable one until upgraded — far too aggressive for a phantom default, and there's no sensible HA_CREATE_USED_* bit for a column default anyway. Route 3:current state — warn and let the DBA decideWhere we are now. But possibly no automatic patching of loaded tables if that seems too risky?Recommendation:route 2. It's ~15 lines plus a share flag, needs no new tooling, and it's the mechanism MariaDB already uses for "an old server wrote something wrong; only a rebuild fixes it". Route 1 I'd argue against regardless of effort. Worth noting it only covers the blob half — the varchar half stays a review list either way, since nothing on disk marks those as affected. |
Column compression is stored in unireg_check and has to be ignored when checking for a default function.
For VARCHAR / VARBINARY there is no way to know whether an empty string default was explicitly set or is the result of this bug.
For TEXT / BINARY this can be automatically repaired when loading the table (second commit here),
but an
ALTER TABLE ... FORCEwould also fix it.MDEV-40417