Skip to content

Commit 6fa4ca2

Browse files
committed
ci(wasip2): document and test wasm32-wasip2 support (Postgres + MySQL)
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 <bailey@cosmonic.com>
1 parent 3da582a commit 6fa4ca2

8 files changed

Lines changed: 442 additions & 1 deletion

File tree

.github/workflows/sqlx.yml

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,3 +732,81 @@ jobs:
732732
env:
733733
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
734734
RUSTFLAGS: --cfg mariadb="${{ matrix.mariadb }}"
735+
736+
wasip2:
737+
name: WASI (wasm32-wasip2)
738+
runs-on: ubuntu-24.04
739+
needs: check
740+
timeout-minutes: 30
741+
services:
742+
postgres:
743+
image: postgres:17
744+
env:
745+
POSTGRES_DB: sqlx
746+
POSTGRES_PASSWORD: password
747+
ports:
748+
- 5432:5432
749+
options: >-
750+
--health-cmd "pg_isready -U postgres"
751+
--health-interval 10s
752+
--health-timeout 5s
753+
--health-retries 10
754+
mysql:
755+
image: mysql:8
756+
env:
757+
MYSQL_DATABASE: sqlx
758+
MYSQL_ROOT_PASSWORD: password
759+
ports:
760+
- 3306:3306
761+
options: >-
762+
--health-cmd "mysqladmin ping -uroot -ppassword --silent"
763+
--health-interval 10s
764+
--health-timeout 5s
765+
--health-retries 10
766+
steps:
767+
- uses: actions/checkout@v5
768+
769+
# https://blog.rust-lang.org/2025/03/02/Rustup-1.28.0.html
770+
- name: Setup Rust
771+
run: |
772+
rustup show active-toolchain || rustup toolchain install
773+
rustup target add wasm32-wasip2
774+
775+
- uses: Swatinem/rust-cache@v2
776+
777+
- name: Install Wasmtime
778+
uses: bytecodealliance/actions/wasmtime/setup@v1
779+
780+
# Build and run the Postgres example as a WASI component, then assert the
781+
# query results (not just the exit code) to catch wrong-value regressions.
782+
- name: Postgres example
783+
working-directory: examples/wasip2/postgres
784+
run: |
785+
cargo build --target wasm32-wasip2 --release
786+
wasmtime run -S inherit-network \
787+
--env DATABASE_URL="$DATABASE_URL" \
788+
target/wasm32-wasip2/release/sqlx-wasip2-postgres.wasm | tee target/output.txt
789+
grep -qF 'alice: 5' target/output.txt
790+
grep -qF 'carol: 4' target/output.txt
791+
grep -qF 'bob: 3' target/output.txt
792+
grep -qF 'total votes: 12' target/output.txt
793+
grep -q 'connected via pool to:' target/output.txt
794+
env:
795+
DATABASE_URL: postgres://postgres:password@127.0.0.1:5432/sqlx
796+
797+
# Build and run the MySQL example as a WASI component, then assert the
798+
# query results (not just the exit code) to catch wrong-value regressions.
799+
- name: MySQL example
800+
working-directory: examples/wasip2/mysql
801+
run: |
802+
cargo build --target wasm32-wasip2 --release
803+
wasmtime run -S inherit-network \
804+
--env DATABASE_URL="$DATABASE_URL" \
805+
target/wasm32-wasip2/release/sqlx-wasip2-mysql.wasm | tee target/output.txt
806+
grep -qF 'alice: 5' target/output.txt
807+
grep -qF 'carol: 4' target/output.txt
808+
grep -qF 'bob: 3' target/output.txt
809+
grep -qF 'total votes: 12' target/output.txt
810+
grep -q 'connected via pool to:' target/output.txt
811+
env:
812+
DATABASE_URL: mysql://root:password@127.0.0.1:3306/sqlx

README.md

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
<span> | </span>
4444
<a href="https://github.com/launchbadge/sqlx/wiki/Ecosystem">
4545
Ecosystem
46-
</a>
46+
</a>
4747
<span> | </span>
4848
<a href="https://discord.gg/uuruzJ7">
4949
Discord
@@ -225,6 +225,59 @@ be removed in the future.
225225

226226
[readme-offline]: sqlx-cli/README.md#enable-building-in-offline-mode-with-query
227227

