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;
+}