More screenshots

    Works with the AI Agents you already use

    Claude CodeClaude CodeCursorCursorCodex CLICodex CLIMuseMuseDotsDots+22 more

    Django ORM Lens: N+1, Migration Safety & Schema Map

    1

    Your agent stops guessing about Django: real N+1 detection, migration-risk review, schema drift and "what breaks if I drop this field", from static analysis. No DB, no settings.

    Free

    1 installSecurity scanned
    Django ORM Lens: N+1, Migration Safety & Schema Map

    Django ORM Lens: N+1, Migration Safety & Schema Map

    Example session with this skill installed

    We're about to merge the PR on branch pr (base master) in our Django shop. Is it safe to deploy? Also the order history page is slow — why?

    • Read your context and instructions
    • Compiled the django orm lens

    Verdict: BLOCK

    Don't merge this as it stands. Migration 0002 renames Product.title to name in the database, but models.py still declares title. After migrate, every query that touches Product fails, and that includes the order history page. It also adds a NOT NULL column with no default, which fails on any table that already has rows.

    Scope checked: branch pr vs master (1 changed file: catalog/migrations/0002_product_weight_and_index.py), 2 apps, 4 models, 3 migrations replayed, all 13 source files scanned. Templates are not in the repo, so I couldn't check them for references.

    SeverityWhereWhatFix
    critical (drift)catalog/migrations/0002_product_weight_and_index.py:17 vs catalog/models.py:14RenameField title -> name in the migration, but the model still declares title. After migrate the column is name, yet Django still selects catalog_product.title. Every Product query errors out.Remove the RenameField from this PR, or rename the Python attribute only (see below)
    criticalcatalog/migrations/0002_product_weight_and_index.py:7AddField weight_grams is NOT NULL with no default. The migration fails on any table that already has rows.Add it as null=True, backfill, then enforce NOT NULL in a later release
    warningcatalog/migrations/0002_product_weight_and_index.py:17The rename also breaks a rolling deploy: instances still on the old code query the old columnRename the attribute only with db_column="title", or do expand/contract
    warningcatalog/migrations/0002_product_weight_and_index.py:13AddIndex holds a SHARE lock on Postgres for the whole build: reads continue, every write to product blocksAddIndexConcurrently in its own non-atomic migration
    found by review0002...py:13 vs catalog/models.pyThe index product_active_price_idx isn't in Product.Meta.indexes, so the next makemigrations will generate a RemoveIndex. No query filters or orders by price either.Drop the index, or declare it in Meta.indexes
    pre-existing (also on master)orders/models.py:20OrderItem.unit_price is declared but no migration creates itSeparate PR

    Good news: the PR does fix one earlier drift. Product.weight_grams was declared on master with no migration, and now it has one. It just needs a safe migration.

    Safe rewrite of 0002

    
    # catalog/migrations/0002_product_weight_grams.py
    from django.db import migrations, models
    
    def backfill_weight(apps, schema_editor):
        Product = apps.get_model("catalog", "Product")
        qs = Product.objects.filter(weight_grams__isnull=True)
        while True:
            ids = list(qs.values_list("pk", flat=True)[:1000])
            if not ids:
                break
            Product.objects.filter(pk__in=ids).update(weight_grams=0)
    
    class Migration(migrations.Migration):
        dependencies = [("catalog", "0001_initial")]
        operations = [
            migrations.AddField(
                model_name="product",
                name="weight_grams",
                field=models.PositiveIntegerField(null=True, blank=True),
            ),
            migrations.RunPython(backfill_weight, migrations.RunPython.noop),
        ]
    

    If you really want title called name, rename the Python attribute only. The column stays, so there's no data move and no rolling-deploy breakage:

    
    # catalog/models.py
    name = models.CharField(max_length=200, db_column="title")
    
    
    # in the migration, state-only
    migrations.SeparateDatabaseAndState(
        state_operations=[migrations.RenameField("product", "title", "name")],
        database_operations=[],
    )
    

    Update in the same commit (from impact title):

    • catalog/serializers.py:9: "title" in ProductSerializer.Meta.fields (certain). Public API field: keep clients working with title = serializers.CharField(source="name").
    • catalog/models.py:21: return self.title (likely)
    • orders/views.py:13: item.product.title (likely)

    If the index is needed, give it its own migration:

    from django.contrib.postgres.operations import AddIndexConcurrently
    from django.db import migrations, models
    
    class Migration(migrations.Migration):
        atomic = False
        dependencies = [("catalog", "0002_product_weight_grams")]
        operations = [
            AddIndexConcurrently(
                "product",
                models.Index(fields=["is_active", "price"], name="product_active_price_idx"),
            ),
        ]
    

    Still referenced

    blast-radius looked up the new name name and reported 11 matches. I dismissed all of them: they are Category.name and name= keyword arguments in migrations, not Product. What actually breaks are the references to the old name title, listed above (1 certain, 2 likely).


    Why the order history page is slow

    orders/views.py:6 order_history is a nested N+1: one query for the orders, then for each order one query for items, and for each item one for product and one more for product.category.

    Queries per request: 1 + N_orders + 2 x N_items. A customer with 20 orders of 3 items means

    141 queries, growing with order history because the page isn't paginated.

    ConfidenceWhereAccessedSuggested
    highorders/views.py:9order.itemsprefetch_related("items")
    mediumorders/views.py:10item.productselect_related("product")
    found by revieworders/views.py:14item.product.categoryone more FK load per item

    Fix: one query

    The page only renders item rows, so query OrderItem directly:

    def order_history(request):
        items = (
            OrderItem.objects
            .filter(order__customer=request.user)
            .select_related("product__category")
            .order_by("-order__created_at", "order_id", "pk")
        )
        rows = [
            {
                "order": item.order_id,
                "product": item.product.title,
                "category": item.product.category.name,
                "qty": item.quantity,
            }
            for item in items
        ]
        return render(request, "orders/history.html", {"rows": rows})
    

    141 queries down to 1. Re-ran the scan on a copy with the fix: 2 findings before, 0 after.

    Also found by review: recent_orders (orders/views.py:20) loops over Order.objects.filter(status="paid")[:50] and reads order.customer.email, which is 50 extra queries, and the slice has no order_by, so "recent" means any 50 rows:

    for order in (Order.objects.filter(status="paid")
                  .select_related("customer")
                  .order_by("-created_at")[:50]):
    

    Pin it with a test

    def test_order_history_query_count(client, django_assert_num_queries, customer_with_orders):
        client.force_login(customer_with_orders)
        with django_assert_num_queries(3):  # session + user + the one OrderItem query
            client.get("/orders/history/")
    

    Next steps (deploy order)

    This PR. Take the RenameField out of 0002 (or switch to db_column / SeparateDatabaseAndState and update the serializer, __str__ and orders/views.py:13 in the same commit). Make weight_grams nullable with a backfill. Move the index to its own concurrent migration and declare it in Meta.indexes, or drop it.
    2. Re-run the review: blast-radius --severity all --only <new migrations> and drift. Expect 0 critical and no blocking drift for catalog.
    3. Separate PR. Fix order_history and recent_orders, add the query-count test.
    4. Separate PR. Sort out the OrderItem.unit_price drift after checking the production schema.
    5. Next release. Enforce NOT NULL on weight_grams once the backfill has run everywhere.

    Connects securely to your tools. The creator never sees your data.

    About this skill

    Most Django skills are advice. This one ships an engine.

    Ask your agent "is this migration safe to deploy?" and it normally skims a few files and guesses. With this skill it runs django-orm-lens, an open-source static analyzer (MIT, on PyPI, ~90★ on GitHub), across the whole project and answers from facts:

    • Migration review that blocks real outages. 16 risk rules: NOT NULL column without a default, AddIndex that locks a Postgres table, renames that break rolling deploys, irreversible RunPython, conflicting leaves. Each finding comes with the safe rewrite (three-step NOT NULL, AddIndexConcurrently, expand/contract).
    • Blast radius. For every destructive operation: the code that still reads the dropped column, grouped by Django layer (serializers, forms, admin, views, templates, tests) and tagged certain / likely / possibly.
    • Schema drift. makemigrations --check without a settings module: fields declared in models.py that no migration creates — the column that will be missing at runtime.
    • N+1 detection. Loops over querysets that touch FK / M2M / reverse relations without select_related / prefetch_related, with the exact fix — then the agent re-runs the scan to prove the fix worked.
    • Safe field / model removal. impact lists every reference before you touch anything, and the skill turns it into a two-release deploy plan.
    • Cascade check. What deleting one row takes with it, walked recursively — "deleting a Category erases order history" before it happens.
    • Schema map. ER diagrams (Mermaid, DBML, D2, PlantUML, Graphviz), model details, relations with on_delete, signal graph, migration DAG, index suggestions from real query usage.

    Safe on any repo. Pure AST parsing: it never imports your project, never needs DJANGO_SETTINGS_MODULE, secrets, installed dependencies or a database. Works on a fresh clone or a PR branch.

    Works everywhere. Uses the django-orm-lens MCP server (13 tools) when connected, otherwise the CLI via uvx — zero install. Includes a one-shot audit.py that merges all analyzers into one verdict (BLOCK / FIX BEFORE DEPLOY / SHIP), with a --changed-since origin/main mode for PR review.

    Honest by design. The skill tells the agent to check renames under the old name, verify name-matched references, walk cascades past the first level, look one level deeper than the N+1 scan, separate what a PR introduced from what was already broken, and say exactly what was checked instead of "no issues".

    What's inside

    • SKILL.md — six playbooks (schema map, migration review, safe schema change, N+1, cascade, signals / migration history) and a report template
    • references/migration-playbook.md — all 16 rules with copy-paste safe migrations and deploy order
    • references/tools.md — every MCP tool and CLI command, MCP install lines for Claude Code, Cursor, VS Code, Codex, Gemini CLI, Windsurf, Claude Desktop
    • scripts/audit.py — one-command audit

    Built by the author of django-orm-lens (VS Code extension, CLI, MCP server). Source: https://github.com/FROWNINGdev/django-orm-lens

    How to use

    Skills on Agensi run inside your AI agent. Copy the prompt, paste it into your agent, and it connects to Agensi and installs this skill for you. If a payment is needed, your agent shows it to you first.

    Reviews

    No reviews yet

    Be one of the first to try it. Every listed skill passes our trust checks below.

    Security scanned

    Passed our 8-point scan before listing

    1 install

    Downloaded by developers to date

    Free forever

    No account required to browse

    Trust & safety

    Security scanned

    Verified clean today

    • Free to download with an account

    Requires

    Python 3.9+
    uv (uvx), pipx or pip
    django-orm-lens (free, MIT, PyPI; fetched automatically by uvx)
    git (only for PR-scoped audits)
    Django Orm Lens (Optional) MCP
    uv (Python tool runner)
    Git

    Listedtoday

    What's inside

    Frequently Asked Questions