Databases
Validate rows before insert and after select, and bind validated column wrappers with SQLx.

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

FeatureDefaultEffect
sqlx-0_8yesBuild against SQLx 0.8
sqlx-0_9Build against SQLx 0.9 (Rust 1.94+)
sqliteyesSQLx SQLite driver
postgresSQLx Postgres driver
mysqlSQLx 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) — serialize value to JSON and validate it against schema type S.
  • 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, transparent Serialize.
  • VldText<S>, VldInt<S>, VldFloat<S>, VldBool<S> — column wrappers over String / i64 / f64 / bool; new() validates, new_unchecked() skips the check; Deref, Display, Serialize, validating Deserialize.
  • VldSqlxError — Validation(VldError) or Serialization(String); converts into sqlx::Error::Encode.
  • prelude — all of the above plus vld::prelude::*.

Gotchas

  • Column schemas need a value field. A wrapper validates {"value": v} against S, so define S with a single value field.
  • Decoding does not re-validate. Decode builds the wrapper with new_unchecked (data from the database is trusted); call validate_row to re-check loaded data.
  • VldInt decodes BIGINT, INTEGER and SMALLINT columns (Postgres INT8 / INT4 / INT2) and encodes as i64. VldFloat decodes DOUBLE and REAL / FLOAT4 (an f32 is widened through its decimal form, so 0.1 stays 0.1) and encodes as f64.
  • A non-finite float (NaN, ±inf) in VldFloat::new is rejected with not_finite.
  • Validated<S, T> stores the original T, not the schema's output: with_default, trim(), coerce() and other transforms are checked against but never applied to the stored value.