Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ All notable changes to this project are documented here. The format is based on

## [Unreleased]

### Fixed

- Removing files from a large index no longer takes hours: `chunks.symbol_id` and
`symbols.parent_id` are now indexed, so a deleted symbol no longer costs a full scan
of both tables. Existing indexes gain the two indexes the next time they are opened,
with no rebuild.

## [2.1.2] - 2026-09-25

### Fixed
Expand Down
2 changes: 2 additions & 0 deletions docs/SCHEMA.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ CREATE TABLE chunks (
symbol_names TEXT NOT NULL DEFAULT '' -- denormalized symbol name, FTS-indexed (mirrored by triggers)
);
CREATE INDEX idx_chunks_file ON chunks(file_id);
CREATE INDEX idx_chunks_symbol ON chunks(symbol_id);

-- Symbols extracted via tree-sitter (definitions).
CREATE TABLE symbols (
Expand All @@ -63,6 +64,7 @@ CREATE TABLE symbols (
CREATE INDEX idx_symbols_name ON symbols(name);
CREATE INDEX idx_symbols_file ON symbols(file_id);
CREATE INDEX idx_symbols_kind ON symbols(kind);
CREATE INDEX idx_symbols_parent ON symbols(parent_id);

-- Graph edges: imports / calls / references / inheritance / dependencies.
-- src is always a known symbol or file; dst may be unresolved (dst_id NULL, dst_name kept).
Expand Down
4 changes: 4 additions & 0 deletions src/codebase_index/storage/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ CREATE TABLE IF NOT EXISTS symbols (
CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_id);
CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind);
-- Every deleted symbol looks up its children for ON DELETE SET NULL here and on
-- chunks.symbol_id; without an index each lookup is a full table scan.
CREATE INDEX IF NOT EXISTS idx_symbols_parent ON symbols(parent_id);

CREATE TABLE IF NOT EXISTS chunks (
id INTEGER PRIMARY KEY,
Expand All @@ -52,6 +55,7 @@ CREATE TABLE IF NOT EXISTS chunks (
symbol_names TEXT NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_chunks_file ON chunks(file_id);
CREATE INDEX IF NOT EXISTS idx_chunks_symbol ON chunks(symbol_id);

CREATE TABLE IF NOT EXISTS edges (
id INTEGER PRIMARY KEY,
Expand Down
29 changes: 29 additions & 0 deletions tests/test_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,35 @@ def test_future_schema_version_raises(tmp_path):
pass


def test_every_foreign_key_column_leads_an_index(tmp_path):
"""Without one, SQLite scans the child table for every deleted parent row."""
with Database(tmp_path / "index.sqlite") as db:
conn = db.conn
tables = [
r[0]
for r in conn.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'fts_%'"
)
]
for table in tables:
leading = {
conn.execute(f"PRAGMA index_info({index['name']})").fetchone()["name"]
for index in conn.execute(f"PRAGMA index_list({table})").fetchall()
}
for fk in conn.execute(f"PRAGMA foreign_key_list({table})").fetchall():
assert fk["from"] in leading, f"{table}.{fk['from']} has no index"


def test_existing_index_gains_new_indexes_on_open(tmp_path):
db_path = tmp_path / "index.sqlite"
with Database(db_path) as db:
db.conn.execute("DROP INDEX idx_chunks_symbol")
db.conn.execute("DROP INDEX idx_symbols_parent")
with Database(db_path) as db:
names = {r[0] for r in db.conn.execute("SELECT name FROM sqlite_master WHERE type='index'")}
assert {"idx_chunks_symbol", "idx_symbols_parent"} <= names


def _open(tmp_path):
return Database(tmp_path / "index.sqlite").open()

Expand Down
Loading