From 3a29ac21c39036fa8a732e8af071fe4051d065ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kr=C3=B6g?= Date: Fri, 31 Jul 2026 10:57:13 +0200 Subject: [PATCH 1/2] MDEV-40417 A column with compression enabled automatically adds DEFAULT '' 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) --- mysql-test/main/column_compression.result | 54 ++++++++++++++++++++++- mysql-test/main/column_compression.test | 34 ++++++++++++++ sql/field.cc | 2 +- sql/field.h | 7 ++- 4 files changed, 94 insertions(+), 3 deletions(-) diff --git a/mysql-test/main/column_compression.result b/mysql-test/main/column_compression.result index 5b35d70a24d92..26d72fac41f49 100644 --- a/mysql-test/main/column_compression.result +++ b/mysql-test/main/column_compression.result @@ -1323,7 +1323,7 @@ LENGTH(a) SHOW CREATE TABLE t1; Table Create Table t1 CREATE TABLE `t1` ( - `a` blob /*M!100301 COMPRESSED*/ NOT NULL DEFAULT '' + `a` blob /*M!100301 COMPRESSED*/ NOT NULL ) ENGINE=CSV DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" DROP TABLE t1; @@ -3051,3 +3051,55 @@ REPLACE INTO t VALUES ('abcdefghijklm'); UPDATE t SET c=MID(c,2,4); DROP TABLE t; # End of 10.6 tests +# +# MDEV-40417 A column with compression enabled automatically adds DEFAULT '' +# +CREATE TABLE t ( +c1 LONGTEXT COMPRESSED NOT NULL, +nc1 LONGTEXT NOT NULL, +c2 LONGTEXT COMPRESSED NOT NULL DEFAULT '', +nc2 LONGTEXT NOT NULL DEFAULT '', +c3 VARCHAR(100) COMPRESSED NOT NULL, +nc3 VARCHAR(100) NOT NULL +) ENGINE=InnoDB; +SHOW CREATE TABLE t; +Table Create Table +t CREATE TABLE `t` ( + `c1` longtext /*M!100301 COMPRESSED*/ NOT NULL, + `nc1` longtext NOT NULL, + `c2` longtext /*M!100301 COMPRESSED*/ NOT NULL DEFAULT '', + `nc2` longtext NOT NULL DEFAULT '', + `c3` varchar(100) /*M!100301 COMPRESSED*/ NOT NULL, + `nc3` varchar(100) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci +SELECT COLUMN_NAME, COLUMN_DEFAULT FROM INFORMATION_SCHEMA.COLUMNS +WHERE TABLE_SCHEMA='test' AND TABLE_NAME='t' ORDER BY ORDINAL_POSITION; +COLUMN_NAME COLUMN_DEFAULT +c1 NULL +nc1 NULL +c2 '' +nc2 '' +c3 NULL +nc3 NULL +# A compressed column must be as mandatory as an uncompressed one +INSERT INTO t (nc1) VALUES ('x'); +ERROR HY000: Field 'c1' doesn't have a default value +INSERT INTO t (c1, nc1, nc3) VALUES ('x', 'x', 'x'); +ERROR HY000: Field 'c3' doesn't have a default value +INSERT INTO t (c1, nc1, c3, nc3) VALUES ('x', 'x', 'x', 'x'); +SELECT * FROM t; +c1 nc1 c2 nc2 c3 nc3 +x x x x +DROP TABLE t; +# ALTER TABLE must not add an implicit default either +CREATE TABLE t (c LONGTEXT COMPRESSED NOT NULL DEFAULT 'x') ENGINE=InnoDB; +ALTER TABLE t MODIFY c LONGTEXT COMPRESSED NOT NULL; +ALTER TABLE t ADD c2 VARCHAR(100) COMPRESSED NOT NULL; +SHOW CREATE TABLE t; +Table Create Table +t CREATE TABLE `t` ( + `c` longtext /*M!100301 COMPRESSED*/ NOT NULL, + `c2` varchar(100) /*M!100301 COMPRESSED*/ NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci +DROP TABLE t; +# End of 10.11 tests diff --git a/mysql-test/main/column_compression.test b/mysql-test/main/column_compression.test index 2aba8862c6679..e45f6dddef9c0 100644 --- a/mysql-test/main/column_compression.test +++ b/mysql-test/main/column_compression.test @@ -594,3 +594,37 @@ UPDATE t SET c=MID(c,2,4); DROP TABLE t; --echo # End of 10.6 tests + +--echo # +--echo # MDEV-40417 A column with compression enabled automatically adds DEFAULT '' +--echo # + +CREATE TABLE t ( + c1 LONGTEXT COMPRESSED NOT NULL, + nc1 LONGTEXT NOT NULL, + c2 LONGTEXT COMPRESSED NOT NULL DEFAULT '', + nc2 LONGTEXT NOT NULL DEFAULT '', + c3 VARCHAR(100) COMPRESSED NOT NULL, + nc3 VARCHAR(100) NOT NULL +) ENGINE=InnoDB; +SHOW CREATE TABLE t; +SELECT COLUMN_NAME, COLUMN_DEFAULT FROM INFORMATION_SCHEMA.COLUMNS +WHERE TABLE_SCHEMA='test' AND TABLE_NAME='t' ORDER BY ORDINAL_POSITION; + +--echo # A compressed column must be as mandatory as an uncompressed one +--error ER_NO_DEFAULT_FOR_FIELD +INSERT INTO t (nc1) VALUES ('x'); +--error ER_NO_DEFAULT_FOR_FIELD +INSERT INTO t (c1, nc1, nc3) VALUES ('x', 'x', 'x'); +INSERT INTO t (c1, nc1, c3, nc3) VALUES ('x', 'x', 'x', 'x'); +SELECT * FROM t; +DROP TABLE t; + +--echo # ALTER TABLE must not add an implicit default either +CREATE TABLE t (c LONGTEXT COMPRESSED NOT NULL DEFAULT 'x') ENGINE=InnoDB; +ALTER TABLE t MODIFY c LONGTEXT COMPRESSED NOT NULL; +ALTER TABLE t ADD c2 VARCHAR(100) COMPRESSED NOT NULL; +SHOW CREATE TABLE t; +DROP TABLE t; + +--echo # End of 10.11 tests diff --git a/sql/field.cc b/sql/field.cc index 57a08128e6989..4ccfc0668082c 100644 --- a/sql/field.cc +++ b/sql/field.cc @@ -10937,7 +10937,7 @@ bool Column_definition::check(THD *thd) We need to do this check here and in mysql_create_prepare_table() as sp_head::fill_field_definition() calls this function. */ - if (!default_value && unireg_check == Field::NONE && (flags & NOT_NULL_FLAG)) + if (!default_value && !has_default_function() && (flags & NOT_NULL_FLAG)) { /* TIMESTAMP columns get implicit DEFAULT value when diff --git a/sql/field.h b/sql/field.h index c31b38d78e623..58bac6623cf9f 100644 --- a/sql/field.h +++ b/sql/field.h @@ -5486,7 +5486,12 @@ class Column_definition: public Sql_alloc, bool has_default_function() const { - return unireg_check != Field::NONE; + /* + TMYSQL_COMPRESSED is not a default function, it is stored in + unireg_check only to remember that the column is COMPRESSED. + */ + return unireg_check != Field::NONE && + unireg_check != Field::TMYSQL_COMPRESSED; } Field *make_field(TABLE_SHARE *share, MEM_ROOT *mem_root, From 80750f509364eaad77da2834e6162ee7bd0bf28c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kr=C3=B6g?= Date: Mon, 3 Aug 2026 14:30:25 +0200 Subject: [PATCH 2/2] MDEV-40417 A column with compression enabled automatically adds DEFAULT '' 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) --- mysql-test/main/column_compression.result | 70 ++++++++++++++++++++++ mysql-test/main/column_compression.test | 48 +++++++++++++++ mysql-test/std_data/MDEV-40417.MYD | Bin 0 -> 24 bytes mysql-test/std_data/MDEV-40417.MYI | Bin 0 -> 1024 bytes mysql-test/std_data/MDEV-40417.frm | Bin 0 -> 840 bytes sql/table.cc | 26 ++++++++ 6 files changed, 144 insertions(+) create mode 100644 mysql-test/std_data/MDEV-40417.MYD create mode 100644 mysql-test/std_data/MDEV-40417.MYI create mode 100644 mysql-test/std_data/MDEV-40417.frm diff --git a/mysql-test/main/column_compression.result b/mysql-test/main/column_compression.result index 26d72fac41f49..dc7342d600d29 100644 --- a/mysql-test/main/column_compression.result +++ b/mysql-test/main/column_compression.result @@ -3102,4 +3102,74 @@ t CREATE TABLE `t` ( `c2` varchar(100) /*M!100301 COMPRESSED*/ NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci DROP TABLE t; +# +# MDEV-40417 upgrade: a table created before the fix is repaired when +# its FRM is read. Only blobs can be repaired - for VARCHAR the FRM +# does not tell an explicit DEFAULT '' from the wrong implicit one, +# so those columns are deliberately left alone. +# +# MDEV-40417.frm was created by 10.11.19 as: +# CREATE TABLE mdev40417 ( +# blob_nodef LONGTEXT COMPRESSED NOT NULL, +# blob_def LONGTEXT COMPRESSED NOT NULL DEFAULT 'x', +# vc_nodef VARCHAR(100) COMPRESSED NOT NULL, +# vc_def VARCHAR(100) COMPRESSED NOT NULL DEFAULT '', +# plain_nodef LONGTEXT NOT NULL, +# pad INT) ENGINE=MyISAM CHARSET=latin1; +# INSERT INTO mdev40417 (plain_nodef, pad) VALUES ('p', 1); +# +SHOW CREATE TABLE mdev40417; +Table Create Table +mdev40417 CREATE TABLE `mdev40417` ( + `blob_nodef` longtext /*M!100301 COMPRESSED*/ NOT NULL, + `blob_def` longtext /*M!100301 COMPRESSED*/ NOT NULL DEFAULT 'x', + `vc_nodef` varchar(100) /*M!100301 COMPRESSED*/ NOT NULL DEFAULT '', + `vc_def` varchar(100) /*M!100301 COMPRESSED*/ NOT NULL DEFAULT '', + `plain_nodef` longtext NOT NULL, + `pad` int(11) DEFAULT NULL +) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci +SELECT COLUMN_NAME, COLUMN_DEFAULT FROM INFORMATION_SCHEMA.COLUMNS +WHERE TABLE_SCHEMA='test' AND TABLE_NAME='mdev40417' ORDER BY ORDINAL_POSITION; +COLUMN_NAME COLUMN_DEFAULT +blob_nodef NULL +blob_def 'x' +vc_nodef '' +vc_def '' +plain_nodef NULL +pad NULL +# The row that the old server allowed to be inserted is still readable +SELECT * FROM mdev40417; +blob_nodef blob_def vc_nodef vc_def plain_nodef pad + x p 1 +# blob_nodef is mandatory again +INSERT INTO mdev40417 (blob_def, vc_nodef, vc_def, plain_nodef) +VALUES ('a', 'b', 'c', 'd'); +ERROR HY000: Field 'blob_nodef' doesn't have a default value +# plain_nodef was never affected by the bug +INSERT INTO mdev40417 (blob_nodef, blob_def, vc_nodef, vc_def) +VALUES ('a', 'b', 'c', 'd'); +ERROR HY000: Field 'plain_nodef' doesn't have a default value +# blob_def keeps its explicit DEFAULT +INSERT INTO mdev40417 (blob_nodef, vc_nodef, vc_def, plain_nodef) +VALUES ('a', 'b', 'c', 'd'); +# vc_nodef is not touched, it still has the old implicit DEFAULT '' +INSERT INTO mdev40417 (blob_nodef, blob_def, plain_nodef) VALUES ('a', 'b', 'd'); +SELECT blob_nodef, blob_def, vc_nodef, vc_def, plain_nodef FROM mdev40417; +blob_nodef blob_def vc_nodef vc_def plain_nodef + x p +a b d +a x b c d +# ALTER TABLE ... FORCE writes the repaired flag back into the FRM +ALTER TABLE mdev40417 FORCE; +SHOW CREATE TABLE mdev40417; +Table Create Table +mdev40417 CREATE TABLE `mdev40417` ( + `blob_nodef` longtext /*M!100301 COMPRESSED*/ NOT NULL, + `blob_def` longtext /*M!100301 COMPRESSED*/ NOT NULL DEFAULT 'x', + `vc_nodef` varchar(100) /*M!100301 COMPRESSED*/ NOT NULL DEFAULT '', + `vc_def` varchar(100) /*M!100301 COMPRESSED*/ NOT NULL DEFAULT '', + `plain_nodef` longtext NOT NULL, + `pad` int(11) DEFAULT NULL +) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci +DROP TABLE mdev40417; # End of 10.11 tests diff --git a/mysql-test/main/column_compression.test b/mysql-test/main/column_compression.test index e45f6dddef9c0..8f58b41c196f5 100644 --- a/mysql-test/main/column_compression.test +++ b/mysql-test/main/column_compression.test @@ -627,4 +627,52 @@ ALTER TABLE t ADD c2 VARCHAR(100) COMPRESSED NOT NULL; SHOW CREATE TABLE t; DROP TABLE t; +--echo # +--echo # MDEV-40417 upgrade: a table created before the fix is repaired when +--echo # its FRM is read. Only blobs can be repaired - for VARCHAR the FRM +--echo # does not tell an explicit DEFAULT '' from the wrong implicit one, +--echo # so those columns are deliberately left alone. +--echo # +--echo # MDEV-40417.frm was created by 10.11.19 as: +--echo # CREATE TABLE mdev40417 ( +--echo # blob_nodef LONGTEXT COMPRESSED NOT NULL, +--echo # blob_def LONGTEXT COMPRESSED NOT NULL DEFAULT 'x', +--echo # vc_nodef VARCHAR(100) COMPRESSED NOT NULL, +--echo # vc_def VARCHAR(100) COMPRESSED NOT NULL DEFAULT '', +--echo # plain_nodef LONGTEXT NOT NULL, +--echo # pad INT) ENGINE=MyISAM CHARSET=latin1; +--echo # INSERT INTO mdev40417 (plain_nodef, pad) VALUES ('p', 1); +--echo # + +--copy_file std_data/MDEV-40417.frm $MYSQLD_DATADIR/test/mdev40417.frm +--copy_file std_data/MDEV-40417.MYD $MYSQLD_DATADIR/test/mdev40417.MYD +--copy_file std_data/MDEV-40417.MYI $MYSQLD_DATADIR/test/mdev40417.MYI + +SHOW CREATE TABLE mdev40417; +SELECT COLUMN_NAME, COLUMN_DEFAULT FROM INFORMATION_SCHEMA.COLUMNS +WHERE TABLE_SCHEMA='test' AND TABLE_NAME='mdev40417' ORDER BY ORDINAL_POSITION; +--echo # The row that the old server allowed to be inserted is still readable +SELECT * FROM mdev40417; + +--echo # blob_nodef is mandatory again +--error ER_NO_DEFAULT_FOR_FIELD +INSERT INTO mdev40417 (blob_def, vc_nodef, vc_def, plain_nodef) +VALUES ('a', 'b', 'c', 'd'); +--echo # plain_nodef was never affected by the bug +--error ER_NO_DEFAULT_FOR_FIELD +INSERT INTO mdev40417 (blob_nodef, blob_def, vc_nodef, vc_def) +VALUES ('a', 'b', 'c', 'd'); +--echo # blob_def keeps its explicit DEFAULT +INSERT INTO mdev40417 (blob_nodef, vc_nodef, vc_def, plain_nodef) +VALUES ('a', 'b', 'c', 'd'); +--echo # vc_nodef is not touched, it still has the old implicit DEFAULT '' +INSERT INTO mdev40417 (blob_nodef, blob_def, plain_nodef) VALUES ('a', 'b', 'd'); +--sorted_result +SELECT blob_nodef, blob_def, vc_nodef, vc_def, plain_nodef FROM mdev40417; + +--echo # ALTER TABLE ... FORCE writes the repaired flag back into the FRM +ALTER TABLE mdev40417 FORCE; +SHOW CREATE TABLE mdev40417; +DROP TABLE mdev40417; + --echo # End of 10.11 tests diff --git a/mysql-test/std_data/MDEV-40417.MYD b/mysql-test/std_data/MDEV-40417.MYD new file mode 100644 index 0000000000000000000000000000000000000000..7b1c9ccc23a2723de200d13ec8fe718bbd2ceaee GIT binary patch literal 24 acmZQ(5N2fj$HV{x6$}iFK&k*lFaQ7@@B#w> literal 0 HcmV?d00001 diff --git a/mysql-test/std_data/MDEV-40417.MYI b/mysql-test/std_data/MDEV-40417.MYI new file mode 100644 index 0000000000000000000000000000000000000000..40ed4e29841dc5eafb93c1b56a2c0fa613665e95 GIT binary patch literal 1024 zcmezOkDZZ$kJal>86jF)%`D7{&M>3LpY3FmVYO12;pM0$C3_A6X8o*nr zI7#})#;^tJKV~yRq?j1~!_*+L;6fvT!N%ZQ=^5table_check_constraints - share->field_check_constraints)); + /* + Tables created before MDEV-40417 was fixed have no FIELDFLAG_NO_DEFAULT + in the FRM for a COMPRESSED NOT NULL column without an explicit DEFAULT + clause, so such a column wrongly looks like it has DEFAULT ''. + + This can be repaired for blobs: an explicit DEFAULT of a blob column is + always stored in the FRM as an expression, see + Column_definition::has_default_expression(), hence a blob that has no + default_value provably had no DEFAULT clause. Note that the check is + also correct for FRMs written after the fix, where the flag is set + already. + + VARCHAR and VARBINARY cannot be repaired here. A constant DEFAULT of a + non-blob column is stored in the default record, exactly like the wrong + implicit default, which makes the two cases indistinguishable. + */ + for (field_ptr= share->field; *field_ptr; field_ptr++) + { + reg_field= *field_ptr; + if (reg_field->compression_method() && !reg_field->default_value && + !reg_field->vcol_info && + (reg_field->flags & (NOT_NULL_FLAG | BLOB_FLAG)) == + (NOT_NULL_FLAG | BLOB_FLAG)) + reg_field->flags|= NO_DEFAULT_VALUE_FLAG; + } + if (options.str) { DBUG_ASSERT(options.length);