Qail is a Rust PostgreSQL toolkit that makes typed AST queries the default for safe, tenant-isolated execution, with optional auto-REST/WebSocket exposure.
If you are searching for a Rust PostgreSQL driver, start with qail-pg + qail-core.
[dependencies]
qail-core = "0.27.10"
qail-pg = "0.27.10"use qail_core::prelude::*;
use qail_pg::PgDriver;
let mut driver = PgDriver::connect("localhost", 5432, "user", "mydb").await?;
let ctx = RlsContext::tenant(tenant_id);
let query = Qail::get("users")
.columns(["id", "email"])
.eq("active", true)
.with_rls(&ctx);
let rows = driver.fetch_all(&query).await?;| Component | Use it for | Primary audience |
|---|---|---|
qail-pg |
PostgreSQL driver (wire protocol, pooling, pipeline, COPY) | Backend app code |
qail-core |
Typed AST, parser, validator, RLS model, transpiler | Query/model layer |
qail-gateway |
Optional auto-REST + WebSocket server | API platform teams |
qail (CLI) |
Schema pull/diff, migrations, codegen, lint | DevEx and CI |
- Driver mode: add
qail-pgand execute AST queries in your app. - Gateway mode: run
qail-gatewaywhen you want auto-generated REST/WebSocket. - Tooling mode: use
qailCLI for schema and migration workflows.
| Need | qail-pg |
tokio-postgres |
sqlx |
|---|---|---|---|
| Query API | Typed Qail AST | SQL strings | SQL strings (+ compile-time checked macros) |
| App-side SQL interpolation path | No on AST path | Yes | Yes |
| Tenant scoping model | Built-in RLS context APIs | App-managed | App-managed |
| Auto-REST/WebSocket companion | Yes (qail-gateway) |
No | No |
Use qail-pg when your priority is typed query construction, tenant isolation discipline, and direct wire-level execution from Rust.
Every SaaS backend has the same three recurring failure modes:
- N+1 query explosions
- SQL injection from ad-hoc string assembly
- Tenant data leaks from missing scope filters
Qail addresses these by making AST-first query execution and explicit tenant context the default.
- SQL string: query text assembled in app code (manual concatenation/interpolation, dynamic formatting, etc.).
- SQL bytes: frontend/backend protocol frames and typed bind-value bytes emitted by the driver.
- Qail claim: "no SQL strings" means no application-level string interpolation path on the AST flow.
- PostgreSQL behavior: PostgreSQL still performs normal parse/plan/execute on received statements (or reuses prepared plans).
Older pre-1.0 QAIL experiments used symbolic text forms such as get::users•@id@email@role[active=true][lim=10] and macro examples such as qail!("get::users:'id'email [ 'active == true ]").
Those forms are historical and are not the canonical 0.27.x API surface. Current application code should use the AST/DSL path:
let query = Qail::get("users")
.columns(["id", "email", "role"])
.eq("active", true)
.limit(10);
let rows = driver.fetch_all(&query).await?;We benchmarked six execution patterns for the same endpoint objective and canonical payload shape (id, name, origin_harbor, dest_harbor) on real PostgreSQL data. The pattern differs (single JOIN vs batched vs N+1), but output is validated as equivalent before timing.
Snapshot runs: March 25, 2026 (--release, BATTLE_ITERATIONS=200, warmup 15, global warmup 15).
| Network Profile | Qail AST (uncached, 1 query) | Gateway/REST ?expand= (1 query) |
GraphQL + DataLoader (2 queries) | GraphQL naive (N+1, 101 queries) |
|---|---|---|---|---|
Loopback (BATTLE_SIMULATED_RTT_US=0) |
146.9us | 164.5us | 146.8us | 4.74ms |
| +250us/query RTT | 475.4us | 486.4us | 779.4us | 35.1ms |
| +1000us/query RTT | 1237.8us | 1248.4us | 2287.0us | 111.6ms |
Main signal: on loopback, single-query and DataLoader are close; once RTT is non-trivial, fewer round trips dominate latency.
Methodology notes (click to expand)
- Schema used: current
swb_staging_localschema (odyssey_connections+harbors) with a 2×LEFT JOIN shape. - Equivalence gate: benchmark aborts if any approach does not produce the same canonical payload.
- Cache equalization: global warmup before timed runs.
- Config emitted:
iterations, per-approach warmup, and run order are printed in output. - RTT simulation knob:
BATTLE_SIMULATED_RTT_USinjects per-query transport delay in the harness. - Measured stats: median + p95 (and avg in raw output), query count per request, JSON bytes for REST variants.
- Run it yourself:
DATABASE_URL=postgresql://orion@localhost:5432/swb_staging_local?sslmode=disable BATTLE_SIMULATED_RTT_US=1000 cargo run -p qail-pg --example battle_comparison --features chrono,uuid,legacy-raw-examples --release
use qail_core::prelude::*;
use qail_pg::PgDriver;
// Connect
let mut driver = PgDriver::connect("localhost", 5432, "user", "mydb").await?;
// Multi-tenant: scope every query to this tenant
let ctx = RlsContext::tenant(tenant_id);
// Build & execute
let orders = Qail::get("orders")
.columns(["id", "total", "status"])
.join(JoinKind::Left, "users", "orders.user_id", "users.id")
.column("users.email AS customer_email")
.eq("orders.status", "paid")
.order_by("orders.created_at", Desc)
.limit(25)
.with_rls(&ctx); // ← tenant-scoped automatically
let rows = driver.fetch_all(&orders).await?;cargo install qail
qail init --name myapp --mode postgres --url postgres://localhost/mydb
qail exec "get users'id'email[active=true]" --url postgres://localhost/mydb
qail pull postgres://localhost/mydb # Introspect → schema.qail
qail diff _ schema.qail --live --url postgres://... # Drift detection
qail migrate up v1:v2 postgres://... # Apply migrations
qail types schema.qail > src/generated/schema.rs # Typed codegen| Threat | String SQL | Qail |
|---|---|---|
| SQL injection | Possible (one mistake) | Prevented on AST path (no app-side SQL interpolation) |
| Tenant data leak | Missing WHERE clause | RLS injected automatically |
| Query abuse | Unbounded depth/joins | AST validates at compile time |
| IDOR | Must check per endpoint | Tenant isolation built into protocol |
// RLS: tenant-first constructors
let ctx = RlsContext::tenant(tenant_id); // Single tenant (preferred)
let ctx = RlsContext::tenant_and_agent(tenant_id, agent_id); // Agent/reseller within tenant
let ctx = RlsContext::global(); // Shared data (tenant_id IS NULL)
let token = SuperAdminToken::for_system_process("admin");
let ctx = RlsContext::super_admin(token); // Full bypass (internal only)
// Every query is automatically scoped
Qail::get("bookings").with_rls(&ctx) // ← no manual WHERE neededInvalid joins fail at compile time, not at 3am in production:
use schema::{users, posts};
// ✅ Compiles — tables are related via ref:users.id
Qail::typed(users::table)
.join_related(posts::table)
.typed_column(users::email())
.typed_eq(users::active(), true)
// ❌ Compile Error — no RelatedTo<Products> impl
Qail::typed(users::table).join_related(products::table)Compile-time data governance — sensitive columns require capability proof:
let admin_cap = CapabilityProvider::mint_admin(); // After JWT verification
Qail::get(users::table)
.with_cap(&admin_cap)
.column(users::email) // Public — always allowed
.column_protected(users::password_hash) // Protected — requires capAuto-REST API server with zero backend code. Point it at a Postgres database, get a full API:
GET /api/{table}?expand=users&sort=-created_at&limit=10
GET /api/{table}/:id
POST /api/{table}
PATCH /api/{table}/:id
DELETE /api/{table}/:id
GET /api/{table}/_explain # EXPLAIN ANALYZE
GET /api/{table}/_aggregate # count, sum, avg, min, max
A complete REST API layer for PostgreSQL:
- ✅ Auto-REST CRUD for all tables
- ✅ FK-based JOIN expansion (
?expand=) + nested expansion - ✅ Full-text search (
?search=) - ✅ WebSocket subscriptions + live queries
- ✅ Event triggers (mutation → webhook with retry)
- ✅ JWT auth (HS256/RS256) + API key auth
- ✅ YAML policy engine + column permissions
- ✅ Query allow-listing + complexity limits
- ✅ Prometheus metrics + request tracing
- ✅ NDJSON streaming + cursor pagination
- ✅ OpenAPI spec generation
qail.rs/
├── core/ AST engine, parser, validator, typed system, RLS, migrations
├── pg/ PostgreSQL driver (binary wire protocol, connection pool)
├── gateway/ Auto-REST API server (Axum)
├── cli/ qail exec, pull, diff, migrate, types
├── encoder/ Wire protocol encoder + FFI/runtime internals
├── qdrant/ Qdrant vector DB driver (optional)
├── workflow/ Workflow engine
└── sdk/ Direct SDKs (TypeScript, Swift, Kotlin)
| Platform | Status | Distribution |
|---|---|---|
| TypeScript | ✅ Supported | npm install @qail/client |
| Swift | ✅ Supported | Source package in sdk/swift |
| Kotlin | ✅ Supported | Gradle module in sdk/kotlin |
| Node.js native binding | ⏸ Deferred | Not shipped yet |
tenant_id is the runtime contract across gateway and RLS paths. Legacy operator_id runtime compatibility aliases were removed in v0.26.0.
Compile-time static analysis catches query-in-loop patterns before they reach production. Powered by QAIL semantic Rust scanning (no syn dependency on the runtime analyzer path).
| Code | Severity | Trigger | Example |
|---|---|---|---|
| N1-001 | ⚠ Warning | Query inside for/while/loop |
for x in items { conn.fetch_all(&q) } |
| N1-002 | ⚠ Warning | Loop variable used in query args | for id in ids { Qail::get("t").eq("id", id) } |
| N1-003 | ⚠ Warning | Function with query called in loop | for x in xs { load_user(conn, x) } |
| N1-004 | ❌ Error | Query in nested loop (depth ≥ 2) | for g in groups { for x in g { ... } } |
// Disable on the next line
// qail-lint:disable-next-line N1-001
conn.fetch_all(&cmd).await?;
// Disable inline
conn.fetch_all(&cmd).await?; // qail-lint:disable-line N1-001Runs automatically via validate() when using Qail's build-time checks:
| Env Var | Values | Default |
|---|---|---|
QAIL_NPLUS1 |
off | warn | deny |
warn |
QAIL_NPLUS1_MAX_WARNINGS |
integer | 50 |
QAIL_SCAN_DIRS |
comma-separated source roots | src |
Monorepo example:
# Scan multiple Rust roots during build validation + N+1 checks
QAIL_SCAN_DIRS="src,apps/api/src,crates/billing/src" cargo buildqail check schema.qail --src ./src # Shows N+1 warnings
qail check schema.qail --src ./src --nplus1-deny # Fails on any N+1N+1 diagnostics appear automatically in your editor for .rs files with diagnostic codes N1-001..N1-004.
// ❌ N+1: one query per item
for id in &ids {
let user = conn.fetch_one(&Qail::get("users").eq("id", id)).await?;
}
// ✅ Batch: single query
let users = conn.fetch_all(
&Qail::get("users").in_vals("id", &ids)
).await?;| Category | Features |
|---|---|
| Core SQL | SELECT, INSERT, UPDATE, DELETE, UPSERT, RETURNING, COPY |
| Joins | INNER, LEFT, RIGHT, FULL, CROSS, LATERAL, self-joins |
| Advanced | CTEs, Subqueries, EXISTS, Window Functions, UNION/INTERSECT/EXCEPT |
| Types | JSON/JSONB, Arrays, UUID, Timestamps, Enums, Composite |
| Security | RLS, TypedQail, Protected Columns, Capability Witnesses |
| Migrations | AST-native diffing, drift detection, impact analysis |
| Schema | Views, Materialized Views, Functions, Triggers, Extensions, Sequences, Grants |
| Performance | Connection Pool, Query Cache (LRU+TTL), Prepared Statements, Binary Protocol |
| Connection | SSL/TLS, SCRAM-SHA-256, Unix Socket |
| Operations | EXPLAIN ANALYZE, Statement Timeout, LOCK TABLE, Batch Transactions |
Activate background pool health maintenance (idle connection cleanup + min_connections backfill) by calling spawn_pool_maintenance after creating the pool:
let pool = qail_pg::PgPool::connect(config).await?;
qail_pg::spawn_pool_maintenance(pool.clone());Apache-2.0 © 2025-2026 Qail Contributors
Built with 🦀 Rust
dev.qail.io