sqlx-cloudflare-do 0.2.0

A Cloudflare Durable Objects driver for sqlx: an sqlx::Database over a Durable Object's SQL storage
Documentation

sqlx-cloudflare-do

A Cloudflare Durable Objects driver for sqlx: a real sqlx::Database over a Durable Object's SQL storage, so code written against sqlx's runtime API runs inside a Rust Durable Object.

CI License: MIT

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

use sqlx_cloudflare_do::{Do, DoConnection};
use worker::{durable_object, DurableObject, Env, Request, Response, State};

#[derive(sqlx::FromRow)]
struct User {
    id: i64,
    name: String,
}

#[durable_object]
pub struct Users {
    conn: DoConnection,
}

impl DurableObject for Users {
    fn new(state: State, _env: Env) -> Self {
        Self { conn: DoConnection::new(state.storage()) }
    }

    async fn fetch(&self, _req: Request) -> worker::Result<Response> {
        let user = sqlx::query_as::<Do, User>("SELECT id, name FROM users WHERE id = ?")
            .bind(1_i64)
            .fetch_one(&self.conn)
            .await
            .map_err(|e| worker::Error::RustError(e.to_string()))?;

        Response::ok(user.name)
    }
}

The class must be SQLite-backed -- new_sqlite_classes in wrangler.toml:

[[durable_objects.bindings]]
name = "USERS"
class_name = "Users"

[[migrations]]
tag = "v1"
new_sqlite_classes = ["Users"]

Install

[dependencies]
sqlx-cloudflare-do = "0.1"
sqlx = { version = "0.9", default-features = false, features = ["derive"] }
worker = "0.8"
# `#[durable_object]` expands to code that names it directly.
wasm-bindgen = "0.2"
sqlx-cloudflare-do sqlx worker Rust
0.1 0.9 0.8 1.94+

The crate runs on wasm32-unknown-unknown, inside a Durable Object.

How it differs from D1

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 -- the two share one definition.

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 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 numbers, so a wider i64 is refused rather than silently rounded. Note that sqlx's QueryBuilder::push_bind panics on a refused value instead of returning the error.
  • rows_affected comes from changes(). Like SQLite's own count, it covers the rows the statement itself inserted, updated or deleted, not those its triggers touched. A statement that wrote nothing reports 0 and no last_insert_rowid.
  • No sqlx::migrate!. Create the schema from the object's constructor.
  • No derived TEXT enums. #[derive(sqlx::Type)] works for #[sqlx(transparent)] newtypes and #[repr(i32)] enums, but sqlx only generates string-backed enums for its built-in drivers.
  • No statement logging. log_statements is accepted and ignored.

License

MIT