ORM
June 14, 2026 ยท View on GitHub
KiteSQL's ORM is available with features = ["orm"]. This also enables the
derive macros used by #[derive(Model)]; #[derive(Projection)] is optional
for DTO-style projections.
kite_sql = { version = "*", features = ["orm"] }
Model
#[derive(Model)] defines the table mapping, cached model operations, typed
field accessors, and migration metadata.
use kite_sql::Model;
#[derive(Default, Debug, PartialEq, Model)]
#[model(table = "users")]
#[model(index(name = "users_name_age_index", columns = "name, age"))]
struct User {
#[model(primary_key)]
id: i32,
#[model(rename = "user_name", varchar = 64)]
name: String,
#[model(default = "18", index)]
age: Option<i32>,
}
Common field attributes are primary_key, unique, index, rename,
default, varchar, char, decimal_precision, decimal_scale, and skip.
Queries
The recommended query entrypoint is bind. The closure receives an
OrmContext, drives the binder directly, and returns a LogicalPlan. Query
chains end with .finish(); mutation chains end with update, delete, or
another operation that already returns a plan.
use kite_sql::db::{DataBaseBuilder, ResultIter};
use kite_sql::orm::OrmQueryResultExt;
let mut database = DataBaseBuilder::path(".").build_in_memory()?;
database.create_table::<User>()?;
database.insert(&User {
id: 1,
name: "Alice".to_string(),
age: Some(18),
})?;
let adults = database
.bind(|ctx| {
ctx.from::<User>()?
.filter(|e| {
let adult = e.column(User::age())?.gte(18)?;
let named_a = e.column(User::name())?.like("A%")?;
adult.and(named_a)
})?
.finish()
})?
.orm::<User>()
.collect::<Result<Vec<_>, _>>()?;
assert_eq!(adults[0].name, "Alice");
# Ok::<(), Box<dyn std::error::Error>>(())
Inside expression closures, e.column(User::id())? resolves through the core
binder and returns a bound expression. Expression methods such as eq, gte,
like, and, or, is_null, and in_list compose directly into core
ScalarExpression values; constants can be passed directly.
Prefer the compact helpers when the query shape is simple:
let rows = database
.bind(|ctx| {
ctx.from::<User>()?
.filter(|e| e.column(User::age())?.gte(18))?
.order_by(User::age().desc())?
.project_scalars((User::id(), User::name()))?
.finish()
})?
.project_tuple::<(i32, String)>()
.collect::<Result<Vec<_>, _>>()?;
# let _ = rows;
# Ok::<(), Box<dyn std::error::Error>>(())
Use project_scalar(...) for one field, project_scalars((...)) for simple
tuples, and project_value/project_tuple when the projection is an expression
or needs aliases. Use order_by for field ordering and already-bound sort
fields, or order_by_expr for computed sort expressions. Call .asc(),
.desc(), .nulls_first(), or .nulls_last() when the default
ascending/nulls-last order is not enough.
Joins and set operations use the same binder-backed style:
let joined = database
.bind(|ctx| {
ctx.from::<User>()?
.inner_join::<Order>(|e| {
e.column(User::id())?.eq(e.column(Order::user_id())?)
})?
.project_scalars((User::name(), Order::amount()))?
.order_by(Order::id())?
.finish()
})?;
let ids = database.bind(|ctx| {
ctx.union(
true,
|ctx| ctx.from::<User>()?.project_scalar(User::id())?.finish(),
|ctx| ctx.from::<Order>()?.project_scalar(Order::user_id())?.finish(),
)
})?;
# let _ = (joined, ids);
# Ok::<(), Box<dyn std::error::Error>>(())
Writes
For model rows, use the direct helpers:
database.insert(&user)?;
database.insert_many(users)?;
let user = database.get::<User>(&1)?;
let all = database.fetch::<User>()?;
# let _ = (user, all);
# Ok::<(), Box<dyn std::error::Error>>(())
For query-shaped writes, start with ctx.mutate::<M>() and finish with
update or delete.
database
.bind(|ctx| {
ctx.mutate::<User>()?
.filter(|e| e.column(User::id())?.eq(1))?
.update(|u| {
u.set_value(User::name(), "Bob")?;
u.set_value(User::age(), None::<i32>)
})
})?
.done()?;
database
.bind(|ctx| {
ctx.mutate::<User>()?
.filter(|e| e.column(User::id())?.eq(2))?
.delete()
})?
.done()?;
# Ok::<(), Box<dyn std::error::Error>>(())
insert_select and overwrite_select accept the same closure style for the
source plan:
database
.bind(|ctx| {
ctx.insert_select::<UserSnapshot, _, _>(["id", "user_name"], |ctx| {
ctx.from::<User>()?
.project_scalars((User::id(), User::name()))?
.finish()
})
})?
.done()?;
# Ok::<(), Box<dyn std::error::Error>>(())
Schema And Maintenance
Common schema helpers are:
create_table::<M>()create_table_if_not_exists::<M>()migrate::<M>()drop_table::<M>()drop_table_if_exists::<M>()truncate::<M>()create_view(...)/create_or_replace_view(...)drop_view(...)/drop_view_if_exists(...)
Introspection helpers include show_tables(), show_views(), describe::<M>(),
and analyze::<M>().
The ORM frontend does not build SQL AST nodes. SQL parsing is the SQL frontend;
ORM queries bind directly into ScalarExpression and LogicalPlan.