Segment merge, durable serialization, and reusable execution infrastructure - #18
Open
nnunley wants to merge 8 commits into
Open
Segment merge, durable serialization, and reusable execution infrastructure#18nnunley wants to merge 8 commits into
nnunley wants to merge 8 commits into
Conversation
nnunley
force-pushed
the
typed-query-program
branch
from
August 13, 2026 16:48
0308389 to
ed404bf
Compare
nnunley
force-pushed
the
typed-query-program
branch
from
August 13, 2026 18:27
ed404bf to
5ee64a0
Compare
The mmap tests built their temp paths by hand from the process id, the clock, and a process-wide counter, then removed the file at the end of the test body. Both halves were wrong. Composing a name predicts that nobody else picked it — nothing checks that the path is free, so a leftover file from a crashed run is silently reused, and the process id does not separate runs in different PID namespaces. And cleanup at the end of the body does not run when a test panics, so every failing run leaked a segment file into the temp directory. TempDir takes uniqueness from an exclusive create by the operating system and removes the tree on drop, including on unwind. Each test now owns a directory rather than a file, so the segment name inside it can be fixed. tempfile is already in Cargo.lock through proptest, so this adds no new crate to the dependency graph.
Introduces AnalysisSchemaId and the schema-aware FieldAnalyzers accessors. This is the shared primitive that both the segment-merge compatibility check and the reusable-execution scratch depend on, so it is factored out ahead of either story.
nnunley
force-pushed
the
typed-query-program
branch
from
August 13, 2026 19:17
5ee64a0 to
38d4731
Compare
Adds logical segment merging: a merge policy that selects candidates and rejects incompatible inputs, validation of owned merge inputs (including the analysis-schema compatibility check), deterministic doc-id remapping, and the merge execution itself. The remap plan is computed before any writing so the result is a pure function of the input segment set and its ordering: the same inputs always produce the same doc-id assignment, which is what makes a merged index reproducible and diffable. Closes with a scoring oracle test that proves a merged segment scores identically to the unmerged sources it replaces. Squashed from five commits: merge policy, input validation, remap planning, merge execution, scoring-equivalence test.
Adds the on-disk form for prepared/compressed segments: the writer path and codec support for compressed postings, rebuilt block metadata so a reopened segment reconstructs its skip structure, and full validation of postings encodings when a segment is opened rather than trusting the header. Validation is deliberately on the full-open path, not just the incremental one: a corrupted or truncated encoding is rejected with a typed error at open time instead of surfacing as a wrong result during scoring. Closes with an end-to-end test that merges segments, serializes the result, reopens it, and round-trips the scoring. Squashed from four commits: writer/codec preparation, block-metadata rebuild, full-open encoding validation, merged round-trip test.
Records the design and the staged plan for reusing execution allocations across queries: what is measured, which scratch buffers are candidates, the ownership rules that keep reuse sound, and the phasing. Committed before the implementation so the following commits can be read against a stated intent. Squashed from two commits: design and plan.
Puts the measurement apparatus in place before any optimization, so the reuse work that follows is checked rather than asserted. Two pieces: - A reference execution index in leit_index (behind bench-internals): a deliberately naive implementation used as a correctness oracle, with parity, statistics, and feature-boundary tests. The feature-boundary test compiles a consumer crate against the public surface to prove the bench-internals feature does not leak. - A scoped allocation counter in leit_wind_tunnel: counts allocations within a scope so a test can assert an exact allocation budget instead of eyeballing a benchmark. Squashed from two commits: reference execution oracle, scoped allocation counter.
Removes per-query allocation from the hot path, one owner at a time: - leit_collect exposes finish_into so top-k result sinks are refilled rather than reallocated. - leit_index owns a preplanned execution scratch, reused across queries through ExecutionWorkspace instead of rebuilt per search. - Compressed decode scratch moves out of the cursor and into the same reusable memory, so decoding a compressed segment no longer allocates per block. Each step is pinned by the allocation counter from the previous commit, and the reference oracle keeps results identical throughout. Closes with the allocation baselines: query-side reuse and the indexing phases measured separately, plus the recorded baseline numbers. Squashed from five commits: top-k sink reuse, preplanned execution scratch, compressed decode scratch, query allocation baseline, indexing allocation phases.
Add Planner::plan_program, lowering a UserQueryProgram AST directly to an ExecutionPlan without the textual parser. The term arm of the textual lowering is extracted into a shared helper so field resolution, default-field expansion, and boost composition are the same code on both paths; parity is pinned by plan-equality and hit-equality tests. ExecutionWorkspace gains plan_program/search_program with the same filter-slot wrapping as the textual variants, and leit_index re-exports the query-builder types so consumers need no direct leit_query dep. Guards: shared max_depth/max_nodes enforcement, iterative tri-color DFS depth computation (linear on shared-child DAGs, rejects deep chains mid-traversal instead of overflowing), cycle rejection for hand-built arenas, and boost validation (NaN/infinite/negative factors and composed-product overflow reject with QueryError::InvalidBoost). Phrase nodes lower to AND of their terms for now: Phase 1 has no positional data. The doc comment records the cross-field caveat so positional support ORs per-field phrase nodes rather than swapping the AND node. Motivation: callers routing natural-language prose through the textual parser hit hard parse failures on operator-looking prose (colons, parens). A typed program lets front-ends guarantee totality and treat the string syntax as one client among several.
nnunley
force-pushed
the
typed-query-program
branch
from
August 13, 2026 20:36
38d4731 to
b99befc
Compare
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.
What
Eight commits, each one idea, resequenced so the branch can be reviewed a commit at a time.
Reading order
test(leit_index): create test segment files with tempfileTempDirgives uniqueness from an exclusive create by the OS and cleanup on drop, including on panic. Net −35 lines.feat(leit_text): add analysis schema identityAnalysisSchemaId+ schema-awareFieldAnalyzersaccessors, factored out ahead of both consumers.feat(leit_index): merge segments deterministicallyfeat(leit_index): serialize merged segments durablydocs: design reusable execution infrastructuretest(index): add reference oracle and allocation measurementbench-internalsdoes not leak), plus a scoped allocation counter so tests assert exact allocation budgets.feat(index): reuse execution allocations across queriesfinish_intofor top-k sinks, a reusable preplanned execution scratch onExecutionWorkspace, and compressed decode scratch moved out of the cursor. Each step pinned by the counter from commit 6, results held constant by the oracle. Ends with recorded allocation baselines.feat(query): typed query pipelineplan_program/search_program.On commit 7
Planner::plan_programlowers aUserQueryProgramdirectly to anExecutionPlan;ExecutionWorkspacegainsplan_program/search_programalongside the textual variants, andleit_indexre-exports the builder types so consumers need no directleit_querydependency.Motivation: callers routing natural-language prose through the textual parser hit hard parse failures on operator-looking input — colons read as field qualifiers, stray parens. A typed program lets a front-end guarantee totality on arbitrary user input and treats the string syntax as one client among several. Downstream consumer: graphrag-rs builds
UserQueryProgramfrom a total parser and callssearch_program; that change rides separately in its own repo.boost * default_boostcomposition are literally the same code on both paths. Parity is pinned by plan-equality and end-to-end hit-equality tests.max_depth/max_nodesguards as the textual path. Depth uses an iterative tri-color DFS with memoized depths: linear on shared-child DAGs, rejects over-deep chains mid-traversal instead of overflowing, and rejects cyclic hand-built arenas.QueryError::InvalidBoost. The textual path cannot produce these inputs, so this surface is typed-path-only.The textual parser is untouched.
Verification
cargo fmt --all --check,taplo fmt --check,cargo clippy --workspace --all-targets --all-features --locked, andcargo test --workspace --locked --all-features. The branch is bisectable and each commit satisfies the repo's formatting and lint rules on its own, not just at the tip.prek run --all-files --hook-stage pre-push: 21/21 hooks pass, includingcargo fmt,taplo fmt, all four clippy variants,--doc, andcargo doc.Changes since the previous push
Relative to the previous push of this branch (
03083893), three fixes, each folded into the commit that introduced the affected lines rather than added on top:cargo fmt --checkfailed on the previous head —pub useordering inleit_index/src/lib.rsplus call formatting inplanner.rs,program_search.rs,plan_program.rs.taplo fmt --checkfailed — three feature arrays needed sorting inleit_wind_tunnelandleit_wind_tunnel_query.A real test bug in
reference_feature_boundary: the temp directory was built by hand from pid +as_nanos(), but both tests in that file run in parallel in the same process, so when they started inside one clock tick they got the identical path — one test then read the other'smain.rswhile the other'sDropdeleted the tree mid-build. Now usestempfile::TempDir, so uniqueness comes from an exclusive create by the OS rather than from a name predicted out of the process id and the clock, and cleanup survives a panicking test.tempfilewas already inCargo.locktransitively viaproptest, so this adds no new crate to the build graph. Separately, the nestedgenerate-lockfilecall now sets its ownCARGO_TARGET_DIRinstead of inheriting an ambient one.The same hand-rolled helper existed in
leit_index/src/segment_format/mmap.rsonmain. That is fixed here too, in commit 1.Provenance
Implemented and reviewed with LLM assistance under red-first TDD; an independent cross-model review produced three robustness findings in the typed query work — parser recursion bound, DAG traversal memoization, boost validation — all fixed and covered by tests here.