mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-10-23 04:12:32 +00:00
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:
@@ -79,3 +79,5 @@ docs/specs/
|
||||
|
||||
# Claude Code personal/local overrides (keep shared .claude/ content tracked)
|
||||
.claude/*.local.*
|
||||
|
||||
output/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user