rust-d1-orm 0.0.1

Query builder / ORM for Cloudflare D1, targeting wasm32-unknown-unknown
Documentation
# 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

| Method | SQL fragment |
|---|---|
| `.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

| Method | Behavior |
|---|---|
| `.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