diff --git a/README.md b/README.md index 2e2f605..368b606 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/crates/sqlx-cloudflare-core/README.md b/crates/sqlx-cloudflare-core/README.md index 37ac75b..2e0e6ba 100644 --- a/crates/sqlx-cloudflare-core/README.md +++ b/crates/sqlx-cloudflare-core/README.md @@ -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: diff --git a/crates/sqlx-cloudflare-d1/README.md b/crates/sqlx-cloudflare-d1/README.md index 7940a96..2b5dc36 100644 --- a/crates/sqlx-cloudflare-d1/README.md +++ b/crates/sqlx-cloudflare-d1/README.md @@ -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] @@ -19,7 +22,11 @@ struct User { } #[worker::event(fetch)] -async fn fetch(_req: worker::Request, env: worker::Env, _ctx: worker::Context) -> worker::Result { +async fn fetch( + _req: worker::Request, + env: worker::Env, + _ctx: worker::Context, +) -> worker::Result { let conn = D1Connection::from_env(&env, "DB")?; let user = sqlx::query_as::("SELECT id, name FROM users WHERE id = ?") @@ -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 = 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 diff --git a/crates/sqlx-cloudflare-do/README.md b/crates/sqlx-cloudflare-do/README.md index 34493d6..82955fe 100644 --- a/crates/sqlx-cloudflare-do/README.md +++ b/crates/sqlx-cloudflare-do/README.md @@ -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] @@ -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. @@ -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. @@ -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 = 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