Skip to main content

vld_sqlx/
lib.rs

1//! # vld-sqlx — SQLx integration for `vld`
2//!
3//! Validate values against a [`vld`] schema before writing them with SQLx
4//! ([`validate_insert`], [`Validated`]) or after loading them
5//! ([`validate_row`], [`validate_rows`]), and bind validated column wrappers
6//! ([`VldText`], [`VldInt`], [`VldFloat`], [`VldBool`]) that implement
7//! `sqlx::Type`, `Encode` and `Decode`.
8//!
9//! Features: `sqlx-0_8` (default) or `sqlx-0_9` — exactly one; `sqlite`
10//! (default), `postgres`, `mysql`.
11//!
12//! ```
13//! use vld_sqlx::prelude::*;
14//!
15//! vld::schema! {
16//!     #[derive(Debug)]
17//!     pub struct NewUserSchema {
18//!         pub name: String  => vld::string().min(1).max(100),
19//!         pub email: String => vld::string().email(),
20//!     }
21//! }
22//!
23//! // Column schemas validate `{"value": ...}`.
24//! vld::schema! {
25//!     #[derive(Debug)]
26//!     pub struct EmailField {
27//!         pub value: String => vld::string().email(),
28//!     }
29//! }
30//!
31//! #[derive(serde::Serialize)]
32//! struct NewUser { name: String, email: String }
33//!
34//! let user = NewUser { name: "Alice".into(), email: "alice@example.com".into() };
35//! validate_insert::<NewUserSchema, _>(&user).unwrap();
36//! // sqlx::query("INSERT ...").bind(&user.name).bind(&user.email).execute(&pool).await?;
37//!
38//! let email = VldText::<EmailField>::new("carol@example.com").unwrap(); // bind(&email)
39//! assert!(VldText::<EmailField>::new("bad").is_err());
40//! # let _ = email;
41//! ```
42//!
43//! Decoding does not re-validate (data from the database is trusted); call
44//! [`validate_row`] to re-check loaded rows.
45//!
46//! Guide: <https://s00d.github.io/vld/integrations/database/sqlx>
47#![warn(missing_docs)]
48
49#[cfg(all(feature = "sqlx-0_8", feature = "sqlx-0_9"))]
50compile_error!("Enable exactly one SQLx version feature: `sqlx-0_8` or `sqlx-0_9`.");
51#[cfg(not(any(feature = "sqlx-0_8", feature = "sqlx-0_9")))]
52compile_error!("Enable one SQLx version feature: `sqlx-0_8` or `sqlx-0_9`.");
53
54#[cfg(feature = "sqlx-0_8")]
55use sqlx08 as sqlx;
56#[cfg(feature = "sqlx-0_9")]
57use sqlx09 as sqlx;
58
59pub use vld;
60
61#[cfg(feature = "sqlx-0_8")]
62type SqlxArgBuf<'q, DB> = <DB as sqlx::Database>::ArgumentBuffer<'q>;
63#[cfg(feature = "sqlx-0_9")]
64type SqlxArgBuf<'q, DB> = <DB as sqlx::Database>::ArgumentBuffer;
65
66// ========================= Error / Validated / wrappers ======================
67
68vld_integration_common::__vld_db_error!(
69    /// Error returned by `vld-sqlx` operations.
70    VldSqlxError
71);
72
73/// A validation failure happens before a value is written: it becomes
74/// [`sqlx::Error::Encode`] (not `Protocol`, which means a broken connection).
75impl From<VldSqlxError> for sqlx::Error {
76    fn from(e: VldSqlxError) -> Self {
77        sqlx::Error::Encode(Box::new(e))
78    }
79}
80
81vld_integration_common::__vld_db_validated!(VldSqlxError);
82
83vld_integration_common::__vld_db_wrappers! {
84    VldSqlxError;
85    text: []
86    int: []
87    float: []
88    bool: []
89}
90
91// ========================= Standalone helpers ================================
92
93/// Validate a value against schema `S` before inserting (or updating).
94///
95/// ```
96/// use vld::prelude::*;
97///
98/// vld::schema! {
99///     #[derive(Debug)]
100///     pub struct ItemSchema {
101///         pub name: String => vld::string().min(1),
102///         pub qty: i64 => vld::number().int().min(0),
103///     }
104/// }
105///
106/// #[derive(serde::Serialize)]
107/// struct NewItem { name: String, qty: i64 }
108///
109/// let item = NewItem { name: "Widget".into(), qty: 5 };
110/// vld_sqlx::validate_insert::<ItemSchema, _>(&item).unwrap();
111///
112/// let v = vld_sqlx::Validated::<ItemSchema, _>::new(item).unwrap();
113/// assert_eq!(v.inner().qty, 5);
114/// ```
115pub fn validate_insert<S, T>(value: &T) -> Result<(), VldSqlxError>
116where
117    S: vld::schema::VldParse,
118    T: serde::Serialize,
119{
120    __vld_validate::<S, T>(value).map(|_| ())
121}
122
123/// Same as [`validate_insert`].
124#[deprecated(
125    since = "0.5.0",
126    note = "identical to `validate_insert`; use it instead"
127)]
128pub fn validate_update<S, T>(value: &T) -> Result<(), VldSqlxError>
129where
130    S: vld::schema::VldParse,
131    T: serde::Serialize,
132{
133    validate_insert::<S, T>(value)
134}
135
136/// Validate a row loaded from the database against schema `S`
137/// (same check as [`validate_insert`]).
138pub fn validate_row<S, T>(value: &T) -> Result<(), VldSqlxError>
139where
140    S: vld::schema::VldParse,
141    T: serde::Serialize,
142{
143    validate_insert::<S, T>(value)
144}
145
146/// Validate a batch of rows against schema `S`.
147///
148/// Returns the index and error of the first invalid row.
149pub fn validate_rows<S, T>(rows: &[T]) -> Result<(), (usize, VldSqlxError)>
150where
151    S: vld::schema::VldParse,
152    T: serde::Serialize,
153{
154    for (i, row) in rows.iter().enumerate() {
155        validate_row::<S, T>(row).map_err(|e| (i, e))?;
156    }
157    Ok(())
158}
159
160// ========================= SQLx trait impls ==================================
161//
162// Encode/Decode delegate to the inner Rust type. Decoding does not re-validate
163// (data from the DB is trusted; use `validate_row` to re-check).
164
165macro_rules! sqlx_delegate {
166    ($Name:ident, $inner:ty) => {
167        impl<S, DB: sqlx::Database> sqlx::Type<DB> for $Name<S>
168        where
169            $inner: sqlx::Type<DB>,
170        {
171            fn type_info() -> DB::TypeInfo {
172                <$inner as sqlx::Type<DB>>::type_info()
173            }
174            fn compatible(ty: &DB::TypeInfo) -> bool {
175                <$inner as sqlx::Type<DB>>::compatible(ty)
176            }
177        }
178
179        impl<'q, S, DB: sqlx::Database> sqlx::Encode<'q, DB> for $Name<S>
180        where
181            $inner: sqlx::Encode<'q, DB>,
182        {
183            fn encode_by_ref(
184                &self,
185                buf: &mut SqlxArgBuf<'q, DB>,
186            ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
187                self.value.encode_by_ref(buf)
188            }
189        }
190
191        impl<'r, S, DB: sqlx::Database> sqlx::Decode<'r, DB> for $Name<S>
192        where
193            $inner: sqlx::Decode<'r, DB>,
194        {
195            fn decode(
196                value: <DB as sqlx::Database>::ValueRef<'r>,
197            ) -> Result<Self, sqlx::error::BoxDynError> {
198                Ok(Self::new_unchecked(
199                    <$inner as sqlx::Decode<'r, DB>>::decode(value)?,
200                ))
201            }
202        }
203    };
204}
205
206sqlx_delegate!(VldText, String);
207sqlx_delegate!(VldBool, bool);
208
209// VldFloat: accepts DOUBLE and REAL / FLOAT4 columns, encodes as f64. A REAL
210// is widened through its decimal form, so `0.1f32` reads back as `0.1`.
211impl<S, DB: sqlx::Database> sqlx::Type<DB> for VldFloat<S>
212where
213    f64: sqlx::Type<DB>,
214    f32: sqlx::Type<DB>,
215{
216    fn type_info() -> DB::TypeInfo {
217        <f64 as sqlx::Type<DB>>::type_info()
218    }
219    fn compatible(ty: &DB::TypeInfo) -> bool {
220        <f64 as sqlx::Type<DB>>::compatible(ty) || <f32 as sqlx::Type<DB>>::compatible(ty)
221    }
222}
223
224impl<'q, S, DB: sqlx::Database> sqlx::Encode<'q, DB> for VldFloat<S>
225where
226    f64: sqlx::Encode<'q, DB>,
227{
228    fn encode_by_ref(
229        &self,
230        buf: &mut SqlxArgBuf<'q, DB>,
231    ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
232        self.value.encode_by_ref(buf)
233    }
234}
235
236impl<'r, S, DB: sqlx::Database> sqlx::Decode<'r, DB> for VldFloat<S>
237where
238    f64: sqlx::Decode<'r, DB> + sqlx::Type<DB>,
239    f32: sqlx::Decode<'r, DB> + sqlx::Type<DB>,
240{
241    fn decode(
242        value: <DB as sqlx::Database>::ValueRef<'r>,
243    ) -> Result<Self, sqlx::error::BoxDynError> {
244        use sqlx::ValueRef as _;
245        let ty = value.type_info().into_owned();
246        let v = if !<f64 as sqlx::Type<DB>>::compatible(&ty)
247            && <f32 as sqlx::Type<DB>>::compatible(&ty)
248        {
249            let f = <f32 as sqlx::Decode<'r, DB>>::decode(value)?;
250            f.to_string().parse().unwrap_or(f64::from(f))
251        } else {
252            <f64 as sqlx::Decode<'r, DB>>::decode(value)?
253        };
254        Ok(Self::new_unchecked(v))
255    }
256}
257
258// VldInt: accepts BIGINT, INTEGER and SMALLINT columns (Postgres INT8/INT4/INT2
259// are distinct types), encodes as i64.
260impl<S, DB: sqlx::Database> sqlx::Type<DB> for VldInt<S>
261where
262    i64: sqlx::Type<DB>,
263    i32: sqlx::Type<DB>,
264    i16: sqlx::Type<DB>,
265{
266    fn type_info() -> DB::TypeInfo {
267        <i64 as sqlx::Type<DB>>::type_info()
268    }
269    fn compatible(ty: &DB::TypeInfo) -> bool {
270        <i64 as sqlx::Type<DB>>::compatible(ty)
271            || <i32 as sqlx::Type<DB>>::compatible(ty)
272            || <i16 as sqlx::Type<DB>>::compatible(ty)
273    }
274}
275
276impl<'q, S, DB: sqlx::Database> sqlx::Encode<'q, DB> for VldInt<S>
277where
278    i64: sqlx::Encode<'q, DB>,
279{
280    fn encode_by_ref(
281        &self,
282        buf: &mut SqlxArgBuf<'q, DB>,
283    ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
284        self.value.encode_by_ref(buf)
285    }
286}
287
288impl<'r, S, DB: sqlx::Database> sqlx::Decode<'r, DB> for VldInt<S>
289where
290    i64: sqlx::Decode<'r, DB> + sqlx::Type<DB>,
291    i32: sqlx::Decode<'r, DB> + sqlx::Type<DB>,
292    i16: sqlx::Decode<'r, DB> + sqlx::Type<DB>,
293{
294    fn decode(
295        value: <DB as sqlx::Database>::ValueRef<'r>,
296    ) -> Result<Self, sqlx::error::BoxDynError> {
297        use sqlx::ValueRef as _;
298        let ty = value.type_info().into_owned();
299        let v = if <i64 as sqlx::Type<DB>>::compatible(&ty) {
300            <i64 as sqlx::Decode<'r, DB>>::decode(value)?
301        } else if <i32 as sqlx::Type<DB>>::compatible(&ty) {
302            <i32 as sqlx::Decode<'r, DB>>::decode(value)?.into()
303        } else if <i16 as sqlx::Type<DB>>::compatible(&ty) {
304            <i16 as sqlx::Decode<'r, DB>>::decode(value)?.into()
305        } else {
306            <i64 as sqlx::Decode<'r, DB>>::decode(value)?
307        };
308        Ok(Self::new_unchecked(v))
309    }
310}
311
312#[cfg(all(test, feature = "postgres"))]
313mod pg_tests {
314    use super::*;
315    use sqlx::Type;
316    use sqlx::postgres::Postgres;
317
318    struct AnySchema;
319
320    #[test]
321    fn vld_float_compatible_with_float4() {
322        for ty in [
323            <f32 as Type<Postgres>>::type_info(),
324            <f64 as Type<Postgres>>::type_info(),
325        ] {
326            assert!(<VldFloat<AnySchema> as Type<Postgres>>::compatible(&ty));
327        }
328    }
329
330    #[test]
331    fn vld_int_compatible_with_int4_and_int2() {
332        for ty in [
333            <i16 as Type<Postgres>>::type_info(),
334            <i32 as Type<Postgres>>::type_info(),
335            <i64 as Type<Postgres>>::type_info(),
336        ] {
337            assert!(<VldInt<AnySchema> as Type<Postgres>>::compatible(&ty));
338        }
339    }
340}
341
342// ========================= Prelude ===========================================
343
344/// Common imports: the sqlx wrappers plus `vld::prelude`.
345pub mod prelude {
346    #[allow(deprecated)]
347    pub use crate::{
348        Validated, VldBool, VldFloat, VldInt, VldSqlxError, VldText, validate_insert, validate_row,
349        validate_rows, validate_update,
350    };
351    pub use vld::prelude::*;
352}