44 * This is the heart of the builder refactor (ROADMAP 3.9): the monolithic buildGraph()
55 * is decomposed into independently testable stages that communicate via PipelineContext.
66 */
7+ import fs from 'node:fs' ;
78import path from 'node:path' ;
89import { performance } from 'node:perf_hooks' ;
910import {
11+ acquireAdvisoryLock ,
1012 closeDbPair ,
1113 getBuildMeta ,
1214 initSchema ,
1315 MIGRATIONS ,
1416 openDb ,
17+ releaseAdvisoryLock ,
1518 setBuildMeta ,
1619} from '../../../db/index.js' ;
1720import { detectWorkspaces , loadConfig } from '../../../infrastructure/config.js' ;
@@ -25,6 +28,7 @@ import { getActiveEngine } from '../../parser.js';
2528import { setWorkspaces } from '../resolve.js' ;
2629import { PipelineContext } from './context.js' ;
2730import { loadPathAliases } from './helpers.js' ;
31+ import { NativeDbProxy } from './native-db-proxy.js' ;
2832import { buildEdges } from './stages/build-edges.js' ;
2933import { buildStructure } from './stages/build-structure.js' ;
3034// Pipeline stages
@@ -110,14 +114,48 @@ function loadAliases(ctx: PipelineContext): void {
110114function setupPipeline ( ctx : PipelineContext ) : void {
111115 ctx . rootDir = path . resolve ( ctx . rootDir ) ;
112116 ctx . dbPath = path . join ( ctx . rootDir , '.codegraph' , 'graph.db' ) ;
113- ctx . db = openDb ( ctx . dbPath ) ;
114- initSchema ( ctx . db ) ;
115117
116- // Detect whether native engine is available, but defer opening NativeDatabase.
117- // The native orchestrator opens it on demand; the JS pipeline defers until
118- // after change detection — avoiding ~5ms open+initSchema+close on no-op rebuilds.
118+ // Detect whether native engine is available.
119119 const enginePref = ctx . opts . engine || 'auto' ;
120- ctx . nativeAvailable = enginePref !== 'wasm' && ! ! loadNative ( ) ?. NativeDatabase ;
120+ const native = enginePref !== 'wasm' ? loadNative ( ) : null ;
121+ ctx . nativeAvailable = ! ! native ?. NativeDatabase ;
122+
123+ // Native-first: use only rusqlite for the entire pipeline (no better-sqlite3).
124+ // This eliminates the dual-connection WAL corruption problem and enables all
125+ // native fast-paths (bulkInsertNodes, classifyRolesFull, etc.).
126+ // Fallback: if native is unavailable or FORCE_JS is set, use better-sqlite3.
127+ if (
128+ ctx . nativeAvailable &&
129+ native ?. NativeDatabase &&
130+ process . env . CODEGRAPH_FORCE_JS_PIPELINE !== '1'
131+ ) {
132+ try {
133+ const dir = path . dirname ( ctx . dbPath ) ;
134+ if ( ! fs . existsSync ( dir ) ) fs . mkdirSync ( dir , { recursive : true } ) ;
135+ acquireAdvisoryLock ( ctx . dbPath ) ;
136+ ctx . nativeDb = native . NativeDatabase . openReadWrite ( ctx . dbPath ) ;
137+ ctx . nativeDb . initSchema ( ) ;
138+ const proxy = new NativeDbProxy ( ctx . nativeDb ) ;
139+ proxy . __lockPath = `${ ctx . dbPath } .lock` ;
140+ ctx . db = proxy as unknown as typeof ctx . db ;
141+ ctx . nativeFirstProxy = true ;
142+ } catch ( err ) {
143+ warn ( `NativeDatabase setup failed, falling back to better-sqlite3: ${ toErrorMessage ( err ) } ` ) ;
144+ try {
145+ ctx . nativeDb ?. close ( ) ;
146+ } catch {
147+ /* ignore */
148+ }
149+ ctx . nativeDb = undefined ;
150+ ctx . nativeFirstProxy = false ;
151+ releaseAdvisoryLock ( `${ ctx . dbPath } .lock` ) ;
152+ ctx . db = openDb ( ctx . dbPath ) ;
153+ initSchema ( ctx . db ) ;
154+ }
155+ } else {
156+ ctx . db = openDb ( ctx . dbPath ) ;
157+ initSchema ( ctx . db ) ;
158+ }
121159
122160 ctx . config = loadConfig ( ctx . rootDir ) ;
123161 ctx . incremental =
@@ -434,16 +472,20 @@ async function runPostNativeAnalysis(
434472 analysisFileSymbols = allFileSymbols ;
435473 }
436474
437- // Reopen nativeDb for analysis features (suspend/resume WAL pattern).
438- const native = loadNative ( ) ;
439- if ( native ?. NativeDatabase ) {
440- try {
441- ctx . nativeDb = native . NativeDatabase . openReadWrite ( ctx . dbPath ) ;
442- if ( ctx . engineOpts ) ctx . engineOpts . nativeDb = ctx . nativeDb ;
443- } catch {
444- ctx . nativeDb = undefined ;
445- if ( ctx . engineOpts ) ctx . engineOpts . nativeDb = undefined ;
475+ // In native-first mode, nativeDb is already open — no reopen needed.
476+ if ( ! ctx . nativeFirstProxy ) {
477+ const native = loadNative ( ) ;
478+ if ( native ?. NativeDatabase ) {
479+ try {
480+ ctx . nativeDb = native . NativeDatabase . openReadWrite ( ctx . dbPath ) ;
481+ if ( ctx . engineOpts ) ctx . engineOpts . nativeDb = ctx . nativeDb ;
482+ } catch {
483+ ctx . nativeDb = undefined ;
484+ if ( ctx . engineOpts ) ctx . engineOpts . nativeDb = undefined ;
485+ }
446486 }
487+ } else if ( ctx . engineOpts ) {
488+ ctx . engineOpts . nativeDb = ctx . nativeDb ;
447489 }
448490
449491 try {
@@ -463,8 +505,8 @@ async function runPostNativeAnalysis(
463505 warn ( `Analysis phases failed after native build: ${ toErrorMessage ( err ) } ` ) ;
464506 }
465507
466- // Close nativeDb after analyses
467- if ( ctx . nativeDb ) {
508+ // Close nativeDb after analyses (skip in native-first — single connection stays open)
509+ if ( ctx . nativeDb && ! ctx . nativeFirstProxy ) {
468510 try {
469511 ctx . nativeDb . exec ( 'PRAGMA wal_checkpoint(TRUNCATE)' ) ;
470512 } catch {
@@ -516,8 +558,8 @@ async function tryNativeOrchestrator(
516558 return undefined ;
517559 }
518560
519- // Open NativeDatabase on demand for the orchestrator .
520- // Deferred from setupPipeline so no-op JS pipeline rebuilds skip the overhead.
561+ // In native-first mode, nativeDb is already open from setupPipeline .
562+ // Otherwise, open it on demand (deferred to skip overhead on no-op rebuilds) .
521563 if ( ! ctx . nativeDb && ctx . nativeAvailable ) {
522564 const native = loadNative ( ) ;
523565 if ( native ?. NativeDatabase ) {
@@ -591,7 +633,8 @@ async function tryNativeOrchestrator(
591633 const needsStructure = ! result . structureHandled ;
592634
593635 if ( needsAnalysis || needsStructure ) {
594- if ( ! handoffWalAfterNativeBuild ( ctx ) ) {
636+ // In native-first mode the proxy is already wired — no WAL handoff needed.
637+ if ( ! ctx . nativeFirstProxy && ! handoffWalAfterNativeBuild ( ctx ) ) {
595638 // DB reopen failed — return partial result
596639 return formatNativeTimingResult ( p , 0 , analysisTiming ) ;
597640 }
@@ -623,6 +666,30 @@ async function tryNativeOrchestrator(
623666// ── Pipeline stages execution ───────────────────────────────────────────
624667
625668async function runPipelineStages ( ctx : PipelineContext ) : Promise < void > {
669+ // ── Native-first mode ────────────────────────────────────────────────
670+ // When ctx.nativeFirstProxy is true, ctx.db is a NativeDbProxy backed by
671+ // the single rusqlite connection (ctx.nativeDb). No dual-connection WAL
672+ // dance is needed — every stage uses the same connection transparently.
673+ if ( ctx . nativeFirstProxy ) {
674+ // Ensure engineOpts.nativeDb is set so stages can use dedicated native methods.
675+ if ( ctx . engineOpts ) {
676+ ctx . engineOpts . nativeDb = ctx . nativeDb ;
677+ }
678+
679+ await collectFiles ( ctx ) ;
680+ await detectChanges ( ctx ) ;
681+ if ( ctx . earlyExit ) return ;
682+ await parseFiles ( ctx ) ;
683+ await insertNodes ( ctx ) ;
684+ await resolveImports ( ctx ) ;
685+ await buildEdges ( ctx ) ;
686+ await buildStructure ( ctx ) ;
687+ await runAnalyses ( ctx ) ;
688+ await finalize ( ctx ) ;
689+ return ;
690+ }
691+
692+ // ── Legacy dual-connection mode (WASM / fallback) ────────────────────
626693 // NativeDatabase is deferred — not opened during setup. collectFiles and
627694 // detectChanges only need better-sqlite3. If no files changed, we exit
628695 // early without ever opening the native connection, saving ~5ms.
0 commit comments