Skip to content

Commit 4fb1793

Browse files
authored
Fix crash when inserting invalid integer into DATETIME column with DuckDB engine (#134)
Fixes #131 Description =========== - Server crashes with assertion failure when executing: INSERT IGNORE INTO t (col1) VALUES (57399) where col1 is a DATETIME column on a DuckDB engine table. - The assertion `mon > 0 && mon < 13 && year <= 9999` in sec_since_epoch() (sql/tztime.cc:356) fails because the MYSQL_TIME struct contains month=0. - InnoDB handles this case gracefully by truncating to zero date with a warning, but DuckDB engine crashes. Cause ===== - MySQL's SQL layer converts the invalid integer 57399 to a zero date (0000-00-00 00:00:00) via number_to_datetime() -> reset(), setting month=0. - In the DuckDB write path, DeltaAppender::append_mysql_field() for MYSQL_TYPE_DATETIME2 calls TIME_to_gmt_sec() without validating the MYSQL_TIME struct, which triggers the assertion in sec_since_epoch(). Fix === - Add a zero/invalid date check (tm.month == 0) in the MYSQL_TYPE_DATETIME2 branch of DeltaAppender::append_mysql_field() before calling TIME_to_gmt_sec(). - When a zero date is detected, compute the timestamp directly using calc_daynr(), consistent with the existing MYSQL_TYPE_NEWDATE handling. - Valid dates (month in [1, 12]) continue to use the original TIME_to_gmt_sec() path.
1 parent be67dab commit 4fb1793

3 files changed

Lines changed: 302 additions & 5 deletions

File tree

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
#
2+
# Setup
3+
#
4+
# =========================================================
5+
# Scenario 1: INSERT with invalid integer (57399)
6+
# This is the original crash case from GitHub issue#131.
7+
# =========================================================
8+
DROP TABLE IF EXISTS t_duckdb, t_innodb;
9+
CREATE TABLE t_duckdb (
10+
id INT NOT NULL AUTO_INCREMENT,
11+
col1 DATETIME NULL DEFAULT '2026-01-01 00:00:00',
12+
PRIMARY KEY (id)
13+
) ENGINE=DuckDB;
14+
CREATE TABLE t_innodb (
15+
id INT NOT NULL AUTO_INCREMENT,
16+
col1 DATETIME NULL DEFAULT '2026-01-01 00:00:00',
17+
PRIMARY KEY (id)
18+
) ENGINE=InnoDB;
19+
INSERT IGNORE INTO t_duckdb (id, col1) VALUES (1, 57399);
20+
Warnings:
21+
Warning 1265 Data truncated for column 'col1' at row 1
22+
INSERT IGNORE INTO t_innodb (id, col1) VALUES (1, 57399);
23+
Warnings:
24+
Warning 1265 Data truncated for column 'col1' at row 1
25+
INSERT INTO t_duckdb (id, col1) VALUES (2, 57399);
26+
ERROR 22007: Incorrect datetime value: '57399' for column 'col1' at row 1
27+
INSERT INTO t_innodb (id, col1) VALUES (2, 57399);
28+
ERROR 22007: Incorrect datetime value: '57399' for column 'col1' at row 1
29+
SET @saved_sql_mode = @@SESSION.sql_mode;
30+
SET sql_mode = '';
31+
INSERT INTO t_duckdb (id, col1) VALUES (2, 57399);
32+
Warnings:
33+
Warning 1265 Data truncated for column 'col1' at row 1
34+
INSERT INTO t_innodb (id, col1) VALUES (2, 57399);
35+
Warnings:
36+
Warning 1265 Data truncated for column 'col1' at row 1
37+
SET sql_mode = @saved_sql_mode;
38+
SELECT * FROM t_duckdb ORDER BY id;
39+
id col1
40+
1 0000-00-00 00:00:00
41+
2 0000-00-00 00:00:00
42+
SELECT * FROM t_innodb ORDER BY id;
43+
id col1
44+
1 0000-00-00 00:00:00
45+
2 0000-00-00 00:00:00
46+
include/assert.inc [Checksum is the same]
47+
# Scenario 1 PASSED: checksums match
48+
# ==================================================
49+
# Scenario 2: INSERT with various invalid integers
50+
# ==================================================
51+
DROP TABLE IF EXISTS t_duckdb, t_innodb;
52+
CREATE TABLE t_duckdb (
53+
id INT NOT NULL AUTO_INCREMENT,
54+
col1 DATETIME NULL DEFAULT '2026-01-01 00:00:00',
55+
PRIMARY KEY (id)
56+
) ENGINE=DuckDB;
57+
CREATE TABLE t_innodb (
58+
id INT NOT NULL AUTO_INCREMENT,
59+
col1 DATETIME NULL DEFAULT '2026-01-01 00:00:00',
60+
PRIMARY KEY (id)
61+
) ENGINE=InnoDB;
62+
INSERT IGNORE INTO t_duckdb (id, col1) VALUES
63+
(1, 0), (2, 99), (3, 101), (4, 57399), (5, 13320199), (6, -1);
64+
Warnings:
65+
Warning 1264 Out of range value for column 'col1' at row 1
66+
Warning 1265 Data truncated for column 'col1' at row 2
67+
Warning 1265 Data truncated for column 'col1' at row 4
68+
Warning 1265 Data truncated for column 'col1' at row 5
69+
Warning 1264 Out of range value for column 'col1' at row 6
70+
INSERT IGNORE INTO t_innodb (id, col1) VALUES
71+
(1, 0), (2, 99), (3, 101), (4, 57399), (5, 13320199), (6, -1);
72+
Warnings:
73+
Warning 1264 Out of range value for column 'col1' at row 1
74+
Warning 1265 Data truncated for column 'col1' at row 2
75+
Warning 1265 Data truncated for column 'col1' at row 4
76+
Warning 1265 Data truncated for column 'col1' at row 5
77+
Warning 1264 Out of range value for column 'col1' at row 6
78+
SELECT * FROM t_duckdb ORDER BY id;
79+
id col1
80+
1 0000-00-00 00:00:00
81+
2 0000-00-00 00:00:00
82+
3 2000-01-01 00:00:00
83+
4 0000-00-00 00:00:00
84+
5 0000-00-00 00:00:00
85+
6 0000-00-00 00:00:00
86+
SELECT * FROM t_innodb ORDER BY id;
87+
id col1
88+
1 0000-00-00 00:00:00
89+
2 0000-00-00 00:00:00
90+
3 2000-01-01 00:00:00
91+
4 0000-00-00 00:00:00
92+
5 0000-00-00 00:00:00
93+
6 0000-00-00 00:00:00
94+
include/assert.inc [Checksum is the same]
95+
# Scenario 2 PASSED: checksums match
96+
# =========================================================
97+
# Scenario 3: INSERT with mixing valid and invalid values
98+
# =========================================================
99+
DROP TABLE IF EXISTS t_duckdb, t_innodb;
100+
CREATE TABLE t_duckdb (
101+
id INT NOT NULL AUTO_INCREMENT,
102+
col1 DATETIME NULL DEFAULT '2026-01-01 00:00:00',
103+
PRIMARY KEY (id)
104+
) ENGINE=DuckDB;
105+
CREATE TABLE t_innodb (
106+
id INT NOT NULL AUTO_INCREMENT,
107+
col1 DATETIME NULL DEFAULT '2026-01-01 00:00:00',
108+
PRIMARY KEY (id)
109+
) ENGINE=InnoDB;
110+
INSERT IGNORE INTO t_duckdb (id, col1) VALUES
111+
(1, '2026-06-15 10:30:00'),
112+
(2, 57399),
113+
(3, '2020-01-01 00:00:00'),
114+
(4, 0),
115+
(5, 20240615103000),
116+
(6, NULL),
117+
(7, '0000-00-00 00:00:00');
118+
Warnings:
119+
Warning 1265 Data truncated for column 'col1' at row 2
120+
Warning 1264 Out of range value for column 'col1' at row 4
121+
Warning 1264 Out of range value for column 'col1' at row 7
122+
INSERT IGNORE INTO t_innodb (id, col1) VALUES
123+
(1, '2026-06-15 10:30:00'),
124+
(2, 57399),
125+
(3, '2020-01-01 00:00:00'),
126+
(4, 0),
127+
(5, 20240615103000),
128+
(6, NULL),
129+
(7, '0000-00-00 00:00:00');
130+
Warnings:
131+
Warning 1265 Data truncated for column 'col1' at row 2
132+
Warning 1264 Out of range value for column 'col1' at row 4
133+
Warning 1264 Out of range value for column 'col1' at row 7
134+
SELECT * FROM t_duckdb ORDER BY id;
135+
id col1
136+
1 2026-06-15 10:30:00
137+
2 0000-00-00 00:00:00
138+
3 2020-01-01 00:00:00
139+
4 0000-00-00 00:00:00
140+
5 2024-06-15 10:30:00
141+
6 NULL
142+
7 0000-00-00 00:00:00
143+
SELECT * FROM t_innodb ORDER BY id;
144+
id col1
145+
1 2026-06-15 10:30:00
146+
2 0000-00-00 00:00:00
147+
3 2020-01-01 00:00:00
148+
4 0000-00-00 00:00:00
149+
5 2024-06-15 10:30:00
150+
6 NULL
151+
7 0000-00-00 00:00:00
152+
include/assert.inc [Checksum is the same]
153+
# Scenario 3 PASSED: checksums match
154+
#
155+
# Cleanup
156+
#
157+
DROP TABLE t_duckdb, t_innodb;
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# ===========================================================
2+
# Test: duckdb_datetime_zero_date
3+
# Description:
4+
# Regression test for GitHub issue#131.
5+
# Inserting an invalid integer (e.g. 57399) into a DATETIME
6+
# column on a DuckDB engine table used to crash the server
7+
# due to an assertion failure in sec_since_epoch() when
8+
# month=0 (zero date).
9+
#
10+
# This test verifies that DuckDB and InnoDB produce
11+
# identical results for various invalid/zero-date INSERT
12+
# scenarios.
13+
#
14+
# Tested feature: DuckDB DATETIME zero-date handling
15+
# Related source: storage/duckdb/delta_appender.cc
16+
# ===========================================================
17+
18+
--echo #
19+
--echo # Setup
20+
--echo #
21+
--write_file $MYSQL_TMP_DIR/init_datetime_test.inc
22+
--disable_warnings
23+
DROP TABLE IF EXISTS t_duckdb, t_innodb;
24+
--enable_warnings
25+
CREATE TABLE t_duckdb (
26+
id INT NOT NULL AUTO_INCREMENT,
27+
col1 DATETIME NULL DEFAULT '2026-01-01 00:00:00',
28+
PRIMARY KEY (id)
29+
) ENGINE=DuckDB;
30+
31+
CREATE TABLE t_innodb (
32+
id INT NOT NULL AUTO_INCREMENT,
33+
col1 DATETIME NULL DEFAULT '2026-01-01 00:00:00',
34+
PRIMARY KEY (id)
35+
) ENGINE=InnoDB;
36+
EOF
37+
38+
--write_file $MYSQL_TMP_DIR/check_duckdb_datetime.inc
39+
SELECT * FROM t_duckdb ORDER BY id;
40+
SELECT * FROM t_innodb ORDER BY id;
41+
42+
--let $checksum_t_innodb = query_get_value(CHECKSUM TABLE t_innodb, Checksum, 1)
43+
--let $checksum_t_duckdb = query_get_value(CHECKSUM TABLE t_duckdb, Checksum, 1)
44+
--let $assert_cond= $checksum_t_innodb = $checksum_t_duckdb
45+
--let $assert_text= Checksum is the same
46+
--source include/assert.inc
47+
EOF
48+
49+
50+
--echo # =========================================================
51+
--echo # Scenario 1: INSERT with invalid integer (57399)
52+
--echo # This is the original crash case from GitHub issue#131.
53+
--echo # =========================================================
54+
--source $MYSQL_TMP_DIR/init_datetime_test.inc
55+
56+
INSERT IGNORE INTO t_duckdb (id, col1) VALUES (1, 57399);
57+
INSERT IGNORE INTO t_innodb (id, col1) VALUES (1, 57399);
58+
59+
--error ER_TRUNCATED_WRONG_VALUE
60+
INSERT INTO t_duckdb (id, col1) VALUES (2, 57399);
61+
--error ER_TRUNCATED_WRONG_VALUE
62+
INSERT INTO t_innodb (id, col1) VALUES (2, 57399);
63+
64+
SET @saved_sql_mode = @@SESSION.sql_mode;
65+
SET sql_mode = '';
66+
INSERT INTO t_duckdb (id, col1) VALUES (2, 57399);
67+
INSERT INTO t_innodb (id, col1) VALUES (2, 57399);
68+
SET sql_mode = @saved_sql_mode;
69+
70+
--source $MYSQL_TMP_DIR/check_duckdb_datetime.inc
71+
--echo # Scenario 1 PASSED: checksums match
72+
73+
74+
--echo # ==================================================
75+
--echo # Scenario 2: INSERT with various invalid integers
76+
--echo # ==================================================
77+
--source $MYSQL_TMP_DIR/init_datetime_test.inc
78+
79+
# 0: zero value
80+
# 101: boundary (min valid YYMMDD-like is 101 -> 2000-01-01)
81+
# 99: below minimum valid
82+
# 57399: invalid month=73
83+
# 13320199: invalid day=99
84+
# -1: negative value
85+
INSERT IGNORE INTO t_duckdb (id, col1) VALUES
86+
(1, 0), (2, 99), (3, 101), (4, 57399), (5, 13320199), (6, -1);
87+
INSERT IGNORE INTO t_innodb (id, col1) VALUES
88+
(1, 0), (2, 99), (3, 101), (4, 57399), (5, 13320199), (6, -1);
89+
90+
--source $MYSQL_TMP_DIR/check_duckdb_datetime.inc
91+
--echo # Scenario 2 PASSED: checksums match
92+
93+
94+
--echo # =========================================================
95+
--echo # Scenario 3: INSERT with mixing valid and invalid values
96+
--echo # =========================================================
97+
--source $MYSQL_TMP_DIR/init_datetime_test.inc
98+
99+
INSERT IGNORE INTO t_duckdb (id, col1) VALUES
100+
(1, '2026-06-15 10:30:00'),
101+
(2, 57399),
102+
(3, '2020-01-01 00:00:00'),
103+
(4, 0),
104+
(5, 20240615103000),
105+
(6, NULL),
106+
(7, '0000-00-00 00:00:00');
107+
INSERT IGNORE INTO t_innodb (id, col1) VALUES
108+
(1, '2026-06-15 10:30:00'),
109+
(2, 57399),
110+
(3, '2020-01-01 00:00:00'),
111+
(4, 0),
112+
(5, 20240615103000),
113+
(6, NULL),
114+
(7, '0000-00-00 00:00:00');
115+
116+
--source $MYSQL_TMP_DIR/check_duckdb_datetime.inc
117+
--echo # Scenario 3 PASSED: checksums match
118+
119+
--echo #
120+
--echo # Cleanup
121+
--echo #
122+
DROP TABLE t_duckdb, t_innodb;
123+
--remove_file $MYSQL_TMP_DIR/init_datetime_test.inc
124+
--remove_file $MYSQL_TMP_DIR/check_duckdb_datetime.inc

storage/duckdb/delta_appender.cc

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -490,12 +490,28 @@ int DeltaAppender::append_mysql_field(const Field *field,
490490

491491
case MYSQL_TYPE_DATETIME2: {
492492
MYSQL_TIME tm;
493-
static_cast<const Field_datetimef *>(field)->get_date(&tm, TIME_FUZZY_DATE);
494-
bool not_used;
495-
longlong sec = my_tz_UTC->TIME_to_gmt_sec(&tm, &not_used);
496-
// write_batch_longlong(col_index, &value);
493+
static_cast<const Field_datetimef *>(field)->get_date(&tm,
494+
TIME_FUZZY_DATE);
495+
longlong ts_us;
496+
if (tm.month == 0) {
497+
/*
498+
Zero date (e.g. 0000-00-00 00:00:00) or invalid date with month=0.
499+
Bypass TIME_to_gmt_sec() which asserts month > 0.
500+
Use calc_daynr() to compute timestamp directly, consistent with
501+
MYSQL_TYPE_NEWDATE handling.
502+
*/
503+
long days =
504+
calc_daynr(tm.year, tm.month, tm.day) - myduck::days_at_timestart;
505+
ts_us = static_cast<longlong>(days) * 86400LL * 1000000LL +
506+
tm.hour * 3600LL * 1000000LL + tm.minute * 60LL * 1000000LL +
507+
tm.second * 1000000LL + tm.second_part;
508+
} else {
509+
bool not_used;
510+
longlong sec = my_tz_UTC->TIME_to_gmt_sec(&tm, &not_used);
511+
ts_us = sec * 1000000LL + tm.second_part;
512+
}
497513
appender->Append<duckdb::timestamp_t>(
498-
static_cast<duckdb::timestamp_t>(sec * 1000000 + tm.second_part));
514+
static_cast<duckdb::timestamp_t>(ts_us));
499515
break;
500516
}
501517

0 commit comments

Comments
 (0)