django-safeql
A whitelisted SQL-to-QuerySet transpiler for Django.
django-safeql takes a restricted subset of SQL, validates it against a
schema you declare (which tables, columns, functions and operators are
allowed), and compiles it straight into a real Django QuerySet. No raw SQL
ever reaches the database. It's built for cases where a query comes from an
untrusted or semi-trusted source, such as an LLM answering questions over
your data, an end-user query box, or a saved-report feature, and still has
to run safely against your existing models on PostgreSQL, SQLite or MySQL.
- No SQL injection surface. The input is parsed into an AST and compiled to ORM calls. Every value becomes a bound parameter, and nothing is ever executed as a raw string.
- Whitelist by construction. Only the tables, columns, operators, functions and aggregates you declare are reachable. Anything else is rejected before it gets near the database.
- One SQL, many backends. The same query compiles to a
QuerySetthat runs on PostgreSQL, SQLite or MySQL. A feature a backend can't run is rejected up front instead of being silently mis-translated. - Real QuerySets out. The result is an ordinary Django
QuerySet, so it composes with everything else in your app (pagination, further.filter(),select_related, and so on).
Install
pip install django-safeql
Requires Django ≥ 4.2. PostgreSQL is the default target and supports the full feature set; SQLite runs the portable subset (see Database targets).
Quickstart
from django_safeql import SQLTranspilerSchema, TableSchema, JsonFieldSchema, SQLToQuerySetTranspiler
from myapp.models import Book, Author
schema = SQLTranspilerSchema(
base_table="book",
base_queryset=Book.objects.all(),
tables={
"book": TableSchema(
queryset=Book.objects.all(),
relation="",
allowed_fields={"id", "title", "status", "author_id", "pages", "details"},
json_fields={
"details": JsonFieldSchema(schema={
"type": "object",
"properties": {
"language": {"type": "string"},
"edition": {"type": "integer"},
},
}),
},
),
"author": TableSchema(
queryset=Author.objects.all(),
relation="author",
allowed_fields={"id", "name"},
),
},
max_limit=1000,
)
transpiler = SQLToQuerySetTranspiler(schema)
queryset = transpiler.to_queryset("""
SELECT book.title, author.name
FROM book
JOIN author ON book.author_id = author.id
WHERE book.status = 'published'
AND book.details->>'language' = 'en'
ORDER BY book.pages DESC
LIMIT 20
""")
list(queryset) # a normal QuerySet, evaluate it however you like
Anything outside the declared schema is rejected before touching the database:
transpiler.to_queryset("SELECT * FROM pg_catalog.pg_user")
# django_safeql.ValidationError: Unknown table: pg_user
transpiler.to_queryset("DELETE FROM book WHERE id = 1")
# django_safeql.UnsupportedSQL: ...
How it works
SQLToQuerySetTranspiler runs SQL through four stages:
- Parse.
sqlglotparses the SQL text (Postgres dialect) into an internal AST. - Annotate. Every column, join and function call is resolved against
your
SQLTranspilerSchema(including JSON Schema-typed JSON fields). - Validate. Anything not explicitly whitelisted (unknown table,
disallowed function, unsupported syntax, LIMIT above your ceiling, and so
on) raises
ValidationErrororUnsupportedSQL. - Codegen. The validated AST is compiled into Django ORM constructs
(
Q,F,Case/When,Subquery,Exists, aggregates, JSON key transforms, casts, date truncation) and returned as aQuerySet.
What's supported
SELECT/WHERE/JOIN/GROUP BY/HAVING/ORDER BY/LIMIT- Comparison, boolean and arithmetic operators
- Scalar aggregates (
COUNT,SUM,AVG,MIN,MAX) and collection aggregates (ARRAY_AGG,STRING_AGG,JSON_AGG,JSON_OBJECT_AGG) - String functions (
LOWER,UPPER,TRIM,SUBSTRING,CONCAT,LEFT,RIGHT,REPEAT,REVERSE,LPAD,RPAD), math functions (ABS,CEIL,FLOOR,ROUND,POWER,SQRT,SIGN,EXP,LN) and date functions (EXTRACT,DATE_TRUNC) - Read-only JSON/JSONB access and functions, validated against a JSON Schema you provide per field
LATERALjoins overjsonb_array_elements,EXISTSsubqueries
Anything not explicitly listed (DDL, writes, arbitrary functions, joins to undeclared tables) is rejected.
Database targets
The transpiler targets PostgreSQL by default. Pass target to compile for
another backend:
transpiler = SQLToQuerySetTranspiler(schema, target="sqlite")
The target is checked against the database your base_queryset runs on before
any query executes, so a mismatch fails fast instead of producing wrong SQL.
Supported targets are "postgresql" (default), "sqlite" and "mysql".
PostgreSQL supports every feature. On other targets, a query that uses a feature
the backend cannot run is rejected with a clear ValidationError rather than
failing at the database. The portable subset always behaves identically:
| Feature | PostgreSQL | SQLite | MySQL |
|---|---|---|---|
SELECT / WHERE / JOIN / GROUP BY / HAVING / ORDER BY / LIMIT |
✅ | ✅ | ✅ |
| Comparison, boolean and arithmetic operators | ✅ | ✅ | ✅ |
Scalar aggregates (COUNT, SUM, AVG, MIN, MAX) |
✅ | ✅ | ✅ |
| String and date functions | ✅ | ✅ | ✅ |
JSON key access -> / ->>, has_key / ?| / ?& |
✅ | ✅ | ✅ |
Casts, CASE, EXISTS, LIKE |
✅ | ✅ | ✅ |
ILIKE |
✅ | ✅ | ✅ |
JSON contains @> |
✅ | ❌ | ✅ |
STRING_AGG, JSON_AGG, JSON_OBJECT_AGG |
✅ | ✅ | ✅ |
ARRAY_AGG (SQLite/MySQL return a list via a JSON array) |
✅ | ✅ | ✅ |
ARRAY_AGG(DISTINCT expr) and ARRAY_AGG(expr ORDER BY col) |
✅ | ✅ | ❌ |
jsonb_array_length |
✅ | ✅ | ✅ |
Other jsonb_* functions and jsonpath |
✅ | ❌ | ❌ |
LATERAL over jsonb_array_elements |
✅ | ✅ | ❌ |
Changelog
License
MIT. See LICENSE.
Metadata
Release files for django-safeql 2.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| django_safeql-2.0.0.tar.gz | 121.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_safeql-2.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 163.2 kB
Release files / django_safeql-2.0.0.tar.gz
| Download URL | django_safeql-2.0.0.tar.gz |
|---|---|
| Size | 121.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1f40cc64560c0b279c7876be57e7fc13bd3d4f01d564b3a2cd7bff7c22f59068
|
|
BLAKE2b-256 checksum How to use checksums |
c8bc95574401863dbe5d048acfc5dff027d63c3f7189915217828bbd46cdebf8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|
Release files / django_safeql-2.0.0-py3-none-any.whl
| Download URL | django_safeql-2.0.0-py3-none-any.whl |
|---|---|
| Size | 42.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4adfb1e99d0d90ba2fedf440925317ae1256ce107d6294f3b70c90a9bed4a257
|
|
BLAKE2b-256 checksum How to use checksums |
c58434d8d35efed766b9fb3e6766c50a90f175fcb30b404327a1027e2ef2f1d5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|