<div align="center">
# sentinel
**Compile-time guarded ORM for PostgreSQL — your data's guardian from compile to production.**
[](https://github.com/cntm-labs/sentinel/actions/workflows/ci.yml)
[](https://github.com/cntm-labs/sentinel/actions/workflows/postgresql.yml)
[](https://codecov.io/gh/cntm-labs/sentinel)
[](https://github.com/cntm-labs/sentinel/actions/workflows/security.yml)
[](https://github.com/cntm-labs/sentinel/actions/workflows/ci.yml)
[](https://crates.io/crates/sntl)
[](https://crates.io/crates/sntl-core)
[](https://crates.io/crates/sntl-macros)
[](https://crates.io/crates/sntl-migrate)
[](https://crates.io/crates/sntl-cli)
[](https://docs.rs/sntl)
[](sntl/)
[](sntl/tests/)
[](./)
[](./)
[](https://www.rust-lang.org/)
[](https://tokio.rs/)
[](https://www.postgresql.org/)
[](https://github.com/rustls/rustls)
</div>
---
N+1 queries, over-fetching, unsafe relation access — caught at **compile time**, not production.
## Quick Start
```toml
[dependencies]
sntl = "0.1"
```
```rust
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.
```rust
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:
```sh
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 relations** — `User<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`](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`](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`](docs/observability-guide.md).
## Development
```sh
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:
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE))
- MIT License ([LICENSE-MIT](LICENSE-MIT))
at your option.