ISS-10710: key the code index's FTS rows by rowid; fast atomic rebuild - #31
Merged
Merged
Conversation
…ebuild PLN-2069 PR 3. Every FTS5 row now carries an explicit rowid taken from the regular table it mirrors -- symbols.rowid for symbol_fts and symbol_trigram, files.rowid for file_path_trigram, and (files.rowid << 20) | line for file_line_fts -- so a reindex deletes a file's rows by rowid seek instead of scanning each table on its UNINDEXED file_path/symbol_id. A single-file reindex of symphony-alpha's index drops from ~1.4 s to a 62 ms median. The full rebuild drops and recreates the four FTS tables instead of emptying them row by row (45.1 s -> 19.1 s for 4.5M line rows), inside a transaction opened with an explicit BEGIN: Python's sqlite3 does not open one for DDL, and an auto-committed DROP would show every reader an empty index for the length of the rebuild. _CODE_INDEXER_SEMANTICS_VERSION goes to 3 so existing workspaces rebuild into the new scheme once. Until that rebuild runs, a single-file reindex returns without writing: the stored FTS rowids mean nothing, so deleting by one would drop another symbol's row. Co-Authored-By: Claude <noreply@anthropic.com>
The pre-prefix symbol_fts migration and the references/call_edges reshape both empty `files` and leave the FTS tables loaded, and an emptied `files` table reads like a never-indexed one -- so the semantics-version guard never fired, the rowid deletes matched nothing and the rowid inserts collided. Also define each FTS5 table's shape once instead of respelling it in the rebuild, and skip the post-rebuild VACUUM of the main DB unless the rowids it may renumber are dense.
A second repo's first index into a shared db_path saw the first repo's FTS rows, forced the full rebuild, and wiped that repo's index. Co-Authored-By: Claude Opus 5 (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.
Campaign
iss-10710-20260919, chunk C3 of 3 (last): delete full-text rows by rowid; make the full rebuild fast and atomicLands PLN-2069 PR 3 for ISS-10710. Based on
main, which already includes C1 (#29) and C2 (#30). Not stacked.What changes
The code index's four FTS5 tables (
symbol_fts,symbol_trigram,file_path_trigram,fts.file_line_fts) could only find a file's rows by scanning the whole table, becausefile_pathandsymbol_idare UNINDEXED columns. Reindexing even one file cost about 1.4–2 s on symphony-alpha (12.4M indexed lines). Now:Every FTS row carries an explicit rowid taken from the regular table it mirrors:
symbol_ftsandsymbol_trigramuse the symbol'ssymbols.rowid;file_path_trigramuses the file'sfiles.rowid;file_line_ftsuses(files.rowid << 20) | line._MAX_FILE_BYTEScaps a file at 1,000,000 lines, under 2^20, and an import-time assertion ties the two together.So a file's rows are deleted directly by rowid.
The full rebuild drops and recreates the FTS tables instead of deleting 12M rows, inside one transaction opened with an explicit
BEGIN. Readers keep answering from the previous index for the whole rebuild (fix(code-intel): serve code tools from the last committed index during a reindex #27)._CODE_INDEXER_SEMANTICS_VERSIONis now 3, so an existing workspace rebuilds into the new scheme once. Until it does, a per-file reindex refuses to write, because deleting by rowid on an old-scheme index would remove another symbol's row.A guard detects a migration that empties
fileswhile leaving FTS rows behind, and forces the same rebuild. It is scoped to this repo's own rows. Such a state would otherwise make the new rowids collide.Files:
src/lemoncrow/pro/capabilities/code_context/engine.py,tests/core/test_code_context.py.Numbers (copy-on-write clones of symphony-alpha's index)
The one file over 200 ms (208 ms) spends 137 ms in tree-sitter parsing, which this change doesn't touch; its index write is 71 ms. The full rebuild takes 29% of the 600 s autosync timeout, so the one-time upgrade rebuild is left to autosync and the installer is unchanged.
Acceptance ledger: 5 criteria, 5 met
file_path/symbol_idtest_reindexes_delete_fts_rows_by_rowid_not_by_file_path_or_symbol_idtraces the SQL of an incremental run and a single-file reindex. Reverting any delete to a column filter fails it.test_reads_during_reindex.pypasses unedited, including the full-rebuild case. It fails if the explicitBEGINis removed.test_code_context.pypass unedited.Deviations from PLN-2069 PR 3
_reindex_files, outside the functions the plan named. It is the only writer that never checked the semantics version, so on an old-scheme index a rowid delete would have removed another symbol's row.file_pathas well assymbol_id. Two entries with the same file in one batch would collide on thefiles.rowid-keyed tables; the campaign required a collision to be impossible, not merely unlikely.closedloop-ai/lemoncrowwithREPO_NOT_IN_PROJECT_POOL. This has no effect on the code.Review: two rounds
Round 1:
/code-review:deepat51da8133returned NEEDS_ATTENTION, with 5 verified findings (3 HIGH, 2 MEDIUM). All were fixed ind8af2ea7:filesleft FTS rows behind, the version probe reported "current", and the new rowid inserts collided with anIntegrityErrorthat never healed. The new_rowid_scheme_trustworthydetects that state and forces the rebuild. Regression test:test_a_migration_that_empties_files_rebuilds_instead_of_colliding_on_rowid, which failed withsqlite3.IntegrityErrorbefore the fix._init_schemaowns. The table shapes are now defined once (_FTS_TABLE_BODY,_fts_create_sql()), with a drift test.VACUUMcould renumberfiles/symbolsrowids out of sync with the FTS tables. Instead of migrating both tables toINTEGER PRIMARY KEY, the post-rebuild VACUUM is skipped once those rowids go sparse, with a test.Round 2: the one campaign re-review (
--since-last-reviewatd8af2ea7) confirmed all five fixed, and found:--db-path), a second repo's first index would force a rebuild that dropped the first repo's rows. No default setup shares a database. Fixed infc907828: the probe is scoped to this repo'sfile_path_trigramandfts.file_line_ftsrows. Regression test:test_a_new_repo_indexed_into_a_shared_db_leaves_the_other_repos_index_intact, which failed before the fix.symbol_ftsand its vocab view is still written out in three places;Verification
All pass:
tests/core/test_code_context.py(98 tests)tests/infra/code_intel(386 tests)tests/test_mypyc_compile_safety.pymypyonengine.pyruffgit diff --checkThe full suite was not run locally; CI runs it here.
Rollout note
After this merges and is installed, each workspace's index rebuilds once on its next autosync pass: about 3 minutes for symphony-alpha. Code search keeps answering throughout, marked as refreshing (#27). Nothing is owed by later chunks: this is the campaign's last.