From 6fa4ca202b4482530c2f389b93591f141be5acc7 Mon Sep 17 00:00:00 2001 From: Bailey Hayes Date: Tue, 16 Jun 2026 09:05:07 -0400 Subject: [PATCH] ci(wasip2): document and test wasm32-wasip2 support (Postgres + MySQL) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SQLx already runs on wasm32-wasip2 with no library changes: the standard `runtime-tokio` feature works there because Tokio's `net` driver builds on WASI via mio. Pooling, LISTEN/NOTIFY, timeouts, and rustls TLS all work. Building for the target requires `--cfg tokio_unstable` and a current-thread runtime (WASI has no threads in wasip2 target). This documents and exercises that support: - README: add a WebAssembly (`wasm32-wasip2`) section covering the `runtime-tokio` setup, the two target requirements, what works, and the remaining limitations (hostname lookup needs `--allow-ip-name-lookup`, native-tls is unavailable, MySQL caching_sha2 over plaintext needs `mysql-rsa`). - examples/wasip2/{postgres,mysql}: runnable pool examples that do a real round-trip — create a table, insert rows in a transaction with bind parameters, read them back ranked, and aggregate. A shared `.cargo/config.toml` sets `--cfg tokio_unstable` so a plain `cargo build --target wasm32-wasip2` works. - CI: a `wasip2` job builds both examples and runs them under Wasmtime against Postgres and MySQL service containers, asserting the query output to catch wasm-specific decode regressions. Signed-off-by: Bailey Hayes --- .github/workflows/sqlx.yml | 78 ++++++++++++++++++++++ README.md | 55 +++++++++++++++- examples/wasip2/.cargo/config.toml | 8 +++ examples/wasip2/README.md | 98 ++++++++++++++++++++++++++++ examples/wasip2/mysql/Cargo.toml | 25 +++++++ examples/wasip2/mysql/src/main.rs | 78 ++++++++++++++++++++++ examples/wasip2/postgres/Cargo.toml | 23 +++++++ examples/wasip2/postgres/src/main.rs | 78 ++++++++++++++++++++++ 8 files changed, 442 insertions(+), 1 deletion(-) create mode 100644 examples/wasip2/.cargo/config.toml create mode 100644 examples/wasip2/README.md create mode 100644 examples/wasip2/mysql/Cargo.toml create mode 100644 examples/wasip2/mysql/src/main.rs create mode 100644 examples/wasip2/postgres/Cargo.toml create mode 100644 examples/wasip2/postgres/src/main.rs diff --git a/.github/workflows/sqlx.yml b/.github/workflows/sqlx.yml index 50ef29dd92..61d2262674 100644 --- a/.github/workflows/sqlx.yml +++ b/.github/workflows/sqlx.yml @@ -732,3 +732,81 @@ jobs: env: DATABASE_URL: mysql://root@localhost:3306/sqlx?sslmode=verify_ca&ssl-ca=.%2Ftests%2Fcerts%2Fca.crt&ssl-key=.%2Ftests%2Fcerts%2Fkeys%2Fclient.key&ssl-cert=.%2Ftests%2Fcerts%2Fclient.crt RUSTFLAGS: --cfg mariadb="${{ matrix.mariadb }}" + + wasip2: + name: WASI (wasm32-wasip2) + runs-on: ubuntu-24.04 + needs: check + timeout-minutes: 30 + services: + postgres: + image: postgres:17 + env: + POSTGRES_DB: sqlx + POSTGRES_PASSWORD: password + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 10s + --health-timeout 5s + --health-retries 10 + mysql: + image: mysql:8 + env: + MYSQL_DATABASE: sqlx + MYSQL_ROOT_PASSWORD: password + ports: + - 3306:3306 + options: >- + --health-cmd "mysqladmin ping -uroot -ppassword --silent" + --health-interval 10s + --health-timeout 5s + --health-retries 10 + steps: + - uses: actions/checkout@v5 + + # https://blog.rust-lang.org/2025/03/02/Rustup-1.28.0.html + - name: Setup Rust + run: | + rustup show active-toolchain || rustup toolchain install + rustup target add wasm32-wasip2 + + - uses: Swatinem/rust-cache@v2 + + - name: Install Wasmtime + uses: bytecodealliance/actions/wasmtime/setup@v1 + + # Build and run the Postgres example as a WASI component, then assert the + # query results (not just the exit code) to catch wrong-value regressions. + - name: Postgres example + working-directory: examples/wasip2/postgres + run: | + cargo build --target wasm32-wasip2 --release + wasmtime run -S inherit-network \ + --env DATABASE_URL="$DATABASE_URL" \ + target/wasm32-wasip2/release/sqlx-wasip2-postgres.wasm | tee target/output.txt + grep -qF 'alice: 5' target/output.txt + grep -qF 'carol: 4' target/output.txt + grep -qF 'bob: 3' target/output.txt + grep -qF 'total votes: 12' target/output.txt + grep -q 'connected via pool to:' target/output.txt + env: + DATABASE_URL: postgres://postgres:password@127.0.0.1:5432/sqlx + + # Build and run the MySQL example as a WASI component, then assert the + # query results (not just the exit code) to catch wrong-value regressions. + - name: MySQL example + working-directory: examples/wasip2/mysql + run: | + cargo build --target wasm32-wasip2 --release + wasmtime run -S inherit-network \ + --env DATABASE_URL="$DATABASE_URL" \ + target/wasm32-wasip2/release/sqlx-wasip2-mysql.wasm | tee target/output.txt + grep -qF 'alice: 5' target/output.txt + grep -qF 'carol: 4' target/output.txt + grep -qF 'bob: 3' target/output.txt + grep -qF 'total votes: 12' target/output.txt + grep -q 'connected via pool to:' target/output.txt + env: + DATABASE_URL: mysql://root:password@127.0.0.1:3306/sqlx diff --git a/README.md b/README.md index ff98f45350..0aebae849e 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ | Ecosystem - + | Discord @@ -225,6 +225,59 @@ be removed in the future. [readme-offline]: sqlx-cli/README.md#enable-building-in-offline-mode-with-query +#### WebAssembly (`wasm32-wasip2`) + +SQLx runs inside a [WebAssembly Component] on the `wasm32-wasip2` target, with +the **Postgres** and **MySQL/MariaDB** drivers connecting over the host's network +via [`wasi:sockets`]. This works in any WASIP2 runtime, such as +[Wasmtime]. + +It uses the standard **`runtime-tokio`** feature with async I/O. Connection pooling (`sqlx::Pool`), `LISTEN`/`NOTIFY` +(`PgListener`), connection timeouts, and TLS (with the `tls-rustls-*` backends) +all work. + +```toml +[dependencies] +sqlx = { version = "0.9", default-features = false, features = ["postgres", "runtime-tokio"] } +# wasm32-wasip2 does not have threads.. Drive futures with a current-thread runtime. +tokio = { version = "1", default-features = false, features = ["rt", "net", "time", "macros"] } +``` + +Two requirements specific to this target: + +1. **`--cfg tokio_unstable`**: Tokio's `net` support on `wasm32` is currently + behind this cfg. Set it for the target in `.cargo/config.toml` so a plain + `cargo build --target wasm32-wasip2` works: + + ```toml + # .cargo/config.toml + [target.wasm32-wasip2] + rustflags = ["--cfg", "tokio_unstable"] + ``` + +2. **Current-thread runtime**: there are no threads, so use + `#[tokio::main(flavor = "current_thread")]` (or + `Builder::new_current_thread()`); the multi-threaded runtime isn't available. + +Remaining notes on this target: + +- TLS works with the `tls-rustls-*` backends (verified with `tls-rustls-ring`, + which negotiates TLS 1.3). `tls-native-tls` is **not** available (it links a + system TLS library). +- MySQL 8's default `caching_sha2_password` over a non-TLS connection requires + the `mysql-rsa` feature. +- Connecting by **hostname** triggers WASI name resolution, which Wasmtime + gates behind `--allow-ip-name-lookup` (separate from `-S inherit-network`). + Connecting by IP literal needs only `-S inherit-network`. + +Runnable examples for both backends, including the `wasmtime` invocation +(`wasmtime run -S inherit-network ...`), live under +[`examples/wasip2`](examples/wasip2). + +[WebAssembly Component]: https://component-model.bytecodealliance.org/ +[`wasi:sockets`]: https://github.com/WebAssembly/wasi-sockets +[Wasmtime]: https://wasmtime.dev/ + ## SQLx is not an ORM! SQLx supports **compile-time checked queries**. It does not, however, do this by providing a Rust diff --git a/examples/wasip2/.cargo/config.toml b/examples/wasip2/.cargo/config.toml new file mode 100644 index 0000000000..31408534d0 --- /dev/null +++ b/examples/wasip2/.cargo/config.toml @@ -0,0 +1,8 @@ +# `tokio::net` support on `wasm32-wasip2` is currently gated behind Tokio's +# `tokio_unstable` cfg. Setting it here means `cargo build --target +# wasm32-wasip2` works in these examples without passing `RUSTFLAGS` by hand. +# +# This config is discovered for both the `postgres` and `mysql` example crates +# because Cargo walks up the directory tree from the build directory. +[target.wasm32-wasip2] +rustflags = ["--cfg", "tokio_unstable"] diff --git a/examples/wasip2/README.md b/examples/wasip2/README.md new file mode 100644 index 0000000000..a9474c29f6 --- /dev/null +++ b/examples/wasip2/README.md @@ -0,0 +1,98 @@ +# SQLx on `wasm32-wasip2` + +These examples show SQLx running inside a [WebAssembly Component] on the +`wasm32-wasip2` target, using a `sqlx::Pool` to connect to a database over the +host network via [`wasi:sockets`]. They run in any WASIP2 runtime; the +commands below use [Wasmtime]. + +| Example | Backend | +| ------------------------ | -------- | +| [`postgres`](./postgres) | Postgres | +| [`mysql`](./mysql) | MySQL | + +## How it works + +The examples use the standard **`runtime-tokio`** feature with async I/O. +Tokio's `net` driver works on `wasm32-wasip2` (via `mio`'s WASI support). + +Two target-specific requirements: + +- **`--cfg tokio_unstable`**: Tokio's `net` support on `wasm32` is gated behind + this cfg. It's set for the `wasm32-wasip2` target in + [`.cargo/config.toml`](./.cargo/config.toml), so a plain + `cargo build --target wasm32-wasip2` works (no `RUSTFLAGS` needed). +- **Current-thread runtime**: WASIP2 does not have threads, so each example uses + `#[tokio::main(flavor = "current_thread")]`. + +Each example opens a `Pool`, creates a table, inserts rows in a transaction, then +reads them back and aggregates — a small round-trip exercising DDL, bind +parameters, `fetch_all`, and `begin`/`commit`. `LISTEN`/`NOTIFY` and TLS +(`tls-rustls-*`) also work on this target; see the +[top-level README](../../README.md#webassembly-wasm32-wasip2). + +## Prerequisites + +```sh +rustup target add wasm32-wasip2 +# Wasmtime: https://wasmtime.dev/ +``` + +## Run + +### Postgres + +```sh +# A database to connect to: +docker run -d --name sqlx-wasip2-pg \ + -e POSTGRES_PASSWORD=password -e POSTGRES_DB=sqlx \ + -p 5432:5432 postgres:17 + +cd postgres +cargo build --target wasm32-wasip2 --release +wasmtime run -S inherit-network \ + --env DATABASE_URL="postgres://postgres:password@127.0.0.1:5432/sqlx" \ + target/wasm32-wasip2/release/sqlx-wasip2-postgres.wasm +``` + +Expected output: + +```text +alice: 5 +carol: 4 +bob: 3 +total votes: 12 +connected via pool to: PostgreSQL 17.x ... +``` + +### MySQL + +```sh +docker run -d --name sqlx-wasip2-mysql \ + -e MYSQL_ROOT_PASSWORD=password -e MYSQL_DATABASE=sqlx \ + -p 3306:3306 mysql:8 + +cd mysql +cargo build --target wasm32-wasip2 --release +wasmtime run -S inherit-network \ + --env DATABASE_URL="mysql://root:password@127.0.0.1:3306/sqlx" \ + target/wasm32-wasip2/release/sqlx-wasip2-mysql.wasm +``` + +Expected output: + +```text +alice: 5 +carol: 4 +bob: 3 +total votes: 12 +connected via pool to: 8.x.x +``` + +> `wasmtime run -S inherit-network` grants the component access to the host +> network; it is required for outbound TCP. These examples connect by IP literal +> (`127.0.0.1`), so that flag alone is sufficient. Connecting by **hostname** +> additionally needs `--allow-ip-name-lookup` to permit WASI DNS resolution. + +[WebAssembly Component]: https://component-model.bytecodealliance.org/ +[`wasi:sockets`]: https://github.com/WebAssembly/wasi-sockets +[Wasmtime]: https://wasmtime.dev/ diff --git a/examples/wasip2/mysql/Cargo.toml b/examples/wasip2/mysql/Cargo.toml new file mode 100644 index 0000000000..36ad979e6c --- /dev/null +++ b/examples/wasip2/mysql/Cargo.toml @@ -0,0 +1,25 @@ +# Standalone example: SQLx + MySQL connection pool on `wasm32-wasip2`. +# +# Detached from the main workspace (note the empty `[workspace]` table) so it +# can pin the exact feature set used for WASI without being unified with the +# rest of the workspace's features. +[package] +name = "sqlx-example-wasip2-mysql" +version = "0.0.0" +edition = "2021" +publish = false + +[workspace] + +[[bin]] +name = "sqlx-wasip2-mysql" +path = "src/main.rs" + +[dependencies] +# `mysql-rsa` enables the public-key password exchange MySQL 8 uses for +# `caching_sha2_password` over a plaintext connection. +sqlx = { path = "../../..", default-features = false, features = ["mysql", "mysql-rsa", "runtime-tokio"] } + +# A current-thread Tokio runtime to drive the pool. On wasm32-wasip2 Tokio's +# `net` driver works under `--cfg tokio_unstable` +tokio = { version = "1", default-features = false, features = ["rt", "net", "time", "macros"] } diff --git a/examples/wasip2/mysql/src/main.rs b/examples/wasip2/mysql/src/main.rs new file mode 100644 index 0000000000..2d57625b20 --- /dev/null +++ b/examples/wasip2/mysql/src/main.rs @@ -0,0 +1,78 @@ +//! SQLx + MySQL **connection pool** example targeting `wasm32-wasip2`. +//! +//! Runs on a current-thread Tokio runtime. On `wasm32-wasip2` Tokio's `net` +//! driver works under `--cfg tokio_unstable` (set in `../.cargo/config.toml`). +//! +//! Build & run: +//! +//! ```sh +//! cargo build --target wasm32-wasip2 --release +//! wasmtime run -S inherit-network --env DATABASE_URL \ +//! target/wasm32-wasip2/release/sqlx-wasip2-mysql.wasm +//! ``` + +use sqlx::mysql::MySqlPoolOptions; +use sqlx::Row; + +// `flavor = "current_thread"` because `wasm32-wasip2` does not have threads. +#[tokio::main(flavor = "current_thread")] +async fn main() { + let url = std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "mysql://root:password@localhost:3306/sqlx".to_owned()); + + let pool = MySqlPoolOptions::new() + .max_connections(5) + .connect(&url) + .await + .expect("failed to connect pool"); + + // Fresh schema (safe to re-run). + sqlx::query("DROP TABLE IF EXISTS wasip2_votes") + .execute(&pool) + .await + .expect("drop table"); + sqlx::query( + "CREATE TABLE wasip2_votes (id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(64) NOT NULL, votes BIGINT NOT NULL)", + ) + .execute(&pool) + .await + .expect("create table"); + + // Seed a few rows inside a transaction, using bind parameters. + let mut tx = pool.begin().await.expect("begin"); + for (name, votes) in [("alice", 5_i64), ("bob", 3), ("carol", 4)] { + sqlx::query("INSERT INTO wasip2_votes (name, votes) VALUES (?, ?)") + .bind(name) + .bind(votes) + .execute(&mut *tx) + .await + .expect("insert"); + } + tx.commit().await.expect("commit"); + + // Read them back, ranked highest-first. + let rows = sqlx::query("SELECT name, votes FROM wasip2_votes ORDER BY votes DESC") + .fetch_all(&pool) + .await + .expect("select"); + for row in &rows { + let name: String = row.get("name"); + let votes: i64 = row.get("votes"); + println!("{name}: {votes}"); + } + + // Aggregate (`SUM(bigint)` is `DECIMAL` in MySQL, so cast back to a signed int). + let total: i64 = sqlx::query_scalar("SELECT CAST(SUM(votes) AS SIGNED) FROM wasip2_votes") + .fetch_one(&pool) + .await + .expect("sum"); + println!("total votes: {total}"); + + let version: String = sqlx::query_scalar("SELECT VERSION()") + .fetch_one(&pool) + .await + .expect("version query failed"); + println!("connected via pool to: {version}"); + + pool.close().await; +} diff --git a/examples/wasip2/postgres/Cargo.toml b/examples/wasip2/postgres/Cargo.toml new file mode 100644 index 0000000000..0bc7940863 --- /dev/null +++ b/examples/wasip2/postgres/Cargo.toml @@ -0,0 +1,23 @@ +# Standalone example: SQLx + Postgres connection pool on `wasm32-wasip2`. +# +# Detached from the main workspace (note the empty `[workspace]` table) so it +# can pin the exact feature set used for WASI without being unified with the +# rest of the workspace's features. +[package] +name = "sqlx-example-wasip2-postgres" +version = "0.0.0" +edition = "2021" +publish = false + +[workspace] + +[[bin]] +name = "sqlx-wasip2-postgres" +path = "src/main.rs" + +[dependencies] +sqlx = { path = "../../..", default-features = false, features = ["postgres", "runtime-tokio"] } + +# A current-thread Tokio runtime to drive the pool. On wasm32-wasip2 Tokio's +# `net` driver works under `--cfg tokio_unstable` +tokio = { version = "1", default-features = false, features = ["rt", "net", "time", "macros"] } diff --git a/examples/wasip2/postgres/src/main.rs b/examples/wasip2/postgres/src/main.rs new file mode 100644 index 0000000000..d58312a89d --- /dev/null +++ b/examples/wasip2/postgres/src/main.rs @@ -0,0 +1,78 @@ +//! SQLx + Postgres **connection pool** example targeting `wasm32-wasip2`. +//! +//! Runs on a current-thread Tokio runtime. On `wasm32-wasip2` Tokio's `net` +//! driver works under `--cfg tokio_unstable` (set in `../.cargo/config.toml`). +//! +//! Build & run: +//! +//! ```sh +//! cargo build --target wasm32-wasip2 --release +//! wasmtime run -S inherit-network --env DATABASE_URL \ +//! target/wasm32-wasip2/release/sqlx-wasip2-postgres.wasm +//! ``` + +use sqlx::postgres::PgPoolOptions; +use sqlx::Row; + +// `flavor = "current_thread"` because `wasm32-wasip2` does not have threads. +#[tokio::main(flavor = "current_thread")] +async fn main() { + let url = std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://postgres:password@localhost:5432/postgres".to_owned()); + + let pool = PgPoolOptions::new() + .max_connections(5) + .connect(&url) + .await + .expect("failed to connect pool"); + + // Fresh schema (safe to re-run). + sqlx::query("DROP TABLE IF EXISTS wasip2_votes") + .execute(&pool) + .await + .expect("drop table"); + sqlx::query( + "CREATE TABLE wasip2_votes (id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL, votes BIGINT NOT NULL)", + ) + .execute(&pool) + .await + .expect("create table"); + + // Seed a few rows inside a transaction, using bind parameters. + let mut tx = pool.begin().await.expect("begin"); + for (name, votes) in [("alice", 5_i64), ("bob", 3), ("carol", 4)] { + sqlx::query("INSERT INTO wasip2_votes (name, votes) VALUES ($1, $2)") + .bind(name) + .bind(votes) + .execute(&mut *tx) + .await + .expect("insert"); + } + tx.commit().await.expect("commit"); + + // Read them back, ranked highest-first. + let rows = sqlx::query("SELECT name, votes FROM wasip2_votes ORDER BY votes DESC") + .fetch_all(&pool) + .await + .expect("select"); + for row in &rows { + let name: String = row.get("name"); + let votes: i64 = row.get("votes"); + println!("{name}: {votes}"); + } + + // Aggregate (`SUM(bigint)` is `NUMERIC` in Postgres, so cast back to `int8`). + let total: i64 = sqlx::query_scalar("SELECT SUM(votes)::int8 FROM wasip2_votes") + .fetch_one(&pool) + .await + .expect("sum"); + println!("total votes: {total}"); + + let version: String = sqlx::query_scalar("SELECT version()") + .fetch_one(&pool) + .await + .expect("version query failed"); + println!("connected via pool to: {version}"); + + pool.close().await; +}