Bug: SQLite WAL mode fails silently on filesystems without mmap MAP_SHARED
Environment
- CodeGraph 1.1.1
- Linux kernel 7.1.0-rc7, ntfs3 filesystem
- Node.js v25.9.0 (node:sqlite)
Symptoms
codegraph init fails with disk I/O error on ntfs3 (and likely other filesystems without shared-memory mmap: WSL2 /mnt/, CIFS, some NFS configurations).
Root Cause
SQLite WAL mode requires mmap(MAP_SHARED) on the WAL-index file. ntfs3 returns EOPNOTSUPP for MAP_SHARED.
The current code in db/index.js:configureConnection() does:
db.pragma('journal_mode = WAL');
However, PRAGMA journal_mode = WAL reports success ("wal") even when the underlying filesystem can't support it. The actual failure happens on the first write (CREATE TABLE ... → SQLITE_IOERR_SHORT_READ, system errno 95 = EOPNOTSUPP), at which point the connection is irreversibly corrupted — switching to DELETE mode afterwards also fails.
The existing comment on lines 156-162 suggests the author expected a graceful fallback:
"SQLite silently keeps the prior mode if WAL can't be enabled"
But SQLite does not silently fall back — it reports WAL as active but then fails on writes.
Testing
Confirmed via direct syscall test:
mmap 32KB MAP_SHARED: 0xffffffffffffffff errno=95 (Operation not supported)
And via SQLite test:
PRAGMA journal_mode=WAL → OK (returns "wal")
CREATE TABLE t(x) → disk I/O error (SQLITE_IOERR_SHORT_READ)
Proposed Fix
Pre-probe WAL support with a separate temporary database before configuring the real connection. If the probe fails, skip the WAL pragma entirely (SQLite defaults to DELETE mode).
Changes to lib/dist/db/index.js:
- Add
walAvailable(dir) function:
function walAvailable(dir) {
const probePath = path.join(dir, '_cg_wal_test.db');
try {
const { DatabaseSync } = require('node:sqlite');
const probe = new DatabaseSync(probePath);
probe.exec('PRAGMA journal_mode = WAL');
probe.exec('CREATE TABLE p(x); DROP TABLE p');
probe.close();
fs.unlinkSync(probePath);
try { fs.unlinkSync(probePath + '-wal'); } catch (_e) {}
try { fs.unlinkSync(probePath + '-shm'); } catch (_e) {}
return true;
} catch {
try { fs.unlinkSync(probePath); } catch (_e) {}
try { fs.unlinkSync(probePath + '-wal'); } catch (_e) {}
try { fs.unlinkSync(probePath + '-shm'); } catch (_e) {}
return false;
}
}
- Modify
configureConnection to accept useWal parameter:
function configureConnection(db, useWal) {
db.pragma('busy_timeout = 5000');
db.pragma('foreign_keys = ON');
if (useWal) {
db.pragma('journal_mode = WAL');
db.pragma('synchronous = NORMAL');
} else {
db.pragma('synchronous = FULL');
}
db.pragma('cache_size = -64000');
db.pragma('temp_store = MEMORY');
db.pragma('mmap_size = 268435456');
}
- Update call sites in
initialize() and open():
configureConnection(db, walAvailable(dir));
// or: configureConnection(db, walAvailable(path.dirname(dbPath)));
Why a separate probe?
Once the main connection's WAL write fails, the database connection is corrupted — even PRAGMA journal_mode = DELETE won't recover it. A separate temporary DB isolates the test.
Related
Tested
- ntfs3: probe correctly returns
false, falls back to DELETE, init succeeds (36,554 nodes)
- btrfs/ext4: probe returns
true, WAL active, normal operation
- Probe file cleanup: verified no
_cg_wal_test.* residue after init
Bug: SQLite WAL mode fails silently on filesystems without
mmap MAP_SHAREDEnvironment
Symptoms
codegraph initfails withdisk I/O erroron ntfs3 (and likely other filesystems without shared-memory mmap: WSL2/mnt/, CIFS, some NFS configurations).Root Cause
SQLite WAL mode requires
mmap(MAP_SHARED)on the WAL-index file. ntfs3 returnsEOPNOTSUPPforMAP_SHARED.The current code in
db/index.js:configureConnection()does:However,
PRAGMA journal_mode = WALreports success ("wal") even when the underlying filesystem can't support it. The actual failure happens on the first write (CREATE TABLE ...→SQLITE_IOERR_SHORT_READ, system errno 95 =EOPNOTSUPP), at which point the connection is irreversibly corrupted — switching to DELETE mode afterwards also fails.The existing comment on lines 156-162 suggests the author expected a graceful fallback:
But SQLite does not silently fall back — it reports WAL as active but then fails on writes.
Testing
Confirmed via direct syscall test:
And via SQLite test:
Proposed Fix
Pre-probe WAL support with a separate temporary database before configuring the real connection. If the probe fails, skip the WAL pragma entirely (SQLite defaults to DELETE mode).
Changes to
lib/dist/db/index.js:walAvailable(dir)function:configureConnectionto acceptuseWalparameter:initialize()andopen():Why a separate probe?
Once the main connection's WAL write fails, the database connection is corrupted — even
PRAGMA journal_mode = DELETEwon't recover it. A separate temporary DB isolates the test.Related
codegraph statusalready reports the effective journal mode, so this is transparent to the daemonTested
false, falls back to DELETE, init succeeds (36,554 nodes)true, WAL active, normal operation_cg_wal_test.*residue after init