66
77import { SqliteDatabase , SqliteBackend , createDatabase } from './sqlite-adapter' ;
88import * as fs from 'fs' ;
9+ import * as os from 'os' ;
910import * as path from 'path' ;
1011import { SchemaVersion } from '../types' ;
1112import { runMigrations , getCurrentVersion , CURRENT_SCHEMA_VERSION } from './migrations' ;
1213
1314export { SqliteDatabase , SqliteBackend } from './sqlite-adapter' ;
1415
16+ /**
17+ * Detect whether a file path lives on a network filesystem (CIFS/NFS/etc.).
18+ *
19+ * On Linux, reads /proc/mounts and finds the deepest matching mount point.
20+ * On macOS, checks for /Volumes/ paths (network mounts land there by default).
21+ * On Windows, UNC paths (\\server\share) are always network.
22+ *
23+ * Returns false on any parse error so the caller degrades gracefully.
24+ */
25+ function isNetworkFilesystem ( filePath : string ) : boolean {
26+ const platform = os . platform ( ) ;
27+ const resolved = path . resolve ( filePath ) ;
28+
29+ if ( platform === 'linux' ) {
30+ try {
31+ const mounts = fs . readFileSync ( '/proc/mounts' , 'utf-8' ) ;
32+ let bestMount = '' ;
33+ let bestFsType = '' ;
34+ for ( const line of mounts . split ( '\n' ) ) {
35+ const parts = line . trim ( ) . split ( / \s + / ) ;
36+ if ( parts . length < 3 ) continue ;
37+ const mountPoint = parts [ 1 ] as string ;
38+ const fsType = parts [ 2 ] as string ;
39+ if ( resolved . startsWith ( mountPoint + '/' ) || resolved === mountPoint ) {
40+ if ( mountPoint . length > bestMount . length ) {
41+ bestMount = mountPoint ;
42+ bestFsType = fsType ;
43+ }
44+ }
45+ }
46+ const networkTypes = new Set ( [
47+ 'cifs' , 'smbfs' , 'smb2' , 'nfs' , 'nfs4' , 'nfs3' ,
48+ 'davfs' , 'fuse.sshfs' , 'fuse.rclone' , 'fuse.s3fs' ,
49+ 'ncpfs' , 'afs' , 'coda' , 'glusterfs' , 'lustre' ,
50+ ] ) ;
51+ return networkTypes . has ( bestFsType . toLowerCase ( ) ) ;
52+ } catch {
53+ return false ;
54+ }
55+ }
56+
57+ if ( platform === 'darwin' ) {
58+ // Network mounts typically appear under /Volumes or /net
59+ return resolved . startsWith ( '/Volumes/' ) || resolved . startsWith ( '/net/' ) ;
60+ }
61+
62+ if ( platform === 'win32' ) {
63+ // UNC paths: \\server\share\...
64+ return resolved . startsWith ( '\\\\' ) ;
65+ }
66+
67+ return false ;
68+ }
69+
1570/**
1671 * Apply connection-level PRAGMAs. Shared by `initialize` and `open` so the two
1772 * paths can't drift.
@@ -25,15 +80,36 @@ export { SqliteDatabase, SqliteBackend } from './sqlite-adapter';
2580 * 2-minute wait presented as a frozen, hung agent. With WAL, reads never block
2681 * on a writer, so this timeout only governs cross-process write contention
2782 * (e.g. the git-hook `codegraph sync` running while the MCP server writes).
83+ *
84+ * On network filesystems (CIFS/NFS/…) WAL mode is skipped: those mounts do
85+ * not support the shared-memory files (-wal/-shm) that WAL requires, and
86+ * mmap I/O is unreliable over the network. MEMORY journal avoids all disk
87+ * I/O for journaling; synchronous=OFF is safe because the index is fully
88+ * rebuildable from source.
2889 */
29- function configureConnection ( db : SqliteDatabase ) : void {
30- db . pragma ( 'busy_timeout = 5000' ) ; // MUST be first — see above
90+ function configureConnection ( db : SqliteDatabase , dbPath : string ) : void {
91+ const networkFs = isNetworkFilesystem ( dbPath ) ;
92+
93+ // busy_timeout MUST come first — lets subsequent pragmas wait out any lock
94+ db . pragma ( networkFs ? 'busy_timeout = 30000' : 'busy_timeout = 5000' ) ;
3195 db . pragma ( 'foreign_keys = ON' ) ;
32- db . pragma ( 'journal_mode = WAL' ) ; // node:sqlite supports WAL on every platform
33- db . pragma ( 'synchronous = NORMAL' ) ; // safe with WAL mode
96+
97+ if ( networkFs ) {
98+ // WAL needs shared-memory files unsupported on most network mounts.
99+ // MEMORY journal avoids all file I/O for journaling (DELETE journal still
100+ // tries to create a -journal file on disk, which fails on CIFS/NFS).
101+ // synchronous=OFF is safe here: the index is fully rebuildable from source.
102+ db . pragma ( 'journal_mode = MEMORY' ) ;
103+ db . pragma ( 'synchronous = OFF' ) ;
104+ // skip mmap — unreliable on network filesystems
105+ } else {
106+ db . pragma ( 'journal_mode = WAL' ) ; // node:sqlite supports WAL on every platform
107+ db . pragma ( 'synchronous = NORMAL' ) ; // safe with WAL mode
108+ db . pragma ( 'mmap_size = 268435456' ) ; // 256 MB memory-mapped I/O
109+ }
110+
34111 db . pragma ( 'cache_size = -64000' ) ; // 64 MB page cache
35112 db . pragma ( 'temp_store = MEMORY' ) ; // temp tables in memory
36- db . pragma ( 'mmap_size = 268435456' ) ; // 256 MB memory-mapped I/O
37113}
38114
39115/**
@@ -61,9 +137,10 @@ export class DatabaseConnection {
61137 }
62138
63139 // Create and configure database
64- const { db, backend } = createDatabase ( dbPath ) ;
140+ const nolock = isNetworkFilesystem ( dbPath ) ;
141+ const { db, backend } = createDatabase ( dbPath , { nolock } ) ;
65142
66- configureConnection ( db ) ;
143+ configureConnection ( db , dbPath ) ;
67144
68145 // Run schema initialization
69146 const schemaPath = path . join ( __dirname , 'schema.sql' ) ;
@@ -89,9 +166,10 @@ export class DatabaseConnection {
89166 throw new Error ( `Database not found: ${ dbPath } ` ) ;
90167 }
91168
92- const { db, backend } = createDatabase ( dbPath ) ;
169+ const nolock = isNetworkFilesystem ( dbPath ) ;
170+ const { db, backend } = createDatabase ( dbPath , { nolock } ) ;
93171
94- configureConnection ( db ) ;
172+ configureConnection ( db , dbPath ) ;
95173
96174 // Check and run migrations if needed
97175 const conn = new DatabaseConnection ( db , dbPath , backend ) ;
0 commit comments