Skip to content

Djudicael/pg_client

Repository files navigation

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 pool feature 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-wasip2 and native targets

Quick Start

Add to your Cargo.toml:

[dependencies]
wasi-pg-client = "0.1"
wstd = "0.6"
wasip2 = "1.0"

Write your application:

use wasi_pg_client::{Config, Connection};

#[wstd::main]
async fn main() -> Result<(), wasi_pg_client::PgError> {
    let config = Config::from_uri("postgresql://user:pass@localhost/mydb")?;
    let mut conn = Connection::connect(&config).await?;

    let result = conn.query("SELECT id, name FROM users").await?;
    for row in result.iter() {
        let id: i32 = row.get(0)?;
        let name: String = row.get(1)?;
        println!("{}: {}", id, name);
    }

    conn.close().await?;
    Ok(())
}

Build and run with wasmtime:

cargo build --target wasm32-wasip2
wasmtime run --wasi inherit-network --wasi inherit-env target/wasm32-wasip2/debug/your_app.wasm

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(
    "SELECT * FROM users WHERE age > $1 AND city = $2",
    &[&18i32, &"Paris"],
).await?;

Streaming Large Results

// Stream rows one at a time (O(1) memory)
let mut stream = conn.query_stream("SELECT * FROM large_table").await?;
while let Some(row) = stream.next().await? {
    let id: i32 = row.get(0)?;
    // Process each row as it arrives
}

// Parameterized streaming — bind parameters and stream results
let mut stream = conn.query_params_stream(
    "SELECT * FROM users WHERE age > $1",
    &[&18i32],
).await?;
while let Some(row) = stream.next().await? {
    let name: String = row.get(1)?;
}

// Cursor-based streaming with fetch size
let mut cursor = conn.query_cursor_stream(
    "SELECT * FROM huge_table WHERE category = $1",
    &[&"electronics"],
    1000, // fetch 1000 rows per round-trip
).await?;

Transactions

// Automatic rollback on error, commit on success
conn.with_transaction(|txn| async {
    txn.execute_params(
        "UPDATE accounts SET balance = balance - $1 WHERE id = $2",
        &[&amount, &from],
    ).await?;
    txn.execute_params(
        "UPDATE accounts SET balance = balance + $1 WHERE id = $2",
        &[&amount, &to],
    ).await?;
    Ok(())
}).await?;

Runtime Parameters

// Set a session-level parameter (tracked for reconnection)
conn.set_param("timezone", "UTC").await?;
conn.set_param("application_name", "my_app").await?;

LISTEN/NOTIFY with Timeout

// Listen for events
conn.listen("events").await?;

// Wait with timeout
if let Some(n) = conn.wait_for_notification_with_timeout(
    std::time::Duration::from_secs(5)
).await? {
    println!("Got notification on {}: {}", n.channel, n.payload);
}

Connection Pool

use wasi_pg_client::{Config, Connection};
use wasi_pg_client::pool::{Pool, PoolConfig};

let pool_config = PoolConfig::default()
    .connection(config)
    .max_size(5);

let pool = Pool::new(pool_config).await?;
let mut guard = pool.acquire().await?;
guard.query("SELECT 1").await?;
guard.release().await;  // preferred over Drop for proper cleanup

Error Handling

use wasi_pg_client::{PgError, ErrorClass};

match conn.execute_params("INSERT INTO users (email) VALUES ($1)", &[&"user@example.com"]).await {
    Ok(result) => println!("Inserted {} rows", result.rows_affected().unwrap_or(0)),
    Err(PgError::Server(e)) if e.is_unique_violation() => {
        println!("Email already exists");
    }
    Err(e) => {
        match wasi_pg_client::classify_error(&e) {
            ErrorClass::Broken => println!("Connection broken — need to reconnect"),
            ErrorClass::Transient => println!("Transient error — can retry"),
            ErrorClass::Permanent => println!("Permanent error — cannot retry"),
        }
        return Err(e);
    }
}

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-full when the tls feature is enabled.
  • Plaintext fallback requires an explicit insecure mode such as sslmode=prefer or sslmode=disable.
  • Cleartext-password and MD5 authentication over plaintext transports are rejected unless you explicitly opt in with insecure configuration.
  • sslmode=require is supported for PostgreSQL compatibility, but it intentionally skips certificate verification and should not be treated as a production-grade verification mode.
  • sslmode=verify-ca verifies 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 AsyncTransport trait is public for custom transports/testing but may still evolve as the transport surface is refined

Testing

# Workspace validation
cargo check --workspace --all-targets
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all --check

# Library tests
cargo test -p wasi-pg-client --lib --all-features

# E2E tests (requires podman or docker, with tokio-transport)
cargo test -p wasi-pg-client \
  --features "pool,tokio-transport,tls" \
  --test e2e_tls --test e2e_pool \
  -- --ignored --test-threads=1

# Build for WASI P2
cargo build --workspace --target wasm32-wasip2

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:

cargo check --manifest-path fuzz/Cargo.toml
cargo fuzz run decode_message
cargo fuzz run decode_message_persistent
cargo fuzz run decode_message_bounded
cargo fuzz run decode_pg_types

Fuzzing is currently a manual/pre-release hardening tool rather than a default CI step.

Thread Safety

  • WASI P2: single-threaded runtime — Connection is Send but not Sync
  • Native (tokio-transport): multi-thread-friendly — Pool is Send + Sync via std::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 Connection are Send
  • No background tasks – pool maintenance is lazy (on acquire)
  • Connection pooling – the library ships with a built-in pool (pool feature), but under WASI's single-threaded execution model (no spawn), 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_dump or 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:

at your option.

About

A pure-Rust PostgreSQL client library for WASI. Compiles to wasm32-wasip2 and native with the same codebase. Features parameterized queries, streaming results, transactions, COPY protocol, TLS, SCRAM auth, and connection pooling.

Topics

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages