From 2a9fee99d8575793a7c7781f27c2e5600da59c14 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 16 Oct 2026 14:22:18 -0700 Subject: [PATCH] fix: avoid SQLite variable limit in thread lookups --- CHANGELOG.md | 2 ++ docs/clustering.md | 2 ++ internal/store/thread_ids_test.go | 40 +++++++++++++++++++++++++++++++ internal/store/vectors.go | 13 ++++------ 4 files changed, 48 insertions(+), 9 deletions(-) create mode 100644 internal/store/thread_ids_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bbf9c02..502adb49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Fix large neighbor and search result lookups failing with SQLite's SQL variable limit. Thanks @masatohoshino for reporting. + ## 0.15.2 - 2026-10-16 **Highlights:** Reliable local database initialization inside Git worktrees and refreshed dependencies. diff --git a/docs/clustering.md b/docs/clustering.md index 74d9bd1e..294c3eb8 100644 --- a/docs/clustering.md +++ b/docs/clustering.md @@ -130,6 +130,8 @@ gitcrawl neighbors owner/repo --number 123 --limit 10 Useful for "what else looks like this?" without committing to a cluster. If the source row is present but lacks a current embedding, the CLI command backfills that one row when an OpenAI key is configured; otherwise it prints the exact `gitcrawl embed --number ... --limit 1` command to run. The TUI's `n` shortcut and "Enter on a member" also load vector neighbors for the selected row. +Large `--limit` values are supported without hitting SQLite's SQL variable limit when loading result metadata. Neighbor search still scores all eligible stored vectors, so larger archives can take longer even with a small result limit. + ## Tuning recipes ### My clusters are too greedy diff --git a/internal/store/thread_ids_test.go b/internal/store/thread_ids_test.go new file mode 100644 index 00000000..1433ec7a --- /dev/null +++ b/internal/store/thread_ids_test.go @@ -0,0 +1,40 @@ +package store + +import ( + "context" + "path/filepath" + "testing" +) + +func TestThreadsByIDsBeyondSQLiteParameterLimit(t *testing.T) { + ctx := context.Background() + st, err := Open(ctx, filepath.Join(t.TempDir(), "gitcrawl.db")) + if err != nil { + t.Fatal(err) + } + defer st.Close() + repoID, threadIDs := seedVectorThreads(t, ctx, st) + otherRepoID, err := st.UpsertRepository(ctx, Repository{Owner: "fixture", Name: "other", FullName: "fixture/other"}) + if err != nil { + t.Fatal(err) + } + otherID, err := st.UpsertThread(ctx, Thread{RepoID: otherRepoID, GitHubID: "other", Number: 1, Kind: "issue", State: "open", Title: "other repository", LabelsJSON: "[]", AssigneesJSON: "[]", RawJSON: "{}"}) + if err != nil { + t.Fatal(err) + } + ids := make([]int64, 32768) + for i := range ids { + ids[i] = int64(i + 100000) + } + ids[0] = threadIDs[0] + ids[len(ids)/2] = otherID + ids[len(ids)-2] = threadIDs[0] + ids[len(ids)-1] = threadIDs[1] + threads, err := st.ThreadsByIDs(ctx, repoID, ids) + if err != nil { + t.Fatalf("large thread lookup: %v", err) + } + if len(threads) != 2 || threads[threadIDs[0]].Number != 301 || threads[threadIDs[1]].Number != 302 { + t.Fatalf("expected only the two requested repository threads, got %+v", threads) + } +} diff --git a/internal/store/vectors.go b/internal/store/vectors.go index 1d20095f..85057f10 100644 --- a/internal/store/vectors.go +++ b/internal/store/vectors.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "math" - "strings" "unicode" ) @@ -138,17 +137,13 @@ func (s *Store) ThreadsByIDs(ctx context.Context, repoID int64, ids []int64) (ma if len(ids) == 0 { return map[int64]Thread{}, nil } - placeholders := make([]string, 0, len(ids)) - args := []any{repoID} - for _, id := range ids { - placeholders = append(placeholders, "?") - args = append(args, id) - } + // Integer slices always marshal successfully; one JSON parameter avoids SQLite's bind limit. + idJSON, _ := json.Marshal(ids) rows, err := s.db.QueryContext(ctx, ` select `+s.threadSelectColumns(ctx, "")+` from threads - where repo_id = ? and id in (`+strings.Join(placeholders, ",")+`) - `, args...) + where repo_id = ? and id in (select value from json_each(?)) + `, repoID, string(idJSON)) if err != nil { return nil, fmt.Errorf("select threads by id: %w", err) }