Skip to main content

sqlite_graphrag/storage/
connection.rs

1//! SQLite connection setup with PRAGMAs and 0600 permissions.
2//!
3//! v1.0.76: opens (or creates) the database file. The `sqlite-vec` extension
4//! was REMOVED; vector similarity is now computed in pure Rust over the
5//! `memory_embeddings(memory_id, embedding BLOB, source)` table. WAL/journal
6//! PRAGMAs and 0600 file permissions on Unix are unchanged.
7
8use crate::errors::AppError;
9use crate::paths::AppPaths;
10use crate::pragmas::{apply_connection_pragmas, apply_init_pragmas, ensure_wal_mode};
11use rusqlite::Connection;
12use std::path::Path;
13
14/// v1.0.76: no-op stub. Kept for source compatibility with callers that
15/// still call `register_vec_extension()` during auto-init. The actual
16/// extension registration is gone; the function is now a marker that
17/// the LLM-only build does not need any vector extension.
18pub fn register_vec_extension() {}
19
20pub fn open_rw(path: &Path) -> Result<Connection, AppError> {
21    let conn = Connection::open(path)?;
22    apply_connection_pragmas(&conn)?;
23    apply_secure_permissions(path);
24    Ok(conn)
25}
26
27pub fn ensure_schema(conn: &mut Connection) -> Result<(), AppError> {
28    crate::migrations::runner()
29        .run(conn)
30        .map_err(|e| AppError::Internal(anyhow::anyhow!("migration failed: {e}")))?;
31    conn.execute_batch(&format!(
32        "PRAGMA user_version = {};",
33        crate::constants::SCHEMA_USER_VERSION
34    ))?;
35    Ok(())
36}
37
38/// Ensures the database file exists and the schema is at the current version.
39///
40/// Behavior:
41/// - DB does not exist: creates the file, applies init PRAGMAs, runs all migrations,
42///   sets `PRAGMA user_version`, and populates `schema_meta` with default values.
43///   Emits `tracing::info!` on creation.
44/// - DB exists with `user_version` below `SCHEMA_USER_VERSION`: runs the remaining
45///   migrations and updates `user_version`. Emits `tracing::warn!` on auto-migration.
46/// - DB exists with `user_version` equal to `SCHEMA_USER_VERSION`: no-op.
47///
48/// This helper unifies the auto-init contract across CRUD handlers so users can run
49/// any subcommand on a fresh directory without invoking `init` first. Idempotent
50/// and safe to call before every handler that needs a ready database.
51pub fn ensure_db_ready(paths: &AppPaths) -> Result<(), AppError> {
52    register_vec_extension();
53    paths.ensure_dirs()?;
54
55    let db_existed = paths.db.exists();
56
57    if !db_existed {
58        tracing::info!(target: "storage",
59            path = %paths.db.display(),
60            schema_version = crate::constants::CURRENT_SCHEMA_VERSION,
61            "creating database (auto-init)"
62        );
63    }
64
65    let mut conn = open_rw(&paths.db)?;
66
67    if !db_existed {
68        apply_init_pragmas(&conn)?;
69    }
70
71    let current_user_version: i64 = conn
72        .query_row("PRAGMA user_version", [], |row| row.get(0))
73        .unwrap_or(0);
74    let target_user_version = crate::constants::SCHEMA_USER_VERSION;
75
76    if current_user_version < target_user_version {
77        if db_existed {
78            tracing::warn!(target: "storage",
79                from = current_user_version,
80                to = target_user_version,
81                path = %paths.db.display(),
82                "auto-migrating database schema"
83            );
84        }
85        crate::migrations::runner()
86            .run(&mut conn)
87            .map_err(|e| AppError::Internal(anyhow::anyhow!("auto-migration failed: {e}")))?;
88        conn.execute_batch(&format!("PRAGMA user_version = {target_user_version};"))?;
89
90        if !db_existed {
91            insert_default_schema_meta(&conn)?;
92        }
93
94        // Defensive re-assertion: refinery's migration runner may open internal
95        // handles that revert journal_mode to delete on some platforms. Re-apply
96        // WAL after migrations to guarantee the documented contract holds for
97        // every command that goes through the auto-init path.
98        ensure_wal_mode(&conn)?;
99    }
100
101    // G41 repair: if V013 is in history but embedding tables are missing,
102    // execute V013 SQL directly. Runs unconditionally because databases
103    // corrupted by G41 already have user_version=50 and skip the block above.
104    crate::commands::migrate::ensure_v013_tables_exist(&conn)?;
105
106    Ok(())
107}
108
109fn insert_default_schema_meta(conn: &Connection) -> Result<(), AppError> {
110    conn.execute(
111        "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', ?1)",
112        rusqlite::params![crate::constants::CURRENT_SCHEMA_VERSION.to_string()],
113    )?;
114    conn.execute(
115        "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('model', 'multilingual-e5-small')",
116        [],
117    )?;
118    conn.execute(
119        "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('dim', '384')",
120        [],
121    )?;
122    conn.execute(
123        "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('created_at', CAST(unixepoch() AS TEXT))",
124        [],
125    )?;
126    conn.execute(
127        "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('sqlite-graphrag_version', ?1)",
128        rusqlite::params![crate::constants::SQLITE_GRAPHRAG_VERSION],
129    )?;
130    Ok(())
131}
132
133/// Applies 600 permissions (owner read/write only) to the SQLite file and its WAL/SHM
134/// companion files on Unix to prevent leaking private memories in shared directories
135/// (e.g. multi-user /tmp, Dropbox, NFS). On Windows, NTFS DACL default is private-to-user
136/// so explicit permission setting is unnecessary; a debug log records the skip. Failures
137/// are silent to avoid blocking the operation when the process does not own the file
138/// (e.g. read-only mount).
139#[allow(unused_variables)]
140fn apply_secure_permissions(path: &Path) {
141    #[cfg(unix)]
142    {
143        use std::os::unix::fs::PermissionsExt;
144        let candidates = [
145            path.to_path_buf(),
146            path.with_extension(format!(
147                "{}-wal",
148                path.extension()
149                    .and_then(|e| e.to_str())
150                    .unwrap_or("sqlite")
151            )),
152            path.with_extension(format!(
153                "{}-shm",
154                path.extension()
155                    .and_then(|e| e.to_str())
156                    .unwrap_or("sqlite")
157            )),
158        ];
159        for file in candidates.iter() {
160            if file.exists() {
161                if let Ok(meta) = std::fs::metadata(file) {
162                    let mut perms = meta.permissions();
163                    perms.set_mode(0o600);
164                    let _ = std::fs::set_permissions(file, perms);
165                }
166            }
167        }
168    }
169    #[cfg(windows)]
170    {
171        tracing::debug!(target: "storage",
172            path = %path.display(),
173            "skipping Unix mode 0o600 on Windows; NTFS DACL default is private-to-user"
174        );
175    }
176}
177
178pub fn open_ro(path: &Path) -> Result<Connection, AppError> {
179    let conn = Connection::open_with_flags(
180        path,
181        rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_URI,
182    )?;
183    conn.execute_batch("PRAGMA foreign_keys = ON;")?;
184    Ok(conn)
185}