Skip to content

Commit 00b2fd7

Browse files
committed
fix: de-duplicate findCallers in contextData, convert bare db.prepare to StmtCache pattern (#558)
- Refactor buildCallers/buildRelatedTests to share pre-fetched caller rows, eliminating redundant DB roundtrip in contextData path - Convert upstreamStmt/nodeByIdStmt in dependencies.ts to StmtCache<T> pattern - Convert defsStmt in impact.ts to StmtCache<T> pattern - Replace r.role as string cast with local variable in roles.ts
1 parent 4b197cd commit 00b2fd7

4 files changed

Lines changed: 52 additions & 18 deletions

File tree

src/domain/analysis/context.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,8 @@ function buildCallees(
127127
return callees;
128128
}
129129

130-
function buildCallers(db: BetterSqlite3Database, node: NodeRow, noTests: boolean) {
131-
let callerRows: Array<RelatedNodeRow & { viaHierarchy?: string }> = findCallers(
130+
function fetchCallerRows(db: BetterSqlite3Database, node: NodeRow) {
131+
const callerRows: Array<RelatedNodeRow & { viaHierarchy?: string }> = findCallers(
132132
db,
133133
node.id,
134134
) as RelatedNodeRow[];
@@ -142,9 +142,16 @@ function buildCallers(db: BetterSqlite3Database, node: NodeRow, noTests: boolean
142142
callerRows.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
143143
}
144144
}
145-
if (noTests) callerRows = callerRows.filter((c) => !isTestFile(c.file));
145+
return callerRows;
146+
}
147+
148+
function buildCallers(
149+
callerRows: Array<RelatedNodeRow & { viaHierarchy?: string }>,
150+
noTests: boolean,
151+
) {
152+
const filtered = noTests ? callerRows.filter((c) => !isTestFile(c.file)) : callerRows;
146153

147-
return callerRows.map((c) => ({
154+
return filtered.map((c) => ({
148155
name: c.name,
149156
kind: c.kind,
150157
file: c.file,
@@ -179,13 +186,11 @@ function buildImplementationInfo(db: BetterSqlite3Database, node: NodeRow, noTes
179186
}
180187

181188
function buildRelatedTests(
182-
db: BetterSqlite3Database,
183-
node: NodeRow,
189+
callerRows: RelatedNodeRow[],
184190
getFileLines: (file: string) => string[] | null,
185191
includeTests: boolean,
186192
) {
187-
const testCallerRows = findCallers(db, node.id) as RelatedNodeRow[];
188-
const testCallers = testCallerRows.filter((c) => isTestFile(c.file));
193+
const testCallers = callerRows.filter((c) => isTestFile(c.file));
189194

190195
const testsByFile = new Map<string, RelatedNodeRow[]>();
191196
for (const tc of testCallers) {
@@ -464,8 +469,9 @@ export function contextData(
464469
depth,
465470
displayOpts,
466471
});
467-
const callers = buildCallers(db, node, noTests);
468-
const relatedTests = buildRelatedTests(db, node, getFileLines, includeTests);
472+
const allCallerRows = fetchCallerRows(db, node);
473+
const callers = buildCallers(allCallerRows, noTests);
474+
const relatedTests = buildRelatedTests(allCallerRows, getFileLines, includeTests);
469475
const complexityMetrics = getComplexityMetrics(db, node.id);
470476
const nodeChildren = getNodeChildrenSafe(db, node.id);
471477
const implInfo = buildImplementationInfo(db, node, noTests);

src/domain/analysis/dependencies.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,26 @@ import {
77
findNodesByFile,
88
openReadonlyOrFail,
99
} from '../../db/index.js';
10+
import { cachedStmt } from '../../db/repository/cached-stmt.js';
1011
import { isTestFile } from '../../infrastructure/test-filter.js';
1112
import { resolveMethodViaHierarchy } from '../../shared/hierarchy.js';
1213
import { normalizeSymbol } from '../../shared/normalize.js';
1314
import { paginateResult } from '../../shared/paginate.js';
14-
import type { BetterSqlite3Database, ImportEdgeRow, NodeRow, RelatedNodeRow } from '../../types.js';
15+
import type {
16+
BetterSqlite3Database,
17+
ImportEdgeRow,
18+
NodeRow,
19+
RelatedNodeRow,
20+
StmtCache,
21+
} from '../../types.js';
1522
import { findMatchingNodes } from './symbol-lookup.js';
1623

24+
type UpstreamRow = { id: number; name: string; kind: string; file: string; line: number };
25+
type NodeByIdRow = { name: string; kind: string; file: string; line: number };
26+
27+
const _upstreamStmtCache: StmtCache<UpstreamRow> = new WeakMap();
28+
const _nodeByIdStmtCache: StmtCache<NodeByIdRow> = new WeakMap();
29+
1730
export function fileDepsData(
1831
file: string,
1932
customDbPath: string,
@@ -71,11 +84,15 @@ function buildTransitiveCallers(
7184
const visited = new Set([nodeId]);
7285
let frontier = callers;
7386

74-
const upstreamStmt = db.prepare(`
87+
const upstreamStmt = cachedStmt(
88+
_upstreamStmtCache,
89+
db,
90+
`
7591
SELECT n.id, n.name, n.kind, n.file, n.line
7692
FROM edges e JOIN nodes n ON e.source_id = n.id
7793
WHERE e.target_id = ? AND e.kind = 'calls'
78-
`);
94+
`,
95+
);
7996

8097
for (let d = 2; d <= depth; d++) {
8198
const nextFrontier: typeof frontier = [];
@@ -328,8 +345,12 @@ function reconstructPath(
328345
pathIds: number[],
329346
parent: Map<number, { parentId: number; edgeKind: string }>,
330347
) {
331-
const nodeCache = new Map<number, { name: string; kind: string; file: string; line: number }>();
332-
const nodeByIdStmt = db.prepare('SELECT name, kind, file, line FROM nodes WHERE id = ?');
348+
const nodeCache = new Map<number, NodeByIdRow>();
349+
const nodeByIdStmt = cachedStmt(
350+
_nodeByIdStmtCache,
351+
db,
352+
'SELECT name, kind, file, line FROM nodes WHERE id = ?',
353+
);
333354
const getNode = (id: number) => {
334355
if (nodeCache.has(id)) return nodeCache.get(id)!;
335356
const row = nodeByIdStmt.get(id) as {

src/domain/analysis/impact.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
findNodeById,
1111
openReadonlyOrFail,
1212
} from '../../db/index.js';
13+
import { cachedStmt } from '../../db/repository/cached-stmt.js';
1314
import { evaluateBoundaries } from '../../features/boundaries.js';
1415
import { coChangeForFiles } from '../../features/cochange.js';
1516
import { ownersForFiles } from '../../features/owners.js';
@@ -18,9 +19,11 @@ import { debug } from '../../infrastructure/logger.js';
1819
import { isTestFile } from '../../infrastructure/test-filter.js';
1920
import { normalizeSymbol } from '../../shared/normalize.js';
2021
import { paginateResult } from '../../shared/paginate.js';
21-
import type { BetterSqlite3Database, NodeRow, RelatedNodeRow } from '../../types.js';
22+
import type { BetterSqlite3Database, NodeRow, RelatedNodeRow, StmtCache } from '../../types.js';
2223
import { findMatchingNodes } from './symbol-lookup.js';
2324

25+
const _defsStmtCache: StmtCache<NodeRow> = new WeakMap();
26+
2427
// --- Shared BFS: transitive callers ---
2528

2629
const INTERFACE_LIKE_KINDS = new Set(['interface', 'trait']);
@@ -338,7 +341,9 @@ function findAffectedFunctions(
338341
noTests: boolean,
339342
): NodeRow[] {
340343
const affectedFunctions: NodeRow[] = [];
341-
const defsStmt = db.prepare(
344+
const defsStmt = cachedStmt(
345+
_defsStmtCache,
346+
db,
342347
`SELECT * FROM nodes WHERE file = ? AND kind IN ('function', 'method', 'class') ORDER BY line`,
343348
);
344349
for (const [file, ranges] of changedRanges) {

src/domain/analysis/roles.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ export function rolesData(
5151

5252
const summary: Record<string, number> = {};
5353
for (const r of rows) {
54-
summary[r.role as string] = (summary[r.role as string] || 0) + 1;
54+
// SQL guarantees role IS NOT NULL
55+
const role = r.role as string;
56+
summary[role] = (summary[role] || 0) + 1;
5557
}
5658

5759
const hc = new Map();

0 commit comments

Comments
 (0)