Skip to content

feat(models): add a default-queryset hook for plugins - #16562

Open
Maffooch wants to merge 7 commits into
devfrom
feat/hierarchy-archive
Open

Maffooch wants to merge 7 commits into
devfrom
feat/hierarchy-archive

Conversation

@Maffooch

@Maffooch Maffooch commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Description

Adds a small extension point for plugins: a registry of default-queryset filters.

dojo/query_hooks.py keeps filters keyed by model label. HookedManager (and HookedBaseManager for BaseModel subclasses) applies every filter registered for its model to the queryset it returns. With nothing registered, the queryset is unchanged, so open source behaves exactly as before.

objects becomes a hooked manager on Product_Type, Product, Engagement, Test, Finding and the rows that hang off them (endpoint and location statuses and references, finding groups, Jira issues and projects, test imports, vulnerability ids, DojoMeta, app analysis, languages, engagement presets, API scan configurations, tool settings, object products). Because objects is declared first it is also the default manager, so a registered filter reaches reverse relations, get() and uniqueness checks. Each of these models also gets an unfiltered manager as the explicit bypass. _base_manager stays a plain manager, so forward foreign key access and the deletion collector are never filtered.

The same models gain two nullable columns, archived_at and archive_batch. Open source never reads or writes them. Migration 0304 adds the columns (nullable, no default, so no table rewrite) and 0305 builds partial indexes on archive_batch with CREATE INDEX CONCURRENTLY in a non-atomic migration, so dojo_finding and dojo_endpoint_status are not locked during the build.

0304 applies its operations to the migration state only and runs the SQL they generate (frozen from sqlmigrate) as one batch at the end. On a database with 10 million findings, the plain version blocked every read of dojo_finding for about 40 seconds: the SQL itself takes milliseconds, but Django rebuilds its model state after each operation, and each table's lock is held from its ALTER until the migration commits. With the SQL run last, the longest wait for a concurrent read was 0.4 seconds. The resulting schema is the same.

A second, smaller hook lets a plugin set excess duplicates aside instead of deleting them. register_excess_duplicate_handler in dojo/tasks.py installs one handler for the chunks the excess-duplicate task (Delete Duplicate Findings) removes. The task calls it inside each chunk's transaction, after its import and row locks are taken, and deletes nothing when it returns True. With no handler, or one that returns False, the chunk is deleted exactly as before.

Merges on its own

  • Nothing changes until a plugin registers a filter. With the registry empty, every queryset is the same one open source returns today, and unittests/test_query_hooks.py checks exactly that on an install with no plugin.
  • It works like register_auth_filter: a plugin registers into a registry at startup rather than patching open source code.
  • Deploying it is cheap. The columns are nullable with no default, so adding them rewrites no table, and the indexes are built with CREATE INDEX CONCURRENTLY. With nothing registered, the manager adds one dict lookup per query.
  • The dependency runs one way. DefectDojo Pro builds on this hook and needs it merged first. Nothing here depends on Pro.

Testing

  • unittests/test_query_hooks.py: with nothing registered every queryset is unchanged; the hooked manager is the default manager and the base manager is not; a registered filter applies to .objects, get() and reverse relations while unfiltered and _base_manager bypass it; registering the same key again replaces the filter. Run with manage.py test unittests.test_query_hooks on an install without any plugin: 4 passed.
  • unittests/test_excess_duplicate_handler.py: with no handler the chunk is deleted; a handler that returns True stops the delete; one that returns False leaves it in place.
  • makemigrations --check reports no changes.
  • ruff --config ruff.toml is clean on the changed files.

Maffooch and others added 4 commits October 23, 2026 01:15
Adds dojo.query_hooks: a registry of default-queryset filters keyed by model,
applied by the HookedManager declared as `objects` on Product_Type, Product,
Engagement, Test, Finding and the rows that hang off them. With nothing
registered the querysets are unchanged. Each of those models also gets an
explicit `unfiltered` manager, and two nullable columns (archived_at,
archive_batch) that open source never reads or writes, with partial indexes
built concurrently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Maffooch
Maffooch requested a review from blakeaowens as a code owner October 26, 2026 22:05
@Maffooch Maffooch added this to the 3.5.0 milestone Oct 26, 2026
@github-actions github-actions Bot added New Migration Adding a new migration file. Take care when merging. unittests labels Oct 26, 2026
Maffooch and others added 3 commits October 26, 2026 17:47
test_endpoint_queries compares Endpoint_Status.values() against literal
rows, which now include the two nullable columns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Measured on 10 million findings, 0304 blocked every read of dojo_finding for
about 40 seconds. Its SQL takes milliseconds, but Django rebuilds its model
state after each of the migration's operations, and each table's
ACCESS EXCLUSIVE lock is taken by its ALTER and held through all the
rebuilding that follows until the migration commits.

The operations are now applied to the migration state only, and the database
work is the SQL they generate (from sqlmigrate), frozen and run as one batch at
the end. The resulting schema is identical; the slowest concurrent read during
the migration dropped to 0.4 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eting them

register_excess_duplicate_handler installs one handler for the chunks the
excess-duplicate task removes. It runs inside the chunk's transaction with
the rows locked; when it returns True nothing is deleted. With no handler,
or one that returns False, the chunk is deleted as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

New Migration Adding a new migration file. Take care when merging. unittests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant