fix: timestamp display precision should respect column schema (#8227) (#8238)

* fix: timestamp display precision should respect column schema (#8227)

Previously, the MySQL writer converted timestamps to NaiveDateTime via
to_chrono_datetime_with_timezone(), then passed the NaiveDateTime to
write_col(). NaiveDateTime::Display uses a fixed 6-digit fractional-
second format, causing:
  - TIMESTAMP(3) to show '.195000' instead of '.195'
  - TIMESTAMP(9) to show '.195123' instead of '.195123456'

Fix: use Timestamp::to_timezone_aware_string() directly, which formats
with chrono's '%.f' specifier — it strips trailing zeros and preserves
full nanosecond fidelity based on the actual stored unit.

Also add comprehensive unit tests in mysql_writer_test.rs covering:
  - All concrete data type → MySQL column type mappings
  - UNSIGNED_FLAG propagation for unsigned integer types
  - Timestamp precision for all four units (0/3/6/9 decimal places)
  - Edge cases: zero subseconds, trailing-zero stripping, Unix epoch,
    negative timestamps, and timezone offset shifts
  - Column-def ordering, empty schema, and decimal variants

Signed-off-by: Divyansh <anshmcs@gmail.com>

* perf: reuse format buffer for timestamp serialization to avoid per-row heap allocation

Signed-off-by: Divyansh <anshmcs@gmail.com>

* refactor: rename test variable and update visibility of create_mysql_column function

Signed-off-by: Divyansh <anshmcs@gmail.com>

* test: fix timestamp test constants and add sqlness case for display precision

- Correct the base epoch constant: 1_748_836_200 is 2025-06-02, not
  2026-06-02; use 1_780_372_200 to match the documented instant.
- Fix subsecond expectations: chrono's %.f renders fractional digits in
  groups of 3 (.100 / .010), it does not strip to .1 / .01.
- Add sqlness case timestamp_precision_display reproducing issue #8227
  over the MySQL protocol (TIMESTAMP(0/3/6/9) rendering).
- cargo fmt reflow of create_mysql_column signature.

Signed-off-by: Divyansh <anshmcs@gmail.com>

* fix: support binary protocol for timestamp column

Signed-off-by: Divyansh <anshmcs@gmail.com>

* test: update sqlness results for new timestamp display precision

Signed-off-by: Divyansh <anshmcs@gmail.com>

---------

Signed-off-by: Divyansh <anshmcs@gmail.com>
This commit is contained in:
Div
2026-07-17 12:52:17 +05:30
committed by GitHub
parent c969fb0a5e
commit 1acdec9334
6 changed files with 730 additions and 26 deletions
+42 -6
View File
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::fmt::Write as FmtWrite;
use std::io;
use std::time::Duration;
@@ -36,6 +37,7 @@ use datatypes::types::jsonb_to_string;
use futures::StreamExt;
use opensrv_mysql::{
Column, ColumnFlags, ColumnType, ErrorKind, OkResponse, QueryResultWriter, RowWriter,
ToMysqlValue,
};
use session::SessionRef;
use session::context::QueryContextRef;
@@ -165,6 +167,21 @@ pub fn handle_err(e: impl ErrorExt, query_ctx: QueryContextRef) -> (ErrorKind, S
struct MysqlResultWriter;
struct PrecisionTimestamp<'a> {
formatted: &'a str,
datetime: chrono::NaiveDateTime,
}
impl<'a> ToMysqlValue for PrecisionTimestamp<'a> {
fn to_mysql_text<W: std::io::Write>(&self, w: &mut W) -> io::Result<()> {
self.formatted.to_mysql_text(w)
}
fn to_mysql_bin<W: std::io::Write>(&self, w: &mut W, c: &Column) -> io::Result<()> {
self.datetime.to_mysql_bin(w, c)
}
}
impl MysqlResultWriter {
async fn write_affected_rows<'a, W: AsyncWrite + Unpin>(
w: QueryResultWriter<'a, W>,
@@ -190,6 +207,9 @@ impl MysqlResultWriter {
) -> Result<()> {
let schema = record_batch.schema.clone();
let record_batch = record_batch.into_df_record_batch();
// Reusable buffer for timestamp formatting — avoids one heap allocation per
// timestamp value per row. Cleared and refilled in the Timestamp arm below.
let mut format_buf = String::new();
for i in 0..record_batch.num_rows() {
for (j, column) in record_batch.columns().iter().enumerate() {
if column.is_null(i) {
@@ -265,8 +285,27 @@ impl MysqlResultWriter {
}
DataType::Timestamp(_, _) => {
let v = datatypes::arrow_array::timestamp_array_value(column, i);
let v = v.to_chrono_datetime_with_timezone(Some(&query_context.timezone()));
row_writer.write_col(v)?;
// Reuse `format_buf` to avoid a per-row heap allocation.
// This mirrors what `Timestamp::as_formatted_string` does
// internally, but writes directly into our pre-allocated buffer.
format_buf.clear();
if let Some(datetime) =
v.to_chrono_datetime_with_timezone(Some(&query_context.timezone()))
{
let _ = write!(
&mut format_buf,
"{}",
datetime.format("%Y-%m-%d %H:%M:%S%.f")
);
row_writer.write_col(PrecisionTimestamp {
formatted: &format_buf,
datetime,
})?;
} else {
let _ =
write!(&mut format_buf, "[Timestamp{}: {}]", v.unit(), v.value());
row_writer.write_col(&*format_buf)?;
}
}
DataType::Interval(interval_unit) => match interval_unit {
IntervalUnit::YearMonth => {
@@ -333,10 +372,7 @@ impl MysqlResultWriter {
}
}
pub(crate) fn create_mysql_column(
data_type: &ConcreteDataType,
column_name: &str,
) -> Result<Column> {
pub fn create_mysql_column(data_type: &ConcreteDataType, column_name: &str) -> Result<Column> {
let column_type = match data_type {
ConcreteDataType::Null(_) => Ok(ColumnType::MYSQL_TYPE_NULL),
ConcreteDataType::Boolean(_) | ConcreteDataType::Int8(_) | ConcreteDataType::UInt8(_) => {
+555 -7
View File
@@ -14,11 +14,23 @@
use std::sync::Arc;
use datatypes::schema::Schema;
use servers::mysql::writer::create_mysql_column_def;
use common_time::timestamp::TimeUnit;
use common_time::{Timestamp, Timezone};
use datatypes::prelude::ConcreteDataType;
use datatypes::schema::{ColumnSchema, Schema};
use datatypes::types::{
IntervalDayTimeType, IntervalMonthDayNanoType, IntervalYearMonthType, TimestampMicrosecondType,
TimestampMillisecondType, TimestampNanosecondType, TimestampSecondType,
};
use opensrv_mysql::{ColumnFlags, ColumnType};
use servers::mysql::writer::{create_mysql_column, create_mysql_column_def};
use crate::mysql::{TestingData, all_datatype_testing_data};
// ── Schema-to-column-def tests ────────────────────────────────────────────────
/// Round-trip: every column schema in `all_datatype_testing_data()` must produce
/// a column definition whose type and name match the expected values.
#[test]
fn test_create_mysql_column_def() {
let TestingData {
@@ -28,12 +40,548 @@ fn test_create_mysql_column_def() {
} = all_datatype_testing_data();
let schema = Arc::new(Schema::new(column_schemas.clone()));
let columns_def = create_mysql_column_def(&schema).unwrap();
assert_eq!(column_schemas.len(), columns_def.len());
assert_eq!(
column_schemas.len(),
columns_def.len(),
"column count must match"
);
for (i, column_def) in columns_def.iter().enumerate() {
let column_schema = &column_schemas[i];
assert_eq!(column_schema.name, column_def.column);
let expected_coltype = mysql_columns_def[i];
assert_eq!(column_def.coltype, expected_coltype);
assert_eq!(
column_schemas[i].name, column_def.column,
"column name mismatch at index {i}"
);
assert_eq!(
mysql_columns_def[i], column_def.coltype,
"column type mismatch at index {i} ({})",
column_schemas[i].name
);
}
}
// ── Individual create_mysql_column type-mapping tests ────────────────────────
/// Verify every concrete data type maps to the correct MySQL column type.
#[test]
fn test_create_mysql_column_type_mapping() {
let cases: &[(&str, ConcreteDataType, ColumnType)] = &[
(
"null_col",
ConcreteDataType::null_datatype(),
ColumnType::MYSQL_TYPE_NULL,
),
(
"bool_col",
ConcreteDataType::boolean_datatype(),
ColumnType::MYSQL_TYPE_TINY,
),
(
"int8_col",
ConcreteDataType::int8_datatype(),
ColumnType::MYSQL_TYPE_TINY,
),
(
"int16_col",
ConcreteDataType::int16_datatype(),
ColumnType::MYSQL_TYPE_SHORT,
),
(
"int32_col",
ConcreteDataType::int32_datatype(),
ColumnType::MYSQL_TYPE_LONG,
),
(
"int64_col",
ConcreteDataType::int64_datatype(),
ColumnType::MYSQL_TYPE_LONGLONG,
),
(
"uint8_col",
ConcreteDataType::uint8_datatype(),
ColumnType::MYSQL_TYPE_TINY,
),
(
"uint16_col",
ConcreteDataType::uint16_datatype(),
ColumnType::MYSQL_TYPE_SHORT,
),
(
"uint32_col",
ConcreteDataType::uint32_datatype(),
ColumnType::MYSQL_TYPE_LONG,
),
(
"uint64_col",
ConcreteDataType::uint64_datatype(),
ColumnType::MYSQL_TYPE_LONGLONG,
),
(
"float32_col",
ConcreteDataType::float32_datatype(),
ColumnType::MYSQL_TYPE_FLOAT,
),
(
"float64_col",
ConcreteDataType::float64_datatype(),
ColumnType::MYSQL_TYPE_DOUBLE,
),
(
"string_col",
ConcreteDataType::string_datatype(),
ColumnType::MYSQL_TYPE_VARCHAR,
),
(
"binary_col",
ConcreteDataType::binary_datatype(),
ColumnType::MYSQL_TYPE_VARCHAR,
),
(
"ts_s_col",
ConcreteDataType::timestamp_second_datatype(),
ColumnType::MYSQL_TYPE_TIMESTAMP,
),
(
"ts_ms_col",
ConcreteDataType::timestamp_millisecond_datatype(),
ColumnType::MYSQL_TYPE_TIMESTAMP,
),
(
"ts_us_col",
ConcreteDataType::timestamp_microsecond_datatype(),
ColumnType::MYSQL_TYPE_TIMESTAMP,
),
(
"ts_ns_col",
ConcreteDataType::timestamp_nanosecond_datatype(),
ColumnType::MYSQL_TYPE_TIMESTAMP,
),
(
"date_col",
ConcreteDataType::date_datatype(),
ColumnType::MYSQL_TYPE_DATE,
),
(
"time_s_col",
ConcreteDataType::time_second_datatype(),
ColumnType::MYSQL_TYPE_TIME,
),
(
"time_ms_col",
ConcreteDataType::time_millisecond_datatype(),
ColumnType::MYSQL_TYPE_TIME,
),
(
"time_us_col",
ConcreteDataType::time_microsecond_datatype(),
ColumnType::MYSQL_TYPE_TIME,
),
(
"time_ns_col",
ConcreteDataType::time_nanosecond_datatype(),
ColumnType::MYSQL_TYPE_TIME,
),
(
"interval_ym",
ConcreteDataType::interval_year_month_datatype(),
ColumnType::MYSQL_TYPE_VARCHAR,
),
(
"interval_dt",
ConcreteDataType::interval_day_time_datatype(),
ColumnType::MYSQL_TYPE_VARCHAR,
),
(
"interval_mdn",
ConcreteDataType::interval_month_day_nano_datatype(),
ColumnType::MYSQL_TYPE_VARCHAR,
),
(
"decimal_col",
ConcreteDataType::decimal128_datatype(10, 2),
ColumnType::MYSQL_TYPE_DECIMAL,
),
];
for (col_name, data_type, expected_type) in cases {
let col = create_mysql_column(data_type, col_name)
.unwrap_or_else(|_| panic!("create_mysql_column failed for {col_name}"));
assert_eq!(
*expected_type, col.coltype,
"type mismatch for column '{col_name}'"
);
assert_eq!(*col_name, col.column, "name mismatch for '{col_name}'");
// collen == 0 means "use MySQL default length"
assert_eq!(0, col.collen, "collen should be 0 for '{col_name}'");
// table should be empty (not populated yet)
assert!(
col.table.is_empty(),
"table should be empty for '{col_name}'"
);
}
}
// ── Column flags tests ────────────────────────────────────────────────────────
/// Unsigned integer types must carry `UNSIGNED_FLAG`; all other types must not.
#[test]
fn test_create_mysql_column_unsigned_flags() {
// Types that must have UNSIGNED_FLAG
let unsigned_types: &[(&str, ConcreteDataType)] = &[
("u8", ConcreteDataType::uint8_datatype()),
("u16", ConcreteDataType::uint16_datatype()),
("u32", ConcreteDataType::uint32_datatype()),
("u64", ConcreteDataType::uint64_datatype()),
];
for (name, dt) in unsigned_types {
let col = create_mysql_column(dt, name).unwrap();
assert!(
col.colflags.contains(ColumnFlags::UNSIGNED_FLAG),
"UNSIGNED_FLAG must be set for {name}"
);
}
// Types that must NOT have UNSIGNED_FLAG
let signed_types: &[(&str, ConcreteDataType)] = &[
("i8", ConcreteDataType::int8_datatype()),
("i16", ConcreteDataType::int16_datatype()),
("i32", ConcreteDataType::int32_datatype()),
("i64", ConcreteDataType::int64_datatype()),
("f32", ConcreteDataType::float32_datatype()),
("f64", ConcreteDataType::float64_datatype()),
("str", ConcreteDataType::string_datatype()),
("ts", ConcreteDataType::timestamp_millisecond_datatype()),
("bool", ConcreteDataType::boolean_datatype()),
("date", ConcreteDataType::date_datatype()),
];
for (name, dt) in signed_types {
let col = create_mysql_column(dt, name).unwrap();
assert!(
!col.colflags.contains(ColumnFlags::UNSIGNED_FLAG),
"UNSIGNED_FLAG must NOT be set for {name}"
);
}
}
// ── 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`
///
#[test]
fn test_timestamp_display_precision_utc() {
// Use UTC timezone for deterministic, timezone-independent output.
let tz = Timezone::from_tz_string("UTC").unwrap();
// Base instant: 2026-06-02 03:50:00 UTC (unix epoch seconds = 1780372200)
// Sub-second component: 195_123_456 ns = 195.123456 ms = 0.195123456 s
let base_s: i64 = 1_780_372_200;
// TIMESTAMP(0) — second precision: no fractional part
let ts = Timestamp::new(base_s, TimeUnit::Second);
assert_eq!(
ts.to_timezone_aware_string(Some(&tz)),
"2026-06-02 03:50:00",
"TIMESTAMP(0) must have no fractional seconds"
);
// TIMESTAMP(3) — millisecond precision: trailing zeros must be stripped
let ts = Timestamp::new(base_s * 1_000 + 195, TimeUnit::Millisecond);
assert_eq!(
ts.to_timezone_aware_string(Some(&tz)),
"2026-06-02 03:50:00.195",
"TIMESTAMP(3) must show exactly 3 fractional digits (no trailing zeros)"
);
// TIMESTAMP(6) — microsecond precision
let ts = Timestamp::new(base_s * 1_000_000 + 195_123, TimeUnit::Microsecond);
assert_eq!(
ts.to_timezone_aware_string(Some(&tz)),
"2026-06-02 03:50:00.195123",
"TIMESTAMP(6) must show exactly 6 fractional digits"
);
// TIMESTAMP(9) — nanosecond precision: full 9-digit fidelity
let ts = Timestamp::new(base_s * 1_000_000_000 + 195_123_456, TimeUnit::Nanosecond);
assert_eq!(
ts.to_timezone_aware_string(Some(&tz)),
"2026-06-02 03:50:00.195123456",
"TIMESTAMP(9) must show all 9 fractional digits"
);
}
/// When the subsecond part is exactly zero, no fractional portion is emitted
/// for any precision level (the `%.f` specifier omits the dot entirely).
#[test]
fn test_timestamp_display_precision_no_subseconds() {
let tz = Timezone::from_tz_string("UTC").unwrap();
let base_s: i64 = 1_780_372_200; // 2026-06-02 03:50:00 UTC
for (unit, value) in [
(TimeUnit::Second, base_s),
(TimeUnit::Millisecond, base_s * 1_000),
(TimeUnit::Microsecond, base_s * 1_000_000),
(TimeUnit::Nanosecond, base_s * 1_000_000_000),
] {
let ts = Timestamp::new(value, unit);
assert_eq!(
ts.to_timezone_aware_string(Some(&tz)),
"2026-06-02 03:50:00",
"zero subseconds must produce no fractional part (unit = {unit:?})"
);
}
}
/// Subsecond digits are rendered in groups of 3 (chrono's `%.f`): a value
/// representable in milliseconds shows 3 digits, microseconds 6, nanoseconds 9.
/// e.g. 100 ms → `.100` (not `.100000`), 10 ms → `.010`.
#[test]
fn test_timestamp_display_precision_trailing_zeros_stripped() {
let tz = Timezone::from_tz_string("UTC").unwrap();
let base_s: i64 = 1_780_372_200;
// 100 ms → ".100"
let ts = Timestamp::new(base_s * 1_000 + 100, TimeUnit::Millisecond);
assert_eq!(
ts.to_timezone_aware_string(Some(&tz)),
"2026-06-02 03:50:00.100",
"100 ms must display as '.100'"
);
// 100_000 µs → ".100" (multiple of 1ms, so 3 digits)
let ts = Timestamp::new(base_s * 1_000_000 + 100_000, TimeUnit::Microsecond);
assert_eq!(
ts.to_timezone_aware_string(Some(&tz)),
"2026-06-02 03:50:00.100",
"100_000 µs must display as '.100'"
);
// 100_000_000 ns → ".100" (multiple of 1ms, so 3 digits)
let ts = Timestamp::new(base_s * 1_000_000_000 + 100_000_000, TimeUnit::Nanosecond);
assert_eq!(
ts.to_timezone_aware_string(Some(&tz)),
"2026-06-02 03:50:00.100",
"100_000_000 ns must display as '.100'"
);
// 10 ms → ".010"
let ts = Timestamp::new(base_s * 1_000 + 10, TimeUnit::Millisecond);
assert_eq!(
ts.to_timezone_aware_string(Some(&tz)),
"2026-06-02 03:50:00.010",
"10 ms must display as '.010'"
);
// 1 ms → ".001"
let ts = Timestamp::new(base_s * 1_000 + 1, TimeUnit::Millisecond);
assert_eq!(
ts.to_timezone_aware_string(Some(&tz)),
"2026-06-02 03:50:00.001",
"1 ms must display as '.001'"
);
}
/// Verify the Unix epoch (timestamp == 0) formats correctly in UTC.
#[test]
fn test_timestamp_display_unix_epoch() {
let tz = Timezone::from_tz_string("UTC").unwrap();
let ts_s = Timestamp::new(0, TimeUnit::Second);
assert_eq!(
ts_s.to_timezone_aware_string(Some(&tz)),
"1970-01-01 00:00:00"
);
let ts_ms = Timestamp::new(0, TimeUnit::Millisecond);
assert_eq!(
ts_ms.to_timezone_aware_string(Some(&tz)),
"1970-01-01 00:00:00"
);
let ts_us = Timestamp::new(0, TimeUnit::Microsecond);
assert_eq!(
ts_us.to_timezone_aware_string(Some(&tz)),
"1970-01-01 00:00:00"
);
let ts_ns = Timestamp::new(0, TimeUnit::Nanosecond);
assert_eq!(
ts_ns.to_timezone_aware_string(Some(&tz)),
"1970-01-01 00:00:00"
);
}
/// Negative timestamps (before the Unix epoch) must format correctly.
#[test]
fn test_timestamp_display_negative_value() {
let tz = Timezone::from_tz_string("UTC").unwrap();
// -1 second = 1969-12-31 23:59:59
let ts = Timestamp::new(-1, TimeUnit::Second);
assert_eq!(
ts.to_timezone_aware_string(Some(&tz)),
"1969-12-31 23:59:59"
);
// -1 millisecond = 1969-12-31 23:59:59.999
let ts = Timestamp::new(-1, TimeUnit::Millisecond);
assert_eq!(
ts.to_timezone_aware_string(Some(&tz)),
"1969-12-31 23:59:59.999"
);
}
/// Verify that timezone offset shifts the display time correctly.
#[test]
fn test_timestamp_display_timezone_offset() {
// 2026-06-02 03:50:00 UTC = 2026-06-02 09:20:00 in +05:30 (India)
let base_s: i64 = 1_780_372_200;
let tz_utc = Timezone::from_tz_string("UTC").unwrap();
let tz_india = Timezone::from_tz_string("+05:30").unwrap();
let tz_neg = Timezone::from_tz_string("-08:00").unwrap();
let ts = Timestamp::new(base_s * 1_000 + 195, TimeUnit::Millisecond);
assert_eq!(
ts.to_timezone_aware_string(Some(&tz_utc)),
"2026-06-02 03:50:00.195"
);
assert_eq!(
ts.to_timezone_aware_string(Some(&tz_india)),
"2026-06-02 09:20:00.195"
);
assert_eq!(
ts.to_timezone_aware_string(Some(&tz_neg)),
"2026-06-01 19:50:00.195"
);
}
// ── Column schema convenience constructors ────────────────────────────────────
/// All four timestamp types should map to `MYSQL_TYPE_TIMESTAMP`.
#[test]
fn test_create_mysql_column_all_timestamp_precisions() {
let timestamp_types: &[(&str, ConcreteDataType)] = &[
(
"ts_s",
ConcreteDataType::Timestamp(datatypes::types::TimestampType::Second(
TimestampSecondType,
)),
),
(
"ts_ms",
ConcreteDataType::Timestamp(datatypes::types::TimestampType::Millisecond(
TimestampMillisecondType,
)),
),
(
"ts_us",
ConcreteDataType::Timestamp(datatypes::types::TimestampType::Microsecond(
TimestampMicrosecondType,
)),
),
(
"ts_ns",
ConcreteDataType::Timestamp(datatypes::types::TimestampType::Nanosecond(
TimestampNanosecondType,
)),
),
];
for (name, dt) in timestamp_types {
let col = create_mysql_column(dt, name).unwrap();
assert_eq!(
ColumnType::MYSQL_TYPE_TIMESTAMP,
col.coltype,
"timestamp type '{name}' must map to MYSQL_TYPE_TIMESTAMP"
);
assert!(
!col.colflags.contains(ColumnFlags::UNSIGNED_FLAG),
"timestamps must not be unsigned"
);
}
}
/// All three interval types should map to `MYSQL_TYPE_VARCHAR`.
#[test]
fn test_create_mysql_column_all_interval_types() {
let interval_types: &[(&str, ConcreteDataType)] = &[
(
"iv_ym",
ConcreteDataType::Interval(datatypes::types::IntervalType::YearMonth(
IntervalYearMonthType,
)),
),
(
"iv_dt",
ConcreteDataType::Interval(datatypes::types::IntervalType::DayTime(
IntervalDayTimeType,
)),
),
(
"iv_mdn",
ConcreteDataType::Interval(datatypes::types::IntervalType::MonthDayNano(
IntervalMonthDayNanoType,
)),
),
];
for (name, dt) in interval_types {
let col = create_mysql_column(dt, name).unwrap();
assert_eq!(
ColumnType::MYSQL_TYPE_VARCHAR,
col.coltype,
"interval type '{name}' must map to MYSQL_TYPE_VARCHAR"
);
}
}
/// create_mysql_column_def must preserve the ordering and names of all columns.
#[test]
fn test_create_mysql_column_def_preserves_order() {
let schemas: Vec<ColumnSchema> = vec![
ColumnSchema::new("alpha", ConcreteDataType::int32_datatype(), true),
ColumnSchema::new("beta", ConcreteDataType::string_datatype(), true),
ColumnSchema::new("gamma", ConcreteDataType::float64_datatype(), true),
ColumnSchema::new(
"delta",
ConcreteDataType::timestamp_millisecond_datatype(),
false,
),
];
let schema = Arc::new(Schema::new(schemas.clone()));
let cols = create_mysql_column_def(&schema).unwrap();
assert_eq!(4, cols.len());
assert_eq!("alpha", cols[0].column);
assert_eq!(ColumnType::MYSQL_TYPE_LONG, cols[0].coltype);
assert_eq!("beta", cols[1].column);
assert_eq!(ColumnType::MYSQL_TYPE_VARCHAR, cols[1].coltype);
assert_eq!("gamma", cols[2].column);
assert_eq!(ColumnType::MYSQL_TYPE_DOUBLE, cols[2].coltype);
assert_eq!("delta", cols[3].column);
assert_eq!(ColumnType::MYSQL_TYPE_TIMESTAMP, cols[3].coltype);
}
/// An empty schema must produce an empty column-def list (no panic).
#[test]
fn test_create_mysql_column_def_empty_schema() {
let schema = Arc::new(Schema::new(vec![]));
let cols = create_mysql_column_def(&schema).unwrap();
assert!(cols.is_empty(), "empty schema must produce no columns");
}
/// Decimal128 must map to MYSQL_TYPE_DECIMAL regardless of precision/scale.
#[test]
fn test_create_mysql_column_decimal_variants() {
for (p, s) in [(10u8, 2i8), (38, 10), (5, 0)] {
let dt = ConcreteDataType::decimal128_datatype(p, s);
let col = create_mysql_column(&dt, "dec").unwrap();
assert_eq!(ColumnType::MYSQL_TYPE_DECIMAL, col.coltype);
assert!(!col.colflags.contains(ColumnFlags::UNSIGNED_FLAG));
}
}