Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,35 @@
> so code written against sqlx's runtime API runs inside a Rust Worker.

[![CI](https://github.com/sqlx-contrib/sqlx-cloudflare/actions/workflows/ci.yml/badge.svg)](https://github.com/sqlx-contrib/sqlx-cloudflare/actions/workflows/ci.yml)
[![Rust (edition 2021)](https://img.shields.io/badge/Rust-2021-black?logo=rust)](https://www.rust-lang.org/)
[![Nix Flake](https://img.shields.io/badge/Nix-Flake-5277C3?logo=nixos&logoColor=white)](https://nixos.wiki/wiki/Flakes)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

> [!NOTE]
> **Pre-1.0.** The API may still change between minor versions.

| Crate | What it drives |
|---|---|
| [`sqlx-cloudflare-d1`](crates/sqlx-cloudflare-d1) | Cloudflare D1, over the Workers D1 binding |
| [`sqlx-cloudflare-do`](crates/sqlx-cloudflare-do) | A Durable Object's SQL storage |
| Crate | Version | Docs | What it is |
|---|---|---|---|
| [`sqlx-cloudflare-d1`](crates/sqlx-cloudflare-d1) | [![crates.io](https://img.shields.io/crates/v/sqlx-cloudflare-d1.svg)](https://crates.io/crates/sqlx-cloudflare-d1) | [![docs.rs](https://img.shields.io/docsrs/sqlx-cloudflare-d1)](https://docs.rs/sqlx-cloudflare-d1) | A driver for Cloudflare D1, over the Workers D1 binding |
| [`sqlx-cloudflare-do`](crates/sqlx-cloudflare-do) | [![crates.io](https://img.shields.io/crates/v/sqlx-cloudflare-do.svg)](https://crates.io/crates/sqlx-cloudflare-do) | [![docs.rs](https://img.shields.io/docsrs/sqlx-cloudflare-do)](https://docs.rs/sqlx-cloudflare-do) | A driver for a Durable Object's SQL storage |
| [`sqlx-cloudflare-core`](crates/sqlx-cloudflare-core) | [![crates.io](https://img.shields.io/crates/v/sqlx-cloudflare-core.svg)](https://crates.io/crates/sqlx-cloudflare-core) | [![docs.rs](https://img.shields.io/docsrs/sqlx-cloudflare-core)](https://docs.rs/sqlx-cloudflare-core) | The internals both drivers share -- not a dependency of yours |

Each driver is independent: depend on the one for the storage you use. Both
are SQLite behind a JavaScript API, so they share their value model, type
mapping and error parsing through
[`sqlx-cloudflare-core`](crates/sqlx-cloudflare-core), which you do not
depend on directly.
mapping and error parsing through `sqlx-cloudflare-core`.

## Which one?

- **D1** is one database your Worker talks to over the network: shared by
every request, queryable across all your data, managed with `wrangler d1`.
Atomic work goes through a batch -- D1 has no interactive transactions.
- **Durable Object storage** is a private SQLite database inside each object,
in the same process as your code: queries take microseconds, every request
to an object sees one consistent database, and transactions are real. Pick
it for per-user, per-tenant or per-room data.

Both drivers map types the same way and share the same limit on integers
(±2^53 − 1, since values cross as JavaScript numbers).

## Development

Expand Down
5 changes: 5 additions & 0 deletions crates/sqlx-cloudflare-core/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# sqlx-cloudflare-core

[![crates.io](https://img.shields.io/crates/v/sqlx-cloudflare-core.svg)](https://crates.io/crates/sqlx-cloudflare-core)
[![docs.rs](https://img.shields.io/docsrs/sqlx-cloudflare-core)](https://docs.rs/sqlx-cloudflare-core)
[![CI](https://github.com/sqlx-contrib/sqlx-cloudflare/actions/workflows/ci.yml/badge.svg)](https://github.com/sqlx-contrib/sqlx-cloudflare/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/sqlx-contrib/sqlx-cloudflare/blob/main/LICENSE)

The shared internals of the sqlx-cloudflare drivers. You want one of those
instead:

Expand Down
50 changes: 43 additions & 7 deletions crates/sqlx-cloudflare-d1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
> A Cloudflare D1 driver for sqlx: a real `sqlx::Database` over the Workers D1
> binding, so code written against sqlx's runtime API runs inside a Rust Worker.

[![crates.io](https://img.shields.io/crates/v/sqlx-cloudflare-d1.svg)](https://crates.io/crates/sqlx-cloudflare-d1)
[![docs.rs](https://img.shields.io/docsrs/sqlx-cloudflare-d1)](https://docs.rs/sqlx-cloudflare-d1)
[![CI](https://github.com/sqlx-contrib/sqlx-cloudflare/actions/workflows/ci.yml/badge.svg)](https://github.com/sqlx-contrib/sqlx-cloudflare/actions/workflows/ci.yml)
[![MSRV](https://img.shields.io/crates/msrv/sqlx-cloudflare-d1)](https://github.com/sqlx-contrib/sqlx-cloudflare/blob/main/Cargo.toml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/sqlx-contrib/sqlx-cloudflare/blob/main/LICENSE)

> [!NOTE]
Expand All @@ -19,7 +22,11 @@ struct User {
}

#[worker::event(fetch)]
async fn fetch(_req: worker::Request, env: worker::Env, _ctx: worker::Context) -> worker::Result<worker::Response> {
async fn fetch(
_req: worker::Request,
env: worker::Env,
_ctx: worker::Context,
) -> worker::Result<worker::Response> {
let conn = D1Connection::from_env(&env, "DB")?;

let user = sqlx::query_as::<D1, User>("SELECT id, name FROM users WHERE id = ?")
Expand Down Expand Up @@ -47,17 +54,46 @@ worker = { version = "0.8", features = ["d1"] }

The crate runs on `wasm32-unknown-unknown`, inside a Worker.

## Batches

D1 has no interactive transactions, so atomic work is a batch: every
statement in one round trip, all applied or none.

```rust
// What each statement did.
let results = conn
.execute_batch([
sqlx::query("UPDATE accounts SET balance = balance - ?1 WHERE id = ?2").bind(10).bind(1),
sqlx::query("UPDATE accounts SET balance = balance + ?1 WHERE id = ?2").bind(10).bind(2),
])
.await?;

// Each statement's rows as well, as a stream -- the shape of sqlc's
// `:batchexec`, `:batchone` and `:batchmany` queries.
use futures_util::TryStreamExt;
use sqlx::Row;

let names: Vec<String> = conn
.fetch_batch([1_i64, 2, 3].map(|id| {
sqlx::query("SELECT name FROM users WHERE id = ?").bind(id)
}))
.map_ok(|result| result.rows()[0].get("name"))
.try_collect()
.await?;
```

The whole batch runs when the stream is first polled; a failure is the one
item it yields, with nothing applied.

## Limitations

- **No `query!` / `query_as!` macros.** sqlx's macros only know its built-in
drivers. The runtime API (`sqlx::query`, `query_as`, `FromRow`) works.
- **No transactions.** D1 cannot hold one open across calls, so `begin()`
fails. Use `D1Connection::execute_batch`, which runs statements atomically
in one round trip.
- **Rows in a batch are keyed by name.** `D1Connection::fetch_batch` streams
each statement's rows and result -- the shape of sqlc's `:batch*` queries.
D1's `batch()` returns rows as objects, so two columns with one name
collapse to the last. Alias them apart inside a batch.
fails. Use a [batch](#batches).
- **Rows in a batch are keyed by name.** D1's `batch()` returns rows as
objects, so in `fetch_batch` two columns with one name collapse to the
last. Alias them apart inside a batch.
- **No `sqlx::Pool`.** Nothing to pool: build a connection per request from
the binding.
- **Integers are limited to ±(2^53 − 1).** D1 passes values as JavaScript
Expand Down
70 changes: 58 additions & 12 deletions crates/sqlx-cloudflare-do/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
> Durable Object's SQL storage, so code written against sqlx's runtime API
> runs inside a Rust Durable Object.

[![crates.io](https://img.shields.io/crates/v/sqlx-cloudflare-do.svg)](https://crates.io/crates/sqlx-cloudflare-do)
[![docs.rs](https://img.shields.io/docsrs/sqlx-cloudflare-do)](https://docs.rs/sqlx-cloudflare-do)
[![CI](https://github.com/sqlx-contrib/sqlx-cloudflare/actions/workflows/ci.yml/badge.svg)](https://github.com/sqlx-contrib/sqlx-cloudflare/actions/workflows/ci.yml)
[![MSRV](https://img.shields.io/crates/msrv/sqlx-cloudflare-do)](https://github.com/sqlx-contrib/sqlx-cloudflare/blob/main/Cargo.toml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/sqlx-contrib/sqlx-cloudflare/blob/main/LICENSE)

> [!NOTE]
Expand Down Expand Up @@ -58,7 +61,7 @@ new_sqlite_classes = ["Users"]

```toml
[dependencies]
sqlx-cloudflare-do = "0.1"
sqlx-cloudflare-do = "0.2"
sqlx = { version = "0.9", default-features = false, features = ["derive"] }
worker = "0.8"
# `#[durable_object]` expands to code that names it directly.
Expand All @@ -67,6 +70,7 @@ wasm-bindgen = "0.2"

| sqlx-cloudflare-do | sqlx | worker | Rust |
|---|---|---|---|
| 0.2 | 0.9 | 0.8 | 1.94+ |
| 0.1 | 0.9 | 0.8 | 1.94+ |

The crate runs on `wasm32-unknown-unknown`, inside a Durable Object.
Expand All @@ -76,22 +80,64 @@ The crate runs on `wasm32-unknown-unknown`, inside a Durable Object.
The database lives in the Durable Object, so a query never leaves the
process: `sql.exec` runs it before returning, and the driver reads every row
out on the spot. Each query's future finishes on its first poll. Types map
exactly as in [`sqlx-cloudflare-d1`](https://crates.io/crates/sqlx-cloudflare-d1) -- the two share
one definition.
exactly as in
[`sqlx-cloudflare-d1`](https://crates.io/crates/sqlx-cloudflare-d1) -- the
two share one definition.

## Transactions

`sql.exec` rejects `BEGIN`, so a transaction is a callback. It commits when
the callback returns `Ok` and rolls back every write when it returns `Err`,
and inside it you read your own writes:

```rust
let id = conn
.transaction(move |tx| async move {
let id: i64 = sqlx::query_scalar("INSERT INTO users (email) VALUES (?) RETURNING id")
.bind(email)
.fetch_one(&tx)
.await?;
sqlx::query("INSERT INTO posts (user_id, title) VALUES (?, 'hello')")
.bind(id)
.execute(&tx)
.await?;
Ok::<_, sqlx::Error>(id)
})
.await?;
```

- **The callback owns what it uses.** It is `'static` -- move data in, and run
queries through the `tx` it is handed, not the connection.
- **It holds the whole object.** No other request reaches the object until
the transaction ends, even while the callback awaits something that is not
storage. That keeps other requests' writes out of it -- and is why it should
be short: no `fetch()` inside, and never a request to the object itself.

## Batches

`execute_batch` runs statements atomically without a callback, and
`fetch_batch` streams each statement's rows and result as well -- the shape of
sqlc's `:batchexec`, `:batchone` and `:batchmany` queries:

```rust
use futures_util::TryStreamExt;
use sqlx::Row;

let names: Vec<String> = conn
.fetch_batch([1_i64, 2, 3].map(|id| {
sqlx::query("SELECT name FROM users WHERE id = ?").bind(id)
}))
.map_ok(|result| result.rows()[0].get("name"))
.try_collect()
.await?;
```

## Limitations

- **No `query!` / `query_as!` macros.** sqlx's macros only know its built-in
drivers. The runtime API (`sqlx::query`, `query_as`, `FromRow`) works.
- **Transactions take a callback, not `begin()`.** `sql.exec` rejects `BEGIN`
and `SAVEPOINT`, so `begin()` fails. `DoConnection::transaction` runs a
callback inside `Storage::transaction` instead: a real transaction that reads
its own writes and rolls back when the callback returns `Err`. The callback
must own what it uses (`'static`), and the object serves no other request
until it ends, so keep it short -- no `fetch()` inside.
- **Batches.** `DoConnection::execute_batch` runs statements atomically, and
`DoConnection::fetch_batch` streams each statement's rows and result as
well -- the shape of sqlc's `:batch*` queries.
- **No `begin()`.** `sql.exec` rejects `BEGIN` and `SAVEPOINT`, so sqlx's
`begin()` fails. Use a [transaction callback](#transactions).
- **No `sqlx::Pool`.** Nothing to pool: the connection is a handle to storage
the object owns. Keep one in the object's struct.
- **Integers are limited to ±(2^53 − 1).** Values cross as JavaScript
Expand Down
Loading