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    /// `String`, `char` and text columns: values use the backend's TEXT cast.
120    Text,
121    /// JSON documents.
122    Json,
123    /// String-backed PostgreSQL `CHAR(n)`; opt in per model column.
124    BpChar,
125    /// `f64` columns.
126    Double,
127    /// `f32` columns.
128    Float,
129    /// `i32` columns, including typed NULLs.
130    Int,
131    /// `i64` columns, including typed NULLs.
132    BigInt,
133    /// `bool` columns.
134    Boolean,
135    /// Timestamps, bytes, UUIDs and custom values: bound unchanged.
136    Other,
137    /// UTC wall-clock timestamps, bound as PostgreSQL `TIMESTAMP`.
138    NaiveTimestamp,
139}
140
141/// Failure to represent a bound value as a model field.
142#[derive(Clone, Copy, Debug, PartialEq, Eq)]
143pub struct ValueTypeError;
144
145impl std::fmt::Display for ValueTypeError {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        f.write_str("value cannot be represented by its model field")
148    }
149}
150
151impl std::error::Error for ValueTypeError {}
152
153/// A model field type which converts to and from a bound [`SqlValue`].
154///
155/// Conversions preserve the field representation: strings accept TEXT and
156/// explicitly selected bpchar values; optional fields accept their typed `NULL`. `AuthEntity` uses this to stage
157/// additional fields and to materialize models without a database round trip.
158pub trait SqlxValue: Sized {
159    /// The column category used to coerce configured raw values.
160    const KIND: ColumnKind;
161
162    /// The typed `NULL` of this field type.
163    fn null() -> SqlValue;
164
165    fn into_sql_value(self) -> SqlValue;
166
167    /// # Errors
168    ///
169    /// Returns an error if the value has a different type.
170    fn from_sql_value(value: SqlValue) -> Result<Self, ValueTypeError>;
171
172    /// Prepare a staged value for `engine`, after application hooks and
173    /// before the write. JSON metadata binds exact JavaScript text on SQLite;
174    /// every other value is bound unchanged.
175    ///
176    /// # Errors
177    ///
178    /// Returns an error if the value cannot be serialized for `engine`.
179    fn prepare(self, engine: Engine) -> AuthResult<Self> {
180        let _ = engine;
181        Ok(self)
182    }
183}
184
185macro_rules! sqlx_value {
186    ($($type:ty => $variant:ident, $kind:ident),* $(,)?) => {
187        $(
188            impl SqlxValue for $type {
189                const KIND: ColumnKind = ColumnKind::$kind;
190                fn null() -> SqlValue {
191                    SqlValue::$variant(None)
192                }
193                fn into_sql_value(self) -> SqlValue {
194                    SqlValue::$variant(Some(self))
195                }
196                fn from_sql_value(value: SqlValue) -> Result<Self, ValueTypeError> {
197                    match value {
198                        SqlValue::$variant(Some(value)) => Ok(value),
199                        _ => Err(ValueTypeError),
200                    }
201                }
202            }
203        )*
204    };
205}
206
207sqlx_value!(
208    bool => Bool, Boolean,
209    i32 => Int, Int,
210    i64 => BigInt, BigInt,
211    f32 => Float, Float,
212    f64 => Double, Double,
213    Vec<u8> => Bytes, Other,
214    DateTime<Utc> => Timestamp, Other,
215    NaiveDateTime => NaiveTimestamp, NaiveTimestamp,
216    uuid::Uuid => Uuid, Other,
217);
218
219impl SqlxValue for serde_json::Value {
220    const KIND: ColumnKind = ColumnKind::Json;
221    fn null() -> SqlValue {
222        SqlValue::Json(None)
223    }
224    fn into_sql_value(self) -> SqlValue {
225        SqlValue::Json(Some(Box::new(self)))
226    }
227    fn from_sql_value(value: SqlValue) -> Result<Self, ValueTypeError> {
228        match value {
229            SqlValue::Json(Some(value)) => Ok(*value),
230            _ => Err(ValueTypeError),
231        }
232    }
233}
234
235impl<T: SqlxValue> SqlxValue for Option<T> {
236    const KIND: ColumnKind = T::KIND;
237    fn null() -> SqlValue {
238        T::null()
239    }
240    fn into_sql_value(self) -> SqlValue {
241        self.map_or_else(T::null, T::into_sql_value)
242    }
243    fn from_sql_value(value: SqlValue) -> Result<Self, ValueTypeError> {
244        if value == T::null()
245            || matches!(
246                (&value, T::null()),
247                (SqlValue::BpChar(None), SqlValue::Text(None))
248            )
249        {
250            Ok(None)
251        } else {
252            T::from_sql_value(value).map(Some)
253        }
254    }
255    fn prepare(self, engine: Engine) -> AuthResult<Self> {
256        self.map(|value| value.prepare(engine)).transpose()
257    }
258}
259
260impl SqlxValue for String {
261    const KIND: ColumnKind = ColumnKind::Text;
262    fn null() -> SqlValue {
263        SqlValue::Text(None)
264    }
265    fn into_sql_value(self) -> SqlValue {
266        SqlValue::Text(Some(self))
267    }
268    fn from_sql_value(value: SqlValue) -> Result<Self, ValueTypeError> {
269        match value {
270            SqlValue::Text(Some(value)) | SqlValue::BpChar(Some(value)) => Ok(value),
271            _ => Err(ValueTypeError),
272        }
273    }
274}