Skip to main content

sqlx_json/
mysql.rs

1//! [`RowExt`](crate::RowExt) implementation for `MySQL` rows.
2//!
3//! Uses `column.type_info().name()` to pick the right Rust type for each column.
4//! `MySQL` 9 reports `information_schema` text columns as `VARBINARY`; these
5//! are decoded as UTF-8 strings rather than base64. Temporal types (`DATE`,
6//! `TIME`, `DATETIME`, `TIMESTAMP`) are decoded via sqlx's `chrono` integration
7//! and serialized as naive ISO 8601 strings (no timezone offset). `DECIMAL`
8//! is decoded via `BigDecimal` to preserve precision; `FLOAT` is decoded as
9//! `f32` (sqlx-mysql strict-checks the column type), `DOUBLE` as `f64`.
10
11use bigdecimal::BigDecimal;
12use sqlx::mysql::MySqlRow;
13use sqlx::types::chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
14
15use crate::numeric::bigdecimal_to_json;
16use crate::prelude::*;
17
18impl RowExt for MySqlRow {
19    fn to_json(&self) -> Value {
20        let columns = self.columns();
21        let mut map = Map::with_capacity(columns.len());
22
23        for column in columns {
24            let idx = column.ordinal();
25            let type_name = column.type_info().name();
26
27            let value = if self.try_get_raw(idx).is_ok_and(|v| v.is_null()) {
28                Value::Null
29            } else {
30                match type_name {
31                    "BOOLEAN" => self.try_get::<bool, _>(idx).map_or(Value::Null, Value::Bool),
32
33                    "TINYINT" | "SMALLINT" | "INT" | "MEDIUMINT" | "BIGINT" | "TINYINT UNSIGNED"
34                    | "SMALLINT UNSIGNED" | "INT UNSIGNED" | "MEDIUMINT UNSIGNED" | "YEAR" => self
35                        .try_get::<i64, _>(idx)
36                        .map_or(Value::Null, |v| Value::Number(v.into())),
37
38                    "BIGINT UNSIGNED" => self.try_get::<u64, _>(idx).map_or(Value::Null, |v| {
39                        i64::try_from(v)
40                            .map_or_else(|_| Value::String(v.to_string()), |signed| Value::Number(signed.into()))
41                    }),
42
43                    "DECIMAL" => self
44                        .try_get::<BigDecimal, _>(idx)
45                        .map_or(Value::Null, |v| bigdecimal_to_json(&v)),
46
47                    "FLOAT" => self.try_get::<f32, _>(idx).map_or(Value::Null, Value::from),
48
49                    "DOUBLE" => self.try_get::<f64, _>(idx).map_or(Value::Null, Value::from),
50
51                    "JSON" => self.try_get::<Value, _>(idx).unwrap_or(Value::Null),
52
53                    // MySQL 9 returns information_schema columns as BINARY/VARBINARY
54                    // even when they contain valid UTF-8. Try String first, then bytes.
55                    "BINARY" | "VARBINARY" => self
56                        .try_get::<String, _>(idx)
57                        .map_or_else(|_| bytes_to_json(self, idx), Value::String),
58
59                    "BLOB" | "TINYBLOB" | "MEDIUMBLOB" | "LONGBLOB" | "BIT" | "GEOMETRY" => bytes_to_json(self, idx),
60
61                    "DATE" => self
62                        .try_get::<NaiveDate, _>(idx)
63                        .map_or(Value::Null, |v| Value::String(v.to_string())),
64
65                    "TIME" => self
66                        .try_get::<NaiveTime, _>(idx)
67                        .map_or(Value::Null, |v| Value::String(v.to_string())),
68
69                    "DATETIME" => self
70                        .try_get::<NaiveDateTime, _>(idx)
71                        .map_or(Value::Null, |v| Value::String(format!("{}T{}", v.date(), v.time()))),
72
73                    // sqlx-mysql's NaiveDateTime decoder only accepts ColumnType::Datetime,
74                    // not Timestamp. DateTime<Utc> accepts both; we then strip the zone
75                    // via naive_utc() so the wire shape matches the naive ISO 8601 form.
76                    "TIMESTAMP" => self.try_get::<DateTime<Utc>, _>(idx).map_or(Value::Null, |v| {
77                        let n = v.naive_utc();
78                        Value::String(format!("{}T{}", n.date(), n.time()))
79                    }),
80
81                    // All other types (VARCHAR, TEXT, ENUM, etc.) → String
82                    _ => self
83                        .try_get::<String, _>(idx)
84                        .map_or_else(|_| bytes_to_json(self, idx), Value::String),
85                }
86            };
87
88            map.insert(column.name().to_string(), value);
89        }
90
91        Value::Object(map)
92    }
93}
94
95/// Extracts a `MySQL` binary column as UTF-8 string, falling back to base64.
96fn bytes_to_json(row: &MySqlRow, idx: usize) -> Value {
97    row.try_get::<Vec<u8>, _>(idx).map_or(Value::Null, |bytes| {
98        String::from_utf8(bytes.clone()).map_or_else(|_| Value::String(BASE64.encode(&bytes)), Value::String)
99    })
100}