Skip to content

fix(python): route path-synthetic module batches through the shared query - #6

Open
sontek wants to merge 8 commits into
all-fixes-combinedfrom
pr/issue-20-path-synthetic-batching
Open

fix(python): route path-synthetic module batches through the shared query#6
sontek wants to merge 8 commits into
all-fixes-combinedfrom
pr/issue-20-path-synthetic-batching

Conversation

@sontek

@sontek sontek commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Python's relational-lookup batches for exact-name, normalized-name, and structural-member queries always fell through to the one-request-at-a-time point-query path, no matter how large the batch. has_path_synthetic_module_units() returning true for Python made all three batched set-query functions bail unconditionally, so a whole-workspace fan-out asking about thousands of names took thousands of sequential SQLite round trips instead of one shared query. On a real large repository this showed up as a worker thread pinned inside that point-query path for an entire 30-second profile sample while every other worker sat idle waiting on the same connection pool.

The three batched functions now merge in the path-synthetic-module answer themselves, reading a lean, path-only view rather than the wide compound view the point-query path uses, so a full batch resolves in one query instead of forcing a bail. That view choice matters: the wide view is a three-way UNION ALL, and joining it here would make SQLite materialize all three arms before the path-only filter could discard the other two, the same compound-view cost this codebase had already measured elsewhere.

Includes two follow-up commits from review: a fix for a path-synthetic module silently dropping out of structural-member lookups, and a cleanup that deduplicates identical SQL construction the fix introduced and drops a parameter left dead by the change.

Refs BrokkAi/bifrost#20

…et query

has_path_synthetic_module_units() returning true for Python forced
set_exact_definition_values, set_normalized_definition_values, and
set_structural_member_values to bail on every batch, regardless of
size, sending each request through relational_definition_values's
one-request-at-a-time point-query fallback instead. That fallback
exists for small batches; Python asks about thousands of names at
once and got routed there anyway (bifrost issue #20).

Both set-query functions that can answer with a path-synthetic module
now also query the same wide view's path arm that the point-query
path already reads through path_units, merging the result in before
dedup. set_structural_member_values only returns non-module members,
so it drops the bail with no matching query added.

Adds a regression test proving a full-size Python exact-name batch
resolves entirely through the batched path (zero point queries)
including a path-synthetic module name in the same batch, and a
round-trip-count benchmark test documenting why the production
regression is disk- and concurrency-bound rather than visible on a
small in-cache SQLite fixture.
…e a lean view for it

review-code found that set_structural_member_values dropped the
|| has_path_synthetic_module_units() bail without adding the
batched_path_units_sql merge its two siblings got, silently losing
path-synthetic submodules as structural members of their package once
a batch crosses SET_QUERY_MIN_REQUESTS. AnalyzerDefinitionLookup
queries a package's own relational name as a StructuralMembers owner
specifically to find its submodules, so this was reachable in
production.

perf-reviewer found the merge itself (in all three functions) queried
the wide live_definition_exact_names/live_definition_normalized_names
views. Those views are a three-way UNION ALL; SQLite materializes all
three arms before a `source_kind = 'path'` filter can discard the
other two, the same compound-view tax this codebase already measured
at 89.4 minutes on an 802K-row table elsewhere. All three call sites
now read the lean, path-only workspace_path_symbol_exact_names /
workspace_path_symbol_normalized_names views instead, which never
join code_units or the anchor tables.

Adds a regression test proving the submodule now resolves inside the
batched path, and an EXPLAIN QUERY PLAN pin proving the path-units
query never touches code_units or workspace_file_anchor_rows.
…param

code-simplifier found set_exact_definition_values and
set_structural_member_values building the byte-identical
batched_path_units_sql call; factored into
batched_exact_name_path_units_sql, reused by both call sites and the
EXPLAIN QUERY PLAN pin's own construction of the same query.

Also drops the now-unused adapter parameter (and its generic bound)
from set_queries_need_live_unit_counts, left over from removing its
has_path_synthetic_module_units() early return, and switches the two
new tests off a hardcoded 64 onto the real SET_QUERY_MIN_REQUESTS
constant (re-exported test-only) so they can't drift from it silently.
code-simplifier caught two doc comments crediting the wide-view
materialization cost to query_view_candidates's doc comment. The real
measurement is issue #2794's mounted_declaration_scan_seeks_live_workspace_files
in store/mod.rs, a different caller of the same wide view.
…short_name)

