Details
-
Bug
-
Status: Closed (View Workflow)
-
Critical
-
Resolution: Fixed
-
11.8
-
None
-
Can result in unexpected behaviour
Description
Field_timestamp::get_copy_func() silently skips the row_end 2038→2106 conversion added by MDEV-32188 when the session's sql_mode includes NO_ZERO_DATE or NO_ZERO_IN_DATE (e.g. sql_mode=TRADITIONAL), or when the source/destination fields are not eq_def().
ALTER TABLE ... FORCE (and any other copy-algorithm ALTER) reports success and gives no error or warning, but the old-format row_end sentinel is left unconverted.
Reproduce
Add "traditional" combination to versioning.old_timestamp test:
--- /dev/null
|
+++ mysql-test/suite/versioning/t/old_timestamp.combinations
|
@@ -0,0 +1,4 @@
|
+[normal]
|
+
|
+[traditional]
|
+sql-mode=TRADITIONAL |
Result
--- /home/midenok/src/mariadb/11.8/src/mysql-test/suite/versioning/r/old_timestamp.result 2026-08-11 16:23:10.685440488 +0300
|
+++ /home/midenok/src/mariadb/11.8/src/mysql-test/suite/versioning/r/old_timestamp.reject 2026-09-29 13:06:31.109875890 +0300
|
@@ -90,17 +90,17 @@
|
set @@time_zone="+00:00";
|
select a,row_end from sv_basic for system_time all;
|
a row_end
|
-1 2106-02-07 06:28:15.999999
|
+1 2038-01-19 03:14:07.999999
|
2 2023-12-19 14:23:22.305292
|
-3 2106-02-07 06:28:15.999999
|
+3 2038-01-19 03:14:07.999999
|
select a,row_end from sv_partition for system_time all;
|
a row_end
|
-1 2106-02-07 06:28:15.999999
|
+1 2038-01-19 03:14:07.999999
|
2 2023-12-19 14:23:22.332359
|
-3 2106-02-07 06:28:15.999999
|
+3 2038-01-19 03:14:07.999999
|
select a,row_foo_end from sv_explicit for system_time all;
|
a row_foo_end
|
-1 2106-02-07 06:28:15.999999
|
+1 2038-01-19 03:14:07.999999
|
2 2023-12-19 14:23:22.359113
|
-3 2106-02-07 06:28:15.999999
|
+3 2038-01-19 03:14:07.999999 |
Root cause
Field_temporal::get_copy_func() (sql/field_conv.cc) returns do_field_datetime instead of do_field_eq whenever:
- sql_mode has NO_ZERO_DATE or NO_ZERO_IN_DATE set, or
- the two fields are not eq_def()
Field::Copy_func *Field_timestamp::get_copy_func(const Field *from) const |
{
|
Field::Copy_func *copy= Field_temporal::get_copy_func(from);
|
if (from->type() == MYSQL_TYPE_TIMESTAMP) |
{
|
if (copy == do_field_datetime) |
return do_field_timestamp; // <-- returns here first |
if (copy == do_field_eq && |
from->table->file->check_versioned_compatibility() &&
|
(flags & VERS_ROW_END) && (from->flags & VERS_ROW_END))
|
return do_field_versioned_timestamp; // <-- never reached |
}
|
return copy; |
}
|
The do_field_datetime check is evaluated before the VERS_ROW_END check, so whenever the generic path picks do_field_datetime, the function returns immediately and the versioned-timestamp byte-conversion branch is never reached. do_field_timestamp/do_field_datetime perform a plain semantic value copy via save_in_field(), which has no knowledge of the row_end sentinel and faithfully re-stores the old (2038) value unchanged.
The bug only manifests for the system-versioned row_end column, since that is the only field carrying the VERS_ROW_END flag; ordinary TIMESTAMP columns are unaffected (they never had a special-case branch to lose).
Fix
Check the VERS_ROW_END conversion need first, independently of what the generic path picked, since row_end is server-maintained and never zero:
Field::Copy_func *Field_timestamp::get_copy_func(const Field *from) const |
{
|
Field::Copy_func *copy= Field_temporal::get_copy_func(from);
|
if (from->type() == MYSQL_TYPE_TIMESTAMP) |
{
|
if ((flags & VERS_ROW_END) && (from->flags & VERS_ROW_END) && |
from->table->file->check_versioned_compatibility() &&
|
!from->table->vers_new_format())
|
return do_field_versioned_timestamp; |
if (copy == do_field_datetime) |
return do_field_timestamp; |
}
|
return copy; |
}
|