The Struggle: "Polyglot Hell"
We are polyglot developers. On Monday, we write Rust. On Tuesday, Go. On Wednesday, we maintain a legacy PHP system.
Every time we switch languages, we have to switch our paradigms:
- In Rust, we choose between Diesel's strict compile-time types or SQLx's raw flexibility.
-
In Go, we balance GORM's convenience
with the performance of raw
database/sql. - In Node.js, we rely on Prisma for safety but pay the cost of heavy binary sidecars.
- In Python, we leverage Django's speed of development but hit performance walls at scale.
- In PHP, we love Eloquent's expressiveness but sometimes need lighter, faster query options.
The problem isn't the data. The problem is the string layer. Every ORM generates SQL strings. Every driver parses them again. Why not skip strings entirely?
The Origin: "I Don't Want You to Suffer"
"I originally built QAIL for internal use to solve my own polyglot headaches. But I realized that keeping it private meant letting other engineers suffer through the same 'Database Dilemma'. I decided to open-source it so we can all skip app-side SQL string plumbing."
The Realization: "SQL is Assembly"
We realized something profound: SQL is not source code. SQL is Assembly.
SQL is verbose, powerful, and dangerous. It is the low-level instruction set of the database engine.
- Raw SQL offers ultimate control, but maintaining raw strings across a large codebase requires immense discipline.
- ORMs provide fantastic productivity, but the abstraction sometimes comes at the cost of fine-grained performance control.
We didn't need a compromise. We needed a Universal AST.
The Savior: QAIL
QAIL is the "Universal AST for Databases". It is not a text syntax that compiles to SQLβit is a native data structure that encodes directly to database wire protocols.
It separates the Code (your query AST) from the Data (your parameters). It creates a unified structure that works the same way in Rust, Go, Node, and PHP.
Qail::get("users")
.columns(["id", "email"])
.filter("active", Eq, true)
.limit(10) SELECT id, email
FROM users
WHERE active = true
LIMIT 10 Now, your team builds with one AST. The Rust dev, the Go dev, and the Frontend dev all use the same data structure.
"Safety shouldn't require a prison of boilerplate. We moved validation from the 'String' layer to the 'AST' layer. Code is data. Data is typed. Injection risk is reduced on the AST path."
By preventing SQL injection at the AST level, QAIL ensures your queries are safe before they even reach the database. Unlike ORMs that generate strings at runtime, QAIL encodes directly to wire protocol bytesβwithout app-side SQL string interpolation on the AST path.
Important: this does not mean PostgreSQL stops parsing. PostgreSQL still runs its normal parse/plan/execute stages on the server.
The Vision: Facts, Meaning, and Time
"Postgres stores facts, Qdrant stores meaning β QAIL decides."
Modern applications don't run on a single database. They need specialized engines for specialized workloads:
| Engine | Role | Examples |
|---|---|---|
| PostgreSQL | Source of truth. ACID transactions. Relational data. | Users, orders, inventory |
| Qdrant | Semantic understanding. Vector embeddings. AI search. | Recommendations, similarity, RAG |
| Native Cache | In-process moka cache with TTL and size bounds. Zero-network result caching. | Query results, prepared stmts, EXPLAIN plans |
QAIL is the decision layer that unifies these engines. One AST. Two powerhouses. Cache built-in.
The Architecture: Native Power
QAIL is written in Pure Rust. It exposes:
- A C-API for Go, Python, and Java (via FFI).
- A WASM Module for Node.js and Browsers.
- A Native Crate for Rust users.
It runs in-process. It has zero network overhead. It turns your query generation into a simple CPU function call.
The AST Builder API
We designed the builder API for Type Safety and Clarity. Every method is purposeful. No hand-built SQL-string plumbing.
| Method | Purpose | Example |
|---|---|---|
Qail::get() | SELECT query | Qail::get("users") |
.columns() | Select specific columns | .columns(["id", "email"]) |
.filter() | WHERE condition | .filter("active", Eq, true) |
.order_by() | Sort results | .order_by("created_at", Desc) |
.limit() | Pagination | .limit(10) |
.left_join() | Join tables | .left_join("profiles", "users.id",
"profiles.user_id") |
π‘οΈ Why No Raw Strings?
QAIL intentionally does not provide a "Raw SQL" escape hatch. This is not a missing featureβit's a deliberate design choice.
The Promise
"If it compiles in QAIL, it is a valid, safe protocol packet."
Soundness by Construction
By forcing queries through the AST path, QAIL enforces strong invariants:
- SQL Injection Risk Removed on AST Path β Code and data are never mixed. Parameters are always separate from query structure.
- Syntax Errors at Construction Time β Invalid query structures fail when building the AST, before ever touching the database.
- Global Safety Invariant β Every query that executes was validated at the type level (compiled languages) or construction time (dynamic languages).
The Rust vs C++ Analogy
| Approach | Philosophy | Result |
|---|---|---|
| C++ (escape hatches) | "Here's safe code, but use pointers if you need" | People use pointers β Bugs happen |
| Rust (no escape) | "If it's not safe, you can't write it" | People adapt β Memory safety strongly enforced |
| ORM with Raw | "Use the builder, or escape to strings" | People use Raw β Injection possible |
| QAIL (pure AST) | "If it's not in the AST, you can't query it" | People adapt β Injection risk reduced by design |
For Those Who Need Raw SQL
If you need raw SQL strings, use sqlx or another
driver. QAIL is for those who want
strong invariants, not string-level flexibility.
"QAIL: Sound by Construction."
π« Why QAIL is NOT an ORM
QAIL is not an Object-Relational Mapper. It does not provide
lazy loading or magic .include() methods. This is
not a limitationβit's a deliberate architectural choice backed by hard data.
What QAIL Is
| ORMs (Prisma, Diesel, SeaORM) | QAIL |
|---|---|
| Object-Relational Mapper | Query Language & Wire Protocol |
| Hide SQL behind objects | Make queries portable & safe |
| Lazy loading hides N+1 | .join() guides you to 1 query |
| "Don't think about SQL" | "Think in queries, not strings" |
Why No Lazy Loading? The Evidence.
We benchmarked six patterns for the same endpoint objective and canonical payload against a real PostgreSQL dataset (`odyssey_connections` + `harbors`, 2x LEFT JOIN). This is a pattern benchmark: single-query JOIN vs batched lookups vs N+1 fan-out.
| Profile | Approach | Median | p95 | DB Queries |
|---|---|---|---|---|
RTT=0us | Qail AST (uncached) | 146.9us | 163.7us | 1 |
RTT=0us | GraphQL + DataLoader | 146.8us | 168.0us | 2 |
RTT=0us | GraphQL naive (N+1) | 4.74ms | 4.88ms | 101 |
RTT=1000us | Qail AST (uncached) | 1237.8us | 1252.5us | 1 |
RTT=1000us | GraphQL + DataLoader | 2287.0us | 2507.0us | 2 |
RTT=1000us | GraphQL naive (N+1) | 111.56ms | 112.19ms | 101 |
Loopback can make single-query and DataLoader appear equivalent. As RTT grows, extra round trips dominate, and one-query plans pull ahead.
"Method first: same canonical payload, equivalence gate enabled, randomized run order, and explicit RTT profile (`BATTLE_SIMULATED_RTT_US`) in the benchmark harness."
Full benchmark methodology & source code β
Typed Schema Codegen
QAIL does provide optional compile-time type safety via
TypedQail<T> and schema codegen:
- TypedQail β Table-typed queries. Invalid
joins fail at compile time via
RelatedTo<T>. - Schema codegen β
qail types schema.qailgenerates typed column accessors. - Protected columns β Capability witnesses guard sensitive fields at compile time.
But unlike ORMs, these are opt-in safety layers on top of the core AST β not mandatory boilerplate.
user.posts().comments().author()
// 3+ hidden queries? Qail::get("posts")
.left_join("comments", "post_id", "id")
.left_join("users", "author_id", "id") What QAIL Focuses On
- β Best-in-class query syntax β 3-5x shorter than SQL
- β Direct wire protocol encoding β No app-side SQL interpolation on AST path
- β Migration tooling with impact analysis β Unique to QAIL
- β Multi-database AST portability β Write once, run anywhere
"QAIL is the query transport layer, not an ORM. If you need magic lazy loading, use Prisma. If you want the high-performance, safety-focused way to talk to your database β with proof β use QAIL."
π QAIL Gateway β Production Today
QAIL Gateway is an auto-REST API server that turns your PostgreSQL database into a full-featured API β with zero backend code. Point it at a database, get instant CRUD, real-time subscriptions, and enterprise security.
"Full-featured data API β without GraphQL complexity. Binary AST protocol with safer defaults than hand-built query strings."
What You Get
| Feature | Status |
|---|---|
| Auto-REST CRUD for all tables | β |
FK-based JOIN expansion (?expand=) | β |
Full-text search (?search=) | β |
| WebSocket subscriptions + live queries | β |
| Event triggers (mutation β webhook) | β |
| JWT auth + webhook auth + RLS | β |
| YAML policy engine + column permissions | β |
| Prometheus metrics + request tracing | β |
| OpenAPI spec + schema introspection | β |
Security Is Compiled In
| Threat | GraphQL / REST | QAIL |
|---|---|---|
| SQL injection | Possible (one mistake) | Prevented on AST path (no app-side SQL interpolation) |
| Tenant data leak | Missing WHERE clause | RLS auto-injected |
| N+1 catastrophe | Default behavior | Detected and discouraged (AST + lint) |
Read the Gateway documentation β
QAIL Gateway powers Sailtix β a multi-operator maritime booking platform with full RLS tenant isolation across 27+ tables.