Databases
vld-sqlx checks a value against a vld schema before you write it with SQLx, and provides column wrappers (VldText, VldInt, VldFloat, VldBool) that implement sqlx::Type, Encode and Decode.
Install
Cargo.toml
[dependencies]
vld = "0.5"
vld-sqlx = "0.5" # SQLx 0.8 + SQLite by default
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
vld-sqlx enables no SQLx runtime or TLS features: pick them on your own sqlx dependency (the same major as the vld-sqlx feature).
Features
| Feature | Default | Effect |
|---|---|---|
sqlx-0_8 | yes | Build against SQLx 0.8 |
sqlx-0_9 | Build against SQLx 0.9 (Rust 1.94+) | |
sqlite | yes | SQLx SQLite driver |
postgres | SQLx Postgres driver | |
mysql | SQLx MySQL driver |
Exactly one of sqlx-0_8 / sqlx-0_9 must be enabled; both (or none) is a compile error. For SQLx 0.9:
vld-sqlx = { version = "0.5", default-features = false, features = ["sqlx-0_9", "postgres"] }
MSRV: 1.85 with sqlx-0_8, 1.94 with sqlx-0_9 (see MSRV).
Example
use sqlx::SqlitePool;
use vld_sqlx::prelude::*;
vld::schema! {
#[derive(Debug)]
pub struct NewUserSchema {
pub name: String => vld::string().min(1).max(100),
pub email: String => vld::string().email(),
pub age: i64 => vld::number().int().min(0).max(150),
}
}
// A column schema: the wrapper is checked as `{"value": ...}`.
vld::schema! {
#[derive(Debug)]
pub struct EmailField {
pub value: String => vld::string().email(),
}
}
#[derive(Debug, serde::Serialize)]
struct NewUser { name: String, email: String, age: i64 }
async fn insert(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
let user = NewUser { name: "Alice".into(), email: "alice@example.com".into(), age: 30 };
validate_insert::<NewUserSchema, _>(&user)?;
sqlx::query("INSERT INTO users (name, email, age) VALUES (?, ?, ?)")
.bind(&user.name)
.bind(&user.email)
.bind(user.age)
.execute(pool)
.await?;
// Validated column value, bound directly.
let email = VldText::<EmailField>::new("carol@example.com")?;
sqlx::query("UPDATE users SET email = ? WHERE name = 'Alice'")
.bind(&email)
.execute(pool)
.await?;
Ok(())
}
API
validate_insert::<S, T>(&value)— serializevalueto JSON and validate it against schema typeS.validate_row::<S, T>(&row)— the same check for a row loaded from the database.validate_rows::<S, T>(&rows)— validate a batch; the error is(index, VldSqlxError)of the first invalid row.Validated<S, T>—Validated::new(value)validates and wraps;inner(),into_inner(),Deref, transparentSerialize.VldText<S>,VldInt<S>,VldFloat<S>,VldBool<S>— column wrappers overString/i64/f64/bool;new()validates,new_unchecked()skips the check;Deref,Display,Serialize, validatingDeserialize.VldSqlxError—Validation(VldError)orSerialization(String); converts intosqlx::Error::Encode.prelude— all of the above plusvld::prelude::*.
Gotchas
- Column schemas need a
valuefield. A wrapper validates{"value": v}againstS, so defineSwith a singlevaluefield. - Decoding does not re-validate.
Decodebuilds the wrapper withnew_unchecked(data from the database is trusted); callvalidate_rowto re-check loaded data. VldIntdecodesBIGINT,INTEGERandSMALLINTcolumns (PostgresINT8/INT4/INT2) and encodes asi64.VldFloatdecodesDOUBLEandREAL/FLOAT4(anf32is widened through its decimal form, so0.1stays0.1) and encodes asf64.- A non-finite float (
NaN,±inf) inVldFloat::newis rejected withnot_finite. Validated<S, T>stores the originalT, not the schema's output:with_default,trim(),coerce()and other transforms are checked against but never applied to the stored value.