Skip to content

feat(sqlite): use WAL with synchronous NORMAL for file databases - #87

Merged
cardmagic merged 9 commits into
mainfrom
feat/sqlite-wal
Oct 22, 2026
Merged

cardmagic merged 9 commits into
mainfrom
feat/sqlite-wal

Conversation

@cardmagic

Copy link
Copy Markdown
Owner

Summary

  • A SQLite file database now uses write-ahead logging (WAL) with synchronous = NORMAL, the settings that the Ruby gem gets from Active Record 7.1 and later. Before, the adapter used SQLite's defaults: a rollback journal with synchronous = FULL, about four fsync calls for each commit.
  • node:sqlite is synchronous, so each slow fsync stopped the whole event loop: timers, HTTP handlers, heartbeats, and lease renewals. In CI run 39180349972, a slow disk made compare-agent-session time out a test that takes 16 ms; only the test files with a file-backed database got 3 to 6 times slower. In a Linux container limited to 30 write IOPS, the same test timed out; with WAL and NORMAL at 20 write IOPS it took 398 ms.
  • New option: sqlite({ path, synchronous: "full" }) flushes each commit before it returns (about one fsync with WAL). Any other value throws a TypeError.
  • The switch to WAL needs a lock, and SQLite refuses it at once while another connection writes. So the adapter switches on its first access, with the same bounded retries and deadline rules as BEGIN IMMEDIATE, not in the constructor. One shared helper now does the busy retry for both.
  • An in-memory database keeps its memory journal.

Durability

  • A process crash loses no commit, and the database stays consistent.
  • With "normal", a power loss or an operating system crash can lose the last commits. "full" keeps the old guarantee for each commit.
  • WAL keeps <path>-wal and <path>-shm next to the database. docs/configuration.md says how to copy the files.

Tests, written first

  • A file database opens with WAL and NORMAL by default (failed with delete and 2 before).
  • synchronous: "full" gives WAL and FULL.
  • A file in the old journal mode switches after another writer releases its lock (failed with "database is locked" when the switch was in the constructor).
  • An in-memory database keeps the memory journal (guard).
  • A synchronous value other than "normal" or "full" throws.
  • The lock tests in sqlite.test.ts and database-deadline.test.ts now start from a WAL file, so they still test transaction retries and deadlines, not the switch.

Checks

  • pnpm run check, pnpm test (709 passed, 32 skipped), and prettier --check . pass.
  • CHANGELOG.md has the entry under "Unreleased". This changes a default, so it needs a release decision.

The adapter used SQLite's defaults: a rollback journal with
synchronous=FULL, about four fsync calls for each commit. node:sqlite
is synchronous, so on a slow disk each commit stopped the event loop,
including timers, HTTP handlers, heartbeats, and lease renewals. A CI
run with a slow disk timed out a 16 ms test after 5 seconds. The Ruby
gem already gets WAL with synchronous=NORMAL from Active Record.

The switch to WAL needs a lock, and SQLite refuses it at once while
another connection writes. So the adapter switches on its first
access, with the same bounded retries and deadline rules as a
transaction, instead of in the constructor.
The adapter now switches a file to WAL on its first access. These tests are about transaction retries and deadlines under writer contention, so their files start in WAL mode, as a file that the adapter made does. The switch under contention has its own test.
A user who wants each commit flushed to disk before it returns can pass synchronous: "full". The database still uses WAL, so a commit costs about one fsync instead of four.
The value goes into a PRAGMA statement, so the adapter accepts only the two documented levels and fails at construction, like lockRetryAttempts.
SQLite answers "memory" when an in-memory database asks for WAL. This guard keeps sqlite({ path: ":memory:" }) working after the WAL default.
The configuration guide states the durability trade-off of
synchronous NORMAL and FULL, the -wal and -shm files, and the switch
on first access. The parity ledger records that both runtimes now use
WAL with synchronous NORMAL for a file database.
@context7

context7 Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Docs7 for cardmagic/solid-objects-js

Result Status Action
Deployment ➖ Not used —
Content review ➖ Did not run. This site has no agent runs available this month. Wait for the monthly reset or check your Docs7 plan. —

Commit 29f1d3a

@greptile-apps

greptile-apps Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[High impact] The PR appears safe to merge; both previous findings are fixed and no new actionable issues were found.

Summary

File-backed SQLite databases now use WAL with synchronous = NORMAL, with "full" available when each commit must be flushed to disk.

  • File-backed SQLite databases now use WAL with NORMAL sync by default.

Reviews (2) · Last reviewed commit: "style: format the SQLite validation test" · Reviewed by Greptile

Comment thread docs/configuration.md Outdated
Comment thread test/sqlite.test.ts Outdated
Review found that closing one adapter does not write the WAL into the
database file while another connection still has it open, and that a
copy of three live files is not a consistent snapshot. The guide now
points to VACUUM INTO or the SQLite backup command for a database in
use. The validation test also casts its bad value without unknown, as
the repository rule asks.
@cardmagic

Copy link
Copy Markdown
Owner Author

@greptileai review

@cardmagic
cardmagic merged commit 9c9d7d5 into main Oct 22, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant