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        Ok(self)
181    }
182}
183
184macro_rules! sqlx_value {
185    ($($type:ty => $variant:ident, $kind:ident),* $(,)?) => {
186        $(
187            impl SqlxValue for $type {
188                const KIND: ColumnKind = ColumnKind::$kind;
189                fn null() -> SqlValue {
190                    SqlValue::$variant(None)
191                }
192                fn into_sql_value(self) -> SqlValue {
193                    SqlValue::$variant(Some(self))
194                }
195                fn from_sql_value(value: SqlValue) -> Result<Self, ValueTypeError> {
196                    match value {
197                        SqlValue::$variant(Some(value)) => Ok(value),
198                        _ => Err(ValueTypeError),
199                    }
200                }
201            }
202        )*
203    };
204}
205
206sqlx_value!(
207    bool => Bool, Boolean,
208    i32 => Int, Int,
209    i64 => BigInt, BigInt,
210    f32 => Float, Float,
211    f64 => Double, Double,
212    Vec<u8> => Bytes, Other,
213    DateTime<Utc> => Timestamp, Other,
214    NaiveDateTime => NaiveTimestamp, NaiveTimestamp,
215    uuid::Uuid => Uuid, Other,
216);
217
218impl SqlxValue for serde_json::Value {
219    const KIND: ColumnKind = ColumnKind::Json;
220    fn null() -> SqlValue {
221        SqlValue::Json(None)
222    }
223    fn into_sql_value(self) -> SqlValue {
224        SqlValue::Json(Some(Box::new(self)))
225    }
226    fn from_sql_value(value: SqlValue) -> Result<Self, ValueTypeError> {
227        match value {
228            SqlValue::Json(Some(value)) => Ok(*value),
229            _ => Err(ValueTypeError),
230        }
231    }
232}
233
234impl<T: SqlxValue> SqlxValue for Option<T> {
235    const KIND: ColumnKind = T::KIND;
236    fn null() -> SqlValue {
237        T::null()
238    }
239    fn into_sql_value(self) -> SqlValue {
240        self.map_or_else(T::null, T::into_sql_value)
241    }
242    fn from_sql_value(value: SqlValue) -> Result<Self, ValueTypeError> {
243        if value == T::null()
244            || matches!(
245                (&value, T::null()),
246                (SqlValue::BpChar(None), SqlValue::Text(None))
247            )
248        {
249            Ok(None)
250        } else {
251            T::from_sql_value(value).map(Some)
252        }
253    }
254    fn prepare(self, engine: Engine) -> AuthResult<Self> {
255        self.map(|value| value.prepare(engine)).transpose()
256    }
257}
258
259impl SqlxValue for String {
260    const KIND: ColumnKind = ColumnKind::Text;
261    fn null() -> SqlValue {
262        SqlValue::Text(None)
263    }
264    fn into_sql_value(self) -> SqlValue {
265        SqlValue::Text(Some(self))
266    }
267    fn from_sql_value(value: SqlValue) -> Result<Self, ValueTypeError> {
268        match value {
269            SqlValue::Text(Some(value)) | SqlValue::BpChar(Some(value)) => Ok(value),
270            _ => Err(ValueTypeError),
271        }
272    }
273}