Skip to content

Read the write-ahead log: Database.AccessMode - #19

Merged
mattt merged 4 commits into
mattt:mainfrom
patp:wal-read
Sep 2, 2026
Merged

mattt merged 4 commits into
mattt:mainfrom
patp:wal-read

Conversation

@patp

@patp patp commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor

Messages keeps chat.db in WAL mode. Database(path:) opens it with immutable=1, which tells SQLite to ignore chat.db-wal, so every message committed since the last checkpoint is invisible. Checkpoints happen every ~1000 pages (4 MB) of writes: on a typical account the newest messages lag by hours and then appear in a batch. Observed today through iMCP: nothing newer than 12:23 while chat.db-wal held eight later messages; they all appeared at 17:30, when Messages checkpointed.

This adds Database.AccessMode:

  • .live — mode=ro only; SQLite reads the -wal/-shm companions. A read-only grant on the directory is enough (SQLite ≥ 3.22 handles a read-only -shm).
  • .immutable — the previous behaviour, for callers whose grant covers chat.db alone.
  • .automatic (default) — opens live, and falls back to immutable when the first read cannot open the companions.

init(path:) keeps working unchanged (mode: defaults to .automatic), accessMode reports what was chosen, and a 1 s busy timeout covers the locks a live connection now shares with Messages. The tests build a WAL-mode fixture with one checkpointed row and one still in the log and check the three modes, including the fallback (companions made unreadable). The README gains a FAQ entry.

Companion change in iMCP: mattt/iMCP#198

🤖 Generated with Claude Code

https://claude.ai/code/session_0187kUP2mjhwMfJkwRbP5DG7

patp and others added 2 commits September 1, 2026 17:39
Messages keeps chat.db in WAL mode, and Database(path:) opened it with
immutable=1, which makes SQLite ignore chat.db-wal. Every message committed
since the last checkpoint was therefore invisible; checkpoints happen every
few MB of writes, so the newest messages lagged by hours and then appeared
in batches.

Database.AccessMode chooses how the file is read: .live (mode=ro, the
-wal/-shm companions are read; a read-only grant on the directory is
enough), .immutable (the previous behaviour, for a grant that covers
chat.db alone) and .automatic (default: live, falling back to immutable
when the first read cannot open the companions). init(path:) is unchanged
for existing callers, accessMode reports the outcome, and a 1 s busy
timeout covers the locks a live connection shares with Messages.

Tests build a WAL-mode fixture with one checkpointed row and one still in
the log and check the three modes, including the fallback.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187kUP2mjhwMfJkwRbP5DG7
A sandboxed app that was granted the Messages folder reads it read-only, so SQLite
has to work with a shared-memory index it cannot write to.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187kUP2mjhwMfJkwRbP5DG7

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The unresolved query-step error handling can silently return incomplete data.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds WAL-aware database access while preserving immutable fallback behavior.

Changes:

  • Introduces live, immutable, and automatic access modes.
  • Adds WAL-mode tests and fallback coverage.
  • Documents stale-message behavior and configuration.
File summaries
File Review
Tests/iMessageTests/AccessModeTests.swift Tests WAL visibility and fallback behavior.
Sources/iMessage/Database.swift Implements access modes and busy timeout. A moderate issue remains: query stepping must throw unless it ends with SQLITE_DONE, preventing silent empty or partial results on SQLITE_BUSY or other errors.
README.md Documents WAL-related message lag.
Review details

Suppressed comments (1)

Sources/iMessage/Database.swift:153

  • canRead collapses every SQLite failure into false, so .automatic silently switches to immutable not only when the WAL companions are inaccessible, but also after transient SQLITE_BUSY, corruption, or an I/O error. In those cases this converts a real failure into a successful connection that can return stale checkpointed data—the exact behavior this mode is intended to avoid. Preserve the SQLite result/extended error code and fall back only for the specific companion-access errors; propagate all other read failures.
            if Database.canRead(handle) {
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread Sources/iMessage/Database.swift Outdated
Comment on lines +105 to +106
// A live connection shares locks with Messages; wait briefly instead of failing.
sqlite3_busy_timeout(handle, 1000)
The step loop treated every non-row status as a clean end of results,
so a step error, or SQLITE_BUSY on a live connection that loses a race
with Messages, silently returned partial data.
Surface it as Error.queryError instead.
Make execute and Bindable internal so the case can be tested directly.
@mattt

mattt commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Thanks, @patp. Reading the WAL by default with an immutable fallback is exactly what this needed, and the docs explain the trade-off well. I pushed one follow-up: the step loop now throws on anything other than SQLITE_DONE, so a busy live connection can't silently return partial results (Copilot caught that one). Merging now.

@mattt
mattt merged commit 11ec80c into mattt:main Sep 2, 2026
3 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.

3 participants