AST-Level Query Rewriting
Tenant Isolation LIMIT Capping
This is the key differentiator between the QQL gateway and a generic HTTP reverse proxy.
Instead of filtering results after execution, the gateway parses the QQL into an AST and rewrites it before execution. The rewritten query is what reaches Qdrant — not the original.
How It Works
Section titled “How It Works”User sends:
QUERY 'company' FROM docs LIMIT 500Policy rule:
- match: claims: role: reader allow: [QUERY] inject: where: field: tenant_id from_claim: org_id op: "=" limits: max_limit: 50Alice's JWT claim: org_id = "acme-corp"
Gateway rewrites to:
QUERY 'company' FROM docs LIMIT 50 WHERE tenant_id = 'acme-corp'- The tenant filter is injected at the AST level — Alice never writes it, can't bypass it
- Qdrant only scores documents matching the filter — not a post-filter
- The LIMIT is capped at 50 by policy
Filter Merging
Section titled “Filter Merging”If the user already has a WHERE clause, the injected filter is AND'd with it:
User sends:
QUERY 'urgent' FROM docs LIMIT 20 WHERE priority = 'high'Gateway rewrites to:
QUERY 'urgent' FROM docs LIMIT 20 WHERE priority = 'high' AND tenant_id = 'acme-corp'CTE Recursion
Section titled “CTE Recursion”For multi-stage queries with CTEs, injection is recursive — the filter is applied to every CTE stage:
User sends:
WITH dense AS (QUERY 'search' USING dense LIMIT 200), sparse AS (QUERY 'search' USING sparse LIMIT 300)QUERY 'search' FROM docs LIMIT 20 PREFETCH (dense, sparse) FUSION RRFGateway rewrites to:
WITH dense AS (QUERY 'search' USING dense LIMIT 200 WHERE tenant_id = 'acme-corp'), sparse AS (QUERY 'search' USING sparse LIMIT 300 WHERE tenant_id = 'acme-corp')QUERY 'search' FROM docs LIMIT 20 WHERE tenant_id = 'acme-corp' PREFETCH (dense, sparse) FUSION RRFMulti-Filter Injection
Section titled “Multi-Filter Injection”When policy uses inject.filters, all filters are AND'd together and injected:
inject: filters: - field: org_id from_claim: org_id op: "=" - field: access value: "confidential" op: "!="Injection result:
WHERE ... AND org_id = 'acme-corp' AND access != 'confidential'Collection Scoping
Section titled “Collection Scoping”If the user tries to access a collection not in the policy allowlist:
collections: ["tenant_*"]A query against shared_docs (not matching tenant_*) is rejected with permission denied before reaching Qdrant.
Injector File Map
Section titled “Injector File Map”| File | Purpose |
|---|---|
server/inject.go | ASTInjector — tenant filter injection, limit cap, collection scoping, CTE recursion |
server/policy.go | Rule matching, EvaluatedPolicy |
server/handler.go | Calls ASTInjector after policy evaluation, before execution |