Skip to content
Merged
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
16 changes: 15 additions & 1 deletion src/domain/graph/builder/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,10 @@ function setupPipeline(ctx: PipelineContext): void {
} catch (err) {
warn(`NativeDatabase init failed, falling back to JS: ${(err as Error).message}`);
ctx.nativeDb = undefined;
initSchema(ctx.db);
}
// Always run JS initSchema so better-sqlite3 sees the schema —
// nativeDb is closed before pipeline stages run (dual-connection guard).
initSchema(ctx.db);
} else {
initSchema(ctx.db);
}
Expand Down Expand Up @@ -156,6 +158,18 @@ function formatTimingResult(ctx: PipelineContext): BuildResult {
// ── Pipeline stages execution ───────────────────────────────────────────

async function runPipelineStages(ctx: PipelineContext): Promise<void> {
// Prevent dual-connection WAL corruption: when both better-sqlite3 (ctx.db)
// and rusqlite (ctx.nativeDb) are open to the same file, Rust writes corrupt
// the DB. Clear nativeDb so all stages use JS fallback paths. See #694.
if (ctx.db && ctx.nativeDb) {
try {
(ctx.nativeDb as { close?: () => void }).close?.();
} catch {
/* ignore close errors */
}
ctx.nativeDb = undefined;
}

await collectFiles(ctx);
await detectChanges(ctx);

Expand Down
37 changes: 24 additions & 13 deletions src/domain/graph/builder/stages/insert-nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ interface PrecomputedFileData {
// ── Native fast-path ─────────────────────────────────────────────────

function tryNativeInsert(ctx: PipelineContext): boolean {
// Disabled: bulkInsertNodes corrupts the DB when both the JS (better-sqlite3)
// and Rust (rusqlite) connections are open to the same WAL-mode file.
// The native path was never operational before — it always crashed on null
// visibility serialisation. See #694 for the dual-connection fix.
if (ctx.db) return false;

// Use NativeDatabase persistent connection (Phase 6.15+).
// Standalone napi functions were removed in 6.17 — falls through to JS if nativeDb unavailable.
if (!ctx.nativeDb?.bulkInsertNodes) return false;
Expand All @@ -52,14 +58,14 @@ function tryNativeInsert(ctx: PipelineContext): boolean {
name: string;
kind: string;
line: number;
endLine?: number | null;
visibility?: string | null;
endLine?: number;
visibility?: string;
children: Array<{
name: string;
kind: string;
line: number;
endLine?: number | null;
visibility?: string | null;
endLine?: number;
visibility?: string;
}>;
}>;
exports: Array<{ name: string; kind: string; line: number }>;
Expand All @@ -72,14 +78,14 @@ function tryNativeInsert(ctx: PipelineContext): boolean {
name: def.name,
kind: def.kind,
line: def.line,
endLine: def.endLine ?? null,
visibility: def.visibility ?? null,
endLine: def.endLine ?? undefined,
visibility: def.visibility ?? undefined,
children: (def.children ?? []).map((c) => ({
name: c.name,
kind: c.kind,
line: c.line,
endLine: c.endLine ?? null,
visibility: c.visibility ?? null,
endLine: c.endLine ?? undefined,
visibility: c.visibility ?? undefined,
})),
})),
exports: symbols.exports.map((exp) => ({
Expand Down Expand Up @@ -340,11 +346,16 @@ export async function insertNodes(ctx: PipelineContext): Promise<void> {
const t0 = performance.now();

// Try native Rust path first — single transaction, no JS↔C overhead
if (ctx.engineName === 'native' && tryNativeInsert(ctx)) {
ctx.timing.insertMs = performance.now() - t0;

// Removed-file hash cleanup is handled inside the native call
return;
if (ctx.engineName === 'native') {
try {
if (tryNativeInsert(ctx)) {
ctx.timing.insertMs = performance.now() - t0;
// Removed-file hash cleanup is handled inside the native call
return;
}
} catch {
// Native insert failed — fall through to JS implementation
}
}

// JS fallback
Expand Down
31 changes: 27 additions & 4 deletions src/features/sequence.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import Database from 'better-sqlite3';
import { openRepo, type Repository } from '../db/index.js';
import { SqliteRepository } from '../db/repository/sqlite-repository.js';
import { findMatchingNodes } from '../domain/queries.js';
import { loadConfig } from '../infrastructure/config.js';
import { isTestFile } from '../infrastructure/test-filter.js';
import { paginateResult } from '../shared/paginate.js';
import type { CodegraphConfig, NodeRowWithFanIn } from '../types.js';
import type { BetterSqlite3Database, CodegraphConfig, NodeRowWithFanIn } from '../types.js';
import { FRAMEWORK_ENTRY_PREFIXES } from './structure.js';

// ─── Alias generation ────────────────────────────────────────────────
Expand Down Expand Up @@ -150,12 +151,34 @@ function annotateDataflow(
repo: Repository,
messages: SequenceMessage[],
idToNode: Map<number, { id: number; name: string; file: string; kind: string; line: number }>,
dbPath?: string,
): void {
const hasTable = repo.hasDataflowTable();
if (!hasTable) return;

let db: BetterSqlite3Database;
let ownDb = false;
if (repo instanceof SqliteRepository) {
db = repo.db;
} else if (dbPath) {
db = new Database(dbPath, { readonly: true }) as unknown as BetterSqlite3Database;
ownDb = true;
} else {
return;
}

if (!hasTable || !(repo instanceof SqliteRepository)) return;
try {
_annotateDataflowImpl(db, messages, idToNode);
} finally {
if (ownDb) db.close();
}
}

const db = repo.db;
function _annotateDataflowImpl(
db: BetterSqlite3Database,
messages: SequenceMessage[],
idToNode: Map<number, { id: number; name: string; file: string; kind: string; line: number }>,
): void {
const nodeByNameFile = new Map<string, { id: number; name: string; file: string }>();
for (const n of idToNode.values()) {
nodeByNameFile.set(`${n.name}|${n.file}`, n);
Expand Down Expand Up @@ -308,7 +331,7 @@ export function sequenceData(
);

if (opts.dataflow && messages.length > 0) {
annotateDataflow(repo, messages, idToNode);
annotateDataflow(repo, messages, idToNode, dbPath);
}

messages.sort((a, b) => {
Expand Down
Loading