[Announcing] Kova: A relational ORM with zero query macros, zero trait imports, and flat compile-time types
Hi Rustaceans!
I’m excited to share Kova, a relational ORM for PostgreSQL (async) and SQLite (sync).
I built Kova because we wanted compile-time SQL verification without the usual trade-offs in the Rust ecosystem:
- No live DB or offline schema dumps needed for
cargo check(zero procedural query macros). - No recursive generic return types that tank compile times and dump 200-line trait solver errors.
- No
ActiveModelwrapper bloat or runtime object-graph N+1 traps.
If you know SQL, you already know Kova.
GitHub: GitHub - edvong/kova: A PostgreSQL and SQLite ORM for Rust · GitHub
A Quick Taste
Notice what’s missing: no use kova::prelude::*, and not a single query macro.
use kova::{Client, Model, Result}; use kova::dsl::func::{Count, Sum, Wildcard}; #[derive(Model)] pub struct User { #[kova(auto)] pub id: i32, pub name: String, pub bio: Option<String>, pub is_active: bool, } #[derive(Model)] pub struct Order { #[kova(auto)] pub id: i32, pub user_id: i32, pub amount: f64, } async fn example(client: &Client) -> Result<()> { // - `User::bio` is `Option<String>`, yet accepts `&str` directly without `Some(...)`. // - `Sum(Order::amount)` is `f64`, yet accepts `50_i16` via lossless compile-time widening. // - Bitwise `&`, `|`, and `!` map directly to SQL `AND`, `OR`, and `NOT`. let stats: Vec<(i32, String, f64, i64)> = client .select::<User>() .inner_join::<Order>(Order::user_id.eq(User::id)) .columns((User::id, User::name, Sum(Order::amount), Count(Wildcard))) .filter(User::is_active.eq(true) & User::bio.eq("Architect") & !User::name.like("%bot%")) .group_by((User::id, User::name)) .having(Sum(Order::amount).gt(50_i16)) .order_by(Sum(Order::amount).desc()) .limit(10) .load_in() .await?; // Compile-time Typestate & Semantic Guards: // ❌ Won't compile: UPDATE is in `Unset` state without `.set()` // client.update::<User>().filter(User::id.eq(1)).execute().await; // ❌ Won't compile: Aggregates lack `CanGroup` and cannot appear in `GROUP BY` // client.select::<User>().group_by(Sum(Order::amount)); // ❌ Won't compile: Non-nullable `User::name` rejects `Option` (no reverse null pollution) // client.select::<User>().filter(User::name.eq(Some("Alice"))); // ❌ Won't compile: Implicit lossy narrowing from `i64` to `i32` is forbidden // client.select::<User>().filter(User::id.eq(100_i64)); Ok(()) } Why Kova is Different (The Fun Engineering Bits)
- Boundary Validation & In-Tree Type Erasure: All capability and type checks (
CanMath,CanGroup,Compatible) happen at the method call boundary. Immediately after validation, phantom markers are erased into flat, constant-width AST nodes. Your query builder types never grow recursively, eliminating LLVM monomorphization bloat. - Orphan-Rule Free Workspaces (
Codec<X>): Want a puremodelscrate in your Cargo workspace? With Kova, yourmodelscrate needs zero database driver dependencies and zero Newtype wrappers. Downstream crates bind zero-copy codecs and SQL capabilities to any third-party type (Uuid,Decimal,Url,DateTime) using a local context markerX. - Postgres is
async, SQLite issync: SQLite is an in-process C library executing in microseconds; wrapping it inasynconly addsspawn_blockingcross-thread scheduling overhead. Kova respects physical I/O boundaries while keeping the query DSL 100% identical. - Single-Query Nested Views & Lexical Self-Join Tags: Assemble deeply nested
Vec<ChildDto>trees in a single round-trip via native DBJSONBaggregation, and disambiguate self-joins or subqueries using zero-sized marker types (.tag::<Buyer>()) instead of error-prone string aliases.
The README on GitHub dives much deeper into the architecture, window analytics, SRFs (unnest, json_tree), FTS5, and our deliberate design trade-offs.
1 post - 1 participant