The query system¶
Design authority:
docs/jdlib/query-system.md— this guide is the developer-facing shape of that page, not a second authority for it. Where the two disagree, the authority page and the code win.
What it is¶
A small, closed query language: predicates and filters that compile to a dialect's SQL, with identifiers checked against an allow-list and the tenant supplied by the framework rather than the caller.
Why it exists¶
Hand-written SQL in an application is where tenant filters get forgotten and identifiers get interpolated. A compiler that takes a specification makes both impossible by construction.
When to use it¶
When an application needs to filter or sort tenant data by input it did not author -- a search box, a report, a saved view. Use it for the parts a caller can influence.
When not to use it¶
For a fixed query an application owns end to end (a by-id read, a known report): write the ORM statement. And never as a general-purpose SQL surface -- the language is deliberately not one (ADR-1).
How it works¶
Filter/Predicate build a small AST (query/ast.py); QueryCompiler walks it with a QueryDialect (PostgresDialect, MysqlDialect), emitting bound parameters for values and quoting identifiers that pass IdentifierPolicy. The tenant is not a field a caller can set: the compiled statement takes it from TENANT_PARAMETER, which the repository binds from the context.
Architecture¶
query/ast.py (the nodes), query/compiler.py (the walk), query/dialect.py (per-database SQL), query/safety.py (identifier policy, operator allow-list), query/specification.py (what a caller may ask for). jdlib.persistence.TenantRepository is what runs the result.
Example¶
examples/enterprise/app/repositories/resources.py builds its list query through the compiler, so the tenant predicate comes from the session's binding and the sort/filter a route accepts is a specification rather than a string.
Security¶
Two refusals are structural: an identifier that is not on the allow-list never reaches the SQL (IdentifierPolicy), and a specification that tries to name another tenant raises CrossTenantQueryError. Complexity is bounded (QueryComplexityError) so a caller cannot turn a filter into a scan.
Reliability¶
Compilation is pure and cheap; the failure modes are typed errors rather than exceptions from the driver, so a bad specification is a 4xx and not a 500.
Observability¶
The compiler's output carries the parameter count and the tables it touched, which is what a span records about a slow query without logging the SQL.
Audit¶
A refused specification is a security event (UnscopedRawSql-class refusals are audited), so an attempt to widen a query is visible in the trail.
Configuration¶
Nothing to configure: the dialect is chosen by the repository from the connection, and the allow-list comes from the models the application declares.
Testing¶
tests/unit/test_query_compiler.py covers the compiler and the dialects; the example's live layer exercises the compiled list query against PostgreSQL.
Common mistakes¶
Interpolating a column name from input; treating the compiler as a general SQL builder; forgetting that the tenant predicate is added by the repository, not the caller; letting an unbounded IN list through.
Production checklist¶
Keep the identifier allow-list tight; log refusals, not SQL; bound the complexity limits to what the database can serve; and check the compiled plan for the queries your busiest route runs.