English · Русский · Español · 中文
Your entire model graph — live in your editor sidebar, gating your CI, and answering your AI agent over MCP. All from static parsing: no database, no runserver, no working venv.
Replaces: graph_models + django-schema-graph + hand-drawn ER diagrams + grep archaeology.
Featured in Django News #347 · PyCoder's Weekly #746
uvx django-orm-lens scan # or: pipx run django-orm-lens scanCold clone, broken venv, no settings module — you still get every app, model, field, and relation of the project in your terminal.
Then pick your surface — three distributions, one parser core:
| You are | Install | You get |
|---|---|---|
| Editor user — VS Code / Cursor / Windsurf / VSCodium | code --install-extension frowningdev.django-orm-lens |
Sidebar tree, live ER diagram, hover cards, 16 QuickFix rules |
| Terminal / CI user | pip install django-orm-lens |
17 subcommands, SARIF + PR annotations, pre-commit hooks, a GitHub Action |
| AI-agent user — Cursor / Claude Code / Aider / Zed / Continue | pip install "django-orm-lens[mcp]" |
13 read-only MCP tools answering schema questions from ground truth |
MCP setup is one JSON block — see Integrations. Point DJANGO_ORM_LENS_ROOT at your Django project's absolute path.
Schema review is a paid category nearly everywhere. A bot that reviews every pull request, analysis that follows a queryset past the function it was built in, a check that catches schema drift, index advice grounded in real table statistics — those normally sit behind a per-seat or per-database subscription.
All of it is here, MIT-licensed, with no tier gate, no seat count, no account, and no telemetry:
| Capability usually sold as a paid tier | Here |
|---|---|
| PR review bot for schema changes — posts once, then updates in place | blast-radius + the Action |
| Analysis that follows a queryset across functions | nplusone |
| Schema drift detection | drift |
| Index proposals from observed QuerySet usage | suggest-indexes |
| Migration risk weighed against real table sizes | blast-radius --stats |
| Blast radius of a destructive migration | blast-radius |
| Cross-layer impact of removing a field | impact |
There is no Pro tier, and none is planned. If the tool saves you an afternoon, a star is the entire ask.
📈 Star growthIf the tool saves you a
grepnext time you touch a strange Django project — a star helps others find it.
VS Code / Cursor / Windsurf (VS Code Marketplace):
code --install-extension frowningdev.django-orm-lens
VSCodium / code-server / Gitpod / any OSS Code fork (Open VSX):
codium --install-extension frowningdev.django-orm-lens
Or search Django ORM Lens in the Extensions view — same publisher frowningdev on both registries.
Terminal & AI coding agents:
pip install django-orm-lens # CLI only pip install "django-orm-lens[mcp]" # + MCP server for AI agents
Requires Python 3.9+. Zero runtime dependencies for the CLI.
Docker (v0.6+):
docker run --rm -v "$PWD:/workspace" ghcr.io/frowningdev/django-orm-lens scan --path .
Multi-arch (amd64 + arm64). No Python required on the host. Good for CI and one-off audits.
🎯 The problemWorks offline. Works on a broken venv. Works on someone else's laptop. Works in CI.
You open a Django project. It has 20 apps. You need to answer a simple question:
"Which app owns the
Ordermodel, and how is it connected toUser?"
Today, that means: Ctrl+P, "models", scroll through 30 hits, open five files, Ctrl+F for class Order, read through 400 lines of ForeignKey('otherapp.Something') strings, try to remember what you learned two files ago.
Half a day gone. Every time. On every project.
✨ With Django ORM LensLive sample — real django-orm-lens er output, rendered by GitHub right here:
erDiagram
User {
CharField display_name
}
Tag {
CharField name
}
Post {
CharField title
DateTimeField created_at
}
Comment {
TextField body
}
Post }o--|| User : "author [CASCADE, as posts]"
Post }o--o{ Tag : "tags [as posts]"
Comment }o--|| Post : "post [CASCADE, as comments]"
Comment }o--|| User : "author [SET_NULL]"
Also included in the extension:
CASCADE, through Model, as related_name), theme-aware, one-click SVG exportForeignKey('app.Model') or ManyToManyField(...), with a one-click jump linkclass Model line: field count, relation count, and an Open ER diagram actionauto / default / dark / forest / neutral for the diagram webviewThe same parser that powers the VS Code extension ships as a standalone Python package — with an optional MCP (Model Context Protocol) server so any MCP-compatible AI agent can navigate your Django schema without importing Django or booting your app.
CLIdjango-orm-lens scan -f json # every app, every model, every field django-orm-lens describe blog.Post # one model in Markdown django-orm-lens list | fzf # flat app.Model — pipes anywhere django-orm-lens er > schema.mmd # ER diagram — Mermaid (default) django-orm-lens er -f dbml > schema.dbml # …or DBML: paste into dbdiagram.io django-orm-lens er -f d2 > schema.d2 # …or D2 / plantuml / dot django-orm-lens diff before.json after.json # what a PR changes structurally django-orm-lens nplusone --format github # N+1 findings as PR annotations django-orm-lens migration-risk -f sarif # SARIF for GitHub Code Scanning django-orm-lens suggest-indexes blog.Post # Meta.indexes proposals from usage django-orm-lens signals # sender→signal→handler graph django-orm-lens migration-deps blog -f mermaid # per-app migration DAG django-orm-lens cascade blog.Author # what one delete() takes down django-orm-lens impact author # what still references a field django-orm-lens blast-radius -f markdown # risks + who still reads them django-orm-lens drift # migrations vs models, no boot django-orm-lens stats-sql # read-only SQL for --stats
impact,blast-radius,driftandstats-sqlship in py-1.7.0 and later.
Every command accepts --path <dir> and --exclude <glob>. nplusone / migration-risk / diff exit code 1 on findings — drop them into CI to block PRs on regressions.
Register it once with your agent and it exposes ten read-only tools:
| Tool | Purpose |
|---|---|
list_apps |
Every Django app in the workspace with model counts |
list_models |
Flat app.Model list, optional app filter |
describe_model |
Full field / relation / Meta detail for one model |
find_relations |
Inbound + outbound relations for one model |
cascade_preview |
Blast radius of one delete(), grouped by on_delete |
er_diagram |
ER diagram — mermaid / dbml / d2 / plantuml / dot |
describe_migration_dependency |
Per-app migration DAG: roots, leaves, cross-app deps |
suggest_indexes |
Meta.indexes proposals from observed QuerySet usage |
signal_graph |
Sender→signal→handler graph from @receiver decorators |
nplusone_scan |
Static N+1 findings for the whole workspace |
# Start it directly django-orm-lens-mcp # Or via the CLI subcommand django-orm-lens mcp
Workspace resolution (py-1.3.0+). Every tool accepts an optional
workspace_root argument on the call. Resolution priority: explicit arg →
$DJANGO_ORM_LENS_ROOT → current working directory. Invalid or non-Django
paths return a structured envelope
({"error": "WORKSPACE_NOT_DJANGO", "hint": "…"}) instead of empty results,
so the agent can self-correct. Optional sandbox via
DJANGO_ORM_LENS_ALLOWED_ROOTS (;-separated on Windows, : elsewhere).
Schema regressions are cheapest to catch the moment they enter a PR. Four zero-config ways to block them:
Blast-radius PR bot — the whole schema review as one comment, updated in place on every push instead of a new comment each time:
name: Schema review on: pull_request permissions: contents: read pull-requests: write # only for `comment: true` jobs: blast-radius: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: FROWNINGdev/django-orm-lens@action-v1 with: command: blast-radius only-changed: true # scope to migrations this PR touches comment: true # post once, then update in place github-token: ${{ github.token }}
The comment goes up before the job fails, so a blocked PR still explains why. only-changed reads the PR's file list from the API rather than git diff, because actions/checkout defaults to fetch-depth: 1 and the base commit is not in the local history. On push events both flags skip with a notice instead of failing, so one workflow covers both triggers.
The Action installs from PyPI, so
blast-radiusanddriftneed py-1.7.0 or later — pin it withversion: 1.7.0if your workflow must not drift. To run an unreleased build instead, addinstall: falseand install the source yourself; this repo's own workflow does exactly that, and is what verifies the Action on every PR.
pre-commit — two hooks, nothing to install locally:
# .pre-commit-config.yaml repos: - repo: https://github.com/FROWNINGdev/django-orm-lens rev: py-v1.8.1 hooks: - id: django-orm-lens-nplusone - id: django-orm-lens-migration-risk
GitHub Action — findings appear as PR annotations with zero extra permissions:
- uses: FROWNINGdev/django-orm-lens@action-v1 with: command: migration-risk # or: nplusone format: github # ::error / ::warning annotations on the diff
SARIF → Code Scanning — findings land in the repo Security tab:
- run: | pip install django-orm-lens django-orm-lens migration-risk --format sarif --exit-zero > lens.sarif - uses: github/codeql-action/upload-sarif@v3 with: sarif_file: lens.sarif
Exit codes are CI-native: diff and nplusone exit 1 on findings, migration-risk and blast-radius exit 1 on critical findings, drift exits 1 when a field is declared but never migrated. Add --exit-zero for report-only mode.
| Client | How to enable | Status |
|---|---|---|
| VS Code | code --install-extension frowningdev.django-orm-lens |
✅ |
| Cursor | same VSIX + optional MCP entry in ~/.cursor/mcp.json |
✅ |
| Windsurf / VSCodium / any Code fork | install the VSIX from the Marketplace or GitHub Releases | ✅ |
| Aider | add django-orm-lens-mcp to your mcp.json |
✅ (via MCP) |
| Continue.dev | register the MCP server in ~/.continue/config.json |
✅ (via MCP) |
| Zed | register the MCP server in Zed settings | ✅ (via MCP) |
| Any MCP-compatible client | point command at django-orm-lens-mcp, set DJANGO_ORM_LENS_ROOT |
✅ |
| pre-commit | repo: https://github.com/FROWNINGdev/django-orm-lens + two hook ids |
✅ |
| GitHub Actions | uses: FROWNINGdev/django-orm-lens@action-v1 — annotations or SARIF |
✅ |
| Discoverable via MCP Registry | official Model Context Protocol server directory | ✅ |
| Plain terminal / CI | pip install django-orm-lens && django-orm-lens scan |
✅ |
The regression suite parses the vendored model graphs of Zulip, Saleor, Wagtail, django CMS, and Mezzanine — 59 models across 13,478 lines of real-world models.py — in about 20 ms end-to-end on a laptop (21 ms best-of-3 on the repo's golden-fixture corpus; a <2 s guard runs in CI on every matrix cell).
Reproduce it yourself:
git clone https://github.com/FROWNINGdev/django-orm-lens && cd django-orm-lens/cli pip install -e . && python -m pytest tests/test_golden_fixtures.py tests/test_golden_snapshots.py -q
models.py sprawl.related_name?") without importing the project.runserver, no manage.py migrate, still works.Django ORM Lens sits at the intersection of editor tooling and AI-agent tooling — a slot no existing package covers:
| Segment | Existing option | What it costs you |
|---|---|---|
| Boot-and-graph | django-extensions graph_models |
Requires Graphviz + Django settings + a working DB URL |
| Web-based viewer | django-schema-graph |
Requires a running Django server; hosts one more thing to break |
| Admin panel | Django Admin | Requires runserver + auth + database — great for data, not for architecture |
| Editor plugin | PyCharm's Django Structure | Locked to PyCharm; no CLI, no AI-agent story |
| MCP server | (none until now) | AI agents guess your schema from source, imperfectly |
Django ORM Lens is the only tool that ships three surfaces from one parser: a VS Code extension (any Code fork), a zero-dep CLI (terminals + CI), and an MCP server (AI agents). All static. All free. All MIT.
🤔 How is this different?| Django ORM Lens | django-extensions graph_models |
django-schema-graph |
Django Admin | PyCharm Django Structure | |
|---|---|---|---|---|---|
| Works without a bootable Django project | ✅ | ❌ | ❌ | ❌ | ⚠️ |
| Zero-install (no graphviz, no server) | ✅ | ❌ | ❌ | ❌ | ❌ (needs PyCharm) |
| Works in VS Code / Cursor / any Code fork | ✅ | ❌ | ❌ | ❌ | ❌ |
| Sidebar tree inside the editor | ✅ | ❌ | ❌ | ❌ | ✅ |
| Live ER diagram | ✅ | ✅ | ✅ | ❌ | ❌ |
Hover cards on ForeignKey |
✅ | ❌ | ❌ | ❌ | ⚠️ |
| CodeLens on model classes | ✅ | ❌ | ❌ | ❌ | ❌ |
Split models/ package support |
✅ | ⚠️ | ⚠️ | ✅ | ✅ |
| CLI for terminal / CI | ✅ | ⚠️ | ❌ | ❌ | ❌ |
| MCP server for AI agents | ✅ | ❌ | ❌ | ❌ | ❌ |
| Discoverable in the MCP Registry | ✅ | ❌ | ❌ | ❌ | ❌ |
| Free & open-source (MIT) | ✅ | ✅ | ✅ | ✅ | ❌ (paid IDE) |
| Django version support | 4.0 – 5.2 | latest | 3.2 – 4.1 (stale since 2023) | latest | latest |
When you want something else
django-schema-graphhas not been updated since 2023-05 and does not test Django 5.x.
Honest boundaries: profiling a live request → django-debug-toolbar. Historical request profiling → django-silk. Query-count assertions inside a test suite → django-perf-rec. Production APM on real traffic → Scout / Sentry. Django ORM Lens deliberately stays static — it's the layer that works before the app can even boot, and the only one your CI and your AI agent can use on any checkout.
⚙️ ConfigurationThe defaults are opinionated and sensible. If you need to tweak:
| Setting | Type | Default | What it does |
|---|---|---|---|
djangoOrmLens.excludeGlobs |
string[] |
See above | Glob patterns to skip when scanning |
djangoOrmLens.autoRefresh |
boolean |
true |
Rescan on models.py changes |
djangoOrmLens.codeFixes.enabled |
boolean |
true |
Master switch for the DOL### diagnostics + QuickFixes |
djangoOrmLens.rules |
object |
{} |
Per-rule severity: { "DOL007": "off", "DOL013": "error" } |
djangoOrmLens.rulesSelect |
string[] |
[] |
Ruff-style select. ["DOL0"] runs only queryset+model rules |
djangoOrmLens.rulesIgnore |
string[] |
[] |
Ruff-style ignore. ["DOL03"] silences form/view rules |
Sixteen editor-side checks (DOL001–DOL032) with Ruff-style codes, per-rule severity, and Clippy-style applicability — plus fifteen CLI-side migration-risk rules and the static N+1 analyzer. Every rule now has its own documentation page.
| Category | Rules | Examples |
|---|---|---|
| Queryset | DOL001–DOL007 |
.count() > 0 → .exists(), FK access in loops (N+1) |
| Model definition | DOL011–DOL015 |
ForeignKey without on_delete, null=True on string fields |
| Datetime | DOL021–DOL022 |
datetime.now() → timezone.now() |
| Forms / views | DOL031–DOL032 |
locals() in render(), Meta.fields = '__all__' |
| Migration risks | 16 rules | NOT NULL add without default, table-locking index builds, irreversible data migrations |
| Static N+1 | 1 analyzer | FK/M2M access in loops without select_related / prefetch_related |
→ Full rule reference — every code with bad/good examples, QuickFix behaviour, and suppression syntax.
Suppress inline# django-orm-lens-disable-next-line DOL007 for user in User.objects.all(): print(user.profile) # not flagged qs.count() > 0 # django-orm-lens-disable-line DOL001 # django-orm-lens-disable DOL011 ← on its own line, kills DOL011 for the rest of the file
Applicability follows Rust's Clippy: safe fixes can be applied automatically ("Fix All"), suggestion fixes are offered as a QuickFix but reviewed, unsafe findings never auto-apply. Fixes are separated from analyzers (Roslyn-style), so one rule can grow multiple fixers over time without touching detection logic.
🧭 CommandsOpen the command palette (Ctrl+Shift+P / Cmd+Shift+P) and type "Django ORM Lens":
| Command | What it does |
|---|---|
Django ORM Lens: Refresh |
Force-rescan the workspace |
Django ORM Lens: Show ER Diagram |
Open the Mermaid ER diagram side-by-side |
Django ORM Lens: Filter Models |
Filter the tree by app / model / field name |
Django ORM Lens: Clear Filter |
Restore the full tree |
Django ORM Lens: Jump to Model |
Programmatic — triggered by tree clicks and hover cards |
Django ORM Lens: Find Reverse References |
Right-click a model — QuickPick of every FK pointing at it |
Django ORM Lens: Generate factory_boy Factory |
Right-click a model or use CodeLens — scaffold a DjangoModelFactory |
Django ORM Lens: Schema Diff (Time-Travel) |
Pick two commits — get a typed diff as a markdown buffer |
Django ORM Lens: Find Impact (What Uses This?) |
Right-click a field or model — workspace-wide reference scan |
Django ORM Lens: Build Query (Insert Snippet) |
Right-click a field or model — pick an ORM template |
Shipped
ForeignKey('app.Model')models/ package supportN fields · N relations · Open ER diagram)CASCADE, SET_NULL, PROTECT, related_name)auto / default / dark / forest / neutral)through_model on M2M edges (contributed by @kingrubic)nplusone — static N+1 detector (FK/M2M access inside loops without select_related/prefetch_related)migration-risk — flags risky operations in migrations/*.py (15 rules today)diff — compare two schema JSON dumps for PR reviewdocker run ghcr.io/frowningdev/django-orm-lenssettings.AUTH_USER_MODEL resolves everywhere: n+1 reverse-relations, signal senders, Mermaid ER, VS Code webview, inbound-relation panel, React ERForeignKey(on_delete=CASCADE, to='User') resolves regardless of kwarg order (Python + TS parity)find_user_model, resolve_related_tail, find_model, iter_workspace_py_files (Python) + findUserModel, resolveRelatedTail (TS)--verbose no longer walks the tree twice; WorkspaceIndex.scanned_files carries the countjti: CharField[str] = models.CharField(...)) now parse — reported by @jsabater (#25) with a clean Django Ninja 1.6 reproclass Container[T](models.Model): now parsesfrom django.db import models as m) and third-party field packages (jsonfield.JSONField) now detectedDOL001..DOL032) with per-rule severity + Ruff-style select/ignore + inline # django-orm-lens-disable-next-linefactory_boy scaffold from any model with Faker providers keyed by field type.select_related, related_name honoured)TreeItem.id, MarkdownString tooltips with command: deep-links, FileDecorationProvider badges, TreeView.badge on the activity bar, three when-gated viewsWelcome statesv1.5.0 — the "one core, three surfaces" wave
--format github PR annotations for nplusone and migration-risksuggest-indexes, signals, migration-deps, cascadeer --format dbml | d2 | plantuml | dot — community-standard diagram exports (dbdiagram.io, D2, PlantUML, Graphviz — dot contributed by @JJordan0C)runpython_no_reverse, alter_unique_together_lock, alter_index_together_deprecated — 15 totaldjango-orm-lens-nplusone, django-orm-lens-migration-risk) + composite GitHub Actiondocs/rules/ — a documentation page for every rule (19 pages)migration-deps (text / json / mermaid)py-1.7 → 1.8 — the schema-intelligence wave
blast-radius — migration risks joined with what still reads the schema they touch, as a PR bot (comment: true, sticky, only-changed)drift — makemigrations --check without booting Djangoimpact <name> — what still references a model or field, grouped by Django layerblast-radius --stats + stats-sql — optional production row counts from read-only SQL you run yourself (the tool never holds a DB credential)nplusone resolves across functions — a queryset returned by a helper is followed into the loop that consumes itblast_radius, drift and impact exposed as MCP tools — thirteen tools for AI agentsdrift documents its !! / ~ marks in the report and in --help — reported by @sevdog (#57)drift follows inheritance from abstract bases — an abstract base's fields count as the concrete child's own, as Django treats them — reported by @sevdog (#58)suggest-index recognises the indexes Django already made — primary key (pk and id are one lookup), db_index, unique, foreign keys, unique_together, UniqueConstraint — reported by @sevdog (#60), same cause independently found by @RinZ27 (#61)TaggableManager is read as the M2M it is — through taggit.TaggedItem to taggit.Tag, through= overrides honoured, in both the Python and the TypeScript parser — contributed by @Guflly (#63, closing #50)DOL021 states the USE_TZ default correctly — False through Django 4.2, True from 5.0, with the startproject template's USE_TZ = True since 4.0 called out as the separate thing it is — and no longer claims timezone.now() is always aware UTC — found by @Justine0211 while translating the page (#52)parity_input.py fixture carries the models import a real models.py would have — contributed by @RinZ27 (#64)Next
.filter() / .exclude() / .annotate() (#3)Later
django-mptt, django-taggit, django-model-utils)Vote by 👍-ing the corresponding issue.
❓ FAQ Do you send any of my code to a server?models/ package. Does that work?
models/*.py alongside classic models.py.
Can I use it with DRF serializers, Wagtail, Oscar, or third-party base models?
models.Model, abstract bases starting with Abstract, common mixins ending in Mixin, and known base names like TimeStampedModel or PolymorphicModel. Non-model classes (ModelAdmin, ModelSerializer, Form, View, Manager, …) are filtered out.
Which AI agents can use the MCP server?
command at the installed django-orm-lens-mcp binary. See the Integrations section.
How do I block schema regressions in CI?
uses: FROWNINGdev/django-orm-lens@action-v1 with format: github for PR annotations), or --format sarif piped into github/codeql-action/upload-sarif for the Security tab. diff / nplusone exit 1 on findings, migration-risk exits 1 on critical findings.
Is there a JetBrains / PyCharm version?
models.py snippet)MIT © FROWNINGdev
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | django-arch-check: Static Checker for Common Django Issues | 0 | 24.29 | 01-06-2026 |
| 2 | django-tasks-db - An ORM-based backend for Django Tasks | 0 | 35 | 21-02-2026 |
| 3 | django-nanopages - Django pages from Markdown | 0 | 26.67 | 25-01-2026 |
| 4 | Django: introducing django-integrity-policy | 0 | 35 | 03-06-2026 |
| 5 | django-modern-rest: REST With Types and Async Support | 0 | 24.29 | 23-04-2026 |
| 6 | oxyde: Type-Safe, Pydantic-Centric Async ORM | 0 | 50 | 20-02-2026 |
| 7 | Oxyde ORM - Django-like Pydantic-driven Async ORM | 0 | 31.84 | 22-03-2026 |
| 8 | onlymaps: A Python Micro-ORM | 0 | 35 | 10-01-2026 |
| 9 | django-nis2-shield: NIS2 Compliance Middleware | 0 | 60 | 30-01-2026 |
| 10 | dj-signals-panel: View Django Signals in the Admin | 0 | 24.29 | 20-04-2026 |