Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/cli/commands/embed.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import path from 'node:path';
import { openReadonlyOrFail } from '../../db/index.js';
import { openReadonlyOrFail, resolveBusyTimeoutMs } from '../../db/index.js';
import { getEmbeddingMeta } from '../../db/repository/embeddings.js';
import {
buildEmbeddings,
Expand All @@ -13,7 +13,7 @@ import type { CommandDefinition } from '../types.js';

function resolveStickyModel(dbPath: string | undefined): string | null {
try {
const db = openReadonlyOrFail(dbPath);
const db = openReadonlyOrFail(dbPath, resolveBusyTimeoutMs(dbPath));
try {
const storedName = getEmbeddingMeta(db, 'model');
if (!storedName) return null;
Expand Down
4 changes: 2 additions & 2 deletions src/cli/shared/open-graph.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { openReadonlyOrFail } from '../../db/index.js';
import { openReadonlyOrFail, resolveBusyTimeoutMs } from '../../db/index.js';
import type { BetterSqlite3Database } from '../../types.js';

/**
Expand All @@ -8,6 +8,6 @@ export function openGraph(opts: { db?: string } = {}): {
db: BetterSqlite3Database;
close: () => void;
} {
const db = openReadonlyOrFail(opts.db);
const db = openReadonlyOrFail(opts.db, resolveBusyTimeoutMs(opts.db));
return { db, close: () => db.close() };
}
46 changes: 39 additions & 7 deletions src/db/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { DEFAULTS, loadConfig } from '../infrastructure/config.js';
import { debug, warn } from '../infrastructure/logger.js';
import { getNative, isNativeAvailable } from '../infrastructure/native.js';
import { DbError, toErrorMessage } from '../shared/errors.js';
import type { BetterSqlite3Database, NativeDatabase } from '../types.js';
import type { BetterSqlite3Database, CodegraphConfig, NativeDatabase } from '../types.js';
import { getDatabase } from './better-sqlite3.js';
import { Repository } from './repository/base.js';
import { NativeRepository } from './repository/native-repository.js';
Expand Down Expand Up @@ -386,6 +386,18 @@ interface ResolvedDbSettings {
busyTimeoutMs: number;
}

/**
* Derive the project rootDir from a possibly-custom DB path, for loadConfig().
* Using findDbPath (not path.resolve(customDbPath)) ensures directory inputs like
* --db /path/to/repo are normalised to .codegraph/graph.db before we strip two levels.
* Convention: resolvedDbPath = <rootDir>/.codegraph/graph.db
* Shared by resolveDbSettings() and resolveBusyTimeoutMs() so rootDir derivation can't drift.
*/
function deriveRootDirFromDbPath(customDbPath: string | undefined): string | undefined {
const resolvedDbPath = customDbPath ? findDbPath(customDbPath) : undefined;
return resolvedDbPath ? path.dirname(path.dirname(resolvedDbPath)) : undefined;
}

/**
* Resolve the effective engine for DB access (explicit opts.engine > config.build.engine >
* 'auto') alongside config.db.busyTimeoutMs, in a single loadConfig() call.
Expand All @@ -400,12 +412,7 @@ function resolveDbSettings(
customDbPath: string | undefined,
engineOpt: 'native' | 'wasm' | 'auto' | undefined,
): ResolvedDbSettings {
// Using findDbPath (not path.resolve(customDbPath)) ensures directory inputs like
// --db /path/to/repo are normalised to .codegraph/graph.db before we strip two levels.
// Convention: resolvedDbPath = <rootDir>/.codegraph/graph.db
const resolvedDbPath = customDbPath ? findDbPath(customDbPath) : undefined;
const rootDir = resolvedDbPath ? path.dirname(path.dirname(resolvedDbPath)) : undefined;
const config = loadConfig(rootDir);
const config = loadConfig(deriveRootDirFromDbPath(customDbPath));
// config.build.engine is already populated from CODEGRAPH_ENGINE env by applyEnvOverrides,
// so this covers both the env-var path and the .codegraphrc.json config-file path.
return {
Expand All @@ -414,6 +421,31 @@ function resolveDbSettings(
};
}

/**
* Resolve the full config for a given DB path, deriving rootDir the same way
* resolveDbSettings()/resolveBusyTimeoutMs() do. Exported so callers that need
* both the busy-timeout and other config values (e.g. withReadonlyDb()) can
* share a single loadConfig() call instead of resolving it twice.
*
* MUST be called before opening any DB handle, for the same reason as
* resolveDbSettings(): loadConfig can throw (e.g. ConfigError via
* resolveSecrets on a malformed llm.apiKeyCommand config), and an
* already-open handle at that point would never be closed.
*/
export function resolveDbConfig(customDbPath?: string): CodegraphConfig {
return loadConfig(deriveRootDirFromDbPath(customDbPath));
}

/**
* Resolve config.db.busyTimeoutMs alone, for the ad-hoc read-only query call
* sites (features/*, domain/analysis/*, domain/search/*) that call
* openReadonlyOrFail() directly and don't need engine selection. Shares
* rootDir derivation with resolveDbSettings() so the two can't drift.
*/
export function resolveBusyTimeoutMs(customDbPath?: string): number {
return resolveDbConfig(customDbPath).db?.busyTimeoutMs ?? DEFAULTS.db.busyTimeoutMs;
}

/** Open a NativeRepository via rusqlite, throwing DbError if the DB file is missing. */
function openRepoNative(customDbPath?: string): { repo: Repository; close(): void } {
const dbPath = findDbPath(customDbPath);
Expand Down
2 changes: 2 additions & 0 deletions src/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export {
openReadonlyWithNative,
openRepo,
releaseAdvisoryLock,
resolveBusyTimeoutMs,
resolveDbConfig,
} from './connection.js';
export { getBuildMeta, initSchema, MIGRATIONS, setBuildMeta } from './migrations.js';
export {
Expand Down
14 changes: 10 additions & 4 deletions src/domain/analysis/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,12 +438,15 @@ export function contextData(
config?: any;
} = {},
) {
return withReadonlyDb(customDbPath, (db) => {
return withReadonlyDb(customDbPath, (db, config) => {
const depth = opts.depth || 0;
const noSource = opts.noSource || false;
const includeTests = opts.includeTests || false;

const { noTests, displayOpts } = resolveAnalysisOpts(opts);
const { noTests, displayOpts } = resolveAnalysisOpts({
...opts,
config: opts.config ?? config,
});

const dbPath = findDbPath(customDbPath);
const repoRoot = path.resolve(path.dirname(dbPath), '..');
Expand Down Expand Up @@ -510,11 +513,14 @@ export function explainData(
config?: any;
} = {},
) {
return withReadonlyDb(customDbPath, (db) => {
return withReadonlyDb(customDbPath, (db, config) => {
const depth = opts.depth || 0;
const kind = isFileLikeTarget(target) ? 'file' : 'function';

const { noTests, displayOpts } = resolveAnalysisOpts(opts);
const { noTests, displayOpts } = resolveAnalysisOpts({
...opts,
config: opts.config ?? config,
});

const dbPath = findDbPath(customDbPath);
const repoRoot = path.resolve(path.dirname(dbPath), '..');
Expand Down
13 changes: 10 additions & 3 deletions src/domain/analysis/diff-impact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { cachedStmt } from '../../db/repository/cached-stmt.js';
import { evaluateBoundaries } from '../../features/boundaries.js';
import { coChangeForFiles } from '../../features/cochange.js';
import { ownersForFiles } from '../../features/owners.js';
import { loadConfig } from '../../infrastructure/config.js';
import { DEFAULTS, loadConfig } from '../../infrastructure/config.js';
import { debug } from '../../infrastructure/logger.js';
import { isTestFile } from '../../infrastructure/test-filter.js';
import { paginateResult } from '../../shared/paginate.js';
Expand Down Expand Up @@ -269,10 +269,17 @@ export function diffImpactData(
config?: any;
} = {},
) {
const db = openReadonlyOrFail(customDbPath);
// Resolve config before opening the DB so config.db.busyTimeoutMs can be
// threaded through to openReadonlyOrFail() (mirrors resolveDbSettings()'s
// ordering in db/connection.ts — loadConfig can throw, and an already-open
// handle at that point would never be closed).
const config = opts.config || loadConfig();
const db = openReadonlyOrFail(
customDbPath,
config.db?.busyTimeoutMs ?? DEFAULTS.db.busyTimeoutMs,
);
try {
const noTests = opts.noTests || false;
const config = opts.config || loadConfig();
const maxDepth = opts.depth || config.analysis?.impactDepth || 3;

const dbPath = findDbPath(customDbPath);
Expand Down
7 changes: 5 additions & 2 deletions src/domain/analysis/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,11 @@ export function exportsData(
config?: any;
} = {},
) {
return withReadonlyDb(customDbPath, (db) => {
const { noTests, displayOpts } = resolveAnalysisOpts(opts);
return withReadonlyDb(customDbPath, (db, config) => {
const { noTests, displayOpts } = resolveAnalysisOpts({
...opts,
config: opts.config ?? config,
});

const dbFilePath = findDbPath(customDbPath);
const repoRoot = path.resolve(path.dirname(dbFilePath), '..');
Expand Down
9 changes: 7 additions & 2 deletions src/domain/analysis/module-map.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import path from 'node:path';
import { openReadonlyOrFail, openReadonlyWithNative, testFilterSQL } from '../../db/index.js';
import {
openReadonlyOrFail,
openReadonlyWithNative,
resolveBusyTimeoutMs,
testFilterSQL,
} from '../../db/index.js';
import { loadConfig } from '../../infrastructure/config.js';
import { debug } from '../../infrastructure/logger.js';
import { isTestFile } from '../../infrastructure/test-filter.js';
Expand Down Expand Up @@ -307,7 +312,7 @@ function getComplexitySummary(db: BetterSqlite3Database, testFilter: string) {
// ---------------------------------------------------------------------------

export function moduleMapData(customDbPath: string, limit = 20, opts: { noTests?: boolean } = {}) {
const db = openReadonlyOrFail(customDbPath);
const db = openReadonlyOrFail(customDbPath, resolveBusyTimeoutMs(customDbPath));
try {
const noTests = opts.noTests || false;

Expand Down
16 changes: 11 additions & 5 deletions src/domain/analysis/query-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import { openReadonlyOrFail, openRepo, type Repository } from '../../db/index.js';
import { loadConfig } from '../../infrastructure/config.js';
import { openReadonlyOrFail, openRepo, type Repository, resolveDbConfig } from '../../db/index.js';
import { DEFAULTS, loadConfig } from '../../infrastructure/config.js';
import type { BetterSqlite3Database, CodegraphConfig } from '../../types.js';

/**
* Open a readonly DB connection, run `fn`, and close the DB on completion.
* Eliminates the duplicated `openReadonlyOrFail` + `try/finally/db.close()` pattern
* that appears in every analysis query function.
*
* Resolves the config once and passes it to `fn` so callers that also need
* `resolveAnalysisOpts` can reuse it as `opts.config` instead of triggering a
* second `loadConfig()` for the same rootDir (#1763 review).
*/
export function withReadonlyDb<T>(
customDbPath: string | undefined,
fn: (db: BetterSqlite3Database) => T,
fn: (db: BetterSqlite3Database, config: CodegraphConfig) => T,
): T {
const db = openReadonlyOrFail(customDbPath);
const config = resolveDbConfig(customDbPath);
const busyTimeoutMs = config.db?.busyTimeoutMs ?? DEFAULTS.db.busyTimeoutMs;
const db = openReadonlyOrFail(customDbPath, busyTimeoutMs);
try {
return fn(db);
return fn(db, config);
} finally {
db.close();
}
Expand Down
6 changes: 3 additions & 3 deletions src/domain/analysis/roles.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { openReadonlyOrFail } from '../../db/index.js';
import { openReadonlyOrFail, resolveBusyTimeoutMs } from '../../db/index.js';
import { buildFileConditionSQL } from '../../db/query-builder.js';
import { isTestFile } from '../../infrastructure/test-filter.js';
import { DEAD_ROLE_PREFIX } from '../../shared/kinds.js';
Expand All @@ -13,7 +13,7 @@ export interface DynamicCallCount {

/** Return a count of flagged dynamic call sink edges, grouped by kind. */
export function dynamicCallsData(customDbPath: string): DynamicCallCount[] {
const db = openReadonlyOrFail(customDbPath);
const db = openReadonlyOrFail(customDbPath, resolveBusyTimeoutMs(customDbPath));
try {
return db
.prepare(
Expand All @@ -39,7 +39,7 @@ export function rolesData(
offset?: number;
} = {},
) {
const db = openReadonlyOrFail(customDbPath);
const db = openReadonlyOrFail(customDbPath, resolveBusyTimeoutMs(customDbPath));
try {
const noTests = opts.noTests || false;
const filterRole = opts.role || null;
Expand Down
9 changes: 5 additions & 4 deletions src/domain/analysis/symbol-lookup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
listFunctionNodes,
openReadonlyOrFail,
Repository,
resolveBusyTimeoutMs,
} from '../../db/index.js';
import { debug } from '../../infrastructure/logger.js';
import { isTestFile } from '../../infrastructure/test-filter.js';
Expand Down Expand Up @@ -95,7 +96,7 @@ export function queryNameData(
customDbPath: string,
opts: { noTests?: boolean; limit?: number; offset?: number } = {},
) {
const db = openReadonlyOrFail(customDbPath);
const db = openReadonlyOrFail(customDbPath, resolveBusyTimeoutMs(customDbPath));
try {
const noTests = opts.noTests || false;
let nodes = db.prepare(`SELECT * FROM nodes WHERE name LIKE ?`).all(`%${name}%`) as NodeRow[];
Expand Down Expand Up @@ -195,7 +196,7 @@ export function whereData(
customDbPath: string,
opts: { noTests?: boolean; file?: boolean; limit?: number; offset?: number } = {},
) {
const db = openReadonlyOrFail(customDbPath);
const db = openReadonlyOrFail(customDbPath, resolveBusyTimeoutMs(customDbPath));
try {
const noTests = opts.noTests || false;
const fileMode = opts.file || false;
Expand All @@ -219,7 +220,7 @@ export function listFunctionsData(
offset?: number;
} = {},
) {
const db = openReadonlyOrFail(customDbPath);
const db = openReadonlyOrFail(customDbPath, resolveBusyTimeoutMs(customDbPath));
try {
const noTests = opts.noTests || false;

Expand All @@ -241,7 +242,7 @@ export function childrenData(
customDbPath: string,
opts: { noTests?: boolean; file?: string; kind?: string; limit?: number; offset?: number } = {},
) {
const db = openReadonlyOrFail(customDbPath);
const db = openReadonlyOrFail(customDbPath, resolveBusyTimeoutMs(customDbPath));
try {
const noTests = opts.noTests || false;

Expand Down
7 changes: 5 additions & 2 deletions src/domain/search/search/hybrid.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { openReadonlyOrFail } from '../../../db/index.js';
import { loadConfig } from '../../../infrastructure/config.js';
import { DEFAULTS, loadConfig } from '../../../infrastructure/config.js';
import type { BetterSqlite3Database, CodegraphConfig } from '../../../types.js';
import { hasFtsIndex } from '../stores/fts5.js';
import { ftsSearchData } from './keyword.js';
Expand Down Expand Up @@ -184,7 +184,10 @@ export async function hybridSearchData(
const k = opts.rrfK ?? searchCfg.rrfK ?? 60;
const topK = (opts.limit ?? searchCfg.topK ?? 15) * 5;

const checkDb = openReadonlyOrFail(customDbPath) as BetterSqlite3Database;
const checkDb = openReadonlyOrFail(
customDbPath,
config.db?.busyTimeoutMs ?? DEFAULTS.db.busyTimeoutMs,
) as BetterSqlite3Database;
const ftsAvailable = hasFtsIndex(checkDb);
checkDb.close();
if (!ftsAvailable) return null;
Expand Down
7 changes: 5 additions & 2 deletions src/domain/search/search/keyword.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { openReadonlyOrFail } from '../../../db/index.js';
import { openReadonlyOrFail, resolveBusyTimeoutMs } from '../../../db/index.js';
import { buildFileConditionSQL } from '../../../db/query-builder.js';
import type { BetterSqlite3Database } from '../../../types.js';
import { normalizeSymbol } from '../../queries.js';
Expand Down Expand Up @@ -41,7 +41,10 @@ export function ftsSearchData(
): FtsSearchResult | null {
const limit = opts.limit || 15;

const db = openReadonlyOrFail(customDbPath) as BetterSqlite3Database;
const db = openReadonlyOrFail(
customDbPath,
resolveBusyTimeoutMs(customDbPath),
) as BetterSqlite3Database;

try {
if (!hasFtsIndex(db)) {
Expand Down
7 changes: 5 additions & 2 deletions src/domain/search/search/prepare.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { openReadonlyOrFail } from '../../../db/index.js';
import { openReadonlyOrFail, resolveBusyTimeoutMs } from '../../../db/index.js';
import { buildFileConditionSQL } from '../../../db/query-builder.js';
import { getEmbeddingCount, getEmbeddingMeta } from '../../../db/repository/embeddings.js';
import { info } from '../../../infrastructure/logger.js';
Expand Down Expand Up @@ -43,7 +43,10 @@ export function prepareSearch(
customDbPath: string | undefined,
opts: PrepareSearchOpts = {},
): PreparedSearch | null {
const db = openReadonlyOrFail(customDbPath) as BetterSqlite3Database;
const db = openReadonlyOrFail(
customDbPath,
resolveBusyTimeoutMs(customDbPath),
) as BetterSqlite3Database;

try {
const count = getEmbeddingCount(db);
Expand Down
4 changes: 2 additions & 2 deletions src/features/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import { buildExtensionSet } from '../ast-analysis/shared.js';
import { walkWithVisitors } from '../ast-analysis/visitor.js';
import { createAstStoreVisitor } from '../ast-analysis/visitors/ast-store-visitor.js';
import { bulkNodeIdsByFile, openReadonlyOrFail } from '../db/index.js';
import { bulkNodeIdsByFile, openReadonlyOrFail, resolveBusyTimeoutMs } from '../db/index.js';
import { buildFileConditionSQL } from '../db/query-builder.js';
import { debug } from '../infrastructure/logger.js';
import { outputResult } from '../infrastructure/result-formatter.js';
Expand Down Expand Up @@ -304,7 +304,7 @@ export function astQueryData(
limit: number;
};
} {
const db = openReadonlyOrFail(customDbPath);
const db = openReadonlyOrFail(customDbPath, resolveBusyTimeoutMs(customDbPath));
const { kind, file, noTests, limit, offset } = opts;

let where = 'WHERE 1=1';
Expand Down
Loading
Loading