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    Ok(())
102}
103
104fn insert_default_schema_meta(conn: &Connection) -> Result<(), AppError> {
105    conn.execute(
106        "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', ?1)",
107        rusqlite::params![crate::constants::CURRENT_SCHEMA_VERSION.to_string()],
108    )?;
109    conn.execute(
110        "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('model', 'multilingual-e5-small')",
111        [],
112    )?;
113    conn.execute(
114        "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('dim', '384')",
115        [],
116    )?;
117    conn.execute(
118        "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('created_at', CAST(unixepoch() AS TEXT))",
119        [],
120    )?;
121    conn.execute(
122        "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('sqlite-graphrag_version', ?1)",
123        rusqlite::params![crate::constants::SQLITE_GRAPHRAG_VERSION],
124    )?;
125    Ok(())
126}
127
128/// Applies 600 permissions (owner read/write only) to the SQLite file and its WAL/SHM
129/// companion files on Unix to prevent leaking private memories in shared directories
130/// (e.g. multi-user /tmp, Dropbox, NFS). On Windows, NTFS DACL default is private-to-user
131/// so explicit permission setting is unnecessary; a debug log records the skip. Failures
132/// are silent to avoid blocking the operation when the process does not own the file
133/// (e.g. read-only mount).
134#[allow(unused_variables)]
135fn apply_secure_permissions(path: &Path) {
136    #[cfg(unix)]
137    {
138        use std::os::unix::fs::PermissionsExt;
139        let candidates = [
140            path.to_path_buf(),
141            path.with_extension(format!(
142                "{}-wal",
143                path.extension()
144                    .and_then(|e| e.to_str())
145                    .unwrap_or("sqlite")
146            )),
147            path.with_extension(format!(
148                "{}-shm",
149                path.extension()
150                    .and_then(|e| e.to_str())
151                    .unwrap_or("sqlite")
152            )),
153        ];
154        for file in candidates.iter() {
155            if file.exists() {
156                if let Ok(meta) = std::fs::metadata(file) {
157                    let mut perms = meta.permissions();
158                    perms.set_mode(0o600);
159                    let _ = std::fs::set_permissions(file, perms);
160                }
161            }
162        }
163    }
164    #[cfg(windows)]
165    {
166        tracing::debug!(target: "storage",
167            path = %path.display(),
168            "skipping Unix mode 0o600 on Windows; NTFS DACL default is private-to-user"
169        );
170    }
171}
172
173pub fn open_ro(path: &Path) -> Result<Connection, AppError> {
174    let conn = Connection::open_with_flags(
175        path,
176        rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_URI,
177    )?;
178    conn.execute_batch("PRAGMA foreign_keys = ON;")?;
179    Ok(conn)
180}