fix(db): chunk deleteResolvedReferences IN-list under the SQLite param limit (#1001) (#1023)

deleteResolvedReferences bound every id into a single unbounded
`IN (...)`, so a list longer than SQLITE_MAX_VARIABLE_NUMBER (32766 on
the bundled node:sqlite) threw "too many SQL variables" — the one IN-list
in queries.ts that #540 missed. It's reachable only through the exported
QueryBuilder (library use): the internal resolution path uses
deleteSpecificResolvedReferences, which binds per-row and is immune, so
the CLI/MCP indexing pipeline was never affected. Wrap it in the same
SQLITE_PARAM_CHUNK_SIZE loop every sibling query uses, and add a
regression test (33k ids, past the real 32766 ceiling) that throws
without the fix.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-27 14:00:55 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent f83a1ecc8e
commit 30dc303f4c
3 changed files with 55 additions and 2 deletions
+44
View File
@@ -97,6 +97,50 @@ describe('getNodesByIds (batch lookup)', () => {
});
});
describe('deleteResolvedReferences (chunking)', () => {
let dir: string;
let db: DatabaseConnection;
let q: QueryBuilder;
beforeEach(() => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'db-perf-delref-'));
db = DatabaseConnection.initialize(path.join(dir, 'test.db'));
q = new QueryBuilder(db.getDb());
});
afterEach(() => {
db.close();
if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
});
it('deletes unresolved refs for more ids than the SQLite parameter limit (#1001)', () => {
// Regression: this method bound every id as one parameter in a single
// IN (...), so passing more ids than SQLITE_MAX_VARIABLE_NUMBER (32766 on
// the bundled node:sqlite) threw "too many SQL variables". Use 33000 to
// clear that ceiling. from_node_id has a FK to nodes, so insert nodes first.
const nodes = Array.from({ length: 33000 }, (_, i) => makeNode(`n${i}`));
q.insertNodes(nodes);
q.insertUnresolvedRefsBatch(
nodes.map((n) => ({
fromNodeId: n.id,
referenceName: 'someName',
referenceKind: 'calls',
line: 1,
column: 0,
}))
);
expect(q.getUnresolvedReferencesCount()).toBe(33000);
const ids = nodes.map((n) => n.id);
expect(() => q.deleteResolvedReferences(ids)).not.toThrow();
expect(q.getUnresolvedReferencesCount()).toBe(0);
});
it('handles an empty input array', () => {
expect(() => q.deleteResolvedReferences([])).not.toThrow();
});
});
describe('insertNode cache invalidation', () => {
let dir: string;
let db: DatabaseConnection;