read_file_claim's diagnostics group_concat takes a different index from its siblings, and its comment says it does not #278
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
h-dv/code-index#278
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found in a performance review during #275 design work. Pre-existing, on a hot path, and the interesting part is that the statement's own comment asserts the opposite of what the planner does.
The claim in the source
crates/daemon/src/local_index.rs, above the statement:What the planner actually does
Three scalar subqueries, one statement, same
WHERE f.path = ?1. Measured withEXPLAIN QUERY PLANagainst a live.code-index/index.db:diagnosticsis not a column ofsqlite_autoindex_file_contributions_1, so that index stops being covering for this subquery and the planner switches toidx_file_contributions_generation. It then walks every contribution in the active generation and filtersfc.file_idper row, while its two siblings do a point seek onfile_id.So it does not read a column off rows the statement already visits. It visits a different row set, through a different index, and its per-call cost scales with the size of the whole active generation rather than with this file.
Cost
Measured by the review lane over 888 invocations against a copy of the live database:
INDEXED BY sqlite_autoindex_file_contributions_1The path matters:
read_file_claimruns once per changed file forchanged_symbolsandreview_diff, and once per path forindex_coverage.Why file it now
Two reasons beyond the number.
It is the #189 shape. That issue read as "+11.5%, the honest price of the fix" and the attribution showed 98.3% of it was ONE pre-existing unsargable clause the fix had merely handed more rows; rewriting the clause took every pinned repo BELOW baseline. #275 (region delegation) doubles the contribution count for delegating files and would hand this statement more rows. Fixing it first is what makes any later #275 cost number attributable rather than a blend of a new feature and an old defect.
A comment that misdescribes a plan is worse than no comment. This one is specific, confident, and load-bearing — it is the justification for folding the subquery into the cached statement at all. A reader optimising this path would believe it and look elsewhere.
Notes for whoever takes it
INDEXED BYor restructuring so thefile_idseek drives. Measure rather than assume — see below.ANALYZEhas never run on this database:SELECT COUNT(*) FROM sqlite_master WHERE name='sqlite_stat1'returns0. Every plan above is a heuristic choice with no statistics behind it, and that is worth knowing before attributing any plan change to any code change.ORDER BYon thegroup_concat. Benign today — its only consumer,decode_extraction_diagnostics, sorts and dedups — but it becomes nondeterministic for any second consumer.refs.influence_component_id -> extraction_components ON DELETE SET NULLhas no backing index, so deleting one component full-scans therefstable (295,583 rows in this database). Unreachable in normal operation today; #275 would make a second component per file routine.Fixed in
e2ef771— 11/11 CI greenread_file_claim's diagnostics subquery now carriesINDEXED BY sqlite_autoindex_file_contributions_1and seeks byfile_idlike its twoEXISTSsiblings.MEASURED, 888 files, timed inside one sqlite process so no startup is counted:
The comment that asserted the opposite — that the subquery reads "one column off rows this statement already visits" — is rewritten rather than appended to, since it was specific, confident, and would have sent anyone optimising this path elsewhere.
Two repairs measured and REJECTED, recorded in the source so neither is re-attempted
ANALYZEfixes it outright — 0.002 s, planner picks thefile_idseek with no hint at all. It is refused, and this tree already knew why:migrations::tests::no_query_planner_stats_are_generatedpins that this schema generates nosqlite_stat1, because MEASURED on rust-analyzer those statistics make the resolver's re-heal 2.8x SLOWER (9.4 s -> 26.3 s). A local win on a warm path that is a global regression on a far hotter one. This was very nearly the fix that shipped.Restructuring to resolve
file_idto a scalar first — the repair that avoids naming a SQLite-generated index — came back at 0.282 s, WORSE THAN SHIPPED, because the scalar subquery is then re-evaluated per row of the generation scan.The pin is gated, because this caller's failure mode is not the precedent's
local_index.rsalready pinsidx_refs_nameand argues it is safe because a failed prepare leaves the field ABSENT — "degraded, never wrong". That argument does not transfer here.read_file_claimswallows errors with.ok().unwrap_or((0, 0, None)), so a prepare that failed because the pinned index was renamed would report NO ACTIVE CONTRIBUTION for every file: degraded AND WRONG, silently, on the surface that tells an operator whether their file is indexed at all.crates/daemon/tests/read_file_claim_plan.rsgrades three things — the pinned name resolves; everyfile_contributionssubquery seeks byfile_id; and, as a POSITIVE CONTROL, the unpinned form still takes the bad plan, so the pin test is measuring the pin rather than the planner's mood. Without that control, deleting the pin would leave a green suite.Mutations — run, and both first verdicts were wrong
Recorded as observed rather than as predicted. Changing the pinned name reddens the SIBLING test, not the schema test, and the record now says which — crediting a test with a mutation its sibling caught proves nothing about the test under it. And a first attempt at that mutation was not the mutation at all: a
sedmatched a trailing quote and rewrote the two ASSERTIONS while leaving the SQL pin intact, so it went red and graded nothing.Verified: fmt 0, clippy
-D warnings0, rustdoc-D warnings0,cargo test --workspace360 binaries / 3926 passed / 0 failed, and all 11 CI jobs green one2ef771.Still open from this issue's "notes" section
Two items deliberately NOT fixed here, because neither is reachable today and both deserve their own measurement:
refs.influence_component_id -> extraction_components ON DELETE SET NULLhas no backing index, so deleting one component full-scansrefs(295,583 rows in this database). Unreachable in normal operation; #275 would make a second component per file routine.ORDER BYon thegroup_concat. Benign today — its only consumer,decode_extraction_diagnostics, sorts and dedups — but nondeterministic for any second consumer.Both are worth a look when #275 is picked up; neither blocks anything now.