86 releases (17 breaking)
Uses new Rust 2024
| new 0.27.9 | Apr 17, 2026 |
|---|---|
| 0.27.6 | Mar 30, 2026 |
| 0.14.8 | Dec 31, 2025 |
#2381 in Database interfaces
Used in 2 crates
2.5MB
64K
SLoC
Contains (Mach-o exe, 35KB) examples/fifty_million_libpq, (Mach-o exe, 35KB) examples/million_libpq
qail-pg
Rust PostgreSQL driver for typed AST queries and native wire-protocol execution
A high-performance async PostgreSQL driver that speaks the wire protocol directly. qail-pg is the driver layer of Qail and is intended for backend code that wants AST-first query construction.
Positioning
qail-pg is a Rust PostgreSQL driver for teams that want:
- a typed AST query surface instead of SQL-string-centric app code
- explicit tenant-scoping APIs on query objects
- direct wire-protocol execution with pipelining and COPY support
Quick Comparison
| Need | qail-pg |
tokio-postgres |
sqlx |
|---|---|---|---|
| Primary query API | Typed Qail AST | SQL strings | SQL strings (+ checked macros) |
| App-side SQL interpolation path | No on AST path | Yes | Yes |
| Built-in tenant context model | Yes (RlsContext) |
App-managed | App-managed |
| Auto-REST companion | Yes (qail-gateway) |
No | No |
SQL String vs SQL Bytes
- SQL string: text query built in app code (format/concat/interpolate).
- SQL bytes: PostgreSQL frontend/backend protocol bytes (
Parse,Bind,Execute, result frames) and encoded bind values. - What qail-pg does: compiles QAIL AST into protocol messages and typed values.
- What PostgreSQL still does: server-side parse/plan/execute is still normal PostgreSQL behavior.
Legacy Syntax Notice
You may still find pre-1.0 search results showing symbolic QAIL strings like get::users•@id@email@role[active=true][lim=10] or old macro examples like qail!("get::users:'id'email [ 'active == true ]").
Those are historical docs from older releases. qail-pg 0.27.x is documented and optimized around the native AST path:
let query = Qail::get("users")
.columns(["id", "email", "role"])
.eq("active", true)
.limit(10);
let rows = driver.fetch_all(&query).await?;
Inspect Real Wire Bytes
Run the built-in demo:
cargo run -p qail-pg --example wire_bytes_demo
For:
Qail::get("users")
.select_all()
.filter("active", Eq, true)
you will see:
- SQL view:
SELECT * FROM users WHERE active = $1 - bind param
$1:74('t'for boolean true) - wire frames:
Parse (P) + Bind (B) + Describe (D) + Execute (E) + Sync (S)
This shows the exact protocol-byte path used by the driver.
Features
- AST-Native - Compiles QAIL AST directly to PostgreSQL wire protocol
- 28% Faster - Benchmarked at 1.36M rows/s COPY (vs asyncpg at 1.06M rows/s)
- Query Pipelining - 24x faster batch operations via
pipeline_execute_count() - SSL/TLS - Production-ready with
tokio-rustls - Password Auth Modes - Supports SCRAM-SHA-256, MD5, and cleartext server flows
- Protocol 3.2 Ready - Requests startup protocol 3.2 by default with one-shot fallback to 3.0 on explicit protocol rejection
- Cancel-Key Compatibility - Supports variable-length cancel keys via bytes-native APIs (legacy i32 wrappers retained for 4-byte keys)
- Connection Pooling - Built-in
PgPool - Transactions - Full
begin/commit/rollbacksupport
Installation
[!CAUTION] Release Candidate: QAIL is now in the release-candidate phase. The API is near-stable and battle-tested in production. Breaking changes are expected to be rare and limited to critical correctness/security fixes before 1.0.
[dependencies]
qail-pg = "0.27.9"
qail-core = "0.27.9"
qail-pg is AST-only. Raw SQL helper APIs were removed.
Quick Start
use qail_core::ast::{QailCmd, builders::*};
use qail_pg::PgDriver;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Connect with password
let mut driver = PgDriver::connect_with_password(
"localhost", 5432, "postgres", "mydb", "password"
).await?;
// Build a query using QAIL AST
let cmd = QailCmd::get("users")
.columns([col("id"), col("name"), col("email")])
.filter(eq("active", true))
.order_by([("created_at", Desc)])
.limit(10);
// Execute and fetch rows
let rows = driver.fetch_all(&cmd).await?;
for row in rows {
let name: String = row.get("name");
println!("User: {}", name);
}
Ok(())
}
High-Performance Batch Operations
// Execute 10,000 queries in a single network round-trip
// pipeline_execute_count() uses AstPipelineMode::Auto by default
let cmds: Vec<QailCmd> = (0..10_000)
.map(|i| QailCmd::add("events")
.columns(["user_id", "event_type"])
.values([Value::Int(i), Value::String("login".to_string())])
).collect();
let count = driver.pipeline_execute_count(&cmds).await?;
println!("Inserted {} rows", count);
// Override strategy explicitly when needed
let count_cached = driver
.pipeline_execute_count_with_mode(&cmds, qail_pg::AstPipelineMode::Cached)
.await?;
COPY Protocol (Bulk Insert)
use qail_pg::protocol::CopyEncoder;
// Build COPY data
let mut encoder = CopyEncoder::new();
for i in 0..1_000_000 {
encoder.begin_row();
encoder.write_i64(i);
encoder.write_str(&format!("user_{}", i));
encoder.end_row();
}
// Execute COPY
driver.copy_bulk_bytes("users", &["id", "name"], encoder.finish()).await?;
Connection Pooling
use qail_pg::PgPool;
// Create a pool with 10 connections
let pool = PgPool::new(
"localhost", 5432, "postgres", "mydb", Some("password"), 10
).await?;
// Acquire a connection
let mut conn = pool.acquire().await?;
let rows = conn.fetch_all(&cmd).await?;
SSL/TLS Support
qail-pg uses tokio-rustls for TLS connections:
// SSL is auto-negotiated during connection
let driver = PgDriver::connect_with_password(
"pg.example.com", 5432, "user", "db", "pass"
).await?;
Ergonomic Expression Builders
qail-pg works seamlessly with qail-core's ergonomic builders:
use qail_core::ast::builders::*;
// COUNT(*) FILTER (WHERE condition)
count_filter(vec![eq("status", "active")]).alias("active_count")
// NOW() - INTERVAL '24 hours'
now_minus("24 hours")
// CASE WHEN ... ELSE ... END
case_when(gt("score", 80), text("pass"))
.otherwise(text("fail"))
.alias("result")
// Type casting
cast(col("amount"), "float8")
Type Support
| PostgreSQL Type | Rust Type |
|---|---|
text, varchar |
String |
int4, int8 |
i32, i64 |
float8 |
f64 |
bool |
bool |
uuid |
uuid::Uuid |
jsonb |
serde_json::Value |
timestamp |
chrono::DateTime<Utc> |
date |
chrono::NaiveDate |
numeric |
rust_decimal::Decimal |
License
Apache-2.0
🤝 Contributing & Support
We welcome issue reports on GitHub! Please provide detailed descriptions to help us reproduce and fix the problem. We aim to address critical issues within 1-5 business days.
Dependencies
~20–36MB
~545K SLoC