# Qail — Rust PostgreSQL Driver + AST Toolkit > **Qail is a Rust PostgreSQL toolkit that makes typed AST queries the default for safe, tenant-isolated execution, with optional auto-REST/WebSocket exposure.** [![Crates.io](https://img.shields.io/badge/crates.io-qail--pg-orange)](https://crates.io/crates/qail-pg) [![Crates.io](https://img.shields.io/badge/crates.io-qail-orange)](https://crates.io/crates/qail) [![Docs](https://img.shields.io/badge/docs-qail.rs-blue)](https://qail.rs/docs) [![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE) [![Version](https://img.shields.io/badge/version-3.0.2-blue)](CHANGELOG.md) [![Claude for Open Source](https://img.shields.io/badge/supported%20by-Claude%20for%20Open%20Source-d97757)](https://claude.com/contact-sales/claude-for-oss) --- ## Start Here (Driver First) If you are searching for a **Rust PostgreSQL driver**, start with `qail-pg` + `qail-core`. ```toml [dependencies] qail-core = "3.0.2" qail-pg = "3.0.2" ``` ```rust use qail_core::prelude::*; use qail_pg::PgDriver; let mut driver = PgDriver::connect("localhost", 5432, "user", "mydb").await?; qail_core::rls::init_scope_registries_from_tables(&[("users", "tenant_id")], &[])?; 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?; ``` ## Product Map | Concept | Crate | Use it for | Primary audience | |---|---|---| | AST Kernel | `qail-core` | Typed AST, parser, validator, RLS model, native access policy | Query/model layer | | Postgres Driver | `qail-pg` | PostgreSQL wire protocol, pooling, pipeline, COPY, LISTEN/NOTIFY | Backend app code | | Access Gateway | `qail-gateway` | AutoREST, WebSocket, OpenAPI, auth/RLS/policy enforcement | API platform teams | | SchemaOps CLI | `qail` | Schema pull/diff, phased migrations, codegen, lint | DevEx and CI | | Flow Engine | `qail-workflow` | Declarative state-machine workflows, waits, retries, side-effect checkpoints | App orchestration | | Flow Ledger | `qail-workflow-postgres` | PostgreSQL workflow state, leases, idempotency, side-effect replay, timeouts | Durable workflow execution | | Vector Bridge | `qail-qdrant` | Qdrant vector search and AST-compatible filters | AI/vector workloads | ## Choose Your Mode 1. Driver mode: add `qail-pg` and execute AST queries in your app. 2. Gateway mode: run `qail-gateway` when you want auto-generated REST/WebSocket. 3. Tooling mode: use `qail` CLI for schema and migration workflows. 4. Workflow mode: add `qail-workflow` and optionally `qail-workflow-postgres` when app workflows need durable resume, timeout, lease, and side-effect replay semantics. ## qail-pg vs tokio-postgres vs sqlx | 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. ## Problem It Solves Every SaaS backend has the same three recurring failure modes: 1. N+1 query explosions 2. SQL injection from ad-hoc string assembly 3. Tenant data leaks from missing scope filters Qail addresses these by making AST-first query execution and explicit tenant context the default. ### SQL String vs SQL Bytes (Exact Meaning) - **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). ### Legacy Syntax Notice 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 `1.x` API surface. Current application code should use the AST/DSL path: ```rust let query = Qail::get("users") .columns(["id", "email", "role"]) .eq("active", true) .limit(10); let rows = driver.fetch_all(&query).await?; ``` --- ## Production Use Qail is extracted from and hardened against live systems, not built as a demo: - **[sailtix.com](https://sailtix.com)** — fast-boat booking platform for Indonesia. Search, booking, payments, and multi-tenant operator settlement run on `qail-pg` + `qail-gateway`, with schema lineage managed by the `qail` CLI. - **[Deck by Qail](https://deck.qail.io)** — operator orders + CRM app on the App Store and Google Play, built on the Qail Gateway auto-REST plane and the Dart SDK. The RLS model, the N+1 analyzer, and the fail-closed migration engine were each shaped by real incidents in these systems before landing as features. ### Support Qail's development is supported by Anthropic's [Claude for Open Source](https://claude.com/contact-sales/claude-for-oss) program. --- ## Benchmark: Pattern Cost Under RTT 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_local` schema (`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_US` injects 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.
--- ## Quick Start ```rust 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 qail_core::rls::init_scope_registries_from_tables( &[("orders", "tenant_id"), ("users", "tenant_id")], &[], )?; 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?; ``` ### CLI ```bash 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 apply --phase expand # Add compatible schema qail migrate apply --phase backfill # Move data in chunks qail migrate apply --phase contract --codebase ./src # Remove old shape safely qail types schema.qail > src/generated/schema.rs # Typed codegen ``` --- ## Why Qail > String SQL ### 🔐 Security Is Compiled In | 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** | ```rust // RLS: tenant-first constructors // Declare the complete registry once at application startup. qail_core::rls::init_scope_registries_from_tables(&[("bookings", "tenant_id")], &[])?; let ctx = RlsContext::tenant(tenant_id); // Single tenant (preferred) 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 can be automatically scoped let bookings = Qail::get("bookings").with_rls(&ctx)?; // ← no manual WHERE needed ``` ### 🔗 Compile-Time Relation Safety Invalid joins fail at compile time, not at 3am in production: ```rust 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 impl Qail::typed(users::table).join_related(products::table) ``` ### 🛡️ Protected Columns Compile-time data governance — sensitive columns require capability proof: ```rust 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 cap ``` --- ## Qail Gateway Auto-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 - ✅ Native access policy + column permissions - ✅ Query allow-listing + complexity limits - ✅ Prometheus metrics + request tracing - ✅ NDJSON streaming + cursor pagination - ✅ OpenAPI spec generation --- ## Architecture ``` 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 ├── qdrant/ Qdrant vector DB driver (optional) ├── workflow/ Workflow engine └── sdk/ Direct SDKs (TypeScript, Swift, Kotlin, Dart) ``` --- ## SDK Status | Platform | Status | Distribution | |----------|--------|--------------| | TypeScript | ✅ Supported | `npm install @qail/client` | | Swift | ✅ Supported | Source package in `sdk/swift` | | Kotlin | ✅ Supported | Gradle module in `sdk/kotlin` | | Dart | ✅ Supported | Pub package in `sdk/dart` | | Node.js native binding | ⏸ Deferred | Not shipped yet | `tenant_id` is the runtime contract across gateway and RLS paths. Legacy `agent_id`/operator-style identity is treated as a secondary scope only when a tenant scope is already present. --- ## N+1 Detection 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). ### Rules | 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 { ... } }` | ### Suppression ```rust // 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-001 ``` ### Build Integration (`build.rs`) Runs 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: ```bash # Scan multiple Rust roots during build validation + N+1 checks QAIL_SCAN_DIRS="src,apps/api/src,crates/billing/src" cargo build ``` ### CLI ```bash qail check schema.qail --src ./src # Shows N+1 warnings qail check schema.qail --src ./src --nplus1-deny # Fails on any N+1 ``` ### LSP N+1 diagnostics appear automatically in your editor for `.rs` files with diagnostic codes `N1-001`..`N1-004`. ### Remediation ```rust // ❌ 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?; ``` --- ## Feature Status (March 2026) | 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 | ### Connection Pool Maintenance Activate background pool health maintenance (idle connection cleanup + `min_connections` backfill) by calling `spawn_pool_maintenance` after creating the pool: ```rust let pool = qail_pg::PgPool::connect(config).await?; qail_pg::spawn_pool_maintenance(pool.clone()); ``` --- ## Documentation - 📖 [Full Documentation](https://qail.rs/docs) - 📊 [Benchmarks](https://qail.rs/benchmarks) - 📋 [Changelog](CHANGELOG.md) - 💡 [The Manifesto](https://qail.rs/philosophy) --- ## License Apache-2.0 © 2025-2026 Qail Contributors

Built with 🦀 Rust
qail.rs