Skip to content
Open
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
25 changes: 21 additions & 4 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 @@ -417,6 +417,24 @@ function deriveRootDirFromDbPath(customDbPath: string | undefined): string | und
return resolvedDbPath ? path.dirname(path.dirname(resolvedDbPath)) : undefined;
}

/**
* Load config with rootDir derived from the resolved DB path, rather than
* process.cwd(). This is the single entry point every read-only query
* function should use to call loadConfig() when it has (or can resolve) a
* `--db` path — so `--db /other/repo/.codegraph/graph.db` reads *that*
* repo's `.codegraphrc.json` instead of the invoking directory's. Shared by
* resolveDbSettings() and resolveBusyTimeoutMs() (below) plus the ad-hoc
* read-only query call sites (features/*, domain/analysis/*, domain/search/*)
* so rootDir derivation can't drift between them (issue #1881).
*
* MUST be called before opening any DB handle: 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 resolveConfigForDbPath(customDbPath?: string): CodegraphConfig {
return loadConfig(deriveRootDirFromDbPath(customDbPath));
}

/**
* 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 @@ -431,7 +449,7 @@ function resolveDbSettings(
customDbPath: string | undefined,
engineOpt: 'native' | 'wasm' | 'auto' | undefined,
): ResolvedDbSettings {
const config = loadConfig(deriveRootDirFromDbPath(customDbPath));
const config = resolveConfigForDbPath(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 @@ -452,8 +470,7 @@ function resolveDbSettings(
* already-open handle at that point would never be closed.
*/
export function resolveBusyTimeoutMs(customDbPath?: string): number {
const config = loadConfig(deriveRootDirFromDbPath(customDbPath));
return config.db?.busyTimeoutMs ?? DEFAULTS.db.busyTimeoutMs;
return resolveConfigForDbPath(customDbPath).db?.busyTimeoutMs ?? DEFAULTS.db.busyTimeoutMs;
}

