wasi-pg-client
A PostgreSQL client library for WASI Preview 2, written in Rust. The workspace is currently in a hardening phase: the main native and WASI build/test matrix is green, secure defaults have been tightened, and the remaining work is mostly around documentation polish, broader fuzzing process integration, and long-tail API refinement rather than known workspace-breaking issues.
Features
- ✅ Full PostgreSQL wire protocol v3 support
- ✅ Parameterized queries (SQL injection prevention)
- ✅ Prepared statements with automatic LRU caching
- ✅ Streaming results (O(1) memory for large queries)
- ✅ Parameterized streaming (
query_params_stream) - ✅ Transactions with RAII guards and savepoints
- ✅ COPY protocol for bulk import/export (CSV + binary)
- ✅ LISTEN/NOTIFY for pub/sub with timeout support
- ✅ TLS via rustls (pure Rust, WASI-compatible)
- ✅ SCRAM-SHA-256 and SCRAM-SHA-256-PLUS channel binding support when TLS channel-binding data is available
- ✅ MD5 authentication (legacy, opt-in)
- ✅ Connection pooling behind
poolfeature flag - ✅ Automatic reconnection with session state rebuild
- ✅ Retry policies for transient errors (serialization failures, deadlocks)
- ✅ Query cancellation via
CancelToken(with TLS support) - ✅ Runtime parameter setting (
set_param) with reconnect re-application - ✅ Structured logging via
tracing - ✅ Compiles to
wasm32-wasip2and native targets
Quick Start
Add to your Cargo.toml:
[]
= "0.1"
= "0.6"
= "1.0"
Write your application:
use ;
async
Build and run with wasmtime:
WASI P2 Requirements
- Target:
wasm32-wasip2(stable since Rust 1.78) - Runtime: wasmtime with
--wasi inherit-network - getrandom: Must use
features = ["wasi"]for cryptographic randomness
Send on WASI
Connection is Send on wasm32-wasip2 (enabled by wstd 0.6+, which uses Arc internally).
Usage Examples
Parameterized Queries
let result = conn.query_params.await?;
Streaming Large Results
// Stream rows one at a time (O(1) memory)
let mut stream = conn.query_stream.await?;
while let Some = stream.next.await?
// Parameterized streaming — bind parameters and stream results
let mut stream = conn.query_params_stream.await?;
while let Some = stream.next.await?
// Cursor-based streaming with fetch size
let mut cursor = conn.query_cursor_stream.await?;
Transactions
// Automatic rollback on error, commit on success
conn.with_transaction.await?;
Runtime Parameters
// Set a session-level parameter (tracked for reconnection)
conn.set_param.await?;
conn.set_param.await?;
LISTEN/NOTIFY with Timeout
// Listen for events
conn.listen.await?;
// Wait with timeout
if let Some = conn.wait_for_notification_with_timeout.await?
Connection Pool
use ;
use ;
let pool_config = default
.connection
.max_size;
let pool = new.await?;
let mut guard = pool.acquire.await?;
guard.query.await?;
guard.release.await; // preferred over Drop for proper cleanup
Error Handling
use ;
match conn.execute_params.await
Feature Flags
| Feature | Default | Description |
|---|---|---|
tls |
✅ | TLS support via rustls |
scram |
✅ | SCRAM-SHA-256 authentication, including SCRAM-SHA-256-PLUS when channel binding is available |
md5-auth |
❌ | MD5 authentication (legacy) |
pool |
❌ | Connection pooling |
tracing |
✅ | Structured logging via tracing |
uuid |
❌ | UUID type support via uuid crate |
serde-json |
❌ | JSON type support via serde_json |
chrono |
❌ | chrono integration for date/time |
test-native |
❌ | Native transport for testing |
tokio-transport |
❌ | Tokio async TCP transport for native builds |
Project Structure
The library is a single crate with two internal protocol layers:
| Module | Purpose | I/O | Async |
|---|---|---|---|
protocol |
Wire protocol encoding/decoding (thin wrapper around postgres-protocol) |
❌ | ❌ |
types |
Type system, OID mapping, ToSql/FromSql (thin wrapper around postgres-types) |
❌ | ❌ |
pool |
Connection pooling (behind pool feature flag) |
✅ | ✅ |
All modules live inside wasi-pg-client — a single cargo add wasi-pg-client pulls everything in.
Security and deployment posture
Current defaults are intentionally conservative:
- TLS defaults to
sslmode=verify-fullwhen thetlsfeature is enabled. - Plaintext fallback requires an explicit insecure mode such as
sslmode=preferorsslmode=disable. - Cleartext-password and MD5 authentication over plaintext transports are rejected unless you explicitly opt in with insecure configuration.
sslmode=requireis supported for PostgreSQL compatibility, but it intentionally skips certificate verification and should not be treated as a production-grade verification mode.sslmode=verify-caverifies the CA chain but intentionally skips hostname verification.accept_invalid_certs(true)disables certificate verification entirely and is for development/testing only.
For production use, prefer:
SslMode::VerifyFull- normal hostname verification
- default certificate validation
- SCRAM-based authentication instead of MD5
API Stability
This is v0.1 — the public API may change between minor versions (semver pre-1.0).
#[non_exhaustive]on all public enums and structs ensures adding new variants/fields isn't breaking- Internal
pub(crate)items can change freely - The
AsyncTransporttrait is public for custom transports/testing but may still evolve as the transport surface is refined
Testing
# Workspace validation
# Library tests
# E2E tests (requires podman or docker, with tokio-transport)
# Build for WASI P2
Fuzzing
The repository includes a dedicated fuzz/ crate with targets for:
- whole-buffer backend message decoding
- incremental/chunked backend framing
- bounded-buffer stress cases
- type-system decode paths across text and binary formats
Typical local commands:
Fuzzing is currently a manual/pre-release hardening tool rather than a default CI step.
Thread Safety
- WASI P2: single-threaded runtime —
ConnectionisSendbut notSync - Native (
tokio-transport): multi-thread-friendly —PoolisSend + Syncviastd::sync::Mutex
Limitations (WASI Preview 2)
- Single-threaded execution model – the typical WASI Preview 2 runtime model is still single-threaded even though key types such as
ConnectionareSend - No background tasks – pool maintenance is lazy (on acquire)
- Connection pooling – the library ships with a built-in pool (
poolfeature), but under WASI's single-threaded execution model (nospawn), a deported pool manager like PgBouncer is the preferred architecture for production deployments. The in-process pool is included for convenience and native builds but is intentionally not the recommended path under wasmtime or similar runtimes. This is expected to change with WASI Preview 3, which introduces multithreading — at that point the in-process pool becomes a first-class option for WASI runtimes. Until then, the pool is included for native builds, testing, and forward compatibility. - No file system access – SSL certificates must be embedded (via
webpki-roots) - No process spawning – cannot run
pg_dumpor external tools - Notification timeout – native tokio builds have a real timeout race; WASI currently keeps a best-effort fallback path
- Runtime DNS / sockets behavior depends on the host runtime – the WASI transport now uses
wasi:sockets/ip-name-lookup, so behavior follows the runtime's implementation rather than the host standard library
License
Dual-licensed under either:
- Apache License, Version 2.0 (LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or https://opensource.org/licenses/MIT)
at your option.