sqlite_graphrag/pragmas.rs
1//! SQLite PRAGMA helpers applied at connection open and on each transaction.
2
3use crate::errors::AppError;
4use rusqlite::Connection;
5
6/// Applies one-time PRAGMAs on a freshly opened connection (e.g. `auto_vacuum`).
7///
8/// Calls [`apply_connection_pragmas`] internally and then sets `wal_autocheckpoint`.
9/// Must be called once per database file, not once per connection.
10///
11/// # Errors
12/// Returns `Err` when any PRAGMA execution fails.
13pub fn apply_init_pragmas(conn: &Connection) -> Result<(), AppError> {
14 conn.execute_batch("PRAGMA auto_vacuum = INCREMENTAL;")?;
15 apply_connection_pragmas(conn)?;
16 conn.execute_batch(&format!(
17 "PRAGMA wal_autocheckpoint = {};",
18 crate::constants::WAL_AUTOCHECKPOINT_PAGES
19 ))?;
20 Ok(())
21}
22
23/// Re-asserts `PRAGMA journal_mode = WAL` after operations that may revert it
24/// (notably refinery-driven migrations, which can open internal handles that
25/// reset the journal mode in some scenarios). Idempotent and cheap; emits
26/// `tracing::warn!` if WAL fails to engage so degraded behaviour is observable.
27pub fn ensure_wal_mode(conn: &Connection) -> Result<(), AppError> {
28 let mode: String = conn.query_row("PRAGMA journal_mode = WAL;", [], |r| r.get(0))?;
29 if mode != "wal" {
30 tracing::warn!(target: "pragmas", mode = %mode, "journal_mode did not switch to WAL after re-assertion");
31 }
32 Ok(())
33}
34
35/// Lightweight WAL + busy_timeout for sidecar queue DBs (enrich / ingest).
36///
37/// # Schema note (GAP-SG-121)
38///
39/// Enrich (`.enrich-queue.sqlite`) and ingest (`.ingest-queue.sqlite`) queues
40/// are **different products**: enrich tracks `(namespace, operation, item_key)`
41/// with dead-letter / claim columns; ingest tracks `file_path` progress
42/// (claude uses `cost_usd`, codex uses token counters). Do **not** unify their
43/// `CREATE TABLE` shapes — only share these connection pragmas.
44///
45/// # Errors
46/// Returns `Err` when any PRAGMA execution fails.
47pub fn apply_sidecar_queue_pragmas(conn: &Connection) -> Result<(), AppError> {
48 conn.pragma_update(None, "journal_mode", "wal")?;
49 // Without busy_timeout, concurrent claim/write contention surfaces as
50 // SQLITE_BUSY immediately (see GAP-SG-76 / rules_rust_sqlite.md).
51 // GAP-SG-87: XDG `db.query_timeout_ms` > factory `BUSY_TIMEOUT_MILLIS`.
52 conn.pragma_update(None, "busy_timeout", resolved_busy_timeout_ms())?;
53 Ok(())
54}
55
56/// Resolved `PRAGMA busy_timeout` (ms): XDG `db.query_timeout_ms` > factory default.
57fn resolved_busy_timeout_ms() -> i32 {
58 let ms = crate::runtime_config::db_query_timeout_ms(crate::constants::QUERY_TIMEOUT_MILLIS);
59 i32::try_from(ms).unwrap_or(crate::constants::BUSY_TIMEOUT_MILLIS).max(0)
60}
61
62/// Applies per-connection PRAGMAs: synchronous, foreign keys, busy timeout, cache, mmap, WAL.
63///
64/// Safe to call on every new connection; all settings are idempotent.
65///
66/// # Errors
67/// Returns `Err` when any PRAGMA execution fails.
68pub fn apply_connection_pragmas(conn: &Connection) -> Result<(), AppError> {
69 conn.execute_batch(&format!(
70 "PRAGMA synchronous = NORMAL;
71 PRAGMA foreign_keys = ON;
72 PRAGMA busy_timeout = {busy};
73 PRAGMA cache_size = {cache};
74 PRAGMA temp_store = MEMORY;
75 PRAGMA mmap_size = {mmap};",
76 busy = resolved_busy_timeout_ms(),
77 cache = crate::constants::CACHE_SIZE_KB,
78 mmap = crate::constants::MMAP_SIZE_BYTES,
79 ))?;
80 let mode: String = conn.query_row("PRAGMA journal_mode = WAL;", [], |r| r.get(0))?;
81 if mode != "wal" {
82 tracing::warn!(target: "pragmas", mode = %mode, "journal_mode did not switch to WAL");
83 }
84 Ok(())
85}