Skip to content

WAL: auto-detect filesystem mmap support and fall back to DELETE mode #990

Description

@wings1848

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:

  1. 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;
    }
}
  1. 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');
}
  1. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions