Skip to main content

architect_sdk/sql/
params.rs

1//! Convert serde_json::Value to bindable SQL parameter values across all dialects.
2
3use serde_json::Value;
4
5/// A value that can be bound to a SQL query parameter.
6/// Variants are dialect-agnostic; feature-gated `Encode` impls below handle the wire format.
7#[derive(Clone, Debug)]
8pub enum BindValue {
9    Null,
10    Bool(bool),
11    I64(i64),
12    F64(f64),
13    String(String),
14    Uuid(uuid::Uuid),
15    Json(Value),
16}
17
18impl BindValue {
19    pub fn from_json(v: &Value) -> Result<Self, crate::error::AppError> {
20        Ok(match v {
21            Value::Null => BindValue::Null,
22            Value::Bool(b) => BindValue::Bool(*b),
23            Value::Number(n) => {
24                if let Some(i) = n.as_i64() {
25                    BindValue::I64(i)
26                } else if let Some(f) = n.as_f64() {
27                    BindValue::F64(f)
28                } else {
29                    BindValue::I64(0)
30                }
31            }
32            Value::String(s) => {
33                if let Ok(u) = uuid::Uuid::parse_str(s) {
34                    BindValue::Uuid(u)
35                } else {
36                    BindValue::String(s.clone())
37                }
38            }
39            Value::Array(_) | Value::Object(_) => BindValue::Json(v.clone()),
40        })
41    }
42}
43
44// ─── PostgreSQL ───────────────────────────────────────────────────────────────
45
46#[cfg(feature = "postgres")]
47mod pg_impl {
48    use super::BindValue;
49    use sqlx::encode::{Encode, IsNull};
50    use sqlx::postgres::{PgTypeInfo, Postgres};
51    use sqlx::Database;
52
53    impl<'q> Encode<'q, Postgres> for BindValue {
54        fn encode_by_ref(
55            &self,
56            buf: &mut <Postgres as Database>::ArgumentBuffer<'q>,
57        ) -> Result<IsNull, Box<dyn std::error::Error + Send + Sync>> {
58            Ok(match self {
59                BindValue::Null => <Option<i32> as Encode<Postgres>>::encode_by_ref(&None, buf)?,
60                BindValue::Bool(b) => {
61                    let s: &str = if *b { "true" } else { "false" };
62                    <&str as Encode<Postgres>>::encode_by_ref(&s, buf)?
63                }
64                BindValue::I64(n) => {
65                    let s = n.to_string();
66                    <&str as Encode<Postgres>>::encode_by_ref(&s.as_str(), buf)?
67                }
68                BindValue::F64(n) => {
69                    let s = format!("{}", n);
70                    <&str as Encode<Postgres>>::encode_by_ref(&s.as_str(), buf)?
71                }
72                BindValue::String(s) => {
73                    <&str as Encode<Postgres>>::encode_by_ref(&s.as_str(), buf)?
74                }
75                BindValue::Uuid(u) => {
76                    let s = u.to_string();
77                    <&str as Encode<Postgres>>::encode_by_ref(&s.as_str(), buf)?
78                }
79                BindValue::Json(v) => {
80                    let s = serde_json::to_string(v)
81                        .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
82                    <&str as Encode<Postgres>>::encode_by_ref(&s.as_str(), buf)?
83                }
84            })
85        }
86    }
87
88    impl sqlx::Type<Postgres> for BindValue {
89        fn type_info() -> PgTypeInfo {
90            // All values are encoded as text on the wire. Non-text columns get an explicit
91            // SQL cast in the query (e.g. $1::integer) so PostgreSQL always knows the target
92            // type, making the parameter OID irrelevant for those columns.
93            PgTypeInfo::with_name("TEXT")
94        }
95
96        fn compatible(_ty: &PgTypeInfo) -> bool {
97            true
98        }
99    }
100}
101
102// ─── MySQL ────────────────────────────────────────────────────────────────────
103
104#[cfg(feature = "mysql")]
105mod mysql_impl {
106    use super::BindValue;
107    use sqlx::encode::{Encode, IsNull};
108    use sqlx::mysql::{MySql, MySqlTypeInfo};
109    use sqlx::Database;
110
111    impl<'q> Encode<'q, MySql> for BindValue {
112        fn encode_by_ref(
113            &self,
114            buf: &mut <MySql as Database>::ArgumentBuffer<'q>,
115        ) -> Result<IsNull, Box<dyn std::error::Error + Send + Sync>> {
116            Ok(match self {
117                BindValue::Null => <Option<i32> as Encode<MySql>>::encode_by_ref(&None, buf)?,
118                BindValue::Bool(b) => <i32 as Encode<MySql>>::encode_by_ref(&(*b as i32), buf)?,
119                BindValue::I64(n) => <i64 as Encode<MySql>>::encode_by_ref(n, buf)?,
120                BindValue::F64(n) => <f64 as Encode<MySql>>::encode_by_ref(n, buf)?,
121                BindValue::String(s) => <String as Encode<MySql>>::encode_by_ref(s, buf)?,
122                BindValue::Uuid(u) => {
123                    let s = u.to_string();
124                    <String as Encode<MySql>>::encode_by_ref(&s, buf)?
125                }
126                BindValue::Json(v) => {
127                    let s = serde_json::to_string(v)
128                        .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
129                    <String as Encode<MySql>>::encode_by_ref(&s, buf)?
130                }
131            })
132        }
133    }
134
135    impl sqlx::Type<MySql> for BindValue {
136        fn type_info() -> MySqlTypeInfo {
137            <String as sqlx::Type<MySql>>::type_info()
138        }
139    }
140}
141
142// ─── SQLite ───────────────────────────────────────────────────────────────────
143
144#[cfg(feature = "sqlite")]
145mod sqlite_impl {
146    use super::BindValue;
147    use sqlx::encode::{Encode, IsNull};
148    use sqlx::sqlite::{Sqlite, SqliteTypeInfo};
149    use sqlx::Database;
150
151    impl<'q> Encode<'q, Sqlite> for BindValue {
152        fn encode_by_ref(
153            &self,
154            buf: &mut <Sqlite as Database>::ArgumentBuffer<'q>,
155        ) -> Result<IsNull, Box<dyn std::error::Error + Send + Sync>> {
156            Ok(match self {
157                BindValue::Null => <Option<i32> as Encode<Sqlite>>::encode_by_ref(&None, buf)?,
158                BindValue::Bool(b) => <i32 as Encode<Sqlite>>::encode_by_ref(&(*b as i32), buf)?,
159                BindValue::I64(n) => <i64 as Encode<Sqlite>>::encode_by_ref(n, buf)?,
160                BindValue::F64(n) => <f64 as Encode<Sqlite>>::encode_by_ref(n, buf)?,
161                BindValue::String(s) => <String as Encode<Sqlite>>::encode_by_ref(s, buf)?,
162                BindValue::Uuid(u) => {
163                    let s = u.to_string();
164                    <String as Encode<Sqlite>>::encode_by_ref(&s, buf)?
165                }
166                BindValue::Json(v) => {
167                    let s = serde_json::to_string(v)
168                        .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
169                    <String as Encode<Sqlite>>::encode_by_ref(&s, buf)?
170                }
171            })
172        }
173    }
174
175    impl sqlx::Type<Sqlite> for BindValue {
176        fn type_info() -> SqliteTypeInfo {
177            <String as sqlx::Type<Sqlite>>::type_info()
178        }
179    }
180}
181
182/// Backward-compat alias — existing call sites referencing PgBindValue continue to compile.
183#[cfg(feature = "postgres")]
184pub type PgBindValue = BindValue;