diff --git a/src/db/repository/cached-stmt.js b/src/db/repository/cached-stmt.js new file mode 100644 index 0000000..6a3ef8c --- /dev/null +++ b/src/db/repository/cached-stmt.js @@ -0,0 +1,19 @@ +/** + * Resolve a cached prepared statement, compiling on first use per db. + * Each `cache` WeakMap must always be called with the same `sql` — + * the sql argument is only used on the first compile; subsequent calls + * return the cached statement regardless of the sql passed. + * + * @param {WeakMap} cache - WeakMap keyed by db instance + * @param {object} db - better-sqlite3 database instance + * @param {string} sql - SQL to compile on first use + * @returns {object} prepared statement + */ +export function cachedStmt(cache, db, sql) { + let stmt = cache.get(db); + if (!stmt) { + stmt = db.prepare(sql); + cache.set(db, stmt); + } + return stmt; +} diff --git a/src/db/repository/cfg.js b/src/db/repository/cfg.js index 1a34357..42fe4b7 100644 --- a/src/db/repository/cfg.js +++ b/src/db/repository/cfg.js @@ -1,5 +1,6 @@ +import { cachedStmt } from './cached-stmt.js'; + // ─── Statement caches (one prepared statement per db instance) ──────────── -// WeakMap keys on the db object so statements are GC'd when the db closes. const _getCfgBlocksStmt = new WeakMap(); const _getCfgEdgesStmt = new WeakMap(); const _deleteCfgEdgesStmt = new WeakMap(); @@ -26,16 +27,13 @@ export function hasCfgTables(db) { * @returns {object[]} */ export function getCfgBlocks(db, functionNodeId) { - let stmt = _getCfgBlocksStmt.get(db); - if (!stmt) { - stmt = db.prepare( - `SELECT id, block_index, block_type, start_line, end_line, label - FROM cfg_blocks WHERE function_node_id = ? - ORDER BY block_index`, - ); - _getCfgBlocksStmt.set(db, stmt); - } - return stmt.all(functionNodeId); + return cachedStmt( + _getCfgBlocksStmt, + db, + `SELECT id, block_index, block_type, start_line, end_line, label + FROM cfg_blocks WHERE function_node_id = ? + ORDER BY block_index`, + ).all(functionNodeId); } /** @@ -45,21 +43,18 @@ export function getCfgBlocks(db, functionNodeId) { * @returns {object[]} */ export function getCfgEdges(db, functionNodeId) { - let stmt = _getCfgEdgesStmt.get(db); - if (!stmt) { - stmt = db.prepare( - `SELECT e.kind, - sb.block_index AS source_index, sb.block_type AS source_type, - tb.block_index AS target_index, tb.block_type AS target_type - FROM cfg_edges e - JOIN cfg_blocks sb ON e.source_block_id = sb.id - JOIN cfg_blocks tb ON e.target_block_id = tb.id - WHERE e.function_node_id = ? - ORDER BY sb.block_index, tb.block_index`, - ); - _getCfgEdgesStmt.set(db, stmt); - } - return stmt.all(functionNodeId); + return cachedStmt( + _getCfgEdgesStmt, + db, + `SELECT e.kind, + sb.block_index AS source_index, sb.block_type AS source_type, + tb.block_index AS target_index, tb.block_type AS target_type + FROM cfg_edges e + JOIN cfg_blocks sb ON e.source_block_id = sb.id + JOIN cfg_blocks tb ON e.target_block_id = tb.id + WHERE e.function_node_id = ? + ORDER BY sb.block_index, tb.block_index`, + ).all(functionNodeId); } /** @@ -68,16 +63,10 @@ export function getCfgEdges(db, functionNodeId) { * @param {number} functionNodeId */ export function deleteCfgForNode(db, functionNodeId) { - let delEdges = _deleteCfgEdgesStmt.get(db); - if (!delEdges) { - delEdges = db.prepare('DELETE FROM cfg_edges WHERE function_node_id = ?'); - _deleteCfgEdgesStmt.set(db, delEdges); - } - let delBlocks = _deleteCfgBlocksStmt.get(db); - if (!delBlocks) { - delBlocks = db.prepare('DELETE FROM cfg_blocks WHERE function_node_id = ?'); - _deleteCfgBlocksStmt.set(db, delBlocks); - } - delEdges.run(functionNodeId); - delBlocks.run(functionNodeId); + cachedStmt(_deleteCfgEdgesStmt, db, 'DELETE FROM cfg_edges WHERE function_node_id = ?').run( + functionNodeId, + ); + cachedStmt(_deleteCfgBlocksStmt, db, 'DELETE FROM cfg_blocks WHERE function_node_id = ?').run( + functionNodeId, + ); } diff --git a/src/db/repository/cochange.js b/src/db/repository/cochange.js index 41451cd..c5a51ee 100644 --- a/src/db/repository/cochange.js +++ b/src/db/repository/cochange.js @@ -1,3 +1,10 @@ +import { cachedStmt } from './cached-stmt.js'; + +// ─── Statement caches (one prepared statement per db instance) ──────────── +const _hasCoChangesStmt = new WeakMap(); +const _getCoChangeMetaStmt = new WeakMap(); +const _upsertCoChangeMetaStmt = new WeakMap(); + /** * Check whether the co_changes table has data. * @param {object} db @@ -5,7 +12,7 @@ */ export function hasCoChanges(db) { try { - return !!db.prepare('SELECT 1 FROM co_changes LIMIT 1').get(); + return !!cachedStmt(_hasCoChangesStmt, db, 'SELECT 1 FROM co_changes LIMIT 1').get(); } catch { return false; } @@ -19,7 +26,11 @@ export function hasCoChanges(db) { export function getCoChangeMeta(db) { const meta = {}; try { - for (const row of db.prepare('SELECT key, value FROM co_change_meta').all()) { + for (const row of cachedStmt( + _getCoChangeMetaStmt, + db, + 'SELECT key, value FROM co_change_meta', + ).all()) { meta[row.key] = row.value; } } catch { @@ -35,7 +46,9 @@ export function getCoChangeMeta(db) { * @param {string} value */ export function upsertCoChangeMeta(db, key, value) { - db.prepare( + cachedStmt( + _upsertCoChangeMetaStmt, + db, 'INSERT INTO co_change_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value', ).run(key, value); } diff --git a/src/db/repository/complexity.js b/src/db/repository/complexity.js index e256a69..f65808b 100644 --- a/src/db/repository/complexity.js +++ b/src/db/repository/complexity.js @@ -1,3 +1,8 @@ +import { cachedStmt } from './cached-stmt.js'; + +// ─── Statement caches (one prepared statement per db instance) ──────────── +const _getComplexityForNodeStmt = new WeakMap(); + /** * Get complexity metrics for a node. * Used by contextData and explainFunctionImpl in queries.js. @@ -6,10 +11,10 @@ * @returns {{ cognitive: number, cyclomatic: number, max_nesting: number, maintainability_index: number, halstead_volume: number }|undefined} */ export function getComplexityForNode(db, nodeId) { - return db - .prepare( - `SELECT cognitive, cyclomatic, max_nesting, maintainability_index, halstead_volume - FROM function_complexity WHERE node_id = ?`, - ) - .get(nodeId); + return cachedStmt( + _getComplexityForNodeStmt, + db, + `SELECT cognitive, cyclomatic, max_nesting, maintainability_index, halstead_volume + FROM function_complexity WHERE node_id = ?`, + ).get(nodeId); } diff --git a/src/db/repository/dataflow.js b/src/db/repository/dataflow.js index 162f925..4cf8eb1 100644 --- a/src/db/repository/dataflow.js +++ b/src/db/repository/dataflow.js @@ -1,3 +1,8 @@ +import { cachedStmt } from './cached-stmt.js'; + +// ─── Statement caches (one prepared statement per db instance) ──────────── +const _hasDataflowTableStmt = new WeakMap(); + /** * Check whether the dataflow table exists and has data. * @param {object} db @@ -5,7 +10,7 @@ */ export function hasDataflowTable(db) { try { - return db.prepare('SELECT COUNT(*) AS c FROM dataflow').get().c > 0; + return cachedStmt(_hasDataflowTableStmt, db, 'SELECT COUNT(*) AS c FROM dataflow').get().c > 0; } catch { return false; } diff --git a/src/db/repository/edges.js b/src/db/repository/edges.js index 53fddb8..a652b56 100644 --- a/src/db/repository/edges.js +++ b/src/db/repository/edges.js @@ -1,21 +1,20 @@ +import { cachedStmt } from './cached-stmt.js'; + // ─── Prepared-statement caches (one per db instance) ──────────────────── -// WeakMap keys on the db object so statements are GC'd when the db closes. const _findCalleesStmt = new WeakMap(); const _findCallersStmt = new WeakMap(); const _findDistinctCallersStmt = new WeakMap(); +const _findAllOutgoingStmt = new WeakMap(); +const _findAllIncomingStmt = new WeakMap(); const _findCalleeNamesStmt = new WeakMap(); const _findCallerNamesStmt = new WeakMap(); +const _findImportTargetsStmt = new WeakMap(); +const _findImportSourcesStmt = new WeakMap(); +const _findImportDependentsStmt = new WeakMap(); +const _findCrossFileCallTargetsStmt = new WeakMap(); +const _countCrossFileCallersStmt = new WeakMap(); const _getClassAncestorsStmt = new WeakMap(); - -/** Resolve a cached prepared statement, compiling on first use per db. */ -function _cached(cache, db, sql) { - let stmt = cache.get(db); - if (!stmt) { - stmt = db.prepare(sql); - cache.set(db, stmt); - } - return stmt; -} +const _findIntraFileCallEdgesStmt = new WeakMap(); // ─── Call-edge queries ────────────────────────────────────────────────── @@ -27,7 +26,7 @@ function _cached(cache, db, sql) { * @returns {{ id: number, name: string, kind: string, file: string, line: number, end_line: number|null }[]} */ export function findCallees(db, nodeId) { - return _cached( + return cachedStmt( _findCalleesStmt, db, `SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line, n.end_line @@ -43,7 +42,7 @@ export function findCallees(db, nodeId) { * @returns {{ id: number, name: string, kind: string, file: string, line: number }[]} */ export function findCallers(db, nodeId) { - return _cached( + return cachedStmt( _findCallersStmt, db, `SELECT n.id, n.name, n.kind, n.file, n.line @@ -59,7 +58,7 @@ export function findCallers(db, nodeId) { * @returns {{ id: number, name: string, kind: string, file: string, line: number }[]} */ export function findDistinctCallers(db, nodeId) { - return _cached( + return cachedStmt( _findDistinctCallersStmt, db, `SELECT DISTINCT n.id, n.name, n.kind, n.file, n.line @@ -77,13 +76,13 @@ export function findDistinctCallers(db, nodeId) { * @returns {{ name: string, kind: string, file: string, line: number, edge_kind: string }[]} */ export function findAllOutgoingEdges(db, nodeId) { - return db - .prepare( - `SELECT n.name, n.kind, n.file, n.line, e.kind AS edge_kind - FROM edges e JOIN nodes n ON e.target_id = n.id - WHERE e.source_id = ?`, - ) - .all(nodeId); + return cachedStmt( + _findAllOutgoingStmt, + db, + `SELECT n.name, n.kind, n.file, n.line, e.kind AS edge_kind + FROM edges e JOIN nodes n ON e.target_id = n.id + WHERE e.source_id = ?`, + ).all(nodeId); } /** @@ -93,13 +92,13 @@ export function findAllOutgoingEdges(db, nodeId) { * @returns {{ name: string, kind: string, file: string, line: number, edge_kind: string }[]} */ export function findAllIncomingEdges(db, nodeId) { - return db - .prepare( - `SELECT n.name, n.kind, n.file, n.line, e.kind AS edge_kind - FROM edges e JOIN nodes n ON e.source_id = n.id - WHERE e.target_id = ?`, - ) - .all(nodeId); + return cachedStmt( + _findAllIncomingStmt, + db, + `SELECT n.name, n.kind, n.file, n.line, e.kind AS edge_kind + FROM edges e JOIN nodes n ON e.source_id = n.id + WHERE e.target_id = ?`, + ).all(nodeId); } // ─── Name-only callee/caller lookups (for embedder) ──────────────────── @@ -111,7 +110,7 @@ export function findAllIncomingEdges(db, nodeId) { * @returns {string[]} */ export function findCalleeNames(db, nodeId) { - return _cached( + return cachedStmt( _findCalleeNamesStmt, db, `SELECT DISTINCT n.name @@ -130,7 +129,7 @@ export function findCalleeNames(db, nodeId) { * @returns {string[]} */ export function findCallerNames(db, nodeId) { - return _cached( + return cachedStmt( _findCallerNamesStmt, db, `SELECT DISTINCT n.name @@ -151,13 +150,13 @@ export function findCallerNames(db, nodeId) { * @returns {{ file: string, edge_kind: string }[]} */ export function findImportTargets(db, nodeId) { - return db - .prepare( - `SELECT n.file, e.kind AS edge_kind - FROM edges e JOIN nodes n ON e.target_id = n.id - WHERE e.source_id = ? AND e.kind IN ('imports', 'imports-type')`, - ) - .all(nodeId); + return cachedStmt( + _findImportTargetsStmt, + db, + `SELECT n.file, e.kind AS edge_kind + FROM edges e JOIN nodes n ON e.target_id = n.id + WHERE e.source_id = ? AND e.kind IN ('imports', 'imports-type')`, + ).all(nodeId); } /** @@ -167,13 +166,13 @@ export function findImportTargets(db, nodeId) { * @returns {{ file: string, edge_kind: string }[]} */ export function findImportSources(db, nodeId) { - return db - .prepare( - `SELECT n.file, e.kind AS edge_kind - FROM edges e JOIN nodes n ON e.source_id = n.id - WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')`, - ) - .all(nodeId); + return cachedStmt( + _findImportSourcesStmt, + db, + `SELECT n.file, e.kind AS edge_kind + FROM edges e JOIN nodes n ON e.source_id = n.id + WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')`, + ).all(nodeId); } /** @@ -184,12 +183,12 @@ export function findImportSources(db, nodeId) { * @returns {object[]} */ export function findImportDependents(db, nodeId) { - return db - .prepare( - `SELECT n.* FROM edges e JOIN nodes n ON e.source_id = n.id - WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')`, - ) - .all(nodeId); + return cachedStmt( + _findImportDependentsStmt, + db, + `SELECT n.* FROM edges e JOIN nodes n ON e.source_id = n.id + WHERE e.target_id = ? AND e.kind IN ('imports', 'imports-type')`, + ).all(nodeId); } // ─── Cross-file and hierarchy queries ────────────────────────────────── @@ -203,13 +202,14 @@ export function findImportDependents(db, nodeId) { */ export function findCrossFileCallTargets(db, file) { return new Set( - db - .prepare( - `SELECT DISTINCT e.target_id FROM edges e - JOIN nodes caller ON e.source_id = caller.id - JOIN nodes target ON e.target_id = target.id - WHERE target.file = ? AND caller.file != ? AND e.kind = 'calls'`, - ) + cachedStmt( + _findCrossFileCallTargetsStmt, + db, + `SELECT DISTINCT e.target_id FROM edges e + JOIN nodes caller ON e.source_id = caller.id + JOIN nodes target ON e.target_id = target.id + WHERE target.file = ? AND caller.file != ? AND e.kind = 'calls'`, + ) .all(file, file) .map((r) => r.target_id), ); @@ -224,12 +224,12 @@ export function findCrossFileCallTargets(db, file) { * @returns {number} */ export function countCrossFileCallers(db, nodeId, file) { - return db - .prepare( - `SELECT COUNT(*) AS cnt FROM edges e JOIN nodes n ON e.source_id = n.id - WHERE e.target_id = ? AND e.kind = 'calls' AND n.file != ?`, - ) - .get(nodeId, file).cnt; + return cachedStmt( + _countCrossFileCallersStmt, + db, + `SELECT COUNT(*) AS cnt FROM edges e JOIN nodes n ON e.source_id = n.id + WHERE e.target_id = ? AND e.kind = 'calls' AND n.file != ?`, + ).get(nodeId, file).cnt; } /** @@ -239,14 +239,14 @@ export function countCrossFileCallers(db, nodeId, file) { * @returns {Set} */ export function getClassHierarchy(db, classNodeId) { - const stmt = _cached( + const ancestors = new Set(); + const queue = [classNodeId]; + const stmt = cachedStmt( _getClassAncestorsStmt, db, `SELECT n.id, n.name FROM edges e JOIN nodes n ON e.target_id = n.id WHERE e.source_id = ? AND e.kind = 'extends'`, ); - const ancestors = new Set(); - const queue = [classNodeId]; while (queue.length > 0) { const current = queue.shift(); const parents = stmt.all(current); @@ -268,14 +268,14 @@ export function getClassHierarchy(db, classNodeId) { * @returns {{ caller_name: string, callee_name: string }[]} */ export function findIntraFileCallEdges(db, file) { - return db - .prepare( - `SELECT caller.name AS caller_name, callee.name AS callee_name - FROM edges e - JOIN nodes caller ON e.source_id = caller.id - JOIN nodes callee ON e.target_id = callee.id - WHERE caller.file = ? AND callee.file = ? AND e.kind = 'calls' - ORDER BY caller.line`, - ) - .all(file, file); + return cachedStmt( + _findIntraFileCallEdgesStmt, + db, + `SELECT caller.name AS caller_name, callee.name AS callee_name + FROM edges e + JOIN nodes caller ON e.source_id = caller.id + JOIN nodes callee ON e.target_id = callee.id + WHERE caller.file = ? AND callee.file = ? AND e.kind = 'calls' + ORDER BY caller.line`, + ).all(file, file); } diff --git a/src/db/repository/embeddings.js b/src/db/repository/embeddings.js index a565af0..9a5af37 100644 --- a/src/db/repository/embeddings.js +++ b/src/db/repository/embeddings.js @@ -1,3 +1,10 @@ +import { cachedStmt } from './cached-stmt.js'; + +// ─── Statement caches (one prepared statement per db instance) ──────────── +const _hasEmbeddingsStmt = new WeakMap(); +const _getEmbeddingCountStmt = new WeakMap(); +const _getEmbeddingMetaStmt = new WeakMap(); + /** * Check whether the embeddings table has data. * @param {object} db @@ -5,7 +12,7 @@ */ export function hasEmbeddings(db) { try { - return !!db.prepare('SELECT 1 FROM embeddings LIMIT 1').get(); + return !!cachedStmt(_hasEmbeddingsStmt, db, 'SELECT 1 FROM embeddings LIMIT 1').get(); } catch { return false; } @@ -18,7 +25,7 @@ export function hasEmbeddings(db) { */ export function getEmbeddingCount(db) { try { - return db.prepare('SELECT COUNT(*) AS c FROM embeddings').get().c; + return cachedStmt(_getEmbeddingCountStmt, db, 'SELECT COUNT(*) AS c FROM embeddings').get().c; } catch { return 0; } @@ -32,7 +39,11 @@ export function getEmbeddingCount(db) { */ export function getEmbeddingMeta(db, key) { try { - const row = db.prepare('SELECT value FROM embedding_meta WHERE key = ?').get(key); + const row = cachedStmt( + _getEmbeddingMetaStmt, + db, + 'SELECT value FROM embedding_meta WHERE key = ?', + ).get(key); return row?.value; } catch { return undefined; diff --git a/src/db/repository/graph-read.js b/src/db/repository/graph-read.js index a350896..bc99e02 100644 --- a/src/db/repository/graph-read.js +++ b/src/db/repository/graph-read.js @@ -1,12 +1,22 @@ +import { cachedStmt } from './cached-stmt.js'; + +// ─── Statement caches (one prepared statement per db instance) ──────────── +const _getCallableNodesStmt = new WeakMap(); +const _getCallEdgesStmt = new WeakMap(); +const _getFileNodesAllStmt = new WeakMap(); +const _getImportEdgesStmt = new WeakMap(); + /** * Get callable nodes (function/method/class) for community detection. * @param {object} db * @returns {{ id: number, name: string, kind: string, file: string }[]} */ export function getCallableNodes(db) { - return db - .prepare("SELECT id, name, kind, file FROM nodes WHERE kind IN ('function','method','class')") - .all(); + return cachedStmt( + _getCallableNodesStmt, + db, + "SELECT id, name, kind, file FROM nodes WHERE kind IN ('function','method','class')", + ).all(); } /** @@ -15,7 +25,11 @@ export function getCallableNodes(db) { * @returns {{ source_id: number, target_id: number }[]} */ export function getCallEdges(db) { - return db.prepare("SELECT source_id, target_id FROM edges WHERE kind = 'calls'").all(); + return cachedStmt( + _getCallEdgesStmt, + db, + "SELECT source_id, target_id FROM edges WHERE kind = 'calls'", + ).all(); } /** @@ -24,7 +38,11 @@ export function getCallEdges(db) { * @returns {{ id: number, name: string, file: string }[]} */ export function getFileNodesAll(db) { - return db.prepare("SELECT id, name, file FROM nodes WHERE kind = 'file'").all(); + return cachedStmt( + _getFileNodesAllStmt, + db, + "SELECT id, name, file FROM nodes WHERE kind = 'file'", + ).all(); } /** @@ -33,7 +51,9 @@ export function getFileNodesAll(db) { * @returns {{ source_id: number, target_id: number }[]} */ export function getImportEdges(db) { - return db - .prepare("SELECT source_id, target_id FROM edges WHERE kind IN ('imports','imports-type')") - .all(); + return cachedStmt( + _getImportEdgesStmt, + db, + "SELECT source_id, target_id FROM edges WHERE kind IN ('imports','imports-type')", + ).all(); } diff --git a/src/db/repository/index.js b/src/db/repository/index.js index 48c7972..f3f0dc8 100644 --- a/src/db/repository/index.js +++ b/src/db/repository/index.js @@ -1,6 +1,7 @@ // Barrel re-export for repository/ modules. export { purgeFileData, purgeFilesData } from './build-stmts.js'; +export { cachedStmt } from './cached-stmt.js'; export { deleteCfgForNode, getCfgBlocks, getCfgEdges, hasCfgTables } from './cfg.js'; export { getCoChangeMeta, hasCoChanges, upsertCoChangeMeta } from './cochange.js'; diff --git a/src/db/repository/nodes.js b/src/db/repository/nodes.js index 8f008fe..7fa3d03 100644 --- a/src/db/repository/nodes.js +++ b/src/db/repository/nodes.js @@ -1,5 +1,6 @@ import { EVERY_SYMBOL_KIND, VALID_ROLES } from '../../kinds.js'; import { NodeQuery } from '../query-builder.js'; +import { cachedStmt } from './cached-stmt.js'; // ─── Query-builder based lookups (moved from src/db/repository.js) ───── @@ -100,13 +101,26 @@ export function iterateFunctionNodes(db, opts = {}) { return _functionNodeQuery(opts).iterate(db); } +// ─── Statement caches (one prepared statement per db instance) ──────────── +// WeakMap keys on the db object so statements are GC'd when the db closes. +const _countNodesStmt = new WeakMap(); +const _countEdgesStmt = new WeakMap(); +const _countFilesStmt = new WeakMap(); +const _findNodeByIdStmt = new WeakMap(); +const _findNodesByFileStmt = new WeakMap(); +const _findFileNodesStmt = new WeakMap(); +const _getNodeIdStmt = new WeakMap(); +const _getFunctionNodeIdStmt = new WeakMap(); +const _bulkNodeIdsByFileStmt = new WeakMap(); +const _findNodeChildrenStmt = new WeakMap(); + /** * Count total nodes. * @param {object} db * @returns {number} */ export function countNodes(db) { - return db.prepare('SELECT COUNT(*) AS cnt FROM nodes').get().cnt; + return cachedStmt(_countNodesStmt, db, 'SELECT COUNT(*) AS cnt FROM nodes').get().cnt; } /** @@ -115,7 +129,7 @@ export function countNodes(db) { * @returns {number} */ export function countEdges(db) { - return db.prepare('SELECT COUNT(*) AS cnt FROM edges').get().cnt; + return cachedStmt(_countEdgesStmt, db, 'SELECT COUNT(*) AS cnt FROM edges').get().cnt; } /** @@ -124,7 +138,7 @@ export function countEdges(db) { * @returns {number} */ export function countFiles(db) { - return db.prepare('SELECT COUNT(DISTINCT file) AS cnt FROM nodes').get().cnt; + return cachedStmt(_countFilesStmt, db, 'SELECT COUNT(DISTINCT file) AS cnt FROM nodes').get().cnt; } // ─── Shared node lookups ─────────────────────────────────────────────── @@ -136,7 +150,7 @@ export function countFiles(db) { * @returns {object|undefined} */ export function findNodeById(db, id) { - return db.prepare('SELECT * FROM nodes WHERE id = ?').get(id); + return cachedStmt(_findNodeByIdStmt, db, 'SELECT * FROM nodes WHERE id = ?').get(id); } /** @@ -146,9 +160,11 @@ export function findNodeById(db, id) { * @returns {object[]} */ export function findNodesByFile(db, file) { - return db - .prepare("SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line") - .all(file); + return cachedStmt( + _findNodesByFileStmt, + db, + "SELECT * FROM nodes WHERE file = ? AND kind != 'file' ORDER BY line", + ).all(file); } /** @@ -158,15 +174,13 @@ export function findNodesByFile(db, file) { * @returns {object[]} */ export function findFileNodes(db, fileLike) { - return db.prepare("SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'").all(fileLike); + return cachedStmt( + _findFileNodesStmt, + db, + "SELECT * FROM nodes WHERE file LIKE ? AND kind = 'file'", + ).all(fileLike); } -// ─── Statement caches (one prepared statement per db instance) ──────────── -// WeakMap keys on the db object so statements are GC'd when the db closes. -const _getNodeIdStmt = new WeakMap(); -const _getFunctionNodeIdStmt = new WeakMap(); -const _bulkNodeIdsByFileStmt = new WeakMap(); - /** * Look up a node's ID by its unique (name, kind, file, line) tuple. * Shared by builder, watcher, structure, complexity, cfg, engine. @@ -178,12 +192,11 @@ const _bulkNodeIdsByFileStmt = new WeakMap(); * @returns {number|undefined} */ export function getNodeId(db, name, kind, file, line) { - let stmt = _getNodeIdStmt.get(db); - if (!stmt) { - stmt = db.prepare('SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?'); - _getNodeIdStmt.set(db, stmt); - } - return stmt.get(name, kind, file, line)?.id; + return cachedStmt( + _getNodeIdStmt, + db, + 'SELECT id FROM nodes WHERE name = ? AND kind = ? AND file = ? AND line = ?', + ).get(name, kind, file, line)?.id; } /** @@ -196,14 +209,11 @@ export function getNodeId(db, name, kind, file, line) { * @returns {number|undefined} */ export function getFunctionNodeId(db, name, file, line) { - let stmt = _getFunctionNodeIdStmt.get(db); - if (!stmt) { - stmt = db.prepare( - "SELECT id FROM nodes WHERE name = ? AND kind IN ('function','method') AND file = ? AND line = ?", - ); - _getFunctionNodeIdStmt.set(db, stmt); - } - return stmt.get(name, file, line)?.id; + return cachedStmt( + _getFunctionNodeIdStmt, + db, + "SELECT id FROM nodes WHERE name = ? AND kind IN ('function','method') AND file = ? AND line = ?", + ).get(name, file, line)?.id; } /** @@ -215,12 +225,11 @@ export function getFunctionNodeId(db, name, file, line) { * @returns {{ id: number, name: string, kind: string, line: number }[]} */ export function bulkNodeIdsByFile(db, file) { - let stmt = _bulkNodeIdsByFileStmt.get(db); - if (!stmt) { - stmt = db.prepare('SELECT id, name, kind, line FROM nodes WHERE file = ?'); - _bulkNodeIdsByFileStmt.set(db, stmt); - } - return stmt.all(file); + return cachedStmt( + _bulkNodeIdsByFileStmt, + db, + 'SELECT id, name, kind, line FROM nodes WHERE file = ?', + ).all(file); } /** @@ -230,7 +239,9 @@ export function bulkNodeIdsByFile(db, file) { * @returns {{ name: string, kind: string, line: number, end_line: number|null }[]} */ export function findNodeChildren(db, parentId) { - return db - .prepare('SELECT name, kind, line, end_line FROM nodes WHERE parent_id = ? ORDER BY line') - .all(parentId); + return cachedStmt( + _findNodeChildrenStmt, + db, + 'SELECT name, kind, line, end_line FROM nodes WHERE parent_id = ? ORDER BY line', + ).all(parentId); }