228+
#### WebAssembly (`wasm32-wasip2`)
229+
230+
SQLx runs inside a [WebAssembly Component] on the `wasm32-wasip2` target, with
231+
the **Postgres** and **MySQL/MariaDB** drivers connecting over the host's network
232+
via [`wasi:sockets`]. This works in any WASIP2 runtime, such as
233+
[Wasmtime].
234+
235+
It uses the standard **`runtime-tokio`** feature with async I/O. Connection pooling (`sqlx::Pool`), `LISTEN`/`NOTIFY`
236+
(`PgListener`), connection timeouts, and TLS (with the `tls-rustls-*` backends)
237+
all work.
238+
239+
```toml
240+
[dependencies]
241+
sqlx = { version = "0.9", default-features = false, features = ["postgres", "runtime-tokio"] }
242+
# wasm32-wasip2 does not have threads.. Drive futures with a current-thread runtime.
243+
tokio = { version = "1", default-features = false, features = ["rt", "net", "time", "macros"] }
244+
```
245+
246+
Two requirements specific to this target:
247+
248+
1. **`--cfg tokio_unstable`**: Tokio's `net` support on `wasm32` is currently
249+
behind this cfg. Set it for the target in `.cargo/config.toml` so a plain
250+
`cargo build --target wasm32-wasip2` works:
251+
252+
```toml
253+
# .cargo/config.toml
254+
[target.wasm32-wasip2]
255+
rustflags = ["--cfg", "tokio_unstable"]
256+
```
257+
258+
2. **Current-thread runtime**: there are no threads, so use
259+
`#[tokio::main(flavor = "current_thread")]` (or
260+
`Builder::new_current_thread()`); the multi-threaded runtime isn't available.
261+
262+
Remaining notes on this target:
263+
264+
- TLS works with the `tls-rustls-*` backends (verified with `tls-rustls-ring`,
265+
which negotiates TLS 1.3). `tls-native-tls` is **not** available (it links a
266+
system TLS library).
267+
- MySQL 8's default `caching_sha2_password` over a non-TLS connection requires
268+
the `mysql-rsa` feature.
269+
- Connecting by **hostname** triggers WASI name resolution, which Wasmtime
270+
gates behind `--allow-ip-name-lookup` (separate from `-S inherit-network`).
271+
Connecting by IP literal needs only `-S inherit-network`.
272+
273+
Runnable examples for both backends, including the `wasmtime` invocation
274+
(`wasmtime run -S inherit-network ...`), live under
275+
[`examples/wasip2`](examples/wasip2).
276+
277+
[WebAssembly Component]: https://component-model.bytecodealliance.org/
278+
[`wasi:sockets`]: https://github.com/WebAssembly/wasi-sockets
279+
[Wasmtime]: https://wasmtime.dev/
280+
228281
## SQLx is not an ORM!
229282

230283
SQLx supports **compile-time checked queries**. It does not, however, do this by providing a Rust

examples/wasip2/.cargo/config.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# `tokio::net` support on `wasm32-wasip2` is currently gated behind Tokio's
2+
# `tokio_unstable` cfg. Setting it here means `cargo build --target
3+
# wasm32-wasip2` works in these examples without passing `RUSTFLAGS` by hand.
4+
#
5+
# This config is discovered for both the `postgres` and `mysql` example crates
6+
# because Cargo walks up the directory tree from the build directory.
7+
[target.wasm32-wasip2]
8+
rustflags = ["--cfg", "tokio_unstable"]