/** Open a NativeRepository via rusqlite, throwing DbError if the DB file is missing. */
Expand Down
1 change: 1 addition & 0 deletions src/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export {
openRepo,
releaseAdvisoryLock,
resolveBusyTimeoutMs,
resolveConfigForDbPath,
} from './connection.js';
export { getBuildMeta, initSchema, MIGRATIONS, setBuildMeta } from './migrations.js';
export {
Expand Down
8 changes: 5 additions & 3 deletions src/domain/analysis/diff-impact.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { findDbPath, openReadonlyOrFail } from '../../db/index.js';
import { findDbPath, openReadonlyOrFail, resolveConfigForDbPath } from '../../db/index.js';
import { cachedStmt } from '../../db/repository/cached-stmt.js';
import { evaluateBoundaries } from '../../features/boundaries.js';
import { coChangeForFiles } from '../../features/cochange.js';
Expand Down Expand Up @@ -272,8 +272,10 @@ export function diffImpactData(
// 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();
// handle at that point would never be closed). Derives rootDir from
// customDbPath (not process.cwd()) so `--db /other/repo/...` reads that
// repo's .codegraphrc.json (issue #1881).
const config = opts.config || resolveConfigForDbPath(customDbPath);
const db = openReadonlyOrFail(
customDbPath,
config.db?.busyTimeoutMs ?? DEFAULTS.db.busyTimeoutMs,
Expand Down
8 changes: 5 additions & 3 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 { DEFAULTS, loadConfig } from '../../../infrastructure/config.js';
import { openReadonlyOrFail, resolveConfigForDbPath } from '../../../db/index.js';
import { DEFAULTS } 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 @@ -178,7 +178,9 @@ export async function hybridSearchData(
customDbPath: string | undefined,
opts: SemanticSearchOpts = {},
): Promise<HybridSearchResult | null> {
const config = opts.config || loadConfig();
// Derive rootDir from customDbPath (not process.cwd()) so `--db /other/repo/...`
// reads that repo's .codegraphrc.json (issue #1881).
const config = opts.config || resolveConfigForDbPath(customDbPath);
const searchCfg = config.search || ({} as CodegraphConfig['search']);
const limit = opts.limit ?? searchCfg.topK ?? 15;
const k = opts.rrfK ?? searchCfg.rrfK ?? 60;
Expand Down
18 changes: 7 additions & 11 deletions src/features/audit.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import path from 'node:path';
import { openReadonlyOrFail } from '../db/index.js';
import { openReadonlyOrFail, resolveConfigForDbPath } from '../db/index.js';
import { normalizeFileFilter } from '../db/query-builder.js';
import { bfsTransitiveCallers } from '../domain/analysis/impact.js';
import { explainData } from '../domain/queries.js';
import { DEFAULTS, loadConfig } from '../infrastructure/config.js';
import { DEFAULTS } from '../infrastructure/config.js';
import { debug } from '../infrastructure/logger.js';
import { isTestFile } from '../infrastructure/test-filter.js';
import { toErrorMessage } from '../shared/errors.js';
Expand Down Expand Up @@ -35,13 +34,7 @@ function resolveThresholds(
config: unknown,
): Record<string, ThresholdEntry> {
try {
const cfg =
config ||
(() => {
const dbDir = customDbPath ? path.dirname(customDbPath) : process.cwd();
const repoRoot = path.resolve(dbDir, '..');
return loadConfig(repoRoot);
})();
const cfg = config || resolveConfigForDbPath(customDbPath);
const userRules = (cfg as Record<string, unknown>).manifesto || {};
const resolved: Record<string, ThresholdEntry> = {};
for (const def of FUNCTION_RULES) {
Expand Down Expand Up @@ -144,7 +137,10 @@ export function auditData(
opts: AuditDataOpts = {},
): AuditResult {
const noTests = opts.noTests || false;
const config = opts.config || loadConfig();
// Derive rootDir from customDbPath (not process.cwd()) so `--db /other/repo/...`
// reads that repo's .codegraphrc.json (issue #1881) — matches resolveThresholds()
// below, which already derived rootDir correctly.
const config = opts.config || resolveConfigForDbPath(customDbPath);
const maxDepth =
opts.depth ||
(config as unknown as { analysis?: { auditDepth?: number } }).analysis?.auditDepth ||
Expand Down
15 changes: 10 additions & 5 deletions src/features/complexity-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
* pagination) from compute-time concerns (AST traversal, metric algorithms).
*/

import { openReadonlyOrFail } from '../db/index.js';
import { openReadonlyOrFail, resolveConfigForDbPath } from '../db/index.js';
import { buildFileConditionSQL } from '../db/query-builder.js';
import { DEFAULTS, loadConfig } from '../infrastructure/config.js';
import { DEFAULTS } 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 @@ -281,14 +281,19 @@ interface ComplexityQueryOpts {
}

/** Resolve query flags + effective manifesto thresholds from opts/config/DEFAULTS. */
function resolveComplexityQueryOptions(opts: ComplexityQueryOpts): {
function resolveComplexityQueryOptions(
customDbPath: string | undefined,
opts: ComplexityQueryOpts,
): {
sort: string;
noTests: boolean;
aboveThreshold: boolean;
thresholds: any;
busyTimeoutMs: number;
} {
const config = opts.config || loadConfig(process.cwd());
// Derive rootDir from customDbPath (not process.cwd()) so `--db /other/repo/...`
// reads that repo's .codegraphrc.json (issue #1881).
const config = opts.config || resolveConfigForDbPath(customDbPath);
return {
sort: opts.sort || 'cognitive',
noTests: opts.noTests || false,
Expand Down Expand Up @@ -330,7 +335,7 @@ export function complexityData(
// resolveDbSettings()'s ordering in db/connection.ts, since loadConfig can
// throw and an already-open handle at that point would never be closed.
const { sort, noTests, aboveThreshold, thresholds, busyTimeoutMs } =
resolveComplexityQueryOptions(opts);
resolveComplexityQueryOptions(customDbPath, opts);
const db = openReadonlyOrFail(customDbPath, busyTimeoutMs);
try {
const { where, params } = buildComplexityWhere({
Expand Down
10 changes: 6 additions & 4 deletions src/features/manifesto.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { openReadonlyOrFail } from '../db/index.js';
import { openReadonlyOrFail, resolveConfigForDbPath } from '../db/index.js';
import { buildFileConditionSQL } from '../db/query-builder.js';
import { findCycles } from '../domain/graph/cycles.js';
import { DEFAULTS, loadConfig } from '../infrastructure/config.js';
import { DEFAULTS } from '../infrastructure/config.js';
import { debug } from '../infrastructure/logger.js';
import { paginateResult } from '../shared/paginate.js';
import type { BetterSqlite3Database, CodegraphConfig, ThresholdRule } from '../types.js';
Expand Down Expand Up @@ -482,8 +482,10 @@ export function manifestoData(
// 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(process.cwd());
// handle at that point would never be closed). Derives rootDir from
// customDbPath (not process.cwd()) so `--db /other/repo/...` reads that
// repo's .codegraphrc.json (issue #1881).
const config = opts.config || resolveConfigForDbPath(customDbPath);
const db = openReadonlyOrFail(
customDbPath,
config.db?.busyTimeoutMs ?? DEFAULTS.db.busyTimeoutMs,
Expand Down
9 changes: 6 additions & 3 deletions src/features/structure-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import {
openReadonlyOrFail,
openReadonlyWithNative,
resolveBusyTimeoutMs,
resolveConfigForDbPath,
testFilterSQL,
} from '../db/index.js';
import { DEFAULTS, loadConfig } from '../infrastructure/config.js';
import { DEFAULTS } from '../infrastructure/config.js';
import { isTestFile } from '../infrastructure/test-filter.js';
import { normalizePath } from '../shared/constants.js';
import { paginateResult } from '../shared/paginate.js';
Expand Down Expand Up @@ -384,8 +385,10 @@ export function moduleBoundariesData(
// 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();
// handle at that point would never be closed). Derives rootDir from
// customDbPath (not process.cwd()) so `--db /other/repo/...` reads that
// repo's .codegraphrc.json (issue #1881).
const config = opts.config || resolveConfigForDbPath(customDbPath);
const db = openReadonlyOrFail(
customDbPath,
config.db?.busyTimeoutMs ?? DEFAULTS.db.busyTimeoutMs,
Expand Down
16 changes: 7 additions & 9 deletions tests/unit/busy-timeout-query-sites.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,10 @@ describe('configured busyTimeoutMs reaches PRAGMA busy_timeout at ad-hoc read-on
});

it('manifestoData (config load reordered to before the db opens) applies the configured busy_timeout', () => {
// manifestoData resolves its config via loadConfig(process.cwd()) — a
// pre-existing, cwd-based resolution this fix intentionally preserves
// (only the *timing* of the call moved, not what it resolves) — so the
// project config must be visible from process.cwd() here, not from dbPath.
const cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(tmpDir);
// manifestoData resolves its config from the resolved DB path's rootDir
// (issue #1881 fix), not process.cwd() — so an unrelated cwd must not
// affect resolution.
const cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(os.tmpdir());
const capture = captureBusyTimeoutPragmas();
try {
manifestoData(dbPath);
Expand All @@ -117,10 +116,9 @@ describe('configured busyTimeoutMs reaches PRAGMA busy_timeout at ad-hoc read-on
});

it('hybridSearchData (already loaded config before the db opens) applies the configured busy_timeout', async () => {
// Same pre-existing cwd-based config resolution as manifestoData (bare
// loadConfig() defaults to process.cwd()) — preserved, not introduced, by
// this fix.
const cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(tmpDir);
// Same DB-path-derived config resolution as manifestoData (issue #1881
// fix) — an unrelated cwd must not affect resolution.
const cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue(os.tmpdir());
const capture = captureBusyTimeoutPragmas();
try {
await hybridSearchData('nonexistent-query', dbPath);
Expand Down
Loading
Loading