sqlite_graphrag/storage/
connection.rs1use 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
14pub 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
38pub 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 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#[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}