Skip to content

maulanasdqn/rust-d1-orm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

rust-d1-orm

Query builder and ORM for Cloudflare D1, written in Rust, targeting wasm32-unknown-unknown.

Works with workers-rs 0.4.

Workspace layout

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)

Install

[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"

Usage

Define a model

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(),
        ]
    }
}

CRUD

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?;

Extended query operators

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

Set builder

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)

Upsert

table.upsert(
    &row,
    &["id"],
    Set::new().field("name", "Alice").field("updated_at", now),
).await?;

table.upsert_ignore(&row, &["id"]).await?;

Pagination

let page = table.paginate(Query::new().eq("active", true), 1, 25).await?;
// page.items, page.total, page.page, page.per_page, page.has_next

Batch

use 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?;

Raw SQL

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?;

Migrations

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.

Notes

  • All writes use RETURNING * — no second SELECT after insert/update.
  • insert_batch uses D1's batch() API — all rows in one round trip.
  • JOINs are not supported by the query builder; use raw_query for those.
  • filter_optional* methods accept Option<T> — pass None to skip the filter (match all rows).

License

MIT

About

Query builder / ORM for Cloudflare D1, targeting wasm32-unknown-unknown

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages