# d1-orm
Query builder and ORM for [Cloudflare D1](https://developers.cloudflare.com/d1/), written in Rust, targeting `wasm32-unknown-unknown`.
Works with [workers-rs](https://github.com/cloudflare/workers-rs) 0.4.
## Install
```toml
[dependencies]
d1-orm = "0.0.1"
```
## Usage
### Define a model
Implement `D1Model` on any `Deserialize` struct that maps to a D1 table:
```rust
use 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()), // Option<T> → NULL when None
self.created_at.clone().into(),
]
}
}
```
### CRUD
```rust
use d1_orm::{Order, Query, Set, Table};
let table = Table::<UserRow>::new(&db);
// INSERT ... RETURNING *
let user = table.insert(&row).await?;
// INSERT batch — single D1 batch() round trip
let users = table.insert_batch(&rows).await?;
// SELECT
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?;
// UPDATE ... RETURNING *
let updated = table.update(
Set::new().field("name", "Alice").nullable_field("bio", None::<String>),
Query::new().eq("id", id),
).await?;
// DELETE
table.delete(Query::new().eq("id", id)).await?;
// COUNT
let n = table.count(Query::new().eq("active", true)).await?;
```
### Query builder reference
| `.eq("col", val)` | `col = ?N` |
| `.ne("col", val)` | `col != ?N` |
| `.gt("col", val)` | `col > ?N` |
| `.gte("col", val)` | `col >= ?N` |
| `.lt("col", val)` | `col < ?N` |
| `.lte("col", val)` | `col <= ?N` |
| `.is_null("col")` | `col IS NULL` |
| `.is_not_null("col")` | `col IS NOT NULL` |
| `.filter_optional("col", opt)` | `(?N IS NULL OR col = ?N)` |
| `.filter_optional_gte("col", opt)` | `(?N IS NULL OR col >= ?N)` |
| `.filter_optional_lte("col", opt)` | `(?N IS NULL OR col <= ?N)` |
| `.order_by("col", Order::Desc)` | `ORDER BY col DESC` |
| `.limit(n)` | `LIMIT n` |
| `.offset(n)` | `OFFSET n` |
`filter_optional*` methods accept `Option<T>` — pass `None` to skip the filter (match all rows).
### Set builder reference
| `.field("col", val)` | `col = ?N` with a non-null value |
| `.nullable_field("col", opt)` | `col = ?N` — binds `NULL` when `opt` is `None` |
## 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. Drop to raw D1 for those.
## License
MIT