Ran a live single-case check of this branch against PostHog, a real
large mixed-language monorepo. AnalyzerDefinitionLookup::prefetch_fqn_in_language
went through the new query_batched_path_units path as expected, then
stayed there: sampling the process twice, 13 minutes apart, caught it
in the exact same call both times.

workspace_file_path_symbol_rows only indexes (exact_fqn) and
(normalized_fqn); there is no index on (package_name, short_name).
The exact-name and structural-member path-arm queries matched on that
unindexed pair, so SQLite had to build its own covering index over
every path-symbol row in the whole workspace, every language, before
answering a single request. The normalized-name query already matched
on the indexed normalized_fqn and was never affected.

Both queries now serialize the full exact_fqn to match at request
key index 3 and use it directly: set_exact_definition_values already
has the request's own rendered tail (equal to its exact_fqn, since a
path-arm match requires an empty prefix); set_structural_member_values
builds the member's exact_fqn from its owner's tail and the member's
identifier, joined with the language's own package separator. Extends
the EXPLAIN QUERY PLAN regression pin to assert the seek instead of
only the absence of code_units/anchor references, which said nothing
about whether the path-arm predicate itself was indexed.
Live-tested the branch against a real large mixed-language monorepo
(the same kind of repository this issue's own evidence came from) and
it was still stuck after 45+ minutes. Sampling the process found it
inside path_units, not the code this issue's fix already touched:
below SET_QUERY_MIN_REQUESTS, or for any query shape with no set
executor at all, definition_values still falls back to path_units on
the wide view for the path-derived answer -- unconditionally, on
every single such call.

path_arm_lean_units gives that point-query fallback the same
exact_fqn/normalized_fqn-indexed route the batched functions already
have, for ExactName, NormalizedName, StructuralMembers, and
VisibleMembers -- the shapes that carry (or can construct) a single
full name. StructuralChildren, Identifier, IdentifierPrefix,
PackageTypes, and PackageTypesInPackage only carry a bare identifier
or a package name; workspace_file_path_symbol_rows has no index on
either, so those need a real new index, not a smarter predicate --
they keep using path_units on the wide view unchanged.

Adds a point-query correctness test and an EXPLAIN QUERY PLAN pin
mirroring the batched-path ones already in this file.
…ean too

AnalyzerDefinitionLookup::prefetch_fqn_in_language batches every exact-name
lookup into one call, then batches its identifier-candidate fallback for
every miss into one more -- both real, single round trips, not the
per-name-call problem this issue started from. But RelationalDefinitionQuery
Identifier has no batched set executor at all, by original design: it always
takes definition_values's per-request point-query path, no matter how large
the Rust-level batch handed to the store is. Its content-side lookup already
has a lean-view treatment from a prior fix; its path-arm lookup did not, and
workspace_file_path_symbol_rows had no index to give it one -- only
(exact_fqn) and (normalized_fqn), from migrations built for shapes that
carry a full name. A miss-heavy fallback on a real large Python monorepo
turned that into path_units's wide-view scan once per identifier miss.

Migration 0037 adds an index on (short_name, file_version_id). path_arm_lean_units
now serves Identifier's two shapes (with or without a file) and
IdentifierPrefix's range form from it. StructuralChildren, PackageTypes, and
PackageTypesInPackage only carry a package name, which this index does not
cover, and keep using the wide view unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant