Files
beads/internal/storage/sqlite/blocked_cache.go
Steve Yegge 62c1f42d9f Add blocked_issues_cache table for GetReadyWork optimization
Introduces a materialized cache table to store blocked issue IDs,
replacing the expensive recursive CTE computation that was causing
~752ms query times on 10K databases (bd-5qim).

The cache is maintained via invalidation on dependency and status
changes, reducing GetReadyWork from O(n²) recursive traversal to
O(1) cache lookup.

Technical details:
- New blocked_issues_cache table with single issue_id column
- ON DELETE CASCADE ensures automatic cleanup
- Migration populates cache using existing recursive CTE logic
- rebuildBlockedCache() fully rebuilds cache on invalidation
- execer interface allows both *sql.DB and *sql.Tx usage

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 19:29:30 -05:00

72 lines
2.1 KiB
Go

package sqlite
import (
"context"
"database/sql"
"fmt"
)
// execer is an interface for types that can execute SQL queries
// Both *sql.DB and *sql.Tx implement this interface
type execer interface {
ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
}
// rebuildBlockedCache completely rebuilds the blocked_issues_cache table
// This is used during cache invalidation when dependencies change
func (s *SQLiteStorage) rebuildBlockedCache(ctx context.Context, tx *sql.Tx) error {
// Use the transaction if provided, otherwise use direct db connection
var exec execer = s.db
if tx != nil {
exec = tx
}
// Clear the cache
if _, err := exec.ExecContext(ctx, "DELETE FROM blocked_issues_cache"); err != nil {
return fmt.Errorf("failed to clear blocked_issues_cache: %w", err)
}
// Rebuild using the recursive CTE logic
query := `
INSERT INTO blocked_issues_cache (issue_id)
WITH RECURSIVE
-- Step 1: Find issues blocked directly by dependencies
blocked_directly AS (
SELECT DISTINCT d.issue_id
FROM dependencies d
JOIN issues blocker ON d.depends_on_id = blocker.id
WHERE d.type = 'blocks'
AND blocker.status IN ('open', 'in_progress', 'blocked')
),
-- Step 2: Propagate blockage to all descendants via parent-child
blocked_transitively AS (
-- Base case: directly blocked issues
SELECT issue_id, 0 as depth
FROM blocked_directly
UNION ALL
-- Recursive case: children of blocked issues inherit blockage
SELECT d.issue_id, bt.depth + 1
FROM blocked_transitively bt
JOIN dependencies d ON d.depends_on_id = bt.issue_id
WHERE d.type = 'parent-child'
AND bt.depth < 50
)
SELECT DISTINCT issue_id FROM blocked_transitively
`
if _, err := exec.ExecContext(ctx, query); err != nil {
return fmt.Errorf("failed to rebuild blocked_issues_cache: %w", err)
}
return nil
}
// invalidateBlockedCache rebuilds the blocked issues cache
// Called when dependencies change or issue status changes
func (s *SQLiteStorage) invalidateBlockedCache(ctx context.Context, tx *sql.Tx) error {
return s.rebuildBlockedCache(ctx, tx)
}