fix(mysql): cap timestamp output at microsecond precision (#9831)

* fix(mysql): cap timestamp output at microsecond precision

Truncate MySQL text timestamp results to microseconds while preserving the existing second and millisecond formatting. Keep stored nanoseconds and explicit integer/string representations unchanged.

Fixes #9828

Signed-off-by: luofucong <luofc@foxmail.com>

* chore: ignore local output directory

Signed-off-by: luofucong <luofc@foxmail.com>

---------

Signed-off-by: luofucong <luofc@foxmail.com>
This commit is contained in:
LFC
2026-10-21 11:34:47 +00:00
committed by GitHub
parent 9f9b35c270
commit 5af1e3301e
10 changed files with 134 additions and 21 deletions
+2
View File
@@ -79,3 +79,5 @@ docs/specs/
# Claude Code personal/local overrides (keep shared .claude/ content tracked)
.claude/*.local.*
output/
+3 -2
View File
@@ -23,7 +23,7 @@ use arrow::datatypes::{
UInt16Type, UInt32Type, UInt64Type,
};
use arrow_schema::{DataType, IntervalUnit};
use chrono::{Datelike, NaiveDateTime};
use chrono::{Datelike, NaiveDateTime, SubsecRound};
use common_decimal::Decimal128;
use common_error::ext::ErrorExt;
use common_error::status_code::StatusCode;
@@ -275,7 +275,8 @@ impl MysqlResultWriter {
write!(
&mut staged_timestamp.formatted,
"{}",
datetime.format("%Y-%m-%d %H:%M:%S%.f")
// MySQL temporal types support at most microsecond precision.
datetime.trunc_subsecs(6).format("%Y-%m-%d %H:%M:%S%.f")
)
.map_err(|_| {
InternalSnafu {
+65 -1
View File
@@ -33,7 +33,9 @@ use datafusion_expr::LogicalPlan;
use datatypes::prelude::VectorRef;
use datatypes::schema::{ColumnSchema, Schema};
use datatypes::value::Value;
use datatypes::vectors::{Int32Vector, TimestampMicrosecondVector, TimestampSecondVector};
use datatypes::vectors::{
Int32Vector, TimestampMicrosecondVector, TimestampNanosecondVector, TimestampSecondVector,
};
use mysql_async::prelude::*;
use mysql_async::{Conn, Row, SslOpts};
use query::QueryEngineFactory;
@@ -859,6 +861,68 @@ async fn test_mysql_timestamp_precision_slot_isolation() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn test_mysql_nanosecond_timestamps_use_microsecond_precision() -> Result<()> {
let schema = Arc::new(Schema::new(vec![ColumnSchema::new(
"ts",
datatypes::prelude::ConcreteDataType::timestamp_nanosecond_datatype(),
true,
)]));
let timestamps = vec![
Some(1_792_572_935_718_870_300),
Some(123_000_789),
Some(999_999_999),
Some(999),
Some(-1),
Some(0),
None,
];
let batch = RecordBatch::new(
schema,
vec![Arc::new(TimestampNanosecondVector::from(timestamps)) as VectorRef],
)
.unwrap();
let table = MemTable::table("nanosecond_timestamps", batch);
let query = "SELECT ts FROM nanosecond_timestamps".to_string();
let (result, health_check) = query_mysql_text_protocol(table.clone(), query.clone()).await;
let rows = result.unwrap();
let expected = [
Some("2026-10-21 08:55:35.718870"),
Some("1970-01-01 00:00:00.123"),
Some("1970-01-01 00:00:00.999999"),
Some("1970-01-01 00:00:00"),
Some("1969-12-31 23:59:59.999999"),
Some("1970-01-01 00:00:00"),
None,
];
assert_eq!(expected.len(), rows.len());
for (row, expected) in rows.iter().zip(expected) {
assert_eq!(
vec![expected.map_or(Value::Null, |s| Value::from(s.as_bytes().to_vec()))],
row.values
);
}
assert_eq!(Some(1), health_check.unwrap());
let (result, health_check) = query_mysql_binary_protocol(table, query).await;
let rows = result.unwrap();
let expected = [
mysql_async::Value::Date(2026, 10, 21, 8, 55, 35, 718_870),
mysql_async::Value::Date(1970, 1, 1, 0, 0, 0, 123_000),
mysql_async::Value::Date(1970, 1, 1, 0, 0, 0, 999_999),
mysql_async::Value::Date(1970, 1, 1, 0, 0, 0, 0),
mysql_async::Value::Date(1969, 12, 31, 23, 59, 59, 999_999),
mysql_async::Value::Date(1970, 1, 1, 0, 0, 0, 0),
mysql_async::Value::NULL,
];
assert_eq!(expected.len(), rows.len());
for (row, expected) in rows.iter().zip(expected) {
assert_eq!(Some(&expected), row.as_ref(0));
}
assert_eq!(Some(1), health_check.unwrap());
Ok(())
}
fn timestamp_table(table_name: &str, timestamp: Option<i64>) -> (TableRef, Arc<Schema>) {
let schema = Arc::new(Schema::new(vec![
ColumnSchema::new(
+2 -7
View File
@@ -264,13 +264,8 @@ fn test_create_mysql_column_unsigned_flags() {
// ── Timestamp precision tests (issue #8227) ───────────────────────────────────
/// Timestamps displayed through the MySQL writer must respect their storage unit.
///
/// Before the fix, `to_chrono_datetime_with_timezone` → `NaiveDateTime::Display`
/// always emitted 6 fractional-second digits, so:
/// - `TIMESTAMP(3)` showed `.195000` instead of `.195`
/// - `TIMESTAMP(9)` showed `.195123` instead of `.195123456`
///
/// The common timestamp string formatter preserves full storage precision.
/// The MySQL writer separately caps temporal output at microseconds (issue #9828).
#[test]
fn test_timestamp_display_precision_utc() {
// Use UTC timezone for deterministic, timezone-independent output.
@@ -1,7 +1,7 @@
-- description: Timestamp display precision over the MySQL protocol should
-- respect the column's storage unit (issue #8227):
-- respect MySQL's maximum microsecond precision (issues #8227 and #9828):
-- - TIMESTAMP(3) must not pad to 6 fractional digits
-- - TIMESTAMP(9) must not truncate to 6 fractional digits
-- - TIMESTAMP(9) must truncate to at most 6 fractional digits
-- SQLNESS PROTOCOL MYSQL
CREATE TABLE ts_precision_display (
ts_s TIMESTAMP(0),
@@ -54,13 +54,26 @@ affected_rows: 1
-- SQLNESS PROTOCOL MYSQL
SELECT * FROM ts_precision_display ORDER BY v;
+---------------------+-------------------------+----------------------------+-------------------------------+---+
| ts_s | ts_ms | ts_us | ts_ns | v |
+---------------------+-------------------------+----------------------------+-------------------------------+---+
| 2026-06-02 03:50:00 | 2026-06-02 03:50:00.195 | 2026-06-02 03:50:00.195123 | 2026-06-02 03:50:00.195123456 | 1 |
| 2026-06-02 03:50:01 | 2026-06-02 03:50:01.100 | 2026-06-02 03:50:01.100 | 2026-06-02 03:50:01.100 | 2 |
| 2026-06-02 03:50:02 | 2026-06-02 03:50:02 | 2026-06-02 03:50:02 | 2026-06-02 03:50:02 | 3 |
+---------------------+-------------------------+----------------------------+-------------------------------+---+
+---------------------+-------------------------+----------------------------+----------------------------+---+
| ts_s | ts_ms | ts_us | ts_ns | v |
+---------------------+-------------------------+----------------------------+----------------------------+---+
| 2026-06-02 03:50:00 | 2026-06-02 03:50:00.195 | 2026-06-02 03:50:00.195123 | 2026-06-02 03:50:00.195123 | 1 |
| 2026-06-02 03:50:01 | 2026-06-02 03:50:01.100 | 2026-06-02 03:50:01.100 | 2026-06-02 03:50:01.100 | 2 |
| 2026-06-02 03:50:02 | 2026-06-02 03:50:02 | 2026-06-02 03:50:02 | 2026-06-02 03:50:02 | 3 |
+---------------------+-------------------------+----------------------------+----------------------------+---+
-- Full nanosecond precision remains available as an integer or string.
-- SQLNESS PROTOCOL MYSQL
SELECT ts_ns::BIGINT AS epoch_ns, ts_ns::STRING AS timestamp_string
FROM ts_precision_display ORDER BY v;
+---------------------+-------------------------------+
| epoch_ns | timestamp_string |
+---------------------+-------------------------------+
| 1780372200195123456 | 2026-06-02T03:50:00.195123456 |
| 1780372201100000000 | 2026-06-02T03:50:01.100 |
| 1780372202000000000 | 2026-06-02T03:50:02 |
+---------------------+-------------------------------+
-- SQLNESS PROTOCOL MYSQL
DROP TABLE ts_precision_display;
@@ -1,7 +1,7 @@
-- description: Timestamp display precision over the MySQL protocol should
-- respect the column's storage unit (issue #8227):
-- respect MySQL's maximum microsecond precision (issues #8227 and #9828):
-- - TIMESTAMP(3) must not pad to 6 fractional digits
-- - TIMESTAMP(9) must not truncate to 6 fractional digits
-- - TIMESTAMP(9) must truncate to at most 6 fractional digits
-- SQLNESS PROTOCOL MYSQL
CREATE TABLE ts_precision_display (
@@ -47,5 +47,10 @@ INSERT INTO ts_precision_display VALUES (
-- SQLNESS PROTOCOL MYSQL
SELECT * FROM ts_precision_display ORDER BY v;
-- Full nanosecond precision remains available as an integer or string.
-- SQLNESS PROTOCOL MYSQL
SELECT ts_ns::BIGINT AS epoch_ns, ts_ns::STRING AS timestamp_string
FROM ts_precision_display ORDER BY v;
-- SQLNESS PROTOCOL MYSQL
DROP TABLE ts_precision_display;
@@ -0,0 +1,8 @@
name = "mysql_timestamp_precision"
reason = "MySQL timestamp results must use at most microseconds while stored nanoseconds survive restart (issue #9828)."
introduced_by = "issue #9828"
topologies = ["standalone", "distributed"]
from_range = ["*"]
to_range = [">=v1.4.0-alpha.0"]
features = ["table", "mysql", "timestamp"]
owner = "servers"
@@ -0,0 +1,3 @@
CREATE TABLE timestamps (ts TIMESTAMP(9) TIME INDEX);
INSERT INTO timestamps VALUES ('2026-10-21 08:55:35.718870300');
@@ -0,0 +1,17 @@
-- SQLNESS PROTOCOL MYSQL
SELECT * FROM timestamps;
+----------------------------+
| ts |
+----------------------------+
| 2026-10-21 08:55:35.718870 |
+----------------------------+
-- SQLNESS PROTOCOL MYSQL
SELECT ts::BIGINT AS epoch_ns, ts::STRING AS timestamp_string FROM timestamps;
+---------------------+-------------------------------+
| epoch_ns | timestamp_string |
+---------------------+-------------------------------+
| 1792572935718870300 | 2026-10-21T08:55:35.718870300 |
+---------------------+-------------------------------+
@@ -0,0 +1,5 @@
-- SQLNESS PROTOCOL MYSQL
SELECT * FROM timestamps;
-- SQLNESS PROTOCOL MYSQL
SELECT ts::BIGINT AS epoch_ns, ts::STRING AS timestamp_string FROM timestamps;