use base64::Engine as _;
use ecat_data::{RdbmsError, Row};
use sqlx::{Column as SqlxColumn, Row as SqlxRow, TypeInfo as _, ValueRef as SqlxValueRef};
fn float_to_json(n: f64) -> serde_json::Value {
if n.is_finite() {
serde_json::Number::from_f64(n).map_or(serde_json::Value::Null, serde_json::Value::Number)
} else if n.is_nan() {
serde_json::Value::String("NaN".into())
} else if n > 0.0 {
serde_json::Value::String("Infinity".into())
} else {
serde_json::Value::String("-Infinity".into())
}
}
fn f32_to_json(n: f32) -> serde_json::Value {
match n.to_string().parse::<f64>() {
Ok(v) => float_to_json(v),
Err(_) => float_to_json(n as f64),
}
}
fn rfc3339(dt: time::OffsetDateTime, col: &str) -> Result<String, RdbmsError> {
dt.to_offset(time::UtcOffset::UTC)
.format(&time::format_description::well_known::Rfc3339)
.map_err(|e| {
RdbmsError::Database(format!(
"failed to format datetime column {col} as RFC3339: {e}"
))
})
}
macro_rules! cell_fn {
($name:ident, $row:ty) => {
cell_fn!(@chain $name, $row);
};
($name:ident, $row:ty, unsigned) => {
cell_fn!(@chain $name, $row, u64);
};
(@chain $name:ident, $row:ty $(, $unsigned:ty)?) => {
pub fn $name(row: &$row, col: &str) -> Result<serde_json::Value, RdbmsError> {
if row.try_get_raw(col).is_ok_and(|v| v.is_null()) {
return Ok(serde_json::Value::Null);
}
if let Ok(n) = row.try_get::<i64, _>(col) {
return Ok(serde_json::Value::Number(n.into()));
}
if let Ok(n) = row.try_get::<i32, _>(col) {
return Ok(serde_json::Value::Number((n as i64).into()));
}
if let Ok(n) = row.try_get::<i16, _>(col) {
return Ok(serde_json::Value::Number((n as i64).into()));
}
if let Ok(n) = row.try_get::<f64, _>(col) {
return Ok(float_to_json(n));
}
if let Ok(n) = row.try_get::<f32, _>(col) {
return Ok(f32_to_json(n));
}
$(
if let Ok(n) = row.try_get::<$unsigned, _>(col) {
return Ok(serde_json::Value::Number(n.into()));
}
)?
if let Ok(b) = row.try_get::<bool, _>(col) {
return Ok(serde_json::Value::Bool(b));
}
if let Ok(dt) = row.try_get::<time::OffsetDateTime, _>(col) {
return Ok(serde_json::Value::String(rfc3339(dt, col)?));
}
if let Ok(dt) = row.try_get::<time::PrimitiveDateTime, _>(col) {
return Ok(serde_json::Value::String(rfc3339(dt.assume_utc(), col)?));
}
if let Ok(d) = row.try_get::<time::Date, _>(col) {
return Ok(serde_json::Value::String(d.to_string()));
}
if let Ok(s) = row.try_get::<String, _>(col) {
return Ok(serde_json::Value::String(s));
}
if let Ok(b) = row.try_get::<Vec<u8>, _>(col) {
return Ok(serde_json::Value::String(
base64::engine::general_purpose::STANDARD.encode(b),
));
}
Err(RdbmsError::Database(format!(
"unsupported column type in result set: {col} ({})",
row.try_get_raw(col)
.map(|v| v.type_info().name().to_string())
.unwrap_or_else(|_| "unknown".into())
)))
}
};
}
cell_fn!(pg_cell_to_json, sqlx::postgres::PgRow);
cell_fn!(mysql_cell_to_json, sqlx::mysql::MySqlRow, unsigned);
cell_fn!(sqlite_cell_to_json, sqlx::sqlite::SqliteRow);
macro_rules! rows_fn {
($name:ident, $row:ty, $cell:ident) => {
pub fn $name(rows: Vec<$row>) -> Result<Vec<Row>, RdbmsError> {
if rows.is_empty() {
return Ok(Vec::new());
}
let columns: Vec<String> = rows[0]
.columns()
.iter()
.map(|c| c.name().to_string())
.collect();
rows.iter()
.map(|row| {
let values = columns
.iter()
.map(|col| $cell(row, col))
.collect::<Result<Vec<serde_json::Value>, RdbmsError>>()?;
Ok(Row::new(columns.clone(), values))
})
.collect()
}
};
}
rows_fn!(pg_rows_to_result, sqlx::postgres::PgRow, pg_cell_to_json);
rows_fn!(
mysql_rows_to_result,
sqlx::mysql::MySqlRow,
mysql_cell_to_json
);
rows_fn!(
sqlite_rows_to_result,
sqlx::sqlite::SqliteRow,
sqlite_cell_to_json
);
#[cfg(test)]
mod tests {
use super::{f32_to_json, rfc3339};
use crate::tests::mem_sqlite;
use ecat_data::SqlExecutor;
#[tokio::test]
async fn datetime_round_trips_as_rfc3339() {
let db = mem_sqlite("cell_datetime").await;
db.execute("CREATE TABLE t (id INTEGER PRIMARY KEY, at TEXT NOT NULL)")
.await
.unwrap();
db.execute_with(
"INSERT INTO t (id, at) VALUES (?, ?)",
&[
serde_json::json!(1),
serde_json::json!("2026-10-05T12:34:56+08:00"),
],
)
.await
.unwrap();
let rows = db.query("SELECT at FROM t").await.unwrap();
let value = rows[0].get("at").unwrap().as_str().unwrap();
assert!(value.starts_with("2026-10-05"), "got: {value}");
assert_eq!(value, "2026-10-05T04:34:56Z", "时间分支未生效: {value}");
}
#[tokio::test]
async fn date_shaped_text_round_trips_as_plain_date() {
let db = mem_sqlite("cell_date").await;
let rows = db.query("SELECT '2026-10-05' AS d").await.unwrap();
assert_eq!(
rows[0].get("d"),
Some(&serde_json::json!("2026-10-05")),
"Date 分支未生效或不是纯日期"
);
}
#[test]
fn f32_uses_shortest_round_trip_representation() {
assert_eq!(f32_to_json(0.1f32), serde_json::json!(0.1));
assert_eq!(f32_to_json(1.5f32), serde_json::json!(1.5));
assert_eq!(f32_to_json(f32::NAN), serde_json::json!("NaN"));
assert_eq!(f32_to_json(f32::INFINITY), serde_json::json!("Infinity"));
}
#[test]
fn rfc3339_errors_on_out_of_range_year() {
let d = time::Date::from_calendar_date(-1, time::Month::January, 1).unwrap();
let msg = rfc3339(d.midnight().assume_utc(), "born_at")
.unwrap_err()
.to_string();
assert!(msg.contains("born_at") && msg.contains("RFC3339"), "{msg}");
}
#[tokio::test]
async fn bytes_become_base64() {
let db = mem_sqlite("cell_bytes").await;
db.execute("CREATE TABLE b (id INTEGER PRIMARY KEY, raw BLOB)")
.await
.unwrap();
db.execute("INSERT INTO b (id, raw) VALUES (1, X'0102FF')")
.await
.unwrap();
let rows = db.query("SELECT raw FROM b").await.unwrap();
assert_eq!(rows[0].get("raw").unwrap().as_str().unwrap(), "AQL/");
}
#[tokio::test]
async fn null_survives_conversion() {
let db = mem_sqlite("cell_null").await;
db.execute("CREATE TABLE n (id INTEGER PRIMARY KEY, v TEXT)")
.await
.unwrap();
db.execute("INSERT INTO n (id, v) VALUES (1, NULL)")
.await
.unwrap();
let rows = db.query("SELECT v FROM n").await.unwrap();
assert!(rows[0].get("v").unwrap().is_null());
}
#[tokio::test]
async fn integers_stay_numbers() {
let db = mem_sqlite("cell_int").await;
let rows = db.query("SELECT 42 AS n").await.unwrap();
assert_eq!(rows[0].get("n"), Some(&serde_json::json!(42)));
}
}