Query builder and ORM for Cloudflare D1, written in Rust, targeting wasm32-unknown-unknown.
Works with workers-rs 0.4.
rust-d1-orm/
├── d1-orm-query # Query / SET DSL — no runtime dep
├── d1-orm-engine # Table<M>, batch, raw SQL, pagination
├── d1-orm-migration # Embedded migration runner
└── rust-d1-orm # Umbrella crate (re-exports all three)
[dependencies]
rust-d1-orm = "0.1"Or pick only the crates you need:
d1-orm-engine = "0.1"
d1-orm-query = "0.1"
d1-orm-migration = "0.1"use rust_d1_orm::{D1Model, opt_js};
use serde::Deserialize;
use worker::wasm_bindgen::JsValue;
#[derive(Deserialize)]
struct UserRow {
id: String,
email: String,
name: String,
bio: Option<String>,
created_at: String,
}
impl D1Model for UserRow {
const TABLE: &'static str = "users";
const COLUMNS: &'static [&'static str] = &["id", "email", "name", "bio", "created_at"];
fn values(&self) -> Vec<JsValue> {
vec![
self.id.clone().into(),
self.email.clone().into(),
self.name.clone().into(),
opt_js(self.bio.clone()),
self.created_at.clone().into(),
]
}
}use rust_d1_orm::{Order, Query, Set, Table};
let table = Table::<UserRow>::new(&db);
let user = table.insert(&row).await?;
let users = table.insert_batch(&rows).await?;
let user = table.find_one(Query::new().eq("id", id)).await?;
let users = table.find_all(
Query::new().eq("active", true).order_by("created_at", Order::Desc).limit(50)
).await?;
let updated = table.update(
Set::new().field("name", "Alice").nullable_field("bio", None::<String>),
Query::new().eq("id", id),
).await?;
table.delete(Query::new().eq("id", id)).await?;
let n = table.count(Query::new().eq("active", true)).await?;Query::new()
.in_("role", vec!["admin", "mod"])
.not_in("status", vec!["banned"])
.like("email", "%@example.com")
.between("score", 10, 100)
.or(|q| q.eq("vip", true).eq("beta", true))
.order_by("created_at", Order::Desc)
.limit(20);| Method | SQL fragment |
|---|---|
.eq("col", val) |
col = ?N |
.ne("col", val) |
col != ?N |
.gt / .gte / .lt / .lte |
col > / >= / < / <= ?N |
.is_null / .is_not_null |
col IS NULL / IS NOT NULL |
.in_("col", vals) |
col IN (?N, ...) |
.not_in("col", vals) |
col NOT IN (?N, ...) |
.like("col", pat) |
col LIKE ?N |
.not_like("col", pat) |
col NOT LIKE ?N |
.between("col", lo, hi) |
col BETWEEN ?N AND ?N+1 |
.filter_optional("col", opt) |
(?N IS NULL OR col = ?N) |
.filter_optional_gte / _lte |
(?N IS NULL OR col >= / <= ?N) |
.or(|q| ...) |
(a = ?N OR b = ?N+1) |
.and(|q| ...) |
(a = ?N AND b = ?N+1) |
.order_by("col", Order::Desc) |
ORDER BY col DESC |
.limit(n) / .offset(n) |
LIMIT n / OFFSET n |
| Method | Behavior |
|---|---|
.field("col", val) |
col = ?N |
.nullable_field("col", opt) |
col = ?N — binds NULL when opt is None |
.raw_expr("col", "col + 1") |
col = col + 1 (no binding) |
table.upsert(
&row,
&["id"],
Set::new().field("name", "Alice").field("updated_at", now),
).await?;
table.upsert_ignore(&row, &["id"]).await?;let page = table.paginate(Query::new().eq("active", true), 1, 25).await?;
// page.items, page.total, page.page, page.per_page, page.has_nextuse rust_d1_orm::Batch;
let results = Batch::new(&db)
.add(db.prepare("INSERT INTO logs (msg) VALUES (?1)").bind(&["a".into()])?)
.add(db.prepare("INSERT INTO logs (msg) VALUES (?1)").bind(&["b".into()])?)
.run().await?;use rust_d1_orm::{raw_query, raw_exec};
use worker::wasm_bindgen::JsValue;
let rows: Vec<UserRow> = raw_query(&db, "SELECT * FROM users WHERE id = ?1", &[id.into()]).await?;
raw_exec(&db, "PRAGMA journal_mode = WAL", &[]).await?;Define migrations as Rust constants with embedded SQL, then run the runner at Worker startup:
use rust_d1_orm::{Migration, MigrationRunner};
const M0001: Migration = Migration {
id: "0001_create_users",
name: "create users table",
up: "CREATE TABLE users (id TEXT PRIMARY KEY, email TEXT NOT NULL, created_at TEXT NOT NULL)",
down: Some("DROP TABLE users"),
};
const M0002: Migration = Migration {
id: "0002_add_bio",
name: "add bio column",
up: "ALTER TABLE users ADD COLUMN bio TEXT",
down: None,
};
// In your Worker fetch handler:
let applied = MigrationRunner::new(&db)
.register(M0001)
.register(M0002)
.run()
.await?;
// Check status
let statuses = MigrationRunner::new(&db)
.register(M0001)
.register(M0002)
.status()
.await?;
// Roll back the last 1 migration
MigrationRunner::new(&db)
.register(M0001)
.register(M0002)
.rollback(1)
.await?;Applied migrations are tracked in _d1_migrations (id, name, applied_at). Each migration runs exactly once; run() is idempotent.
- All writes use
RETURNING *— no second SELECT after insert/update. insert_batchuses D1'sbatch()API — all rows in one round trip.- JOINs are not supported by the query builder; use
raw_queryfor those. filter_optional*methods acceptOption<T>— passNoneto skip the filter (match all rows).
MIT