Skip to content

AST Injection

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.

User sends:

QUERY 'company' FROM docs LIMIT 500

Policy rule:

- match:
claims:
role: reader
allow: [QUERY]
inject:
where:
field: tenant_id
from_claim: org_id
op: "="
limits:
max_limit: 50

Alice'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

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'

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 RRF

Gateway 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 RRF

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'

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.

FilePurpose
server/inject.goASTInjector — tenant filter injection, limit cap, collection scoping, CTE recursion
server/policy.goRule matching, EvaluatedPolicy
server/handler.goCalls ASTInjector after policy evaluation, before execution