Skip to main content

alibi_sqlx/
value.rs

1//! Typed SQL values bound to `SQLx` arguments.
2//!
3//! Every variant carries its own nullable type, so PostgreSQL receives a typed
4//! `NULL` for each column rather than an untyped text parameter.
5
6use crate::pool::Engine;
7use alibi_core::error::AuthResult;
8use chrono::{DateTime, NaiveDateTime, Utc};
9
10/// A typed value bound to one SQL parameter.
11#[derive(Clone, Debug, PartialEq)]
12pub enum SqlValue {
13    Bool(Option<bool>),
14    Int(Option<i32>),
15    BigInt(Option<i64>),
16    Float(Option<f32>),
17    Double(Option<f64>),
18    Text(Option<String>),
19    /// PostgreSQL fixed-width character value (`bpchar`), including typed NULL.
20    /// Other engines bind the exact string as ordinary text.
21    BpChar(Option<String>),
22    Bytes(Option<Vec<u8>>),
23    Json(Option<Box<serde_json::Value>>),
24    /// Timezone-aware timestamp (PostgreSQL `TIMESTAMPTZ`).
25    Timestamp(Option<DateTime<Utc>>),
26    /// UTC wall-clock timestamp (PostgreSQL `TIMESTAMP WITHOUT TIME ZONE`).
27    NaiveTimestamp(Option<NaiveDateTime>),
28    Uuid(Option<uuid::Uuid>),
29}
30
31impl SqlValue {
32    /// Whether this value binds SQL `NULL`.
33    #[must_use]
34    pub const fn is_null(&self) -> bool {
35        matches!(
36            self,
37            Self::Bool(None)
38                | Self::Int(None)
39                | Self::BigInt(None)
40                | Self::Float(None)
41                | Self::Double(None)
42                | Self::Text(None)
43                | Self::BpChar(None)
44                | Self::Bytes(None)
45                | Self::Json(None)
46                | Self::Timestamp(None)
47                | Self::NaiveTimestamp(None)
48                | Self::Uuid(None)
49        )
50    }
51}
52
53macro_rules! sql_value_from {
54    ($($type:ty => $variant:ident),* $(,)?) => {
55        $(
56            impl From<$type> for SqlValue {
57                fn from(value: $type) -> Self {
58                    Self::$variant(Some(value))
59                }
60            }
61            impl From<Option<$type>> for SqlValue {
62                fn from(value: Option<$type>) -> Self {
63                    Self::$variant(value)
64                }
65            }
66        )*
67    };
68}
69
70sql_value_from!(
71    bool => Bool,
72    i32 => Int,
73    i64 => BigInt,
74    f32 => Float,
75    f64 => Double,
76    String => Text,
77    Vec<u8> => Bytes,
78    DateTime<Utc> => Timestamp,
79    NaiveDateTime => NaiveTimestamp,
80    uuid::Uuid => Uuid,
81);
82
83impl From<&str> for SqlValue {
84    fn from(value: &str) -> Self {
85        Self::Text(Some(value.to_owned()))
86    }
87}
88
89impl From<Option<&str>> for SqlValue {
90    fn from(value: Option<&str>) -> Self {
91        Self::Text(value.map(str::to_owned))
92    }
93}
94
95impl From<&String> for SqlValue {
96    fn from(value: &String) -> Self {
97        Self::Text(Some(value.clone()))
98    }
99}
100
101impl From<serde_json::Value> for SqlValue {
102    fn from(value: serde_json::Value) -> Self {
103        Self::Json(Some(Box::new(value)))
104    }
105}
106
107impl From<Option<serde_json::Value>> for SqlValue {
108    fn from(value: Option<serde_json::Value>) -> Self {
109        Self::Json(value.map(Box::new))
110    }
111}
112
113/// Storage category of a model column, derived from its Rust field type.
114///
115/// Configured additional fields use it to coerce raw JavaScript values before
116/// binding, exactly as the `SeaORM` adapter coerces by its column type.
117#[derive(Clone, Copy, Debug, PartialEq, Eq)]
118pub enum ColumnKind {
119    /// A Better Auth Number stored with INTEGER affinity.
120    Number,
121    /// `String`, `char` and text columns: values use the backend's TEXT cast.
122    Text,
123    /// JSON documents.
124    Json,
125    /// String-backed PostgreSQL `CHAR(n)`; opt in per model column.
126    BpChar,
127    /// `f64` columns.
128    Double,
129    /// `f32` columns.
130    Float,
131    /// `i32` columns, including typed NULLs.
132    Int,
133    /// `i64` columns, including typed NULLs.
134    BigInt,
135    /// `bool` columns.
136    Boolean,
137    /// Timestamps, bytes, UUIDs and custom values: bound unchanged.
138    Other,
139    /// UTC wall-clock timestamps, bound as PostgreSQL `TIMESTAMP`.
140    NaiveTimestamp,
141}
142
143/// Failure to represent a bound value as a model field.
144#[derive(Clone, Copy, Debug, PartialEq, Eq)]
145pub struct ValueTypeError;
146
147impl std::fmt::Display for ValueTypeError {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        f.write_str("value cannot be represented by its model field")
150    }
151}
152
153impl std::error::Error for ValueTypeError {}
154
155/// A model field type which converts to and from a bound [`SqlValue`].
156///
157/// Conversions preserve the field representation: strings accept TEXT and
158/// explicitly selected bpchar values; optional fields accept their typed `NULL`. `AuthEntity` uses this to stage
159/// additional fields and to materialize models without a database round trip.
160pub trait SqlxValue: Sized {
161    /// The column category used to coerce configured raw values.
162    const KIND: ColumnKind;
163
164    /// The typed `NULL` of this field type.
165    fn null() -> SqlValue;
166
167    fn into_sql_value(self) -> SqlValue;
168
169    /// # Errors
170    ///
171    /// Returns an error if the value has a different type.
172    fn from_sql_value(value: SqlValue) -> Result<Self, ValueTypeError>;
173
174    /// Prepare a staged value for `engine`, after application hooks and
175    /// before the write. JSON metadata binds exact JavaScript text on SQLite;
176    /// every other value is bound unchanged.
177    ///
178    /// # Errors
179    ///
180    /// Returns an error if the value cannot be serialized for `engine`.
181    fn prepare(self, _engine: Engine) -> AuthResult<Self> {
182        Ok(self)
183    }
184}
185
186macro_rules! sqlx_value {
187    ($($type:ty => $variant:ident, $kind:ident),* $(,)?) => {
188        $(
189            impl SqlxValue for $type {
190                const KIND: ColumnKind = ColumnKind::$kind;
191                fn null() -> SqlValue {
192                    SqlValue::$variant(None)
193                }
194                fn into_sql_value(self) -> SqlValue {
195                    SqlValue::$variant(Some(self))
196                }
197                fn from_sql_value(value: SqlValue) -> Result<Self, ValueTypeError> {
198                    match value {
199                        SqlValue::$variant(Some(value)) => Ok(value),
200                        _ => Err(ValueTypeError),
201                    }
202                }
203            }
204        )*
205    };
206}
207
208sqlx_value!(
209    bool => Bool, Boolean,
210    i32 => Int, Int,
211    i64 => BigInt, BigInt,
212    f32 => Float, Float,
213    f64 => Double, Double,
214    Vec<u8> => Bytes, Other,
215    DateTime<Utc> => Timestamp, Other,
216    NaiveDateTime => NaiveTimestamp, NaiveTimestamp,
217    uuid::Uuid => Uuid, Other,
218);
219
220impl SqlxValue for serde_json::Value {
221    const KIND: ColumnKind = ColumnKind::Json;
222    fn null() -> SqlValue {
223        SqlValue::Json(None)
224    }
225    fn into_sql_value(self) -> SqlValue {
226        SqlValue::Json(Some(Box::new(self)))
227    }
228    fn from_sql_value(value: SqlValue) -> Result<Self, ValueTypeError> {
229        match value {
230            SqlValue::Json(Some(value)) => Ok(*value),
231            _ => Err(ValueTypeError),
232        }
233    }
234}
235
236impl<T: SqlxValue> SqlxValue for Option<T> {
237    const KIND: ColumnKind = T::KIND;
238    fn null() -> SqlValue {
239        T::null()
240    }
241    fn into_sql_value(self) -> SqlValue {
242        self.map_or_else(T::null, T::into_sql_value)
243    }
244    fn from_sql_value(value: SqlValue) -> Result<Self, ValueTypeError> {
245        if value == T::null()
246            || matches!(
247                (&value, T::null()),
248                (SqlValue::BpChar(None), SqlValue::Text(None))
249            )
250        {
251            Ok(None)
252        } else {
253            T::from_sql_value(value).map(Some)
254        }
255    }
256    fn prepare(self, engine: Engine) -> AuthResult<Self> {
257        self.map(|value| value.prepare(engine)).transpose()
258    }
259}
260
261impl SqlxValue for String {
262    const KIND: ColumnKind = ColumnKind::Text;
263    fn null() -> SqlValue {
264        SqlValue::Text(None)
265    }
266    fn into_sql_value(self) -> SqlValue {
267        SqlValue::Text(Some(self))
268    }
269    fn from_sql_value(value: SqlValue) -> Result<Self, ValueTypeError> {
270        match value {
271            SqlValue::Text(Some(value)) | SqlValue::BpChar(Some(value)) => Ok(value),
272            _ => Err(ValueTypeError),
273        }
274    }
275}