11import path from 'node:path' ;
22import { openReadonlyOrFail , openReadonlyWithNative , testFilterSQL } from '../../db/index.js' ;
3- import { cachedStmt } from '../../db/repository/cached-stmt.js' ;
43import { loadConfig } from '../../infrastructure/config.js' ;
54import { debug } from '../../infrastructure/logger.js' ;
65import { isTestFile } from '../../infrastructure/test-filter.js' ;
76import { DEAD_ROLE_PREFIX } from '../../shared/kinds.js' ;
8- import type { BetterSqlite3Database , StmtCache } from '../../types.js' ;
7+ import type { BetterSqlite3Database } from '../../types.js' ;
98import { findCycles } from '../graph/cycles.js' ;
109import { LANGUAGE_REGISTRY } from '../parser.js' ;
1110
12- // ---------------------------------------------------------------------------
13- // Statement caches (one prepared statement per db instance)
14- // ---------------------------------------------------------------------------
15-
16- const _fileNodesStmtCache : StmtCache < { id : number ; file : string } > = new WeakMap ( ) ;
17- const _allNodesStmtCache : StmtCache < { id : number ; file : string } > = new WeakMap ( ) ;
18-
1911export const FALSE_POSITIVE_NAMES = new Set ( [
2012 'run' ,
2113 'get' ,
@@ -52,48 +44,11 @@ export const FALSE_POSITIVE_CALLER_THRESHOLD = 20;
5244// Section helpers
5345// ---------------------------------------------------------------------------
5446
55- const _fileNodesStmt : StmtCache < { id : number ; file : string } > = new WeakMap ( ) ;
56- const _allNodesIdFileStmt : StmtCache < { id : number ; file : string } > = new WeakMap ( ) ;
57-
58- function buildTestFileIds ( db : BetterSqlite3Database ) : Set < number > {
59- const allFileNodes = cachedStmt (
60- _fileNodesStmt ,
61- db ,
62- "SELECT id, file FROM nodes WHERE kind = 'file'" ,
63- ) . all ( ) ;
64- const testFileIds = new Set < number > ( ) ;
65- const testFiles = new Set < string > ( ) ;
66- for ( const n of allFileNodes ) {
67- if ( isTestFile ( n . file ) ) {
68- testFileIds . add ( n . id ) ;
69- testFiles . add ( n . file ) ;
70- }
71- }
72- const allNodes = cachedStmt ( _allNodesIdFileStmt , db , 'SELECT id, file FROM nodes' ) . all ( ) ;
73- for ( const n of allNodes ) {
74- if ( testFiles . has ( n . file ) ) testFileIds . add ( n . id ) ;
75- }
76- return testFileIds ;
77- }
78-
79- function countNodesByKind ( db : BetterSqlite3Database , testFileIds : Set < number > | null ) {
80- let nodeRows : Array < { kind : string ; c : number } > ;
81- if ( testFileIds ) {
82- const allNodes = db . prepare ( 'SELECT id, kind, file FROM nodes' ) . all ( ) as Array < {
83- id : number ;
84- kind : string ;
85- file : string ;
86- } > ;
87- const filtered = allNodes . filter ( ( n ) => ! testFileIds . has ( n . id ) ) ;
88- const counts : Record < string , number > = { } ;
89- for ( const n of filtered ) counts [ n . kind ] = ( counts [ n . kind ] || 0 ) + 1 ;
90- nodeRows = Object . entries ( counts ) . map ( ( [ kind , c ] ) => ( { kind, c } ) ) ;
91- } else {
92- nodeRows = db . prepare ( 'SELECT kind, COUNT(*) as c FROM nodes GROUP BY kind' ) . all ( ) as Array < {
93- kind : string ;
94- c : number ;
95- } > ;
96- }
47+ function countNodesByKind ( db : BetterSqlite3Database , noTests : boolean ) {
48+ const testFilter = testFilterSQL ( 'file' , noTests ) ;
49+ const nodeRows = db
50+ . prepare ( `SELECT kind, COUNT(*) as c FROM nodes WHERE 1=1 ${ testFilter } GROUP BY kind` )
51+ . all ( ) as Array < { kind : string ; c : number } > ;
9752 const byKind : Record < string , number > = { } ;
9853 let total = 0 ;
9954 for ( const r of nodeRows ) {
@@ -103,20 +58,21 @@ function countNodesByKind(db: BetterSqlite3Database, testFileIds: Set<number> |
10358 return { total, byKind } ;
10459}
10560
106- function countEdgesByKind ( db : BetterSqlite3Database , testFileIds : Set < number > | null ) {
61+ function countEdgesByKind ( db : BetterSqlite3Database , noTests : boolean ) {
10762 let edgeRows : Array < { kind : string ; c : number } > ;
108- if ( testFileIds ) {
109- const allEdges = db . prepare ( 'SELECT source_id, target_id, kind FROM edges' ) . all ( ) as Array < {
110- source_id : number ;
111- target_id : number ;
112- kind : string ;
113- } > ;
114- const filtered = allEdges . filter (
115- ( e ) => ! testFileIds . has ( e . source_id ) && ! testFileIds . has ( e . target_id ) ,
116- ) ;
117- const counts : Record < string , number > = { } ;
118- for ( const e of filtered ) counts [ e . kind ] = ( counts [ e . kind ] || 0 ) + 1 ;
119- edgeRows = Object . entries ( counts ) . map ( ( [ kind , c ] ) => ( { kind, c } ) ) ;
63+ if ( noTests ) {
64+ // Join edges with source node to filter out test files in SQL
65+ const srcFilter = testFilterSQL ( 'ns.file' , true ) ;
66+ const tgtFilter = testFilterSQL ( 'nt.file' , true ) ;
67+ edgeRows = db
68+ . prepare ( `
69+ SELECT e.kind, COUNT(*) as c FROM edges e
70+ JOIN nodes ns ON e.source_id = ns.id
71+ JOIN nodes nt ON e.target_id = nt.id
72+ WHERE 1=1 ${ srcFilter } ${ tgtFilter }
73+ GROUP BY e.kind
74+ ` )
75+ . all ( ) as Array < { kind : string ; c : number } > ;
12076 } else {
12177 edgeRows = db . prepare ( 'SELECT kind, COUNT(*) as c FROM edges GROUP BY kind' ) . all ( ) as Array < {
12278 kind : string ;
@@ -157,16 +113,25 @@ function findHotspots(db: BetterSqlite3Database, noTests: boolean, limit: number
157113 const hotspotRows = db
158114 . prepare ( `
159115 SELECT n.file,
160- (SELECT COUNT(*) FROM edges WHERE target_id = n.id ) as fan_in,
161- (SELECT COUNT(*) FROM edges WHERE source_id = n.id ) as fan_out
116+ COALESCE(fi.cnt, 0 ) as fan_in,
117+ COALESCE(fo.cnt, 0 ) as fan_out
162118 FROM nodes n
119+ LEFT JOIN (
120+ SELECT target_id, COUNT(*) AS cnt FROM edges
121+ WHERE kind NOT IN ('contains', 'parameter_of', 'receiver')
122+ GROUP BY target_id
123+ ) fi ON fi.target_id = n.id
124+ LEFT JOIN (
125+ SELECT source_id, COUNT(*) AS cnt FROM edges
126+ WHERE kind NOT IN ('contains', 'parameter_of', 'receiver')
127+ GROUP BY source_id
128+ ) fo ON fo.source_id = n.id
163129 WHERE n.kind = 'file' ${ testFilter }
164- ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id)
165- + (SELECT COUNT(*) FROM edges WHERE source_id = n.id) DESC
130+ ORDER BY COALESCE(fi.cnt, 0) + COALESCE(fo.cnt, 0) DESC
131+ LIMIT ?
166132 ` )
167- . all ( ) as Array < { file : string ; fan_in : number ; fan_out : number } > ;
168- const filtered = noTests ? hotspotRows . filter ( ( r ) => ! isTestFile ( r . file ) ) : hotspotRows ;
169- return filtered . slice ( 0 , limit ) . map ( ( r ) => ( {
133+ . all ( limit ) as Array < { file : string ; fan_in : number ; fan_out : number } > ;
134+ return hotspotRows . map ( ( r ) => ( {
170135 file : r . file ,
171136 fanIn : r . fan_in ,
172137 fanOut : r . fan_out ,
@@ -275,20 +240,12 @@ function computeQualityMetrics(
275240}
276241
277242function countRoles ( db : BetterSqlite3Database , noTests : boolean ) {
278- let roleRows : Array < { role : string ; c : number } > ;
279- if ( noTests ) {
280- const allRoleNodes = db
281- . prepare ( 'SELECT role, file FROM nodes WHERE role IS NOT NULL' )
282- . all ( ) as Array < { role : string ; file : string } > ;
283- const filtered = allRoleNodes . filter ( ( n ) => ! isTestFile ( n . file ) ) ;
284- const counts : Record < string , number > = { } ;
285- for ( const n of filtered ) counts [ n . role ] = ( counts [ n . role ] || 0 ) + 1 ;
286- roleRows = Object . entries ( counts ) . map ( ( [ role , c ] ) => ( { role, c } ) ) ;
287- } else {
288- roleRows = db
289- . prepare ( 'SELECT role, COUNT(*) as c FROM nodes WHERE role IS NOT NULL GROUP BY role' )
290- . all ( ) as Array < { role : string ; c : number } > ;
291- }
243+ const testFilter = testFilterSQL ( 'file' , noTests ) ;
244+ const roleRows = db
245+ . prepare (
246+ `SELECT role, COUNT(*) as c FROM nodes WHERE role IS NOT NULL ${ testFilter } GROUP BY role` ,
247+ )
248+ . all ( ) as Array < { role : string ; c : number } > ;
292249 const roles : Record < string , number > & { dead ?: number } = { } ;
293250 let deadTotal = 0 ;
294251 for ( const r of roleRows ) {
@@ -344,13 +301,23 @@ export function moduleMapData(customDbPath: string, limit = 20, opts: { noTests?
344301
345302 const nodes = db
346303 . prepare ( `
347- SELECT n.* ,
348- (SELECT COUNT(*) FROM edges WHERE source_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver') ) as out_edges,
349- (SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver') ) as in_edges
304+ SELECT n.file ,
305+ COALESCE(fo.cnt, 0 ) as out_edges,
306+ COALESCE(fi.cnt, 0 ) as in_edges
350307 FROM nodes n
308+ LEFT JOIN (
309+ SELECT source_id, COUNT(*) AS cnt FROM edges
310+ WHERE kind NOT IN ('contains', 'parameter_of', 'receiver')
311+ GROUP BY source_id
312+ ) fo ON fo.source_id = n.id
313+ LEFT JOIN (
314+ SELECT target_id, COUNT(*) AS cnt FROM edges
315+ WHERE kind NOT IN ('contains', 'parameter_of', 'receiver')
316+ GROUP BY target_id
317+ ) fi ON fi.target_id = n.id
351318 WHERE n.kind = 'file'
352319 ${ testFilter }
353- ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind NOT IN ('contains', 'parameter_of', 'receiver') ) DESC
320+ ORDER BY COALESCE(fi.cnt, 0 ) DESC
354321 LIMIT ?
355322 ` )
356323 . all ( limit ) as Array < { file : string ; in_edges : number ; out_edges : number } > ;
@@ -486,10 +453,9 @@ export function statsData(customDbPath: string, opts: { noTests?: boolean; confi
486453
487454 // ── JS fallback ───────────────────────────────────────────────────
488455 const testFilter = testFilterSQL ( 'n.file' , noTests ) ;
489- const testFileIds = noTests ? buildTestFileIds ( db ) : null ;
490456
491- const { total : totalNodes , byKind : nodesByKind } = countNodesByKind ( db , testFileIds ) ;
492- const { total : totalEdges , byKind : edgesByKind } = countEdgesByKind ( db , testFileIds ) ;
457+ const { total : totalNodes , byKind : nodesByKind } = countNodesByKind ( db , noTests ) ;
458+ const { total : totalEdges , byKind : edgesByKind } = countEdgesByKind ( db , noTests ) ;
493459
494460 const hotspots = findHotspots ( db , noTests , 5 ) ;
495461 const embeddings = getEmbeddingsInfo ( db ) ;
0 commit comments