Home Expressions
Docs
Drivers Gateway SDKs Benchmarks
Changelog
GitHub
Blog Status Roadmap

June 4, 2026 12 min read

QAIL.rs v1.3.0: Native Vertical Policy and the Audit Pass Behind It

A detailed release note for QAIL.rs v1.3.0: native vertical access policy for table, operation, and column boundaries, plus the audit fixes around PostgreSQL caching, migrations, gateway numerics, workflow safety, Qdrant encoding, and SDK paths.

RustReleasePostgreSQLSecurityQAIL

QAIL.rs v1.3.0 deserves a longer explanation than a compact changelog row. The main feature is native vertical access policy: QAIL can now check table, operation, and column permissions before a QAIL AST reaches PostgreSQL. The rest of the release is the audit pass that hardened the execution paths around that new policy surface.

The Short Version

Why RLS Was Not Enough

RLS is very good at answering one question: which rows can this database role see or mutate? It is horizontal isolation. It does not naturally model every application-level vertical rule, such as whether an operator may read orders.private_note, whether a support role may update orders.status but not orders.total, or whether a generated API route should reject a RETURNING clause that exposes a protected column.

Before v1.3.0, QAIL had gateway policy machinery and PostgreSQL RLS helpers, but there was no core-native policy model that every AST entry point could share. v1.3.0 moves that vertical check into qail_core::access so it is not only a gateway YAML concern. The policy can be evaluated before the AST is encoded or sent to the driver.

The New Policy Shape

The policy is deny-by-default unless configured otherwise. A table policy can define allowed operations, denied operations, read column rules, write column rules, returning column rules, role gates, and scope gates. A wildcard table policy can act as fallback, and super-admin bypass is only available through the core SuperAdminToken path.

                            [access]
enabled = true
path = "access-policy.toml"
                        
                            default_decision = "deny"

[tables.orders]
operations = ["read", "update"]
read_columns = { only = ["id", "status", "total", "created_at"] }
write_columns = { only = ["status"] }
returning_columns = { only = ["id", "status", "updated_at"] }
require_any_role = ["operator", "administrator"]
require_scopes = ["orders:read"]
                        

The important part is where this runs. It is not a cosmetic response filter. If a request projects a denied column, filters on a denied column, writes a denied payload column, returns a denied column, or hides a denied read inside a subquery, the request is rejected before normal database execution.

What Gateway Now Enforces

A Realistic Before and After

The old operational answer for vertical permissions was usually a mix of route discipline, carefully chosen SELECT lists, RLS for row ownership, and gateway policy behavior. That works until a second endpoint, transaction path, binary AST endpoint, nested expansion, or RETURNING clause forgets the same rule.

                            // Operator can read public order fields.
Qail::get("orders")
    .columns(["id", "status", "total"])
    .eq("tenant_id", tenant_id);

// Same subject should not read this.
Qail::get("orders")
    .columns(["id", "private_note"]);
                        

With native access policy enabled, the second query fails because private_note is outside the read column rule. The same policy is applied if private_note appears in a filter, window partition, payload expression, RETURNING projection, transaction query, or nested gateway route.

Prepared Statements Now Track Backend Reality

The PostgreSQL driver changes in this release are about not pretending the client cache knows more than the backend. Hot statements are promoted only after parse success. Stale hot entries are evicted on preprepare failure. Cached statements survive execute errors when the statement identity is still valid. Stale AST handles are reparsed, stale single-statement state is cleared, prepared identity is aligned, and NOTIFY flushes drain before returning.

The rule is simple: if the backend statement lifecycle did not reach a valid state, the QAIL cache should not remember it as valid.

Migration Verification Got Stricter

MERGE, Source Queries, and Existing-Row Reads

The vertical policy work made one subtle class of bugs more visible: write statements can read too. MERGE sources, INSERT ... ON CONFLICT DO UPDATE expressions, update payload expressions, and source queries may depend on existing row data. v1.3.0 tightens those paths so value expressions that read existing columns require read access to those columns.

Gateway Numeric and Replay Safety

The gateway hardening in this release focuses on precision and replay correctness. Oversized REST integers are preserved, precise numeric responses stay precise, Qdrant JSON integer drift is rejected, branch replay integers compare exactly, and tenant guard exemptions on REST writes stay explicit instead of accidental.

Workflow, Encoder, Qdrant, and SDK Fixes

Compatibility and Upgrade Notes

Validation

This release was checked with the Rust workspace test suite, Clippy, TypeScript/Kotlin/Swift SDK tests, and live PostgreSQL lab coverage for strict migrations, MERGE, access-checked execution, seeded RLS, and gateway native access policy behavior. The gateway policy live test exercises allowed projections, denied private columns, denied filters, denied write payloads, transaction denial, and administrator bypass.

Bottom Line

QAIL started as an AST-native way to express database intent without handing strings around. v1.3.0 makes that intent more useful for production authorization: the AST can now be checked against a native vertical policy before execution, while PostgreSQL RLS continues to own row isolation. That combination is the release story.

← Back to Blog