indexer: batch per-file DB statements in index_repo (#105) - #118
Merged
IceRhymers merged 10 commits intoJul 25, 2026
Conversation
… cross-version parity diff
_union_membership had no guard against a duplicated (path, content_sha) in the injected items seam, unlike _flush_file_batch's own dedup guard -- a duplicate landing in the membership-only class reached write_chunks_batch twice for the same file_id, raising a UNIQUE VIOLATION on uq_chunks_file_id_chunk_index and rolling back the whole branch transaction (reproduced against real Postgres, both before and after this fix). The guard now lives in write_chunks_batch itself, covering every caller. Also updates the Lakebase-deferred chunk_writer stubs in test_store_chunk_writer.py and test_reconcile.py to call write_chunks_batch once per batch instead of looping the old per-file write_chunks, matching indexer/job.py's real closure, and adds membership-only + duplicate-item coverage to test_chunk_batching.py, which runs locally against codesearch-pg. Found by an independent code-reviewer agent pass over issue #105.
5 tasks
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.
Refs #105
Summary
Batches
index_repo's per-file DB writes. Today each file issues 3–7sequential round trips (file upsert, symbol delete+insert, edge
delete+insert, chunk delete+insert). This processes files in batches of up
to
_BATCH_MAX_FILES=500/_BATCH_MAX_CONTENT_BYTESinside the existingsingle
(repo, branch)transaction:INSERT ... ON CONFLICT ... RETURNING id, path, content_shaper batch for
files; ids are mapped back by(path, content_sha),never by row order (
RETURNINGon a multi-rowDO UPDATEmakes noordering guarantee) — a missing key raises and rolls back the whole
transaction rather than risk attaching one file's rows to another.
DELETE ... WHERE file_id = ANY(:ids)+ one param-budgeted bulk insertper batch for
symbolsandreference_edges(newindexer/bulk.pyhelper, explicit multi-row
VALUESrather thanexecutemanyso theround-trip count is driver-independent and measurable).
chunk_writerseam reshaped from per-file to per-batch(
indexer/chunk_store.write_chunks_batch), keeping the"precomputed vectors only, no network inside the transaction" contract.
ON CONFLICT DO UPDATEusesexcluded.*for per-row columns, nevera Python literal, to avoid the last-file-in-batch silently overwriting
every conflicting row.
Acceptance criteria
0.0133statements/file on a 600-file first-time index(
test_statement_count_per_file_meets_the_acceptance_criterion).6.38× on local Postgres (loopback, not Lakebase) — see
docs/perf/issue-105-measurements.md§3 for the full before/after numbersand both caveats (round-trip latency is understated on loopback; the
unindexed
symbols.file_idseq scan is not). Carried as not ticked,open on epic [Epic] Indexer performance: delta indexing, batched writes, streaming ingest #110 pending a real Lakebase run.
33-distinct-row fixture indexed identically on this branch and on
732a7d7(pre-indexer: batch per-file DB statements in index_repo #105), dumped and diffed field-for-field forfiles,symbols, andreference_edges— empty diff. Reproduced indocs/perf/issue-105-measurements.md§2, plustest_batch_size_invariance_produces_a_byte_identical_corpusat_BATCH_MAX_FILES ∈ {1, 2, 7, 500}.whole
(repo, branch)transaction(
test_mid_batch_generator_failure_rolls_back_the_whole_transaction); CASconflict still rolls back the batched form too.
Independent review
An independent
code-revieweragent pass (not the executor that wrote thiscode) found two MEDIUM issues, both fixed in
cc34de6before this PR opened:_union_membershiphad no dedup guard on the injecteditemsseam(unlike
_flush_file_batch's own guard), so a duplicated(path, content_sha)reaching the membership-only class fed the samefile_idtowrite_chunks_batchtwice — a realUNIQUE VIOLATIONonuq_chunks_file_id_chunk_indexthat poisons the transaction. Reproducedagainst real Postgres both before and after the fix. The guard now lives
in
write_chunks_batchitself (mirrors_flush_file_batch's dedup,last-occurrence-wins, one WARNING), covering every caller rather than
just this one call site.
chunk_writerstubs intests/integration/test_store_chunk_writer.pyandtests/integration/test_reconcile.pylooped the old per-filewrite_chunks, so even when those modules eventually run against a realLakebase branch they'd exercise the pre-indexer: batch per-file DB statements in index_repo #105 statement shape, not
write_chunks_batch. Both now callwrite_chunks_batchonce per batch,matching
indexer/job.py's real closure;test_chunk_batching.py(whichdoes run locally) gained membership-only and duplicate-item coverage
reproducing the fixed regression end-to-end.
Three LOW findings were left as-is (documented, not fixed):
insert_rowsassumes homogeneous row dicts with no assertion;
_FILE_UPSERT_COLUMNSis ahand-maintained constant not derived from the actual column count;
_union_membership's chunk write has no batch-size bound (unlike thechanged/new path) — not the dominant memory term today since
job._precompute_chunk_writeralready retains the whole branch'sembeddings.
Gates run
make lint(ruff check + format + mypy): green.make test(unit): 1244 passed, 1 known false positive —test_semantics_change_bumps_the_index_semantics_versionfires becauseindexer/ingest.py(added by indexer: single-pass in-memory tarball ingestion (drop extract-to-disk) #106, not yet merged pastintegration/indexer-performance) is new relative toorigin/master, thetripwire's local fallback base. Verified green (9/9) against the correct
sibling base:
GITHUB_BASE_REF=integration/indexer-performance uv run pytest tests/unit/test_semantics_version_tripwire.py.make test-integrationagainst localcodesearch-pg(the onlyintegration gate available — no CI Lakebase job is provisioned
repo-wide): all batching-relevant suites pass, including the rollback and
statement-count tests above. The remaining failures/errors are
lakebase_vector/lakebase_tokenizer-dependent suites this Postgresimage can't provide, plus two pre-existing failures
(
test_commit_search.py,test_mcp_server.py) unrelated to this diff andreproducible on the unmodified base branch.
Known limits
symbolshas no index onfile_id(pre-existing, documented indocs/runbooks/indexing-parallelism.md§2.3); this change reduces it toone sequential scan per ≤500 files instead of one per file, but doesn't
remove it.