Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .github/workflows/sqlx.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
55 changes: 54 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
<span> | </span>
<a href="https://github.com/launchbadge/sqlx/wiki/Ecosystem">
Ecosystem
</a>
</a>
<span> | </span>
<a href="https://discord.gg/uuruzJ7">
Discord
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions examples/wasip2/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -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"]
98 changes: 98 additions & 0 deletions examples/wasip2/README.md
Original file line number Diff line number Diff line change
@@ -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/
25 changes: 25 additions & 0 deletions examples/wasip2/mysql/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"] }
78 changes: 78 additions & 0 deletions examples/wasip2/mysql/src/main.rs
Original file line number Diff line number Diff line change
@@ -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;
}
23 changes: 23 additions & 0 deletions examples/wasip2/postgres/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"] }
Loading
Loading