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 ActiveModel wrapper 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 pure models crate in your Cargo workspace? With Kova, your models crate 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 marker X.
  • Postgres is async, SQLite is sync: SQLite is an in-process C library executing in microseconds; wrapping it in async only adds spawn_blocking cross-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 DB JSONB aggregation, 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

Read full topic