From 1dd310948917a550ed2ac6fcc1198ac4189c0293 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Tue, 14 Oct 2025 13:02:22 -0700 Subject: [PATCH] perf: Add composite index on dependencies(depends_on_id, type) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hierarchical blocking query recursively joins on dependencies with a type filter. Without a composite index, SQLite must scan all dependencies for a given depends_on_id and filter by type afterward. With 10k+ issues and many dependencies per issue, this could cause noticeable slowdowns in ready work calculations. Changes: - Added idx_dependencies_depends_on_type composite index to schema - Added automatic migration for existing databases - Index creation is silent and requires no user intervention The recursive CTE now efficiently seeks (depends_on_id, type) pairs directly instead of post-filtering. Resolves: bd-59 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- internal/storage/sqlite/schema.go | 1 + internal/storage/sqlite/sqlite.go | 35 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/internal/storage/sqlite/schema.go b/internal/storage/sqlite/schema.go index 74167ceb..43d43ba2 100644 --- a/internal/storage/sqlite/schema.go +++ b/internal/storage/sqlite/schema.go @@ -39,6 +39,7 @@ CREATE TABLE IF NOT EXISTS dependencies ( CREATE INDEX IF NOT EXISTS idx_dependencies_issue ON dependencies(issue_id); CREATE INDEX IF NOT EXISTS idx_dependencies_depends_on ON dependencies(depends_on_id); +CREATE INDEX IF NOT EXISTS idx_dependencies_depends_on_type ON dependencies(depends_on_id, type); -- Labels table CREATE TABLE IF NOT EXISTS labels ( diff --git a/internal/storage/sqlite/sqlite.go b/internal/storage/sqlite/sqlite.go index 89c51222..1d27fe6b 100644 --- a/internal/storage/sqlite/sqlite.go +++ b/internal/storage/sqlite/sqlite.go @@ -60,6 +60,11 @@ func New(path string) (*SQLiteStorage, error) { return nil, fmt.Errorf("failed to migrate external_ref column: %w", err) } + // Migrate existing databases to add composite index on dependencies + if err := migrateCompositeIndexes(db); err != nil { + return nil, fmt.Errorf("failed to migrate composite indexes: %w", err) + } + return &SQLiteStorage{ db: db, }, nil @@ -201,6 +206,36 @@ func migrateExternalRefColumn(db *sql.DB) error { return nil } +// migrateCompositeIndexes checks if composite indexes exist and creates them if missing. +// This ensures existing databases get performance optimizations from new indexes. +func migrateCompositeIndexes(db *sql.DB) error { + // Check if idx_dependencies_depends_on_type exists + var indexName string + err := db.QueryRow(` + SELECT name FROM sqlite_master + WHERE type='index' AND name='idx_dependencies_depends_on_type' + `).Scan(&indexName) + + if err == sql.ErrNoRows { + // Index doesn't exist, create it + _, err := db.Exec(` + CREATE INDEX idx_dependencies_depends_on_type ON dependencies(depends_on_id, type) + `) + if err != nil { + return fmt.Errorf("failed to create composite index idx_dependencies_depends_on_type: %w", err) + } + // Index created successfully + return nil + } + + if err != nil { + return fmt.Errorf("failed to check for composite index: %w", err) + } + + // Index exists, no migration needed + return nil +} + // getNextIDForPrefix atomically generates the next ID for a given prefix // Uses the issue_counters table for atomic, cross-process ID generation func (s *SQLiteStorage) getNextIDForPrefix(ctx context.Context, prefix string) (int, error) {