N+1 queries, over-fetching, unsafe relation access — caught at compile time, not production.
[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(())
}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 healthCompared 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.
- Compile-time guards — N+1, over-fetching, and unsafe relation access caught before runtime
- Type-state relations —
User<Bare>vsUser<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
Instrumentationtrait hooks every wire site and every macro invocation; ships with a tracing/OTel adapter (seedocs/observability-guide.md) - Fixture-isolated test harness —
#[sntl::test(migrations, fixtures)]spins a fresh PG database per test viaCREATE DATABASE ... TEMPLATE; first test pays the migration cost, the rest clone in milliseconds (seedocs/testing-guide.md) - Transactional reducers —
#[sntl::reducer]wraps any async fn inBEGIN/COMMIT/ROLLBACKwith isolation control, panic safety, andReducerBegin/Commit/Rollbackevents (seedocs/reducer-guide.md)
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, andsntl-migrateare implemented today. Seedocs/migration-guide.mdfor thesntl-migrateuser guide.
sntl-coreis published on crates.io as a name reservation and will be filled in in a future release.Observability (v0.4+):
sntlshipssntl::observability::SntlTracing, a bridge oversentinel-driverv4.0+'sInstrumentationtrait. It hooks every wire-trip and everyquery!()/ migration call — feedingdb.system,sntl.macro, andsntl.query_idinto anytracing-compatible backend (Jaeger, Zipkin, OTLP). Seedocs/observability-guide.md.Day-one DX (v0.5+):
query!().fetch_*(&pool)works directly — no manualpool.acquire()round-trip;fetch_stream(&mut conn)returns a lazyRowStreamfor million-row scans;#[sntl::test]spins a fresh isolated PG database per test viaCREATE DATABASE ... TEMPLATE. Seedocs/testing-guide.mdanddocs/migration-from-sqlx.md.
cargo check --workspace # Type check
cargo test --workspace # Run all tests
cargo clippy --workspace --all-targets -- -D warnings # Lint
cargo fmt --all # FormatRust 1.85 (declared via rust-version in Cargo.toml).
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT License (LICENSE-MIT)
at your option.