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