Skip to main content

rust_ef_mysql/
row_conversion.rs

1use rust_ef::provider::DbValue;
2#[cfg(feature = "tracing")]
3use sqlx::Column;
4use sqlx::Row;
5
6/// Converts a cell from a `sqlx::mysql::MySqlRow` into a native `DbValue`.
7///
8/// Dispatches by attempting `try_get` on common Rust types in a specific
9/// order (bool → i16 → i32 → i64 → f32 → f64 → NaiveDateTime → NaiveDate →
10/// Uuid → String → Vec<u8>). This mirrors the PostgreSQL provider's
11/// `cell_to_db_value` approach and returns native `DbValue` variants directly
12/// (no String round-trip), eliminating the v1.5 silent-data-loss path where
13/// non-String columns were stringified and re-parsed.
14///
15/// `Ok(None)` from `try_get::<Option<T>>` indicates a SQL NULL — returned as
16/// `DbValue::Null`. `Err` indicates the column's native type is not
17/// representable as `T`, so we fall through to the next candidate type.
18pub(crate) fn cell_to_db_value(row: &sqlx::mysql::MySqlRow, col_idx: usize) -> DbValue {
19    // bool (TINYINT(1)) — must precede integer types, since MySQL booleans
20    // are stored as 0/1 and would otherwise be matched by i16/i32/i64.
21    if let Ok(v) = row.try_get::<Option<bool>, _>(col_idx) {
22        return v.map(DbValue::Bool).unwrap_or(DbValue::Null);
23    }
24    if let Ok(v) = row.try_get::<Option<i16>, _>(col_idx) {
25        return v.map(DbValue::I16).unwrap_or(DbValue::Null);
26    }
27    if let Ok(v) = row.try_get::<Option<i32>, _>(col_idx) {
28        return v.map(DbValue::I32).unwrap_or(DbValue::Null);
29    }
30    // i64 covers BIGINT (signed). UNSIGNED BIGINT (u64) may overflow i64 but
31    // sqlx returns it as i64 when possible; values > i64::MAX are rare in
32    // practice and will be caught by the String fallback below.
33    if let Ok(v) = row.try_get::<Option<i64>, _>(col_idx) {
34        return v.map(DbValue::I64).unwrap_or(DbValue::Null);
35    }
36    if let Ok(v) = row.try_get::<Option<f32>, _>(col_idx) {
37        return v.map(DbValue::F32).unwrap_or(DbValue::Null);
38    }
39    if let Ok(v) = row.try_get::<Option<f64>, _>(col_idx) {
40        return v.map(DbValue::F64).unwrap_or(DbValue::Null);
41    }
42    // NaiveDateTime (DATETIME, TIMESTAMP)
43    if let Ok(v) = row.try_get::<Option<chrono::NaiveDateTime>, _>(col_idx) {
44        return v.map(DbValue::NaiveDateTime).unwrap_or(DbValue::Null);
45    }
46    // NaiveDate (DATE)
47    if let Ok(v) = row.try_get::<Option<chrono::NaiveDate>, _>(col_idx) {
48        return v.map(DbValue::NaiveDate).unwrap_or(DbValue::Null);
49    }
50    // Uuid (CHAR(36))
51    if let Ok(v) = row.try_get::<Option<uuid::Uuid>, _>(col_idx) {
52        return v.map(DbValue::Uuid).unwrap_or(DbValue::Null);
53    }
54    // String (VARCHAR, TEXT, CHAR) — also catches DECIMAL since MySQL
55    // returns DECIMAL as string to preserve precision.
56    if let Ok(v) = row.try_get::<Option<String>, _>(col_idx) {
57        return v.map(DbValue::String).unwrap_or(DbValue::Null);
58    }
59    // bytes (BLOB, BINARY)
60    if let Ok(v) = row.try_get::<Option<Vec<u8>>, _>(col_idx) {
61        return v.map(DbValue::Bytes).unwrap_or(DbValue::Null);
62    }
63    // Unknown / unsupported type — last resort. Emit a tracing warning so
64    // production users can diagnose silent NULLs from unrecognized column
65    // types (e.g. GEOMETRY, JSON when not auto-detected as String).
66    #[cfg(feature = "tracing")]
67    {
68        let col_name = row
69            .columns()
70            .get(col_idx)
71            .map(|c| c.name())
72            .unwrap_or("unknown");
73        tracing::warn!(
74            column = col_name,
75            index = col_idx,
76            "MySQL column type unrecognized, returning NULL"
77        );
78    }
79    DbValue::Null
80}