Skip to main content

sqlx_json/
postgres.rs

1//! [`RowExt`](crate::RowExt) implementation for `PostgreSQL` rows.
2//!
3//! Type names are normalized to uppercase because sqlx may return either case
4//! depending on the query context. Integer types use size-specific Rust types
5//! (`i16`, `i32`, `i64`) because sqlx enforces strict type matching for
6//! `PostgreSQL`. Temporal types (`DATE`, `TIME`, `TIMESTAMP`, `TIMESTAMPTZ`)
7//! are decoded via sqlx's `chrono` integration and serialized as RFC 3339
8//! strings; `TIMESTAMPTZ` is normalized to UTC and emitted with a `Z` suffix.
9
10use base64::Engine as _;
11use base64::engine::general_purpose::STANDARD as BASE64;
12use serde_json::{Map, Value};
13use sqlx::postgres::PgRow;
14use sqlx::types::chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, Utc};
15use sqlx::{Column, Row, TypeInfo, ValueRef};
16
17use crate::RowExt;
18
19impl RowExt for PgRow {
20    fn to_json(&self) -> Value {
21        let columns = self.columns();
22        let mut map = Map::with_capacity(columns.len());
23
24        for column in columns {
25            let idx = column.ordinal();
26            let type_name = column.type_info().name().to_ascii_uppercase();
27
28            let value = if self.try_get_raw(idx).is_ok_and(|v| v.is_null()) {
29                Value::Null
30            } else {
31                match type_name.as_str() {
32                    "BOOL" => self.try_get::<bool, _>(idx).map_or(Value::Null, Value::Bool),
33
34                    "INT8" => self
35                        .try_get::<i64, _>(idx)
36                        .map_or(Value::Null, |v| Value::Number(v.into())),
37
38                    "INT4" | "OID" => self
39                        .try_get::<i32, _>(idx)
40                        .map_or(Value::Null, |v| Value::Number(i64::from(v).into())),
41
42                    "INT2" => self
43                        .try_get::<i16, _>(idx)
44                        .map_or(Value::Null, |v| Value::Number(i64::from(v).into())),
45
46                    "FLOAT4" | "FLOAT8" | "NUMERIC" | "MONEY" => self
47                        .try_get::<f64, _>(idx)
48                        .ok()
49                        .and_then(serde_json::Number::from_f64)
50                        .map_or(Value::Null, Value::Number),
51
52                    "BYTEA" => self
53                        .try_get::<Vec<u8>, _>(idx)
54                        .map_or(Value::Null, |bytes| Value::String(BASE64.encode(&bytes))),
55
56                    "JSON" | "JSONB" => self.try_get::<Value, _>(idx).unwrap_or(Value::Null),
57
58                    "DATE" => self
59                        .try_get::<NaiveDate, _>(idx)
60                        .map_or(Value::Null, |v| Value::String(v.to_string())),
61
62                    "TIME" => self
63                        .try_get::<NaiveTime, _>(idx)
64                        .map_or(Value::Null, |v| Value::String(v.to_string())),
65
66                    "TIMESTAMP" => self
67                        .try_get::<NaiveDateTime, _>(idx)
68                        .map_or(Value::Null, |v| Value::String(format!("{}T{}", v.date(), v.time()))),
69
70                    "TIMESTAMPTZ" => self.try_get::<DateTime<Utc>, _>(idx).map_or(Value::Null, |v| {
71                        let n = v.naive_utc();
72                        Value::String(format!("{}T{}Z", n.date(), n.time()))
73                    }),
74
75                    _ => self.try_get::<String, _>(idx).map_or(Value::Null, Value::String),
76                }
77            };
78
79            map.insert(column.name().to_string(), value);
80        }
81
82        Value::Object(map)
83    }
84}