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            PgTypeInfo::with_name("TEXT")
91        }
92    }
93}
94
95// ─── MySQL ────────────────────────────────────────────────────────────────────
96
97#[cfg(feature = "mysql")]
98mod mysql_impl {
99    use super::BindValue;
100    use sqlx::encode::{Encode, IsNull};
101    use sqlx::mysql::{MySql, MySqlTypeInfo};
102    use sqlx::Database;
103
104    impl<'q> Encode<'q, MySql> for BindValue {
105        fn encode_by_ref(
106            &self,
107            buf: &mut <MySql as Database>::ArgumentBuffer<'q>,
108        ) -> Result<IsNull, Box<dyn std::error::Error + Send + Sync>> {
109            Ok(match self {
110                BindValue::Null => <Option<i32> as Encode<MySql>>::encode_by_ref(&None, buf)?,
111                BindValue::Bool(b) => <i32 as Encode<MySql>>::encode_by_ref(&(*b as i32), buf)?,
112                BindValue::I64(n) => <i64 as Encode<MySql>>::encode_by_ref(n, buf)?,
113                BindValue::F64(n) => <f64 as Encode<MySql>>::encode_by_ref(n, buf)?,
114                BindValue::String(s) => <String as Encode<MySql>>::encode_by_ref(s, buf)?,
115                BindValue::Uuid(u) => {
116                    let s = u.to_string();
117                    <String as Encode<MySql>>::encode_by_ref(&s, buf)?
118                }
119                BindValue::Json(v) => {
120                    let s = serde_json::to_string(v)
121                        .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
122                    <String as Encode<MySql>>::encode_by_ref(&s, buf)?
123                }
124            })
125        }
126    }
127
128    impl sqlx::Type<MySql> for BindValue {
129        fn type_info() -> MySqlTypeInfo {
130            <String as sqlx::Type<MySql>>::type_info()
131        }
132    }
133}
134
135// ─── SQLite ───────────────────────────────────────────────────────────────────
136
137#[cfg(feature = "sqlite")]
138mod sqlite_impl {
139    use super::BindValue;
140    use sqlx::encode::{Encode, IsNull};
141    use sqlx::sqlite::{Sqlite, SqliteTypeInfo};
142    use sqlx::Database;
143
144    impl<'q> Encode<'q, Sqlite> for BindValue {
145        fn encode_by_ref(
146            &self,
147            buf: &mut <Sqlite as Database>::ArgumentBuffer<'q>,
148        ) -> Result<IsNull, Box<dyn std::error::Error + Send + Sync>> {
149            Ok(match self {
150                BindValue::Null => <Option<i32> as Encode<Sqlite>>::encode_by_ref(&None, buf)?,
151                BindValue::Bool(b) => <i32 as Encode<Sqlite>>::encode_by_ref(&(*b as i32), buf)?,
152                BindValue::I64(n) => <i64 as Encode<Sqlite>>::encode_by_ref(n, buf)?,
153                BindValue::F64(n) => <f64 as Encode<Sqlite>>::encode_by_ref(n, buf)?,
154                BindValue::String(s) => <String as Encode<Sqlite>>::encode_by_ref(s, buf)?,
155                BindValue::Uuid(u) => {
156                    let s = u.to_string();
157                    <String as Encode<Sqlite>>::encode_by_ref(&s, buf)?
158                }
159                BindValue::Json(v) => {
160                    let s = serde_json::to_string(v)
161                        .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?;
162                    <String as Encode<Sqlite>>::encode_by_ref(&s, buf)?
163                }
164            })
165        }
166    }
167
168    impl sqlx::Type<Sqlite> for BindValue {
169        fn type_info() -> SqliteTypeInfo {
170            <String as sqlx::Type<Sqlite>>::type_info()
171        }
172    }
173}
174
175/// Backward-compat alias — existing call sites referencing PgBindValue continue to compile.
176#[cfg(feature = "postgres")]
177pub type PgBindValue = BindValue;