sntl 0.1.1

Compile-time guarded ORM for PostgreSQL — your data's guardian from compile to production
Documentation

sentinel

Compile-time guarded ORM for PostgreSQL — your data's guardian from compile to production.

CI PostgreSQL Integration codecov Security MSRV

crates.io sntl crates.io sntl-core crates.io sntl-macros crates.io sntl-migrate crates.io sntl-cli docs.rs

Rust Tests Config Total Lines

Rust Tokio PostgreSQL rustls


N+1 queries, over-fetching, unsafe relation access — caught at compile time, not production.

Quick Start

[dependencies]
sntl = "0.1"
use sntl::prelude::*;

#[derive(Model)]
#[model(table = "users")]
struct User {
    #[primary_key]
    id: i64,
    name: String,
    email: String,
}

#[tokio::main]
async fn main() -> Result<(), sntl::core::Error> {
    let config = Config::parse("postgres://user:pass@localhost/mydb")?;
    let pool = Pool::connect(config, 10).await?;
    let conn = pool.get().await?;

    // Type-safe query — wrong column names won't compile
    let users = User::select()
        .filter(User::EMAIL.eq("alice@example.com"))
        .fetch_all(&conn)
        .await?;

    Ok(())
}

Compile-time SQL validation

Sentinel ships an sqlx-style query!() family that pulls types from a checked-in .sentinel/ cache. The schema and per-query metadata are produced by sntl prepare against a live PostgreSQL, then committed alongside the code so CI builds work offline.

use sntl::driver::Connection;

async fn examples(conn: &mut Connection) -> sntl::Result<()> {
    // Anonymous record — one struct field per output column.
    let row = sntl::query!("SELECT id, email FROM users WHERE id = $1", 42i32)
        .fetch_one(conn)
        .await?;
    let _: i32 = row.id;

    // Typed dispatch — your struct must impl FromRow.
    #[derive(sntl::FromRow)]
    struct User { id: i32, email: String }
    let user = sntl::query_as!(User, "SELECT id, email FROM users WHERE id = $1", 42i32)
        .fetch_one(conn)
        .await?;

    // Single-column projection.
    let count: i64 = sntl::query_scalar!("SELECT COUNT(*) FROM users")
        .fetch_one(conn)
        .await?;

    // Pipelined batch — single network round-trip for N queries.
    let _results = sntl::query_pipeline!(
        conn,
        a: "SELECT id FROM users WHERE id = $1", 1i32;
        b: "SELECT id FROM users WHERE id = $1", 2i32;
    ).await?;
    Ok(())
}

Bypass the cache temporarily with sntl::query_unchecked! / query_as_unchecked!, or load SQL from disk with sntl::query_file! / query_file_as!.

The companion CLI provides:

sntl prepare   # scan workspace, pull schema, write .sentinel/
sntl check     # validate cache vs current source (CI-friendly)
sntl doctor    # diagnose config, DB, and cache health

Compared to sqlx: the offline cache is the source of truth (no DATABASE_URL required at compile time); pipelined batches are first-class; nullable inference can be overridden per-call with nullable = [...] / non_null = [...].

See docs/migration-from-sqlx.md for a side-by-side migration guide.

Features

  • Compile-time guards — N+1, over-fetching, and unsafe relation access caught before runtime
  • Type-state relationsUser<Bare> vs User<WithPosts>, compile error on unloaded access
  • Partial types#[derive(Partial)] generates narrow select types, no over-fetching
  • Reducer pattern#[reducer] for transactions with auto-commit/rollback
  • Deadlock prevention — auto-reorder locks by ID
  • 4-layer query system — from simple CRUD to raw SQL, always type-safe, always parameterized
  • Zero unsafe in core — security by construction
  • Built on sentinel-driver — SCRAM-SHA-256, pipeline mode, binary format, rustls
  • Production observability — single Instrumentation trait hooks every wire site and every macro invocation; ships with a tracing/OTel adapter (see docs/observability-guide.md)

Architecture

sentinel/
├── sntl           # Main crate — models, queries, transactions, types, query! family
├── sntl-macros    # Proc macros — derive(Model), derive(Partial), derive(FromRow), query!()
├── sntl-schema    # Shared SQL parsing, nullability, and .sentinel/ cache I/O
├── sntl-cli       # CLI binary — `sntl prepare`, `sntl check`, `sntl doctor`, `sntl migrate ...`
├── sntl-migrate   # Forward-only migrations + schema-diff scaffolder (v0.3)
└── sntl-core      # Core traits extraction (planned)

sntl, sntl-macros, sntl-schema, sntl-cli, and sntl-migrate are implemented today. See docs/migration-guide.md for the sntl-migrate user guide.

sntl-core is published on crates.io as a name reservation and will be filled in in a future release.

Observability (v0.4+): sntl ships sntl::observability::SntlTracing, a bridge over sentinel-driver v3.0+'s Instrumentation trait. It hooks every wire-trip and every query!() / migration call — feeding db.system, sntl.macro, and sntl.query_id into any tracing-compatible backend (Jaeger, Zipkin, OTLP). See docs/observability-guide.md.

Development

cargo check --workspace                                # Type check
cargo test --workspace                                 # Run all tests
cargo clippy --workspace --all-targets -- -D warnings  # Lint
cargo fmt --all                                        # Format

MSRV

Rust 1.85 (declared via rust-version in Cargo.toml).

License

Licensed under either of:

at your option.