Context
BinaryIndexStore::open_leaf_handle on local storage reads the entire leaf blob (std::fs::read) even when the caller only needs the leaflet directory. The metadata-only count walks were converted to the prefix-reading, cached open_leaf_dir (see the dir-only leaf opens work), and fast_string_fold / fast_min_max_string already use a dir-first + lazy-escalation pattern. A sweep of the remaining ~24 open_leaf_handle(..., false) call sites in fluree-db-query found three tiers.
Tier 1 — pure directory-only sites (mechanical open_leaf_dir conversions)
These never call load_columns; the full-blob read is pure waste:
| site |
function |
walk size |
count_plan_exec.rs:3469 |
predicate_objects_all_iri (optional-chain gate) |
every leaf of a predicate |
fast_post_order_limit.rs:368 |
base_predicate_o_type (type-homogeneity prepass for ORDER BY LIMIT) |
every leaf of a predicate |
count_plan_exec.rs:1023 |
collect_k_first_subject_bounds_psot (partition bounds) |
k leaves |
fast_count.rs:417 |
boundary_leaf_pid_extent (numeric-compare extent) |
~2 leaves/query |
The first two are the same shape as the global distinct-count fix and walk a predicate's whole leaf list per query. Conversion also picks up the decoded-dir cache for free.
Tier 2 — conditional column loads (dir-first + lazy escalation)
~14 sites load columns only for some leaflets (mixed o_type_const, boundary leaflets of a range, threshold-interior skipping), e.g.:
fast_count.rs:753/855 — LANG-filter count: loads OType column only for mixed leaflets
fast_count.rs:1369 — literal-count leaflet fallback: same mixed-leaflet condition
fast_count.rs:461 — numeric-compare count: skips leaflets provably all-in/all-out of the threshold
fast_count.rs:1459 — blank-node subject count: decodes only range-boundary leaflets
fast_path_common.rs:977/1171/1404/1688/1877, count_plan_exec.rs:2832/3682, fast_predicate_scalar_agg.rs:373, fast_exists_join_count_distinct_object.rs:97 — various per-leaflet skip conditions
These should follow the fast_string_fold.rs pattern: open_leaf_dir first, open a handle only when a leaflet actually needs columns. The payoff per site is proportional to its skip rate.
Building block: lazy local leaf handle
The infrastructure for doing this well already exists end-to-end:
ContentStoreRangeFetcher::fetch_range (binary_index_store.rs) already does positional read_at on local files
RangeReadLeafHandle already implements lazy per-leaflet column loads over byte ranges — it is just only constructed for the remote path
Exposing a lazy handle for local leaves (e.g. open_leaf_handle_lazy) fixes the remaining flaw in the existing hybrid sites too: today, escalating for a single mixed leaflet reads the whole blob; a lazy handle would pread only that leaflet's column blocks. With it, Tier 2 migrations become the same mechanical pattern everywhere.
Tier 3 — leave alone
Sites that decode essentially every leaflet (collect_subjects_for_predicate_*, PsotSubjectCountIter, collect_subjects_with_object_in, collect_post_desc_topk*): one sequential full-blob read beats many small preads when you're decoding everything. Keep open_leaf_handle there — this is per-site judgment, not a blanket swap. (binary_history.rs needs the sidecar via need_replay=true; also correct as-is.)
Suggested order
- Tier 1 conversions (one-liners, immediate win on the two full-predicate walks)
open_leaf_handle_lazy building block + tests
- Tier 2 migrations, highest-skip-rate sites first, each validated against its existing fast-path tests
Context
BinaryIndexStore::open_leaf_handleon local storage reads the entire leaf blob (std::fs::read) even when the caller only needs the leaflet directory. The metadata-only count walks were converted to the prefix-reading, cachedopen_leaf_dir(see the dir-only leaf opens work), andfast_string_fold/fast_min_max_stringalready use a dir-first + lazy-escalation pattern. A sweep of the remaining ~24open_leaf_handle(..., false)call sites influree-db-queryfound three tiers.Tier 1 — pure directory-only sites (mechanical
open_leaf_dirconversions)These never call
load_columns; the full-blob read is pure waste:count_plan_exec.rs:3469predicate_objects_all_iri(optional-chain gate)fast_post_order_limit.rs:368base_predicate_o_type(type-homogeneity prepass for ORDER BY LIMIT)count_plan_exec.rs:1023collect_k_first_subject_bounds_psot(partition bounds)fast_count.rs:417boundary_leaf_pid_extent(numeric-compare extent)The first two are the same shape as the global distinct-count fix and walk a predicate's whole leaf list per query. Conversion also picks up the decoded-dir cache for free.
Tier 2 — conditional column loads (dir-first + lazy escalation)
~14 sites load columns only for some leaflets (mixed
o_type_const, boundary leaflets of a range, threshold-interior skipping), e.g.:fast_count.rs:753/855— LANG-filter count: loads OType column only for mixed leafletsfast_count.rs:1369— literal-count leaflet fallback: same mixed-leaflet conditionfast_count.rs:461— numeric-compare count: skips leaflets provably all-in/all-out of the thresholdfast_count.rs:1459— blank-node subject count: decodes only range-boundary leafletsfast_path_common.rs:977/1171/1404/1688/1877,count_plan_exec.rs:2832/3682,fast_predicate_scalar_agg.rs:373,fast_exists_join_count_distinct_object.rs:97— various per-leaflet skip conditionsThese should follow the
fast_string_fold.rspattern:open_leaf_dirfirst, open a handle only when a leaflet actually needs columns. The payoff per site is proportional to its skip rate.Building block: lazy local leaf handle
The infrastructure for doing this well already exists end-to-end:
ContentStoreRangeFetcher::fetch_range(binary_index_store.rs) already does positionalread_aton local filesRangeReadLeafHandlealready implements lazy per-leaflet column loads over byte ranges — it is just only constructed for the remote pathExposing a lazy handle for local leaves (e.g.
open_leaf_handle_lazy) fixes the remaining flaw in the existing hybrid sites too: today, escalating for a single mixed leaflet reads the whole blob; a lazy handle would pread only that leaflet's column blocks. With it, Tier 2 migrations become the same mechanical pattern everywhere.Tier 3 — leave alone
Sites that decode essentially every leaflet (
collect_subjects_for_predicate_*,PsotSubjectCountIter,collect_subjects_with_object_in,collect_post_desc_topk*): one sequential full-blob read beats many small preads when you're decoding everything. Keepopen_leaf_handlethere — this is per-site judgment, not a blanket swap. (binary_history.rsneeds the sidecar vianeed_replay=true; also correct as-is.)Suggested order
open_leaf_handle_lazybuilding block + tests