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}