Skip to main content

sqlx_json/
sqlite.rs

1//! [`RowExt`](crate::RowExt) implementation for `SQLite` rows.
2
3use base64::Engine as _;
4use base64::engine::general_purpose::STANDARD as BASE64;
5use serde_json::{Map, Value};
6use sqlx::sqlite::SqliteRow;
7use sqlx::{Column, Row, TypeInfo, ValueRef};
8
9use crate::RowExt;
10
11impl RowExt for SqliteRow {
12    fn to_json(&self) -> Value {
13        let columns = self.columns();
14        let mut map = Map::with_capacity(columns.len());
15
16        for column in columns {
17            let idx = column.ordinal();
18            let type_name = column.type_info().name();
19
20            let value = if self.try_get_raw(idx).is_ok_and(|v| v.is_null()) {
21                Value::Null
22            } else {
23                match type_name {
24                    "BOOLEAN" | "BOOL" => self.try_get::<bool, _>(idx).map_or(Value::Null, Value::Bool),
25
26                    "INTEGER" | "INT" | "BIGINT" | "SMALLINT" | "TINYINT" | "MEDIUMINT" => self
27                        .try_get::<i64, _>(idx)
28                        .map_or(Value::Null, |v| Value::Number(v.into())),
29
30                    "REAL" | "FLOAT" | "DOUBLE" | "NUMERIC" => self
31                        .try_get::<f64, _>(idx)
32                        .ok()
33                        .and_then(serde_json::Number::from_f64)
34                        .map_or(Value::Null, Value::Number),
35
36                    "BLOB" => self
37                        .try_get::<Vec<u8>, _>(idx)
38                        .map_or(Value::Null, |bytes| Value::String(BASE64.encode(&bytes))),
39
40                    "TEXT" | "VARCHAR" | "CHAR" | "CLOB" | "DATE" | "DATETIME" | "TIMESTAMP" | "TIME" => {
41                        self.try_get::<String, _>(idx).map_or(Value::Null, Value::String)
42                    }
43
44                    // SQLite reports "NULL" type for expressions like COUNT(*), SUM().
45                    // The value is not null (checked above), so probe: i64 → f64 → String.
46                    _ => dynamic_probe(self, idx),
47                }
48            };
49
50            map.insert(column.name().to_string(), value);
51        }
52
53        Value::Object(map)
54    }
55}
56
57/// Probes a `SQLite` value by trying types in order: i64 → f64 → String → Null.
58fn dynamic_probe(row: &SqliteRow, idx: usize) -> Value {
59    if let Ok(n) = row.try_get::<i64, _>(idx) {
60        return Value::Number(n.into());
61    }
62    if let Ok(f) = row.try_get::<f64, _>(idx) {
63        return serde_json::Number::from_f64(f).map_or(Value::Null, Value::Number);
64    }
65    row.try_get::<String, _>(idx).map_or(Value::Null, Value::String)
66}
67
68// Unit tests for row conversion are not possible without a database connection
69// because sqlx row types have no public constructors. All conversion tests
70// are covered by the integration test suite (./tests/run.sh).