Add a filter for findings whose owner cannot act - #375
Merged
Conversation
Closing an account does not remove its assignments, because dropping them would hide that somebody had picked the work up. What is left is a row with a name on it that nobody is acting on, and nothing could find those, so the sweep after somebody leaves a team could not be run. The value used to be a correlated subquery, evaluated above the LIMIT, so filtering on it would have moved the evaluation below and run once per table row. Measured on 5,000 findings before changing anything: output-only loops=50, and with a filter the same subplan twice, loops=50 and loops=5000. An outer join is loops=1 either way, so the output-only path did not regress. Outer and not inner: an unassigned finding has no user row, and an inner join removes every one of them from every list. The existing assignee tests all assign before asserting, so none of them would have caught that. The token vocabulary lives in four places and nothing compared them. A contract test does now, and found the frontend missing on its first run.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closing an account does not remove its assignments, and that is deliberate: dropping them would hide that somebody had picked the work up. What it leaves is a row with a name on it that nobody is acting on, and nothing in the product could find those. The sweep after somebody leaves a team could not be run, which is the other half of the assignment work.
?assignee=inactivereturns them. The token names a state rather than a person, because the parameter already says whose, so it reads the wayunassigneddoes. Narrow on purpose: a wider word like "unactionable" would mean one thing today and promise several, and every later reason a finding cannot move would become a decision about whether it belongs inside that word. Another reason gets another value and these keep their meaning.The performance question the previous code left open is now answered rather than assumed.
assignee_is_activewas a correlated subquery, which the planner evaluates above the LIMIT, so it ran once per returned row while the value was output-only and would have run once per table row under a filter. The comment there said to convert it to an outer join and re-measure rather than assume, so that is what happened, on 5,000 findings in one scan because the original 117-row measurement puts the two hypotheses a factor of two apart where noise lives:loops=50for a 50-row pageloops=50for the projection andloops=5000for the filterloops=1Measured through
list_project_vulnerabilitieswith EXPLAIN ANALYZE on the statement the service actually emits, not a hand-written copy, because a copy measures the transcription. The output-only path moved to the join too: it is no worse there, and two spellings of one value is how one of them gets fixed alone later.loops=1is the property. The hash join and sequential scan ofusersthat produced it are what this database size chose, and a deployment with far more users may plan differently without the per-row repetition coming back. That distinction is in the comment so the next person does not read a different plan as this note being wrong.Outer and not inner, which is the sharp edge. An unassigned finding has no user row, so an inner join removes every one of them from every list in the product. Making the join inner fails fourteen tests, and none of the fourteen is about that: they all assign before asserting, so the symptom an operator would actually meet was unwatched. A test now asks the unfiltered list for a finding nobody owns.
The filter is
is_(False)rather than a negation, because an unassigned row is NULL here and a negation reads as though it should match.The
if/elsethat dispatched two tokens became explicit branches with an unreachable final one. A third value added to the old shape would have fallen silently into theunassignedarm.The token vocabulary lives in four places: the service, two route patterns and the frontend set. Nothing compared them. A contract test does now, reading all four rather than restating them, which would be a fifth copy. It found the frontend missing on its first run, which without it would have shipped as an option nobody could reach with nothing reporting it.
Verification: mypy over 916 files, ruff, 297 backend tests, the frontend type-check, and the two locale files compared key by key (1,081 each) because this worktree cannot run
i18n:check. Reversing each choice fails a different test: the inner join, the negation instead ofis_(False), a token added to one place only, and a token renamed in the guide.