From b599938ce7a4012204e8a4f38efa087f0a32f561 Mon Sep 17 00:00:00 2001 From: Svetlin Ralchev Date: Mon, 28 Sep 2026 08:02:06 +0400 Subject: [PATCH 1/2] feat(hd): add sqlx-cloudflare-hd, sqlx's Postgres driver over Hyperdrive Hyperdrive speaks the Postgres wire protocol, so this crate writes no driver: it opens the Worker's socket to a Hyperdrive binding, settles TLS through the Workers runtime's startTls(), and hands the socket to sqlx-postgres. `connect`, `connect_with` and `options` return sqlx's own `PgConnection` and `PgConnectOptions`, so consumers get `sqlx::Postgres` with all of its types, transactions and `sqlx::migrate!`. It needs two sqlx changes that are not released yet, proposed in transact-rs/sqlx#4426: `PgConnection::connect_with_socket`, and an `Instant` that does not panic on wasm32-unknown-unknown. Until they ship, the workspace patches every sqlx crate to that branch, pinned to a commit, and the crate is `publish = false` and out of the release-please config. The test Worker runs ten scenarios under `wrangler dev --local` against a scratch Postgres that run.sh starts with SCRAM auth and no TLS, so the dev shell gains postgresql and `make test-worker` a third runner. --- Cargo.lock | 160 +- Cargo.toml | 17 + Makefile | 8 +- README.md | 1 + crates/sqlx-cloudflare-hd/Cargo.toml | 44 + crates/sqlx-cloudflare-hd/README.md | 72 + crates/sqlx-cloudflare-hd/src/lib.rs | 148 ++ crates/sqlx-cloudflare-hd/src/socket.rs | 122 ++ crates/sqlx-cloudflare-hd/tests/driver.rs | 51 + .../tests/worker/.gitignore | 4 + .../tests/worker/Cargo.lock | 1593 +++++++++++++++++ .../tests/worker/Cargo.toml | 27 + .../migrations/20260928000001_widgets.sql | 1 + .../migrations/20260928000002_widget_seed.sql | 1 + crates/sqlx-cloudflare-hd/tests/worker/run.sh | 52 + .../tests/worker/src/lib.rs | 269 +++ .../tests/worker/wrangler.toml | 14 + flake.nix | 3 + 18 files changed, 2507 insertions(+), 80 deletions(-) create mode 100644 crates/sqlx-cloudflare-hd/Cargo.toml create mode 100644 crates/sqlx-cloudflare-hd/README.md create mode 100644 crates/sqlx-cloudflare-hd/src/lib.rs create mode 100644 crates/sqlx-cloudflare-hd/src/socket.rs create mode 100644 crates/sqlx-cloudflare-hd/tests/driver.rs create mode 100644 crates/sqlx-cloudflare-hd/tests/worker/.gitignore create mode 100644 crates/sqlx-cloudflare-hd/tests/worker/Cargo.lock create mode 100644 crates/sqlx-cloudflare-hd/tests/worker/Cargo.toml create mode 100644 crates/sqlx-cloudflare-hd/tests/worker/migrations/20260928000001_widgets.sql create mode 100644 crates/sqlx-cloudflare-hd/tests/worker/migrations/20260928000002_widget_seed.sql create mode 100755 crates/sqlx-cloudflare-hd/tests/worker/run.sh create mode 100644 crates/sqlx-cloudflare-hd/tests/worker/src/lib.rs create mode 100644 crates/sqlx-cloudflare-hd/tests/worker/wrangler.toml diff --git a/Cargo.lock b/Cargo.lock index beb9d60..f1b36dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,15 +55,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - [[package]] name = "block-buffer" version = "0.12.1" @@ -104,7 +95,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", - "cpufeatures 0.3.1", + "cpufeatures", "rand_core", ] @@ -125,15 +116,6 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - [[package]] name = "cpufeatures" version = "0.3.1" @@ -173,16 +155,6 @@ version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "crypto-common" version = "0.2.2" @@ -201,24 +173,14 @@ dependencies = [ "cmov", ] -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer 0.10.4", - "crypto-common 0.1.6", -] - [[package]] name = "digest" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.1", - "crypto-common 0.2.2", + "block-buffer", + "crypto-common", "ctutils", ] @@ -400,9 +362,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "rand_core", + "wasm-bindgen", ] [[package]] @@ -458,7 +422,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ - "digest 0.11.3", + "digest", ] [[package]] @@ -633,6 +597,15 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + [[package]] name = "libsqlite3-sys" version = "0.37.0" @@ -677,7 +650,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" dependencies = [ "cfg-if", - "digest 0.11.3", + "digest", ] [[package]] @@ -695,6 +668,24 @@ dependencies = [ "autocfg", ] +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "objc2-core-foundation", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -918,19 +909,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures 0.3.1", - "digest 0.11.3", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", + "cpufeatures", + "digest", ] [[package]] @@ -940,8 +920,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures 0.3.1", - "digest 0.11.3", + "cpufeatures", + "digest", ] [[package]] @@ -971,8 +951,7 @@ dependencies = [ [[package]] name = "sqlx" version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" +source = "git+https://github.com/iamralch/sqlx?rev=4313dae17eb0d985e905022fe5e9ab862ec14e86#4313dae17eb0d985e905022fe5e9ab862ec14e86" dependencies = [ "sqlx-core", "sqlx-macros", @@ -1024,11 +1003,24 @@ dependencies = [ "worker", ] +[[package]] +name = "sqlx-cloudflare-hd" +version = "0.0.0" +dependencies = [ + "getrandom", + "serde_json", + "sqlx", + "sqlx-core", + "sqlx-postgres", + "tokio", + "ureq", + "worker", +] + [[package]] name = "sqlx-core" version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b44e85bf579a8eeb4ceaa77a3a523baf2bf0e9bac7e40f405d537b5d2d5ccb" +source = "git+https://github.com/iamralch/sqlx?rev=4313dae17eb0d985e905022fe5e9ab862ec14e86#4313dae17eb0d985e905022fe5e9ab862ec14e86" dependencies = [ "base64 0.22.1", "bytes", @@ -1049,18 +1041,18 @@ dependencies = [ "percent-encoding", "serde", "serde_json", - "sha2 0.10.9", + "sha2", "smallvec", "thiserror", "tracing", "url", + "web-time", ] [[package]] name = "sqlx-macros" version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" +source = "git+https://github.com/iamralch/sqlx?rev=4313dae17eb0d985e905022fe5e9ab862ec14e86#4313dae17eb0d985e905022fe5e9ab862ec14e86" dependencies = [ "proc-macro2", "quote", @@ -1072,8 +1064,7 @@ dependencies = [ [[package]] name = "sqlx-macros-core" version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb8d96de5fdc85a5c4ec813432b523ec637e80ba98f046555f75f7908ddac7c3" +source = "git+https://github.com/iamralch/sqlx?rev=4313dae17eb0d985e905022fe5e9ab862ec14e86#4313dae17eb0d985e905022fe5e9ab862ec14e86" dependencies = [ "cfg-if", "dotenvy", @@ -1084,8 +1075,9 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2 0.10.9", + "sha2", "sqlx-core", + "sqlx-postgres", "syn 2.0.119", "thiserror", "url", @@ -1094,13 +1086,12 @@ dependencies = [ [[package]] name = "sqlx-mysql" version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90b8020fe17c5f2c245bfa2505d7ef59c5604839527c740266ad2214acebea27" +source = "git+https://github.com/iamralch/sqlx?rev=4313dae17eb0d985e905022fe5e9ab862ec14e86#4313dae17eb0d985e905022fe5e9ab862ec14e86" dependencies = [ "bitflags", "byteorder", "bytes", - "digest 0.11.3", + "digest", "dotenvy", "either", "futures-core", @@ -1110,7 +1101,7 @@ dependencies = [ "percent-encoding", "serde", "sha1", - "sha2 0.11.0", + "sha2", "sqlx-core", "thiserror", "tracing", @@ -1119,13 +1110,13 @@ dependencies = [ [[package]] name = "sqlx-postgres" version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87a2bdd6e83f6b3ea525ca9fee568030508b58355a43d0b2c1674d5f79dcd65e" +source = "git+https://github.com/iamralch/sqlx?rev=4313dae17eb0d985e905022fe5e9ab862ec14e86#4313dae17eb0d985e905022fe5e9ab862ec14e86" dependencies = [ "atoi", "base64 0.22.1", "bitflags", "byteorder", + "crc", "dotenvy", "etcetera", "futures-channel", @@ -1141,7 +1132,7 @@ dependencies = [ "rand", "serde", "serde_json", - "sha2 0.11.0", + "sha2", "smallvec", "sqlx-core", "stringprep", @@ -1153,8 +1144,7 @@ dependencies = [ [[package]] name = "sqlx-sqlite" version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488e99c397a62007e4229aec669a179816339afc6d2620ca6fa420dbee2e982c" +source = "git+https://github.com/iamralch/sqlx?rev=4313dae17eb0d985e905022fe5e9ab862ec14e86#4313dae17eb0d985e905022fe5e9ab862ec14e86" dependencies = [ "atoi", "flume", @@ -1494,11 +1484,27 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "whoami" version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c" +dependencies = [ + "libc", + "libredox", + "objc2-system-configuration", + "web-sys", +] [[package]] name = "windows-link" diff --git a/Cargo.toml b/Cargo.toml index e6b3806..b0fd858 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = ["crates/*"] exclude = [ "crates/sqlx-cloudflare-d1/tests/worker", "crates/sqlx-cloudflare-do/tests/worker", + "crates/sqlx-cloudflare-hd/tests/worker", ] [workspace.package] @@ -43,7 +44,13 @@ sqlx-core = { version = "0.9", default-features = false, features = ["offline"] # The facade, for tests only: proving a consumer naming `sqlx::Database` sees # a driver's database type. sqlx = { version = "0.9", default-features = false } +# The Postgres driver itself, for sqlx-cloudflare-hd: Hyperdrive speaks the +# Postgres wire protocol, so there is no driver to write, only a socket to +# hand it. `offline` for the same reason as `sqlx-core` above. +sqlx-postgres = { version = "0.9", default-features = false, features = ["offline"] } worker = { version = "0.8.6", default-features = false } +# Only for the `AsyncRead`/`AsyncWrite` traits `worker::Socket` implements. +tokio = { version = "1", default-features = false } wasm-bindgen = "0.2" js-sys = "0.3" futures-core = { version = "0.3", default-features = false } @@ -51,3 +58,13 @@ futures-util = { version = "0.3", default-features = false } log = { version = "0.4", default-features = false } ureq = { version = "3", default-features = false } serde_json = "1" + +# sqlx-cloudflare-hd needs two changes sqlx has not released yet -- a way to +# hand `PgConnection` a socket, and an `Instant` that does not panic on +# wasm32-unknown-unknown -- proposed in transact-rs/sqlx#4426. Until they ship, +# every sqlx crate comes from that branch, pinned to a commit: a patch applies +# to the whole graph, so D1 and DO build against it too. +[patch.crates-io] +sqlx = { git = "https://github.com/iamralch/sqlx", rev = "4313dae17eb0d985e905022fe5e9ab862ec14e86" } +sqlx-core = { git = "https://github.com/iamralch/sqlx", rev = "4313dae17eb0d985e905022fe5e9ab862ec14e86" } +sqlx-postgres = { git = "https://github.com/iamralch/sqlx", rev = "4313dae17eb0d985e905022fe5e9ab862ec14e86" } diff --git a/Makefile b/Makefile index 58021ca..354c5a4 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ # checked on the host says nothing about the one target that matters. # # `make test` needs no Worker: each driver's tests/driver.rs skips itself -# unless its D1_WORKER_URL or DO_WORKER_URL is set. `make test-worker` builds +# unless its D1_WORKER_URL, DO_WORKER_URL or HD_WORKER_URL is set. `make test-worker` builds # and serves each driver's test Worker in turn and sets it. WASM := --target wasm32-unknown-unknown @@ -29,12 +29,14 @@ test: # The integration tests: builds each driver's test Worker, serves it with # `wrangler dev --local` on fresh local storage -- a D1 database, a Durable -# Object -- and runs every scenario in it. No Cloudflare account involved -- -# see run.sh next to each test Worker. +# Object, a scratch Postgres behind a Hyperdrive binding -- and runs every +# scenario in it. No Cloudflare account involved -- see run.sh next to each +# test Worker. .PHONY: test-worker test-worker: crates/sqlx-cloudflare-d1/tests/worker/run.sh crates/sqlx-cloudflare-do/tests/worker/run.sh + crates/sqlx-cloudflare-hd/tests/worker/run.sh # Both targets for clippy too: `cfg(target_arch = "wasm32")` code is invisible # to a host-only run. diff --git a/README.md b/README.md index 368b606..237021f 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ |---|---|---|---| | [`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-hd`](crates/sqlx-cloudflare-hd) | unreleased | -- | sqlx's own Postgres driver over a Hyperdrive binding -- waiting on [transact-rs/sqlx#4426](https://github.com/transact-rs/sqlx/issues/4426) | | [`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 diff --git a/crates/sqlx-cloudflare-hd/Cargo.toml b/crates/sqlx-cloudflare-hd/Cargo.toml new file mode 100644 index 0000000..af8b086 --- /dev/null +++ b/crates/sqlx-cloudflare-hd/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "sqlx-cloudflare-hd" +description = "Cloudflare Hyperdrive for sqlx: sqlx's own Postgres driver over a Workers Hyperdrive binding" +version = "0.0.0" # literal, not inherited -- see [workspace.package] +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +readme = "README.md" +keywords = ["sqlx", "cloudflare", "hyperdrive", "workers", "postgres"] +categories = ["database", "wasm"] +# The test Worker is not part of the library. +exclude = ["tests/worker"] +# Not until sqlx releases what this crate needs (transact-rs/sqlx#4426): until +# then it builds against a git branch, which crates.io will not take. Nor is it +# in the release-please config, for the same reason. +publish = false + +[lints] +workspace = true + +[dependencies] +sqlx-core = { workspace = true } +sqlx-postgres = { workspace = true } +worker = { workspace = true } +tokio = { workspace = true } + +# sqlx-postgres draws its SCRAM nonces through `rand`, and on +# wasm32-unknown-unknown `getrandom` refuses to build until told where +# randomness comes from: `wasm_js` is the JS host's `crypto.getRandomValues`. +# Enabled here so no consumer has to know. +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] +getrandom = { version = "0.4", features = ["wasm_js"] } + +[dev-dependencies] +# The facade, for the crate docs' example: what a consumer writes against. +sqlx = { workspace = true, features = ["postgres"] } +# `tests/driver.rs` drives the test Worker over plain HTTP on localhost, so no TLS. +ureq = { workspace = true } +serde_json = { workspace = true } + +[package.metadata.docs.rs] +all-features = true +targets = ["wasm32-unknown-unknown"] diff --git a/crates/sqlx-cloudflare-hd/README.md b/crates/sqlx-cloudflare-hd/README.md new file mode 100644 index 0000000..72b150b --- /dev/null +++ b/crates/sqlx-cloudflare-hd/README.md @@ -0,0 +1,72 @@ +# sqlx-cloudflare-hd + +> Cloudflare Hyperdrive for sqlx: sqlx's own Postgres driver over a Workers +> Hyperdrive binding, so a Rust Worker gets a real `sqlx::Postgres` connection. + +[![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) + +> [!WARNING] +> **Not published yet.** This crate needs two sqlx changes that have not been +> released -- `PgConnection::connect_with_socket` and an `Instant` that works +> on `wasm32-unknown-unknown` -- proposed in +> [transact-rs/sqlx#4426](https://github.com/transact-rs/sqlx/issues/4426). +> Until they ship, it builds against that branch and cannot go to crates.io. + +```rust +use sqlx::Connection; + +#[derive(sqlx::FromRow)] +struct User { + id: i64, + name: String, +} + +#[worker::event(fetch)] +async fn fetch( + _req: worker::Request, + env: worker::Env, + _ctx: worker::Context, +) -> worker::Result { + let mut conn = sqlx_cloudflare_hd::connect(&env, "HYPERDRIVE") + .await + .map_err(|e| worker::Error::RustError(e.to_string()))?; + + let user = sqlx::query_as::<_, User>("SELECT id, name FROM users WHERE id = $1") + .bind(1_i64) + .fetch_one(&mut conn) + .await + .map_err(|e| worker::Error::RustError(e.to_string()))?; + + conn.close() + .await + .map_err(|e| worker::Error::RustError(e.to_string()))?; + worker::Response::ok(user.name) +} +``` + +There is no driver in here. Hyperdrive speaks the Postgres wire protocol, so +this crate opens the Worker's socket to the binding, settles TLS through the +Workers runtime, and hands the socket to sqlx's Postgres driver. What comes +back is a plain `PgConnection`: every type sqlx-postgres maps, `query_as`, +`#[derive(sqlx::FromRow)]`, transactions and `sqlx::migrate!`. + +Connect once per request. Hyperdrive pools the connections to your database, +and a Worker cannot share a socket across requests anyway, so there is no +`sqlx::Pool` -- it also needs an async runtime a Worker does not have. For the +same reason there is no `PgListener`. + +## Options + +`connect` uses the options Hyperdrive hands out. To change any of them while +keeping the credentials, start from `options` and connect with `connect_with`: + +```rust +let hyperdrive = env.hyperdrive("HYPERDRIVE")?; +let options = sqlx_cloudflare_hd::options(&hyperdrive)?.application_name("my-worker"); +let mut conn = sqlx_cloudflare_hd::connect_with(&hyperdrive, &options).await?; +``` + +The socket always goes to Hyperdrive's host and port. `sslmode` is honoured, +but the Workers runtime performs the TLS handshake, so the certificate options +are not used. diff --git a/crates/sqlx-cloudflare-hd/src/lib.rs b/crates/sqlx-cloudflare-hd/src/lib.rs new file mode 100644 index 0000000..90c27a3 --- /dev/null +++ b/crates/sqlx-cloudflare-hd/src/lib.rs @@ -0,0 +1,148 @@ +//! Cloudflare Hyperdrive for sqlx. +//! +//! Hyperdrive speaks the Postgres wire protocol, so there is no driver here: +//! this crate opens the Worker's socket to a Hyperdrive binding and hands it to +//! sqlx's own Postgres driver. What you get back is a plain +//! [`PgConnection`] -- `sqlx::Postgres`, with +//! all of its types, `query_as`, `#[derive(sqlx::FromRow)]`, transactions and +//! `sqlx::migrate!`. +//! +//! ```no_run +//! use sqlx::Connection; +//! +//! async fn user_name(env: &worker::Env, id: i64) -> Result { +//! // One connection per request: Hyperdrive pools the connections to +//! // the database behind it. +//! let mut conn = sqlx_cloudflare_hd::connect(env, "HYPERDRIVE").await?; +//! +//! let name = sqlx::query_scalar("SELECT name FROM users WHERE id = $1") +//! .bind(id) +//! .fetch_one(&mut conn) +//! .await?; +//! +//! conn.close().await?; +//! Ok(name) +//! } +//! ``` +//! +//! # What does not work +//! +//! - **`sqlx::Pool`.** It needs an async runtime to spawn its maintenance +//! tasks, and a Worker cannot share a socket across requests anyway. +//! Hyperdrive is the pool: connect once per request. +//! - **`PgListener`** (`LISTEN`/`NOTIFY`), which also spawns a task. +//! +//! This crate only *runs* on `wasm32-unknown-unknown`, inside a Worker. It +//! compiles on other targets so that docs and `cargo check` work. + +mod socket; + +use std::io; + +use sqlx_core::net::Socket as _; +use sqlx_core::Error; +use sqlx_postgres::{PgConnectOptions, PgConnection, PgSslMode}; +use worker::{Hyperdrive, SecureTransport}; + +use crate::socket::HdSocket; + +/// A Postgres `SSLRequest`: length 8, then the request code 80877103. +const SSL_REQUEST: [u8; 8] = [0, 0, 0, 8, 0x04, 0xd2, 0x16, 0x2f]; + +/// Connects to the database behind the Hyperdrive binding called `binding`. +/// +/// # Errors +/// +/// When there is no binding by that name, or it is not a Hyperdrive +/// configuration; when the socket, TLS or the Postgres handshake fails; and +/// when the credentials Hyperdrive holds are rejected. +pub async fn connect(env: &worker::Env, binding: &str) -> Result { + let hyperdrive = env + .hyperdrive(binding) + .map_err(|e| Error::Configuration(e.to_string().into()))?; + connect_with(&hyperdrive, &options(&hyperdrive)?).await +} + +/// The options Hyperdrive hands out, parsed from its connection string. +/// +/// Start here to change anything before [`connect_with`] -- the application +/// name, the statement cache -- while keeping the credentials. +/// +/// # Errors +/// +/// When the connection string does not parse, which would be a Hyperdrive bug. +pub fn options(hyperdrive: &Hyperdrive) -> Result { + hyperdrive.connection_string().parse() +} + +/// Connects through `hyperdrive` with `options`. +/// +/// The socket always goes to Hyperdrive's host and port; the host, port and +/// socket path in `options` are ignored. `ssl_mode` is honoured, but the +/// Workers runtime performs the TLS handshake, so the certificate options +/// (`ssl_root_cert` and the client certificate and key) are not used. +/// +/// # Errors +/// +/// When the socket, TLS or the Postgres handshake fails, or the credentials +/// are rejected. +pub async fn connect_with( + hyperdrive: &Hyperdrive, + options: &PgConnectOptions, +) -> Result { + let mode = options.get_ssl_mode(); + let transport = if wants_tls(mode) { + SecureTransport::StartTls + } else { + SecureTransport::Off + }; + let socket = worker::Socket::builder() + .secure_transport(transport) + .connect(hyperdrive.host(), hyperdrive.port()) + .map_err(|e| Error::Io(io::Error::other(e.to_string())))?; + + let socket = negotiate_tls(HdSocket::new(socket), mode).await?; + + // TLS is settled -- upgraded, or declined as `mode` allows -- so sqlx must + // not ask the server again. + let options = options.clone().ssl_mode(PgSslMode::Disable); + PgConnection::connect_with_socket(&options, socket).await +} + +fn wants_tls(mode: PgSslMode) -> bool { + !matches!(mode, PgSslMode::Disable | PgSslMode::Allow) +} + +/// Asks the server for TLS, as sqlx itself would, but upgrades through the +/// Workers runtime's `startTls()` rather than a TLS library of sqlx's. +async fn negotiate_tls(mut socket: HdSocket, mode: PgSslMode) -> Result { + if !wants_tls(mode) { + return Ok(socket); + } + + let mut request = &SSL_REQUEST[..]; + while !request.is_empty() { + let n = socket.write(request).await?; + request = &request[n..]; + } + socket.flush().await?; + + // The server answers with a single byte: `S` to go ahead, `N` to decline. + let mut reply = [0u8; 1]; + let mut buf = &mut reply[..]; + if socket.read(&mut buf).await? == 0 { + return Err(Error::Io(io::Error::new( + io::ErrorKind::UnexpectedEof, + "server closed the connection during TLS negotiation", + ))); + } + + match reply[0] { + b'S' => Ok(HdSocket::new(socket.into_inner()?.start_tls())), + b'N' if matches!(mode, PgSslMode::Prefer) => Ok(socket), + b'N' => Err(Error::Tls("server does not support TLS".into())), + other => Err(Error::Protocol(format!( + "unexpected response to SSLRequest: {other:#04x}" + ))), + } +} diff --git a/crates/sqlx-cloudflare-hd/src/socket.rs b/crates/sqlx-cloudflare-hd/src/socket.rs new file mode 100644 index 0000000..e5f9eae --- /dev/null +++ b/crates/sqlx-cloudflare-hd/src/socket.rs @@ -0,0 +1,122 @@ +//! A `worker::Socket` as sqlx's [`Socket`]. + +use std::io; +use std::pin::Pin; +use std::task::{ready, Context, Poll}; + +use sqlx_core::io::ReadBuf; +use sqlx_core::net::Socket; +use tokio::io::{AsyncRead, AsyncWrite}; + +/// How much [`HdSocket::try_write`] queues before it asks sqlx to wait for a +/// flush. +const WRITE_BUF_LIMIT: usize = 64 * 1024; + +/// How much one read from the Worker's socket takes at most. +const READ_CHUNK: usize = 8 * 1024; + +/// A `worker::Socket` as sqlx's readiness-based [`Socket`]. +/// +/// sqlx asks "can I read / write now?" and then tries without a waker; a +/// `worker::Socket` only offers tokio's poll-based `AsyncRead` and +/// `AsyncWrite`. So reads land in `read_buf` from `poll_read_ready` and +/// `try_read` drains it, and writes queue in `write_buf` until +/// `poll_write_ready` or `poll_flush` pushes them out. +pub(crate) struct HdSocket { + inner: worker::Socket, + read_buf: Vec, + eof: bool, + write_buf: Vec, +} + +impl HdSocket { + pub(crate) fn new(inner: worker::Socket) -> Self { + Self { + inner, + read_buf: Vec::new(), + eof: false, + write_buf: Vec::new(), + } + } + + /// The Worker's socket back, for `start_tls`. + /// + /// Fails if the server has already sent bytes nobody has read. Before a TLS + /// upgrade those would be plaintext an attacker injected, which must not be + /// read as though it came over TLS (the attack behind CVE-2021-23222). + pub(crate) fn into_inner(self) -> io::Result { + if !self.read_buf.is_empty() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "server sent unexpected data after accepting TLS", + )); + } + Ok(self.inner) + } + + fn poll_drain(&mut self, cx: &mut Context<'_>) -> Poll> { + while !self.write_buf.is_empty() { + let n = ready!(Pin::new(&mut self.inner).poll_write(cx, &self.write_buf))?; + if n == 0 { + return Poll::Ready(Err(io::ErrorKind::WriteZero.into())); + } + self.write_buf.drain(..n); + } + Poll::Ready(Ok(())) + } +} + +impl Socket for HdSocket { + fn try_read(&mut self, buf: &mut dyn ReadBuf) -> io::Result { + if self.read_buf.is_empty() { + return if self.eof { + Ok(0) + } else { + Err(io::ErrorKind::WouldBlock.into()) + }; + } + let n = self.read_buf.len().min(buf.remaining_mut()); + buf.put_slice(&self.read_buf[..n]); + self.read_buf.drain(..n); + Ok(n) + } + + fn try_write(&mut self, buf: &[u8]) -> io::Result { + let room = WRITE_BUF_LIMIT.saturating_sub(self.write_buf.len()); + if room == 0 { + return Err(io::ErrorKind::WouldBlock.into()); + } + let n = buf.len().min(room); + self.write_buf.extend_from_slice(&buf[..n]); + Ok(n) + } + + fn poll_read_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + if !self.read_buf.is_empty() || self.eof { + return Poll::Ready(Ok(())); + } + let mut chunk = [0u8; READ_CHUNK]; + let mut chunk = tokio::io::ReadBuf::new(&mut chunk); + ready!(Pin::new(&mut self.inner).poll_read(cx, &mut chunk))?; + if chunk.filled().is_empty() { + self.eof = true; + } else { + self.read_buf.extend_from_slice(chunk.filled()); + } + Poll::Ready(Ok(())) + } + + fn poll_write_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.poll_drain(cx) + } + + fn poll_flush(&mut self, cx: &mut Context<'_>) -> Poll> { + ready!(self.poll_drain(cx))?; + Pin::new(&mut self.inner).poll_flush(cx) + } + + fn poll_shutdown(&mut self, cx: &mut Context<'_>) -> Poll> { + ready!(self.poll_drain(cx))?; + Pin::new(&mut self.inner).poll_shutdown(cx) + } +} diff --git a/crates/sqlx-cloudflare-hd/tests/driver.rs b/crates/sqlx-cloudflare-hd/tests/driver.rs new file mode 100644 index 0000000..5090cd5 --- /dev/null +++ b/crates/sqlx-cloudflare-hd/tests/driver.rs @@ -0,0 +1,51 @@ +//! The integration tests: every scenario in the test Worker (`tests/worker`), +//! run inside `wrangler dev --local` against a scratch Postgres behind a local +//! Hyperdrive binding. +//! +//! Skipped unless `HD_WORKER_URL` names a running test Worker, so plain +//! `cargo test` stays offline. `make test-worker` builds the Worker, starts it +//! on a fresh database and runs this with the URL set. + +use std::io::Read; + +fn get(url: &str) -> String { + let mut body = String::new(); + ureq::get(url) + .call() + .unwrap_or_else(|error| panic!("GET {url}: {error}")) + .into_body() + .into_reader() + .read_to_string(&mut body) + .unwrap(); + body +} + +#[test] +fn every_scenario_passes_against_local_hyperdrive() { + let Ok(base) = std::env::var("HD_WORKER_URL") else { + eprintln!("HD_WORKER_URL is unset; skipping -- run `make test-worker`"); + return; + }; + let base = base.trim_end_matches('/'); + + let scenarios: Vec = serde_json::from_str(&get(base)).unwrap(); + assert!(!scenarios.is_empty(), "the Worker lists no scenarios"); + + // All of them, then one failure listing every scenario that failed: + // stopping at the first would hide how much else is broken. + let failures: Vec = scenarios + .iter() + .filter_map(|name| { + let body = get(&format!("{base}/{name}")); + match serde_json::from_str::(&body) { + Ok(outcome) if outcome["ok"] == true => None, + Ok(outcome) => Some(format!("{name}: {}", outcome["error"])), + // A panic in the Worker answers with an error page, not JSON. + Err(_) => Some(format!("{name}: {body}")), + } + }) + .collect(); + + assert!(failures.is_empty(), "failed:\n{}", failures.join("\n")); + eprintln!("{} scenarios passed", scenarios.len()); +} diff --git a/crates/sqlx-cloudflare-hd/tests/worker/.gitignore b/crates/sqlx-cloudflare-hd/tests/worker/.gitignore new file mode 100644 index 0000000..c3b7cfb --- /dev/null +++ b/crates/sqlx-cloudflare-hd/tests/worker/.gitignore @@ -0,0 +1,4 @@ +/target +/build +/.wrangler +/node_modules diff --git a/crates/sqlx-cloudflare-hd/tests/worker/Cargo.lock b/crates/sqlx-cloudflare-hd/tests/worker/Cargo.lock new file mode 100644 index 0000000..19ace1b --- /dev/null +++ b/crates/sqlx-cloudflare-hd/tests/worker/Cargo.lock @@ -0,0 +1,1593 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "js-sys", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crossbeam-queue" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "crypto-common", + "ctutils", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" +dependencies = [ + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "etcetera" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" +dependencies = [ + "cfg-if", + "windows-sys", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "rand_core", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1d20bef17f513b9b3004532233187769cd072d790971f4e4da0e346eb6401e8" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "objc2-core-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" +dependencies = [ + "serde", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "sqlx" +version = "0.9.0" +source = "git+https://github.com/iamralch/sqlx?rev=4313dae17eb0d985e905022fe5e9ab862ec14e86#4313dae17eb0d985e905022fe5e9ab862ec14e86" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-cloudflare-hd" +version = "0.0.0" +dependencies = [ + "getrandom", + "sqlx-core", + "sqlx-postgres", + "tokio", + "worker", +] + +[[package]] +name = "sqlx-cloudflare-hd-test-worker" +version = "0.0.0" +dependencies = [ + "serde_json", + "sqlx", + "sqlx-cloudflare-hd", + "worker", +] + +[[package]] +name = "sqlx-core" +version = "0.9.0" +source = "git+https://github.com/iamralch/sqlx?rev=4313dae17eb0d985e905022fe5e9ab862ec14e86#4313dae17eb0d985e905022fe5e9ab862ec14e86" +dependencies = [ + "base64", + "bytes", + "cfg-if", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.16.1", + "hashlink", + "indexmap", + "log", + "memchr", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", + "tracing", + "url", + "web-time", +] + +[[package]] +name = "sqlx-macros" +version = "0.9.0" +source = "git+https://github.com/iamralch/sqlx?rev=4313dae17eb0d985e905022fe5e9ab862ec14e86#4313dae17eb0d985e905022fe5e9ab862ec14e86" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.119", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.9.0" +source = "git+https://github.com/iamralch/sqlx?rev=4313dae17eb0d985e905022fe5e9ab862ec14e86#4313dae17eb0d985e905022fe5e9ab862ec14e86" +dependencies = [ + "cfg-if", + "dotenvy", + "either", + "heck", + "hex", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-postgres", + "syn 2.0.119", + "thiserror", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.9.0" +source = "git+https://github.com/iamralch/sqlx?rev=4313dae17eb0d985e905022fe5e9ab862ec14e86#4313dae17eb0d985e905022fe5e9ab862ec14e86" +dependencies = [ + "bitflags", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-core", + "futures-util", + "generic-array", + "log", + "percent-encoding", + "serde", + "sha1", + "sha2", + "sqlx-core", + "thiserror", + "tracing", +] + +[[package]] +name = "sqlx-postgres" +version = "0.9.0" +source = "git+https://github.com/iamralch/sqlx?rev=4313dae17eb0d985e905022fe5e9ab862ec14e86#4313dae17eb0d985e905022fe5e9ab862ec14e86" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "rand", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.9.0" +source = "git+https://github.com/iamralch/sqlx?rev=4313dae17eb0d985e905022fe5e9ab862ec14e86#4313dae17eb0d985e905022fe5e9ab862ec14e86" +dependencies = [ + "atoi", + "flume", + "form_urlencoded", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "thiserror", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "pin-project-lite", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.6", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94ce9bdac782bbca70d6ac48a3ba18b873c848a706d6dc3bd9a03c1b96f04693" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "whoami" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c" +dependencies = [ + "libc", + "libredox", + "objc2-system-configuration", + "web-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "worker" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057bce8ec66c80b7e270d884c312e2a4ff60eb997fd02936656106c06a964e5" +dependencies = [ + "async-trait", + "bytes", + "chrono", + "futures-channel", + "futures-util", + "http", + "http-body", + "js-sys", + "matchit", + "pin-project", + "serde", + "serde-wasm-bindgen", + "serde_json", + "serde_urlencoded", + "strum", + "tokio", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "worker-macros", + "worker-sys", +] + +[[package]] +name = "worker-macros" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d7389670b55aab89760be149007aeff773cdad7b650d807f7fb556d04bb304" +dependencies = [ + "async-trait", + "proc-macro2", + "quote", + "strum", + "syn 3.0.6", + "wasm-bindgen", + "wasm-bindgen-macro-support", + "worker-sys", +] + +[[package]] +name = "worker-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a440092e7ae7c5feb5507fbdc6aee0cc241d132757c83e1729766a24cddff82" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33811428bee40dbceb6d545e95754741d17a6aef9a4849f0fd62e2ba4f412a78" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/crates/sqlx-cloudflare-hd/tests/worker/Cargo.toml b/crates/sqlx-cloudflare-hd/tests/worker/Cargo.toml new file mode 100644 index 0000000..65fd642 --- /dev/null +++ b/crates/sqlx-cloudflare-hd/tests/worker/Cargo.toml @@ -0,0 +1,27 @@ +# The Worker the integration tests run against. Not part of the published +# crate, and its own package rather than a workspace member: it only builds +# for wasm32-unknown-unknown, through `worker-build`, and keeps its own lock. +[package] +name = "sqlx-cloudflare-hd-test-worker" +version = "0.0.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +sqlx-cloudflare-hd = { path = "../.." } +sqlx = { version = "0.9", default-features = false, features = ["postgres", "derive", "macros", "migrate"] } +worker = "0.8.6" +serde_json = "1" + +# The same sqlx branch as the workspace (see its Cargo.toml): a patch there +# does not reach a package outside it. +[patch.crates-io] +sqlx = { git = "https://github.com/iamralch/sqlx", rev = "4313dae17eb0d985e905022fe5e9ab862ec14e86" } +sqlx-core = { git = "https://github.com/iamralch/sqlx", rev = "4313dae17eb0d985e905022fe5e9ab862ec14e86" } +sqlx-postgres = { git = "https://github.com/iamralch/sqlx", rev = "4313dae17eb0d985e905022fe5e9ab862ec14e86" } + +[profile.release] +opt-level = "s" diff --git a/crates/sqlx-cloudflare-hd/tests/worker/migrations/20260928000001_widgets.sql b/crates/sqlx-cloudflare-hd/tests/worker/migrations/20260928000001_widgets.sql new file mode 100644 index 0000000..6ec4cfc --- /dev/null +++ b/crates/sqlx-cloudflare-hd/tests/worker/migrations/20260928000001_widgets.sql @@ -0,0 +1 @@ +CREATE TABLE widgets (id int8 PRIMARY KEY, name text NOT NULL); diff --git a/crates/sqlx-cloudflare-hd/tests/worker/migrations/20260928000002_widget_seed.sql b/crates/sqlx-cloudflare-hd/tests/worker/migrations/20260928000002_widget_seed.sql new file mode 100644 index 0000000..f91e27d --- /dev/null +++ b/crates/sqlx-cloudflare-hd/tests/worker/migrations/20260928000002_widget_seed.sql @@ -0,0 +1 @@ +INSERT INTO widgets VALUES (1, 'sprocket'), (2, 'gear'); diff --git a/crates/sqlx-cloudflare-hd/tests/worker/run.sh b/crates/sqlx-cloudflare-hd/tests/worker/run.sh new file mode 100755 index 0000000..9ff0faa --- /dev/null +++ b/crates/sqlx-cloudflare-hd/tests/worker/run.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# Starts a scratch Postgres, builds the test Worker, serves it with +# `wrangler dev --local` with its Hyperdrive binding pointed at that Postgres, +# and runs `tests/driver.rs` against it. Part of what `make test-worker` runs. +# +# Scratch means a fresh cluster in a temporary directory, on TCP only, with +# SCRAM passwords and no TLS: the connection goes through the password +# handshake, and the default `sslmode=prefer` through the declined-TLS path. +# The user and password are throwaway values for this cluster alone. +set -euo pipefail + +cd "$(dirname "$0")" +port="${HD_WORKER_PORT:-8789}" +pg_port="${HD_PG_PORT:-55433}" +state="$(mktemp -d)" +export WRANGLER_SEND_METRICS=false + +cleanup() { + kill "${wrangler:-}" 2>/dev/null || true + pg_ctl -D "$state/pg" -m immediate stop >/dev/null 2>&1 || true + rm -rf "$state" +} +trap cleanup EXIT + +echo hd >"$state/pwfile" +initdb -D "$state/pg" -U hd --auth=scram-sha-256 --pwfile="$state/pwfile" >/dev/null +# No Unix socket: its path would sit under $TMPDIR, which on macOS is longer +# than a socket path may be. +pg_ctl -D "$state/pg" -l "$state/postgres.log" -w \ + -o "-p $pg_port -c listen_addresses=127.0.0.1 -c unix_socket_directories=''" start >/dev/null +PGPASSWORD=hd psql -X -h 127.0.0.1 -p "$pg_port" -U hd -d postgres -qc 'CREATE DATABASE hd' + +export CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HD="postgres://hd:hd@127.0.0.1:$pg_port/hd" + +worker-build --release + +wrangler dev --local --port "$port" >"$state/wrangler.log" 2>&1 & +wrangler=$! + +for _ in $(seq 1 60); do + if curl -sf "http://localhost:$port/" >/dev/null; then + break + fi + if ! kill -0 "$wrangler" 2>/dev/null; then + cat "$state/wrangler.log" + exit 1 + fi + sleep 1 +done + +cd ../.. +HD_WORKER_URL="http://localhost:$port" cargo test --test driver -- --nocapture diff --git a/crates/sqlx-cloudflare-hd/tests/worker/src/lib.rs b/crates/sqlx-cloudflare-hd/tests/worker/src/lib.rs new file mode 100644 index 0000000..06ff31a --- /dev/null +++ b/crates/sqlx-cloudflare-hd/tests/worker/src/lib.rs @@ -0,0 +1,269 @@ +//! The scenarios the integration tests run, each inside a real Worker against +//! a scratch Postgres behind a local Hyperdrive binding. +//! +//! `GET /` lists the scenarios; `GET /` runs one on a connection of +//! its own, as a request would, and answers `{"ok": true}` or +//! `{"ok": false, "error": "..."}`. The assertions live here, in Rust next to +//! the code they exercise, and `tests/driver.rs` on the host only has to walk +//! the list -- so adding a scenario is one function and one line in +//! `SCENARIOS`. + +use std::future::Future; +use std::pin::Pin; + +use sqlx::error::ErrorKind; +use sqlx::postgres::{PgConnection, PgSslMode}; +use sqlx::{Connection, FromRow, Row}; +use worker::{event, Context, Env, Request, Response}; + +type Outcome = Result<(), String>; +type Scenario = fn(Env) -> Pin>>; + +macro_rules! scenarios { + ($($name:ident),* $(,)?) => { + const SCENARIOS: &[(&str, Scenario)] = &[ + $((stringify!($name), |env| Box::pin($name(env)))),* + ]; + }; +} + +scenarios![ + binds_and_scalars, + many_rows_span_many_reads, + unique_violation, + transactions, + prepared_statement_reuse, + from_row, + migrations, + ssl_disable_connects, + ssl_require_fails_without_server_tls, + connect_with_custom_options, +]; + +#[event(fetch)] +async fn fetch(req: Request, env: Env, _ctx: Context) -> worker::Result { + let name = req.path().trim_start_matches('/').to_owned(); + + if name.is_empty() { + let names: Vec<_> = SCENARIOS.iter().map(|(name, _)| *name).collect(); + return Response::from_json(&names); + } + + let Some((_, scenario)) = SCENARIOS.iter().find(|(n, _)| *n == name) else { + return Response::error(format!("no scenario `{name}`"), 404); + }; + + let body = match scenario(env).await { + Ok(()) => serde_json::json!({ "ok": true }), + Err(error) => serde_json::json!({ "ok": false, "error": error }), + }; + + Response::from_json(&body) +} + +/// `Err` with the message unless `condition` holds. +fn ensure(condition: bool, message: impl Into) -> Outcome { + if condition { + Ok(()) + } else { + Err(message.into()) + } +} + +fn fail(error: sqlx::Error) -> String { + error.to_string() +} + +async fn connect(env: &Env) -> Result { + sqlx_cloudflare_hd::connect(env, "HD").await.map_err(fail) +} + +async fn binds_and_scalars(env: Env) -> Outcome { + let mut conn = connect(&env).await?; + let row = sqlx::query( + "SELECT $1::int4 + 1, $2::int8, $3::float8, $4::bool, $5::text, $6::bytea, NULL::int4", + ) + .bind(41_i32) + .bind(i64::MAX) + .bind(1.5_f64) + .bind(true) + .bind("héllo") + .bind(vec![0_u8, 255, 7]) + .fetch_one(&mut conn) + .await + .map_err(fail)?; + let got = ( + row.try_get::(0).map_err(fail)?, + row.try_get::(1).map_err(fail)?, + row.try_get::(2).map_err(fail)?, + row.try_get::(3).map_err(fail)?, + row.try_get::(4).map_err(fail)?, + row.try_get::, _>(5).map_err(fail)?, + row.try_get::, _>(6).map_err(fail)?, + ); + let want = (42, i64::MAX, 1.5, true, "héllo".to_owned(), vec![0, 255, 7], None); + ensure(got == want, format!("got {got:?}, want {want:?}")) +} + +async fn many_rows_span_many_reads(env: Env) -> Outcome { + let mut conn = connect(&env).await?; + // About 200 KiB of rows: far more than one read from the socket. + let rows = sqlx::query("SELECT g, repeat('x', 100) || g FROM generate_series(1, 2000) g") + .fetch_all(&mut conn) + .await + .map_err(fail)?; + let last: i32 = rows.last().ok_or("no rows")?.try_get(0).map_err(fail)?; + ensure( + rows.len() == 2000 && last == 2000, + format!("{} rows, last {last}", rows.len()), + ) +} + +async fn unique_violation(env: Env) -> Outcome { + let mut conn = connect(&env).await?; + sqlx::raw_sql("CREATE TEMP TABLE t (id int8 PRIMARY KEY); INSERT INTO t VALUES (1)") + .execute(&mut conn) + .await + .map_err(fail)?; + let error = sqlx::query("INSERT INTO t VALUES (1)") + .execute(&mut conn) + .await + .err() + .ok_or("duplicate insert succeeded")?; + let kind = error.as_database_error().map(|e| e.kind()); + ensure( + matches!(kind, Some(ErrorKind::UniqueViolation)), + format!("{kind:?}: {error}"), + ) +} + +async fn transactions(env: Env) -> Outcome { + let mut conn = connect(&env).await?; + sqlx::query("CREATE TEMP TABLE t (id int8 PRIMARY KEY)") + .execute(&mut conn) + .await + .map_err(fail)?; + + let mut tx = conn.begin().await.map_err(fail)?; + sqlx::query("INSERT INTO t VALUES (1)") + .execute(&mut *tx) + .await + .map_err(fail)?; + tx.rollback().await.map_err(fail)?; + + let mut tx = conn.begin().await.map_err(fail)?; + sqlx::query("INSERT INTO t VALUES (2)") + .execute(&mut *tx) + .await + .map_err(fail)?; + tx.commit().await.map_err(fail)?; + + let ids: Vec = sqlx::query_scalar("SELECT id FROM t") + .fetch_all(&mut conn) + .await + .map_err(fail)?; + ensure(ids == [2], format!("ids {ids:?}")) +} + +async fn prepared_statement_reuse(env: Env) -> Outcome { + let mut conn = connect(&env).await?; + for i in 0..3_i64 { + let n: i64 = sqlx::query_scalar("SELECT $1::int8 * 2") + .bind(i) + .fetch_one(&mut conn) + .await + .map_err(fail)?; + ensure(n == i * 2, format!("{i} * 2 = {n}"))?; + } + conn.close().await.map_err(fail) +} + +async fn from_row(env: Env) -> Outcome { + #[derive(Debug, PartialEq, FromRow)] + struct User { + id: i64, + name: String, + email: Option, + } + + let mut conn = connect(&env).await?; + let user: User = + sqlx::query_as("SELECT 7::int8 AS id, 'Ada' AS name, NULL::text AS email") + .fetch_one(&mut conn) + .await + .map_err(fail)?; + let want = User { + id: 7, + name: "Ada".into(), + email: None, + }; + ensure(user == want, format!("got {user:?}")) +} + +async fn migrations(env: Env) -> Outcome { + let mut conn = connect(&env).await?; + // Start clean, so every run applies both migrations from scratch. + sqlx::raw_sql("DROP TABLE IF EXISTS widgets, _sqlx_migrations") + .execute(&mut conn) + .await + .map_err(fail)?; + let migrator = sqlx::migrate!("./migrations"); + migrator.run(&mut conn).await.map_err(|e| e.to_string())?; + // A second run finds everything applied and does nothing. + migrator.run(&mut conn).await.map_err(|e| e.to_string())?; + + let names: Vec = sqlx::query_scalar("SELECT name FROM widgets ORDER BY id") + .fetch_all(&mut conn) + .await + .map_err(fail)?; + let applied: i64 = sqlx::query_scalar("SELECT count(*) FROM _sqlx_migrations WHERE success") + .fetch_one(&mut conn) + .await + .map_err(fail)?; + ensure( + names == ["sprocket", "gear"] && applied == 2, + format!("widgets {names:?}, {applied} migrations applied"), + ) +} + +// The scratch Postgres has TLS off, so the default `prefer` -- what every +// other scenario connects with -- is already the declined-TLS path. These two +// cover the modes either side of it. + +async fn ssl_disable_connects(env: Env) -> Outcome { + let hd = env.hyperdrive("HD").map_err(|e| e.to_string())?; + let options = sqlx_cloudflare_hd::options(&hd) + .map_err(fail)? + .ssl_mode(PgSslMode::Disable); + let mut conn = sqlx_cloudflare_hd::connect_with(&hd, &options) + .await + .map_err(fail)?; + conn.ping().await.map_err(fail) +} + +async fn ssl_require_fails_without_server_tls(env: Env) -> Outcome { + let hd = env.hyperdrive("HD").map_err(|e| e.to_string())?; + let options = sqlx_cloudflare_hd::options(&hd) + .map_err(fail)? + .ssl_mode(PgSslMode::Require); + match sqlx_cloudflare_hd::connect_with(&hd, &options).await { + Ok(_) => Err("connected without TLS under `require`".into()), + Err(sqlx::Error::Tls(_)) => Ok(()), + Err(error) => Err(format!("wrong error: {error:?}")), + } +} + +async fn connect_with_custom_options(env: Env) -> Outcome { + let hd = env.hyperdrive("HD").map_err(|e| e.to_string())?; + let options = sqlx_cloudflare_hd::options(&hd) + .map_err(fail)? + .application_name("hd-test"); + let mut conn = sqlx_cloudflare_hd::connect_with(&hd, &options) + .await + .map_err(fail)?; + let name: String = sqlx::query_scalar("SELECT current_setting('application_name')") + .fetch_one(&mut conn) + .await + .map_err(fail)?; + ensure(name == "hd-test", format!("application_name {name:?}")) +} diff --git a/crates/sqlx-cloudflare-hd/tests/worker/wrangler.toml b/crates/sqlx-cloudflare-hd/tests/worker/wrangler.toml new file mode 100644 index 0000000..94ba2f8 --- /dev/null +++ b/crates/sqlx-cloudflare-hd/tests/worker/wrangler.toml @@ -0,0 +1,14 @@ +# Served by `wrangler dev --local` only: no Cloudflare account is involved, +# and the Hyperdrive id below names nothing real. run.sh starts a scratch +# Postgres and points the binding at it through +# CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HD. +name = "sqlx-cloudflare-hd-test" +main = "build/worker/shim.mjs" +compatibility_date = "2026-01-01" + +[build] +command = "worker-build --release" + +[[hyperdrive]] +binding = "HD" +id = "00000000000000000000000000000000" diff --git a/flake.nix b/flake.nix index 0c8692d..bb4a1ae 100644 --- a/flake.nix +++ b/flake.nix @@ -44,6 +44,9 @@ pkgs.worker-build pkgs.wrangler pkgs.nodejs + # sqlx-cloudflare-hd's integration tests: its run.sh starts a + # scratch cluster for the local Hyperdrive binding to point at. + pkgs.postgresql ]; }; } From 9499455dc20b793128f76bc7b675bc0b2118e5d8 Mon Sep 17 00:00:00 2001 From: Svetlin Ralchev Date: Mon, 28 Sep 2026 08:25:09 +0400 Subject: [PATCH 2/2] refactor(hd): put Postgres behind a `postgres` feature and module Hyperdrive also fronts MySQL, so the crate is laid out as sqlx is: one feature per database, none by default, each adding a module that returns that sqlx driver's connection. Postgres moves to `postgres::connect`, `postgres::connect_with` and `postgres::options`; the socket adapter and the binding lookup stay shared, and `HdSocket::connect` opens the socket for any driver. The sqlx dev-dependency gains `macros`, as D1's has: it is what turns on `sqlx-postgres/offline`, and without it a build with no features of this crate's own gets sqlx-postgres without `describe`. --- crates/sqlx-cloudflare-hd/Cargo.toml | 22 ++- crates/sqlx-cloudflare-hd/README.md | 39 +++-- crates/sqlx-cloudflare-hd/src/lib.rs | 152 +++--------------- crates/sqlx-cloudflare-hd/src/postgres.rs | 131 +++++++++++++++ crates/sqlx-cloudflare-hd/src/socket.rs | 15 ++ .../tests/worker/Cargo.toml | 2 +- .../tests/worker/src/lib.rs | 14 +- 7 files changed, 219 insertions(+), 156 deletions(-) create mode 100644 crates/sqlx-cloudflare-hd/src/postgres.rs diff --git a/crates/sqlx-cloudflare-hd/Cargo.toml b/crates/sqlx-cloudflare-hd/Cargo.toml index af8b086..5cc07fe 100644 --- a/crates/sqlx-cloudflare-hd/Cargo.toml +++ b/crates/sqlx-cloudflare-hd/Cargo.toml @@ -19,13 +19,22 @@ publish = false [lints] workspace = true +# One feature per database Hyperdrive fronts, as sqlx has: each pulls in that +# sqlx driver and adds its module. None by default -- pick the one your +# Hyperdrive configuration points at. +[features] +default = [] +postgres = ["dep:sqlx-postgres", "dep:tokio"] + [dependencies] sqlx-core = { workspace = true } -sqlx-postgres = { workspace = true } +sqlx-postgres = { workspace = true, optional = true } worker = { workspace = true } -tokio = { workspace = true } +# Only for the `AsyncRead`/`AsyncWrite` traits the socket adapter uses, so only +# with a driver that needs the adapter. +tokio = { workspace = true, optional = true } -# sqlx-postgres draws its SCRAM nonces through `rand`, and on +# The sqlx drivers draw their auth nonces through `rand`, and on # wasm32-unknown-unknown `getrandom` refuses to build until told where # randomness comes from: `wasm_js` is the JS host's `crypto.getRandomValues`. # Enabled here so no consumer has to know. @@ -33,8 +42,11 @@ tokio = { workspace = true } getrandom = { version = "0.4", features = ["wasm_js"] } [dev-dependencies] -# The facade, for the crate docs' example: what a consumer writes against. -sqlx = { workspace = true, features = ["postgres"] } +# The facade, for the docs' example: what a consumer writes against. `macros` +# is sqlx's default, and the one that turns on `sqlx-postgres/offline` to match +# the workspace's `sqlx-core/offline` -- without it, a build with no features of +# this crate's own gets sqlx-postgres through here alone, missing `describe`. +sqlx = { workspace = true, features = ["postgres", "macros"] } # `tests/driver.rs` drives the test Worker over plain HTTP on localhost, so no TLS. ureq = { workspace = true } serde_json = { workspace = true } diff --git a/crates/sqlx-cloudflare-hd/README.md b/crates/sqlx-cloudflare-hd/README.md index 72b150b..7244116 100644 --- a/crates/sqlx-cloudflare-hd/README.md +++ b/crates/sqlx-cloudflare-hd/README.md @@ -28,7 +28,7 @@ async fn fetch( env: worker::Env, _ctx: worker::Context, ) -> worker::Result { - let mut conn = sqlx_cloudflare_hd::connect(&env, "HYPERDRIVE") + let mut conn = sqlx_cloudflare_hd::postgres::connect(&env, "HYPERDRIVE") .await .map_err(|e| worker::Error::RustError(e.to_string()))?; @@ -45,11 +45,29 @@ async fn fetch( } ``` -There is no driver in here. Hyperdrive speaks the Postgres wire protocol, so -this crate opens the Worker's socket to the binding, settles TLS through the -Workers runtime, and hands the socket to sqlx's Postgres driver. What comes -back is a plain `PgConnection`: every type sqlx-postgres maps, `query_as`, -`#[derive(sqlx::FromRow)]`, transactions and `sqlx::migrate!`. +There is no driver in here. Hyperdrive speaks the wire protocol of the +database behind it, so this crate opens the Worker's socket to the binding, +settles TLS through the Workers runtime, and hands the socket to sqlx's own +driver. For Postgres, what comes back is a plain `PgConnection`: every type +sqlx-postgres maps, `query_as`, `#[derive(sqlx::FromRow)]`, transactions and +`sqlx::migrate!`. + +## Install + +One feature per database, as sqlx has, and none by default: + +```toml +[dependencies] +sqlx-cloudflare-hd = { version = "0.1", features = ["postgres"] } +sqlx = { version = "0.9", default-features = false, features = ["postgres", "derive"] } +worker = "0.8" +``` + +| Feature | Module | Connection | +|---|---|---| +| `postgres` | `sqlx_cloudflare_hd::postgres` | `sqlx::PgConnection` | + +MySQL, which Hyperdrive also fronts, is not supported yet. Connect once per request. Hyperdrive pools the connections to your database, and a Worker cannot share a socket across requests anyway, so there is no @@ -58,13 +76,14 @@ same reason there is no `PgListener`. ## Options -`connect` uses the options Hyperdrive hands out. To change any of them while -keeping the credentials, start from `options` and connect with `connect_with`: +`postgres::connect` uses the options Hyperdrive hands out. To change any of +them while keeping the credentials, start from `postgres::options` and connect +with `postgres::connect_with`: ```rust let hyperdrive = env.hyperdrive("HYPERDRIVE")?; -let options = sqlx_cloudflare_hd::options(&hyperdrive)?.application_name("my-worker"); -let mut conn = sqlx_cloudflare_hd::connect_with(&hyperdrive, &options).await?; +let options = sqlx_cloudflare_hd::postgres::options(&hyperdrive)?.application_name("my-worker"); +let mut conn = sqlx_cloudflare_hd::postgres::connect_with(&hyperdrive, &options).await?; ``` The socket always goes to Hyperdrive's host and port. `sslmode` is honoured, diff --git a/crates/sqlx-cloudflare-hd/src/lib.rs b/crates/sqlx-cloudflare-hd/src/lib.rs index 90c27a3..e4ca4b9 100644 --- a/crates/sqlx-cloudflare-hd/src/lib.rs +++ b/crates/sqlx-cloudflare-hd/src/lib.rs @@ -1,148 +1,34 @@ //! Cloudflare Hyperdrive for sqlx. //! -//! Hyperdrive speaks the Postgres wire protocol, so there is no driver here: -//! this crate opens the Worker's socket to a Hyperdrive binding and hands it to -//! sqlx's own Postgres driver. What you get back is a plain -//! [`PgConnection`] -- `sqlx::Postgres`, with -//! all of its types, `query_as`, `#[derive(sqlx::FromRow)]`, transactions and -//! `sqlx::migrate!`. +//! Hyperdrive speaks the wire protocol of the database behind it, so there is +//! no driver here: this crate opens the Worker's socket to a Hyperdrive +//! binding and hands it to sqlx's own driver for that database. What you get +//! back is that driver's connection type, with everything sqlx gives it. //! -//! ```no_run -//! use sqlx::Connection; +//! One module per database, each behind a feature of the same name: //! -//! async fn user_name(env: &worker::Env, id: i64) -> Result { -//! // One connection per request: Hyperdrive pools the connections to -//! // the database behind it. -//! let mut conn = sqlx_cloudflare_hd::connect(env, "HYPERDRIVE").await?; -//! -//! let name = sqlx::query_scalar("SELECT name FROM users WHERE id = $1") -//! .bind(id) -//! .fetch_one(&mut conn) -//! .await?; -//! -//! conn.close().await?; -//! Ok(name) -//! } -//! ``` +//! - [`postgres`] -- a [`PgConnection`](sqlx_postgres::PgConnection). //! //! # What does not work //! -//! - **`sqlx::Pool`.** It needs an async runtime to spawn its maintenance -//! tasks, and a Worker cannot share a socket across requests anyway. -//! Hyperdrive is the pool: connect once per request. -//! - **`PgListener`** (`LISTEN`/`NOTIFY`), which also spawns a task. +//! **`sqlx::Pool`.** It needs an async runtime to spawn its maintenance tasks, +//! and a Worker cannot share a socket across requests anyway. Hyperdrive is the +//! pool: connect once per request. //! //! This crate only *runs* on `wasm32-unknown-unknown`, inside a Worker. It //! compiles on other targets so that docs and `cargo check` work. +// Everything below is shared by the drivers, so it only exists with one of +// them: widen the `cfg`s to `any(...)` as drivers are added. +#[cfg(feature = "postgres")] mod socket; -use std::io; - -use sqlx_core::net::Socket as _; -use sqlx_core::Error; -use sqlx_postgres::{PgConnectOptions, PgConnection, PgSslMode}; -use worker::{Hyperdrive, SecureTransport}; - -use crate::socket::HdSocket; - -/// A Postgres `SSLRequest`: length 8, then the request code 80877103. -const SSL_REQUEST: [u8; 8] = [0, 0, 0, 8, 0x04, 0xd2, 0x16, 0x2f]; - -/// Connects to the database behind the Hyperdrive binding called `binding`. -/// -/// # Errors -/// -/// When there is no binding by that name, or it is not a Hyperdrive -/// configuration; when the socket, TLS or the Postgres handshake fails; and -/// when the credentials Hyperdrive holds are rejected. -pub async fn connect(env: &worker::Env, binding: &str) -> Result { - let hyperdrive = env - .hyperdrive(binding) - .map_err(|e| Error::Configuration(e.to_string().into()))?; - connect_with(&hyperdrive, &options(&hyperdrive)?).await -} - -/// The options Hyperdrive hands out, parsed from its connection string. -/// -/// Start here to change anything before [`connect_with`] -- the application -/// name, the statement cache -- while keeping the credentials. -/// -/// # Errors -/// -/// When the connection string does not parse, which would be a Hyperdrive bug. -pub fn options(hyperdrive: &Hyperdrive) -> Result { - hyperdrive.connection_string().parse() -} - -/// Connects through `hyperdrive` with `options`. -/// -/// The socket always goes to Hyperdrive's host and port; the host, port and -/// socket path in `options` are ignored. `ssl_mode` is honoured, but the -/// Workers runtime performs the TLS handshake, so the certificate options -/// (`ssl_root_cert` and the client certificate and key) are not used. -/// -/// # Errors -/// -/// When the socket, TLS or the Postgres handshake fails, or the credentials -/// are rejected. -pub async fn connect_with( - hyperdrive: &Hyperdrive, - options: &PgConnectOptions, -) -> Result { - let mode = options.get_ssl_mode(); - let transport = if wants_tls(mode) { - SecureTransport::StartTls - } else { - SecureTransport::Off - }; - let socket = worker::Socket::builder() - .secure_transport(transport) - .connect(hyperdrive.host(), hyperdrive.port()) - .map_err(|e| Error::Io(io::Error::other(e.to_string())))?; - - let socket = negotiate_tls(HdSocket::new(socket), mode).await?; - - // TLS is settled -- upgraded, or declined as `mode` allows -- so sqlx must - // not ask the server again. - let options = options.clone().ssl_mode(PgSslMode::Disable); - PgConnection::connect_with_socket(&options, socket).await -} - -fn wants_tls(mode: PgSslMode) -> bool { - !matches!(mode, PgSslMode::Disable | PgSslMode::Allow) -} - -/// Asks the server for TLS, as sqlx itself would, but upgrades through the -/// Workers runtime's `startTls()` rather than a TLS library of sqlx's. -async fn negotiate_tls(mut socket: HdSocket, mode: PgSslMode) -> Result { - if !wants_tls(mode) { - return Ok(socket); - } - - let mut request = &SSL_REQUEST[..]; - while !request.is_empty() { - let n = socket.write(request).await?; - request = &request[n..]; - } - socket.flush().await?; - - // The server answers with a single byte: `S` to go ahead, `N` to decline. - let mut reply = [0u8; 1]; - let mut buf = &mut reply[..]; - if socket.read(&mut buf).await? == 0 { - return Err(Error::Io(io::Error::new( - io::ErrorKind::UnexpectedEof, - "server closed the connection during TLS negotiation", - ))); - } +#[cfg(feature = "postgres")] +pub mod postgres; - match reply[0] { - b'S' => Ok(HdSocket::new(socket.into_inner()?.start_tls())), - b'N' if matches!(mode, PgSslMode::Prefer) => Ok(socket), - b'N' => Err(Error::Tls("server does not support TLS".into())), - other => Err(Error::Protocol(format!( - "unexpected response to SSLRequest: {other:#04x}" - ))), - } +/// Looks up the Hyperdrive binding called `name` in the Worker's environment. +#[cfg(feature = "postgres")] +fn binding(env: &worker::Env, name: &str) -> Result { + env.hyperdrive(name) + .map_err(|e| sqlx_core::Error::Configuration(e.to_string().into())) } diff --git a/crates/sqlx-cloudflare-hd/src/postgres.rs b/crates/sqlx-cloudflare-hd/src/postgres.rs new file mode 100644 index 0000000..86ccf2a --- /dev/null +++ b/crates/sqlx-cloudflare-hd/src/postgres.rs @@ -0,0 +1,131 @@ +//! Postgres behind Hyperdrive, through sqlx's own Postgres driver. Needs the +//! `postgres` feature. +//! +//! What you get back is a plain [`PgConnection`] -- `sqlx::Postgres`, with all +//! of its types, `query_as`, `#[derive(sqlx::FromRow)]`, transactions and +//! `sqlx::migrate!`. +//! +//! ```no_run +//! use sqlx::Connection; +//! +//! async fn user_name(env: &worker::Env, id: i64) -> Result { +//! // One connection per request: Hyperdrive pools the connections to +//! // the database behind it. +//! let mut conn = sqlx_cloudflare_hd::postgres::connect(env, "HYPERDRIVE").await?; +//! +//! let name = sqlx::query_scalar("SELECT name FROM users WHERE id = $1") +//! .bind(id) +//! .fetch_one(&mut conn) +//! .await?; +//! +//! conn.close().await?; +//! Ok(name) +//! } +//! ``` +//! +//! No `PgListener` (`LISTEN`/`NOTIFY`): it spawns a task, which needs an async +//! runtime a Worker does not have. + +use std::io; + +use sqlx_core::net::Socket as _; +use sqlx_core::Error; +use sqlx_postgres::{PgConnectOptions, PgConnection, PgSslMode}; +use worker::{Hyperdrive, SecureTransport}; + +use crate::socket::HdSocket; + +/// A Postgres `SSLRequest`: length 8, then the request code 80877103. +const SSL_REQUEST: [u8; 8] = [0, 0, 0, 8, 0x04, 0xd2, 0x16, 0x2f]; + +/// Connects to the database behind the Hyperdrive binding called `binding`. +/// +/// # Errors +/// +/// When there is no binding by that name, or it is not a Hyperdrive +/// configuration; when the socket, TLS or the Postgres handshake fails; and +/// when the credentials Hyperdrive holds are rejected. +pub async fn connect(env: &worker::Env, binding: &str) -> Result { + let hyperdrive = crate::binding(env, binding)?; + connect_with(&hyperdrive, &options(&hyperdrive)?).await +} + +/// The options Hyperdrive hands out, parsed from its connection string. +/// +/// Start here to change anything before [`connect_with`] -- the application +/// name, the statement cache -- while keeping the credentials. +/// +/// # Errors +/// +/// When the connection string does not parse, which would be a Hyperdrive bug. +pub fn options(hyperdrive: &Hyperdrive) -> Result { + hyperdrive.connection_string().parse() +} + +/// Connects through `hyperdrive` with `options`. +/// +/// The socket always goes to Hyperdrive's host and port; the host, port and +/// socket path in `options` are ignored. `ssl_mode` is honoured, but the +/// Workers runtime performs the TLS handshake, so the certificate options +/// (`ssl_root_cert` and the client certificate and key) are not used. +/// +/// # Errors +/// +/// When the socket, TLS or the Postgres handshake fails, or the credentials +/// are rejected. +pub async fn connect_with( + hyperdrive: &Hyperdrive, + options: &PgConnectOptions, +) -> Result { + let mode = options.get_ssl_mode(); + let transport = if wants_tls(mode) { + SecureTransport::StartTls + } else { + SecureTransport::Off + }; + let socket = HdSocket::connect(hyperdrive, transport)?; + let socket = negotiate_tls(socket, mode).await?; + + // TLS is settled -- upgraded, or declined as `mode` allows -- so sqlx must + // not ask the server again. + let options = options.clone().ssl_mode(PgSslMode::Disable); + PgConnection::connect_with_socket(&options, socket).await +} + +fn wants_tls(mode: PgSslMode) -> bool { + !matches!(mode, PgSslMode::Disable | PgSslMode::Allow) +} + +/// Asks the server for TLS, as sqlx itself would, but upgrades through the +/// Workers runtime's `startTls()` rather than a TLS library of sqlx's. +async fn negotiate_tls(mut socket: HdSocket, mode: PgSslMode) -> Result { + if !wants_tls(mode) { + return Ok(socket); + } + + let mut request = &SSL_REQUEST[..]; + while !request.is_empty() { + let n = socket.write(request).await?; + request = &request[n..]; + } + socket.flush().await?; + + // The server answers with a single byte: `S` to go ahead, `N` to decline. + let mut reply = [0u8; 1]; + let mut buf = &mut reply[..]; + if socket.read(&mut buf).await? == 0 { + return Err(Error::Io(io::Error::new( + io::ErrorKind::UnexpectedEof, + "server closed the connection during TLS negotiation", + ))); + } + + match reply[0] { + b'S' => Ok(HdSocket::new(socket.into_inner()?.start_tls())), + b'N' if matches!(mode, PgSslMode::Prefer) => Ok(socket), + b'N' => Err(Error::Tls("server does not support TLS".into())), + other => Err(Error::Protocol(format!( + "unexpected response to SSLRequest: {other:#04x}" + ))), + } +} diff --git a/crates/sqlx-cloudflare-hd/src/socket.rs b/crates/sqlx-cloudflare-hd/src/socket.rs index e5f9eae..d5a2d8e 100644 --- a/crates/sqlx-cloudflare-hd/src/socket.rs +++ b/crates/sqlx-cloudflare-hd/src/socket.rs @@ -30,6 +30,21 @@ pub(crate) struct HdSocket { } impl HdSocket { + /// Opens a socket to `hyperdrive`'s host and port. + /// + /// `StartTls` for a driver that may upgrade the socket later, with + /// [`into_inner`](Self::into_inner) and `start_tls`; `Off` otherwise. + pub(crate) fn connect( + hyperdrive: &worker::Hyperdrive, + transport: worker::SecureTransport, + ) -> Result { + let socket = worker::Socket::builder() + .secure_transport(transport) + .connect(hyperdrive.host(), hyperdrive.port()) + .map_err(|e| sqlx_core::Error::Io(io::Error::other(e.to_string())))?; + Ok(Self::new(socket)) + } + pub(crate) fn new(inner: worker::Socket) -> Self { Self { inner, diff --git a/crates/sqlx-cloudflare-hd/tests/worker/Cargo.toml b/crates/sqlx-cloudflare-hd/tests/worker/Cargo.toml index 65fd642..32df4e8 100644 --- a/crates/sqlx-cloudflare-hd/tests/worker/Cargo.toml +++ b/crates/sqlx-cloudflare-hd/tests/worker/Cargo.toml @@ -11,7 +11,7 @@ publish = false crate-type = ["cdylib"] [dependencies] -sqlx-cloudflare-hd = { path = "../.." } +sqlx-cloudflare-hd = { path = "../..", features = ["postgres"] } sqlx = { version = "0.9", default-features = false, features = ["postgres", "derive", "macros", "migrate"] } worker = "0.8.6" serde_json = "1" diff --git a/crates/sqlx-cloudflare-hd/tests/worker/src/lib.rs b/crates/sqlx-cloudflare-hd/tests/worker/src/lib.rs index 06ff31a..6d1e8f1 100644 --- a/crates/sqlx-cloudflare-hd/tests/worker/src/lib.rs +++ b/crates/sqlx-cloudflare-hd/tests/worker/src/lib.rs @@ -75,7 +75,7 @@ fn fail(error: sqlx::Error) -> String { } async fn connect(env: &Env) -> Result { - sqlx_cloudflare_hd::connect(env, "HD").await.map_err(fail) + sqlx_cloudflare_hd::postgres::connect(env, "HD").await.map_err(fail) } async fn binds_and_scalars(env: Env) -> Outcome { @@ -232,10 +232,10 @@ async fn migrations(env: Env) -> Outcome { async fn ssl_disable_connects(env: Env) -> Outcome { let hd = env.hyperdrive("HD").map_err(|e| e.to_string())?; - let options = sqlx_cloudflare_hd::options(&hd) + let options = sqlx_cloudflare_hd::postgres::options(&hd) .map_err(fail)? .ssl_mode(PgSslMode::Disable); - let mut conn = sqlx_cloudflare_hd::connect_with(&hd, &options) + let mut conn = sqlx_cloudflare_hd::postgres::connect_with(&hd, &options) .await .map_err(fail)?; conn.ping().await.map_err(fail) @@ -243,10 +243,10 @@ async fn ssl_disable_connects(env: Env) -> Outcome { async fn ssl_require_fails_without_server_tls(env: Env) -> Outcome { let hd = env.hyperdrive("HD").map_err(|e| e.to_string())?; - let options = sqlx_cloudflare_hd::options(&hd) + let options = sqlx_cloudflare_hd::postgres::options(&hd) .map_err(fail)? .ssl_mode(PgSslMode::Require); - match sqlx_cloudflare_hd::connect_with(&hd, &options).await { + match sqlx_cloudflare_hd::postgres::connect_with(&hd, &options).await { Ok(_) => Err("connected without TLS under `require`".into()), Err(sqlx::Error::Tls(_)) => Ok(()), Err(error) => Err(format!("wrong error: {error:?}")), @@ -255,10 +255,10 @@ async fn ssl_require_fails_without_server_tls(env: Env) -> Outcome { async fn connect_with_custom_options(env: Env) -> Outcome { let hd = env.hyperdrive("HD").map_err(|e| e.to_string())?; - let options = sqlx_cloudflare_hd::options(&hd) + let options = sqlx_cloudflare_hd::postgres::options(&hd) .map_err(fail)? .application_name("hd-test"); - let mut conn = sqlx_cloudflare_hd::connect_with(&hd, &options) + let mut conn = sqlx_cloudflare_hd::postgres::connect_with(&hd, &options) .await .map_err(fail)?; let name: String = sqlx::query_scalar("SELECT current_setting('application_name')")