Fix query planner bug leading to HTTP 500 NET-536#70
Merged
Conversation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Fix: query planner routed simplified relations to the wrong scan table
What broke
Hotblocks returned
500 Internal Server Errorfor queries like this:{"logs": [{"transaction": true}], "fromBlock": N, "includeAllBlocks": true}. The response was:Where
26693is the row count of the chunk'slogstable and30966is the row count of the same chunk'stransactionstable.Root cause
PlanBuilder::simplify()incrates/query/src/plan/plan.rsrewrites the plan when a query has a predicate-less selector (the no-filter shape selects the whole table; the simplification adds a direct full scan and drops anything redundant).When the surviving relation isn't a "full rel" (its
input_keydoesn't match the input table'sprimary_key), the relation still needs to be evaluated against rows from itsinput_table. The current code keyed the new scan onrel.output_table()instead:In
execute_scanseach scan reads rows fromscan.tableand pushes their row indexes intorelation_inputs[rel_idx].eval_jointhen interprets those indexes as positions inrel.input_table(). With the wrong key,transactionsrow indexes (0..30966) were fed into a slot thateval_joinlater applied as arow_selectionto thelogstable (0..26693 rows) → out-of-bounds inpagination.rs.For EVM log selectors the trigger is structural:
logs.primary_key = [block_number, log_index]but the join'sinput_key = [block_number, transaction_index], sois_full_rel(rel)returnsfalseand the relation always survives simplification.Fix
Rel::input_table()accessor added next to the existingoutput_table().simplify()now keys the new scan onrel.input_table().To safeguard against other occurrences,
PlanBuilder::assert_scan_relation_invariant()runs at the end ofsimplify()and checks that every scan owns only relations whoseinput_tablematches the scan's own table — the contractexecute_scansalready assumes. Running thisassert!in release is cheap — planning runs once per request, and the cost is a short vector walk.Tests
Added unit tests in
plan.rs:simplify_attaches_surviving_relations_to_input_table_scan— the direct reproducer.simplify_drops_full_rel_and_replaces_with_direct_scan— covers the full-rel branch: relation is dropped and replaced with a direct scan.simplify_preserves_invariant_with_mixed_predicated_and_unpredicated_scans— predicated and predicate-less scans on the same table coexist; invariant still holds.assert_scan_relation_invariant_panics_on_input_table_mismatch— independently verifies the guard fires on a manually mismatched plan.