Skip to main content

django-safeql: run untrusted or AI-generated SQL safely

PyPI CI Python Django License

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 QuerySet that 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:

  1. Parse. sqlglot parses the SQL text (Postgres dialect) into an internal AST.
  2. Annotate. Every column, join and function call is resolved against your SQLTranspilerSchema (including JSON Schema-typed JSON fields).
  3. Validate. Anything not explicitly whitelisted (unknown table, disallowed function, unsupported syntax, LIMIT above your ceiling, and so on) raises ValidationError or UnsupportedSQL.
  4. 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 a QuerySet.

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
  • LATERAL joins over jsonb_array_elements, EXISTS subqueries

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

CHANGELOG.md

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)

Source distribution for django-safeql 2.0.0
File Size Uploaded
django_safeql-2.0.0.tar.gz 121.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-safeql 2.0.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

2.0.0 This release

2 release files

1.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page