1 unstable release
Uses new Rust 2024
| new 0.1.2 | May 7, 2026 |
|---|
#6 in #soft-delete
Used in qraft
140KB
3.5K
SLoC
qraft
qraft is a typed SQL query builder for rust, built around generated schema modules and quex for execution.
You define a rust struct, derive Qraft, and get a table handle, typed columns, and model helpers such as query(), find(), and create(). The point is simple: write query code in rust, keep column names and result shapes checked, and stop passing SQL around as loose strings unless you actually want raw SQL.
What it covers
qraft handles the parts of query building that tend to get messy once a project grows:
- typed
select,insert,update, anddeletebuilders - derive macros for models, row decoding, projections, pivots, and CTEs
- common table expressions
- aliasing for self-joins and readable multi-table queries
- optional soft-delete scopes and restore helpers
quexpool and executor types re-exported from the top-level crate
Install
Pick the features you need. Most projects will want derive plus one database backend.
[dependencies]
qraft = { version = "0.1.1", features = ["derive", "sqlite"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
Feature flags:
derive: enablesQraft,Cte,FromRow,Projection,Insertable, andPivotsqlite,postgres,mariadb: enables the matchingquexbackendsoft-delete: enables derived soft-delete scopes and restore helperschrono,serde_json,uuid: enables optional type supporthash: enablesHashed, a bcrypt-backed text wrapper for stored passwords
Quick start
This is the core workflow. Derive Qraft, then build queries with the generated schema module.
use qraft::prelude::*;
#[derive(Debug, qraft::Qraft)]
struct User {
id: i64,
email: String,
active: bool,
}
let sql = User::query()
.filter(users::active.eq(true))
.to_debug_sql::<qraft::Postgres>();
assert_eq!(
sql,
r#"select * from "users" where "users"."active" = $1; params=[true]"#
);
The derive generates a users module with:
users::table- typed column values such as
users::idandusers::email - helpers on the model, including
User::query(),User::find(id), andUser::create(values)
That gives you a fairly direct style of query code. You are still writing SQL-shaped logic, but with rust types attached to it.
Running queries
qraft re-exports quex, so you can open a pool and execute queries from the same crate surface.
use qraft::prelude::*;
use qraft::quex;
#[derive(Debug, qraft::Qraft)]
struct AuditLog {
id: i64,
message: String,
}
# async fn run() -> qraft::Result<()> {
let pool = quex::Pool::connect("sqlite::memory:")?.build().await?;
quex::query(
r#"
create table audit_logs (
id integer primary key,
message text not null
)
"#,
)
.execute(&pool)
.await?;
AuditLog::create((
audit_logs::id.eq(1),
audit_logs::message.eq("created"),
))
.execute(&pool)
.await?;
let row = AuditLog::find(1).one(&pool).await?;
assert_eq!(row.message, "created");
# Ok(())
# }
Common table expressions
CTEs use the same pattern. Derive Cte, build the inner query, then attach it with with(...).
use qraft::prelude::*;
use qraft::query::select;
#[derive(Debug, qraft::Qraft)]
struct User {
id: i64,
email: String,
active: bool,
}
#[derive(Debug, qraft::Cte)]
struct RecentUser {
id: i64,
email: String,
}
let sql = with(recent_users::from(
select((users::id, users::email))
.from(users::table)
.filter(users::active.eq(true)),
))
.select(recent_users::star)
.from(recent_users::table)
.to_debug_sql::<qraft::Postgres>();
assert_eq!(
sql,
r#"with "recent_users"("id", "email") as (select "users"."id", "users"."email" from "users" where "users"."active" = $1) select "recent_users".* from "recent_users"; params=[true]"#
);
This matters once queries stop being one flat select. You can keep intermediate shapes named and typed instead of dropping to handwritten SQL early.
Soft deletes
With the soft-delete feature, a model can hide deleted rows by default and expose explicit scopes when you need them.
use qraft::prelude::*;
#[derive(Debug, qraft::Qraft)]
#[qraft(soft_delete)]
struct User {
id: i64,
email: String,
deleted_at: Option<String>,
}
let active = User::query().to_debug_sql::<qraft::Sqlite>();
let deleted = User::only_deleted().to_debug_sql::<qraft::Sqlite>();
assert_eq!(
active,
r#"select * from "users" where ("users"."deleted_at") is null; params=[]"#
);
assert_eq!(
deleted,
r#"select * from "users" where ("users"."deleted_at") is not null; params=[]"#
);
The derived API also adds:
User::with_deleted()User::only_deleted()query.restore(exec)query.restore_query()query.delete(exec)for soft deletequery.force_delete(exec)for physical delete
Hashed values
With the hash feature, qraft re-exports Hashed. It stores bcrypt hashes as text and gives you helpers for hashing and verification.
Hashed implements Encode, Decode, and Compatible<Text>, so it can be stored in text columns and used directly in derived models.
use qraft::Hashed;
# async fn run() -> Result<(), Box<dyn std::error::Error>> {
let password = Hashed::make("hunter2").await?;
assert!(password.verify("hunter2").await?);
assert!(!password.verify("wrong-password").await?);
# Ok(())
# }
This is useful when a model field should decode from the database as a password hash instead of a plain String.
Derives
Qraft: maps a model to a table and generates the schema moduleCte: declares a named common table expressionFromRow: decodes a result row into a custom rust typeProjection: derives projected field metadataInsertable: derives insert assignmentsPivot: derives projected fields for pivot types
Workspace
crates/qraft: public API and re-exportscrates/core: query builders, expressions, lowering, and execution gluecrates/derive: proc macros exposed byqraftcrates/derive-core: shared derive implementationcrates/expression-macro: expression macro support
License
qraft is available under MIT or Apache-2.0.
Dependencies
~2.4–4MB
~71K SLoC