A small, Postgres-first async ORM-ish library with type-level loaded/unloaded relations.
use dbkit::prelude::*;Define models with #[model] and use the generated query and relation APIs:
use dbkit::{model, Database};
#[model(table = "users")]
#[derive(Debug)]
struct User {
#[key]
#[autoincrement]
id: i64,
name: String,
#[unique]
email: String,
#[has_many]
todos: dbkit::HasMany<Todo>,
}
#[model(table = "todos")]
#[derive(Debug)]
struct Todo {
#[key]
id: i64,
#[index]
user_id: i64,
#[belongs_to(key = user_id, references = id)]
user: dbkit::BelongsTo<User>,
title: String,
}
#[tokio::main]
async fn main() -> Result<(), dbkit::Error> {
let db = Database::connect("postgres://...").await?;
let users: Vec<User<Vec<Todo>>> = User::query()
.filter(User::email.ilike("%@example.com"))
.with(User::todos.selectin())
.all(&db)
.await?;
for user in &users {
for todo in &user.todos {
println!("{} / {}", user.name, todo.title);
}
}
Ok(())
}The loaded graph is part of the Rust type. If a relation is not requested, that field stays
dbkit::NotLoaded; once you add .with(...), normal field access is available at the matching
depth.
If a Rust field needs a different DB column name, use #[dbkit(column = "...")]:
#[dbkit(column = "type")]
type_: String,Required and nullable model fields stay distinct throughout queries and mutations. None works
only with nullable fields, and SQL functions keep nullable results nullable when PostgreSQL can
return NULL.
Insert with the generated insert type:
let created = User::insert(UserInsert {
name: "Alex".to_string(),
email: "a@b.com".to_string(),
})
.returning_all()
.one(&db)
.await?
.expect("inserted");Update one loaded row with into_active():
let mut active = created.into_active();
active.name = "Renamed".into();
let updated = active.update(&db).await?;Delete one loaded row with the active model:
let deleted = created.into_active().delete(&db).await?;Use query-builder updates/deletes for bulk or conditional mutations.
- Querying
- Mutations
- Relations
- Expressions and aggregation
- PostgreSQL string functions
- Postgres types
- Database
- Implement true joined eager loading (single-query join decoding).
- Add aggregation/projection support:
select_only,column_as,group_by,sum,count,min,max, filtered aggregates, and mapping into custom result structs (e.g.,into_model::<T>()for aggregates). - Add SQL function expressions in queries (e.g.,
COALESCE,DATE_TRUNC,UPPER). - Add JSON column support (
serde_json::Value) for insert/update/filter. - Add Postgres array column support (e.g.,
Vec<String>) for insert/update/filter. - Generalize Postgres array support beyond
Vec<String>(e.g.,Vec<i64>,Vec<uuid::Uuid>,Vec<bool>). - Add bulk insert support (multi-row
insert_many). - Add dynamic condition builder helpers (e.g.,
Condition::any/Condition::all). - Allow
order_byon expressions or aliases (e.g.,date_trunc(...),total). - Add
between(a, b)convenience for columns/expressions. - Add locking options:
for_update,skip_locked,nowait. - Add optional helpers:
count(),exists(),paginate(). - Add typed conflict helpers:
on_conflict_do_nothing,on_conflict_do_update. - Add active model
save()that chooses insert vs update. - Store
#[unique]/#[index]as metadata (even if no-op).
load(...)requires an executor argument:user.load(User::todos, &ex).- Relation state sealing is looser than spec (any
Vec<T>/Option<T>satisfies the state trait).