examples/wasip2/README.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# SQLx on `wasm32-wasip2`
2+
3+
These examples show SQLx running inside a [WebAssembly Component] on the
4+
`wasm32-wasip2` target, using a `sqlx::Pool` to connect to a database over the
5+
host network via [`wasi:sockets`]. They run in any WASIP2 runtime; the
6+
commands below use [Wasmtime].
7+
8+
| Example | Backend |
9+
| ------------------------ | -------- |
10+
| [`postgres`](./postgres) | Postgres |
11+
| [`mysql`](./mysql) | MySQL |
12+
13+
## How it works
14+
15+
The examples use the standard **`runtime-tokio`** feature with async I/O.
16+
Tokio's `net` driver works on `wasm32-wasip2` (via `mio`'s WASI support).
17+
18+
Two target-specific requirements:
19+
20+
- **`--cfg tokio_unstable`**: Tokio's `net` support on `wasm32` is gated behind
21+
this cfg. It's set for the `wasm32-wasip2` target in
22+
[`.cargo/config.toml`](./.cargo/config.toml), so a plain
23+
`cargo build --target wasm32-wasip2` works (no `RUSTFLAGS` needed).
24+
- **Current-thread runtime**: WASIP2 does not have threads, so each example uses
25+
`#[tokio::main(flavor = "current_thread")]`.
26+
27+
Each example opens a `Pool`, creates a table, inserts rows in a transaction, then
28+
reads them back and aggregates — a small round-trip exercising DDL, bind
29+
parameters, `fetch_all`, and `begin`/`commit`. `LISTEN`/`NOTIFY` and TLS
30+
(`tls-rustls-*`) also work on this target; see the
31+
[top-level README](../../README.md#webassembly-wasm32-wasip2).
32+
33+
## Prerequisites
34+
35+
```sh
36+
rustup target add wasm32-wasip2
37+
# Wasmtime: https://wasmtime.dev/
38+
```
39+
40+
## Run
41+
42+
### Postgres
43+
44+
```sh
45+
# A database to connect to:
46+
docker run -d --name sqlx-wasip2-pg \
47+
-e POSTGRES_PASSWORD=password -e POSTGRES_DB=sqlx \
48+
-p 5432:5432 postgres:17
49+
50+
cd postgres
51+
cargo build --target wasm32-wasip2 --release
52+
wasmtime run -S inherit-network \
53+
--env DATABASE_URL="postgres://postgres:password@127.0.0.1:5432/sqlx" \
54+
target/wasm32-wasip2/release/sqlx-wasip2-postgres.wasm
55+
```
56+
57+
Expected output:
58+
59+
```text
60+
alice: 5
61+
carol: 4
62+
bob: 3
63+
total votes: 12
64+
connected via pool to: PostgreSQL 17.x ...
65+
```
66+
67+
### MySQL
68+
69+
```sh
70+
docker run -d --name sqlx-wasip2-mysql \
71+
-e MYSQL_ROOT_PASSWORD=password -e MYSQL_DATABASE=sqlx \
72+
-p 3306:3306 mysql:8
73+
74+
cd mysql
75+
cargo build --target wasm32-wasip2 --release
76+
wasmtime run -S inherit-network \
77+
--env DATABASE_URL="mysql://root:password@127.0.0.1:3306/sqlx" \
78+
target/wasm32-wasip2/release/sqlx-wasip2-mysql.wasm
79+
```
80+
81+
Expected output:
82+
83+
```text
84+
alice: 5
85+
carol: 4
86+
bob: 3
87+
total votes: 12
88+
connected via pool to: 8.x.x
89+
```
90+
91+
> `wasmtime run -S inherit-network` grants the component access to the host
92+
> network; it is required for outbound TCP. These examples connect by IP literal
93+
> (`127.0.0.1`), so that flag alone is sufficient. Connecting by **hostname**
94+
> additionally needs `--allow-ip-name-lookup` to permit WASI DNS resolution.
95+
96+
[WebAssembly Component]: https://component-model.bytecodealliance.org/
97+
[`wasi:sockets`]: https://github.com/WebAssembly/wasi-sockets
98+
[Wasmtime]: https://wasmtime.dev/

examples/wasip2/mysql/Cargo.toml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Standalone example: SQLx + MySQL connection pool on `wasm32-wasip2`.
2+
#
3+
# Detached from the main workspace (note the empty `[workspace]` table) so it
4+
# can pin the exact feature set used for WASI without being unified with the
5+
# rest of the workspace's features.
6+
[package]
7+
name = "sqlx-example-wasip2-mysql"
8+
version = "0.0.0"
9+
edition = "2021"
10+
publish = false
11+
12+
[workspace]
13+
14+
[[bin]]
15+
name = "sqlx-wasip2-mysql"
16+
path = "src/main.rs"
17+
18+
[dependencies]
19+
# `mysql-rsa` enables the public-key password exchange MySQL 8 uses for
20+
# `caching_sha2_password` over a plaintext connection.
21+
sqlx = { path = "../../..", default-features = false, features = ["mysql", "mysql-rsa", "runtime-tokio"] }
22+
23+
# A current-thread Tokio runtime to drive the pool. On wasm32-wasip2 Tokio's
24+
# `net` driver works under `--cfg tokio_unstable`
25+
tokio = { version = "1", default-features = false, features = ["rt", "net", "time", "macros"] }

examples/wasip2/mysql/src/main.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//! SQLx + MySQL **connection pool** example targeting `wasm32-wasip2`.
2+
//!
3+
//! Runs on a current-thread Tokio runtime. On `wasm32-wasip2` Tokio's `net`
4+
//! driver works under `--cfg tokio_unstable` (set in `../.cargo/config.toml`).
5+
//!
6+
//! Build & run:
7+
//!
8+
//! ```sh
9+
//! cargo build --target wasm32-wasip2 --release
10+
//! wasmtime run -S inherit-network --env DATABASE_URL \
11+
//! target/wasm32-wasip2/release/sqlx-wasip2-mysql.wasm
12+
//! ```
13+
14+
use sqlx::mysql::MySqlPoolOptions;
15+
use sqlx::Row;
16+
17+
// `flavor = "current_thread"` because `wasm32-wasip2` does not have threads.
18+
#[tokio::main(flavor = "current_thread")]
19+
async fn main() {
20+
let url = std::env::var("DATABASE_URL")
21+
.unwrap_or_else(|_| "mysql://root:password@localhost:3306/sqlx".to_owned());
22+
23+
let pool = MySqlPoolOptions::new()
24+
.max_connections(5)
25+
.connect(&url)
26+
.await
27+
.expect("failed to connect pool");
28+
29+
// Fresh schema (safe to re-run).
30+
sqlx::query("DROP TABLE IF EXISTS wasip2_votes")
31+
.execute(&pool)
32+
.await
33+
.expect("drop table");
34+
sqlx::query(
35+
"CREATE TABLE wasip2_votes (id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(64) NOT NULL, votes BIGINT NOT NULL)",
36+
)
37+
.execute(&pool)
38+
.await
39+
.expect("create table");
40+
41+
// Seed a few rows inside a transaction, using bind parameters.
42+
let mut tx = pool.begin().await.expect("begin");
43+
for (name, votes) in [("alice", 5_i64), ("bob", 3), ("carol", 4)] {
44+
sqlx::query("INSERT INTO wasip2_votes (name, votes) VALUES (?, ?)")
45+
.bind(name)
46+
.bind(votes)
47+
.execute(&mut *tx)
48+
.await
49+
.expect("insert");
50+
}
51+
tx.commit().await.expect("commit");
52+
53+
// Read them back, ranked highest-first.
54+
let rows = sqlx::query("SELECT name, votes FROM wasip2_votes ORDER BY votes DESC")
55+
.fetch_all(&pool)
56+
.await
57+
.expect("select");
58+
for row in &rows {
59+
let name: String = row.get("name");
60+
let votes: i64 = row.get("votes");
61+
println!("{name}: {votes}");
62+
}
63+
64+
// Aggregate (`SUM(bigint)` is `DECIMAL` in MySQL, so cast back to a signed int).
65+
let total: i64 = sqlx::query_scalar("SELECT CAST(SUM(votes) AS SIGNED) FROM wasip2_votes")
66+
.fetch_one(&pool)
67+
.await
68+
.expect("sum");
69+
println!("total votes: {total}");
70+
71+
let version: String = sqlx::query_scalar("SELECT VERSION()")
72+
.fetch_one(&pool)
73+
.await
74+
.expect("version query failed");
75+
println!("connected via pool to: {version}");
76+
77+
pool.close().await;
78+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Standalone example: SQLx + Postgres connection pool on `wasm32-wasip2`.
2+
#
3+
# Detached from the main workspace (note the empty `[workspace]` table) so it
4+
# can pin the exact feature set used for WASI without being unified with the
5+
# rest of the workspace's features.
6+
[package]
7+
name = "sqlx-example-wasip2-postgres"
8+
version = "0.0.0"
9+
edition = "2021"
10+
publish = false
11+
12+
[workspace]
13+
14+
[[bin]]
15+
name = "sqlx-wasip2-postgres"
16+
path = "src/main.rs"
17+
18+
[dependencies]
19+
sqlx = { path = "../../..", default-features = false, features = ["postgres", "runtime-tokio"] }
20+
21+
# A current-thread Tokio runtime to drive the pool. On wasm32-wasip2 Tokio's
22+
# `net` driver works under `--cfg tokio_unstable`
23+
tokio = { version = "1", default-features = false, features = ["rt", "net", "time", "macros"] }

0 commit comments

Comments
 (0)