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> {
22 let conn = Connection::open(path)?;
23 apply_connection_pragmas(&conn)?;
24 apply_secure_permissions(path);
25 adopt_embedding_dim(&conn);
26 Ok(conn)
27}
28
29fn adopt_embedding_dim(conn: &Connection) {
44 if crate::constants::embedding_dim_from_runtime().is_some() {
45 return;
46 }
47 if let Ok(value) = conn.query_row(
48 "SELECT value FROM schema_meta WHERE key = 'dim'",
49 [],
50 |row| row.get::<_, String>(0),
51 ) {
52 if let Ok(dim) = value.parse::<usize>() {
53 crate::constants::set_active_embedding_dim(dim);
54 }
55 }
56}
57
58pub fn ensure_schema(conn: &mut Connection) -> Result<(), AppError> {
60 crate::migrations::runner()
61 .run(conn)
62 .map_err(|e| AppError::Internal(anyhow::anyhow!("migration failed: {e}")))?;
63 conn.execute_batch(&format!(
64 "PRAGMA user_version = {};",
65 crate::constants::SCHEMA_USER_VERSION
66 ))?;
67 Ok(())
68}
69
70pub fn ensure_db_ready(paths: &AppPaths) -> Result<(), AppError> {
84 register_vec_extension();
85 paths.ensure_dirs()?;
86
87 let db_existed = paths.db.exists();
88
89 if !db_existed {
90 tracing::info!(target: "storage",
91 path = %paths.db.display(),
92 schema_version = crate::constants::CURRENT_SCHEMA_VERSION,
93 "creating database (auto-init)"
94 );
95 }
96
97 let mut conn = open_rw(&paths.db)?;
98
99 if !db_existed {
100 apply_init_pragmas(&conn)?;
101 }
102
103 let current_user_version: i64 = conn
104 .query_row("PRAGMA user_version", [], |row| row.get(0))
105 .unwrap_or(0);
106 let target_user_version = crate::constants::SCHEMA_USER_VERSION;
107
108 if current_user_version < target_user_version {
109 if db_existed {
110 tracing::warn!(target: "storage",
111 from = current_user_version,
112 to = target_user_version,
113 path = %paths.db.display(),
114 "auto-migrating database schema"
115 );
116 }
117 crate::migrations::runner()
118 .run(&mut conn)
119 .map_err(|e| AppError::Internal(anyhow::anyhow!("auto-migration failed: {e}")))?;
120 conn.execute_batch(&format!("PRAGMA user_version = {target_user_version};"))?;
121
122 if !db_existed {
123 insert_default_schema_meta(&conn)?;
124 }
125
126 ensure_wal_mode(&conn)?;
131 }
132
133 crate::commands::migrate::ensure_v013_tables_exist(&conn)?;
137
138 sync_embedding_dim_meta(&conn)?;
144
145 Ok(())
146}
147
148fn sync_embedding_dim_meta(conn: &Connection) -> Result<(), AppError> {
158 let db_dim: Option<usize> = conn
159 .query_row(
160 "SELECT value FROM schema_meta WHERE key = 'dim'",
161 [],
162 |row| row.get::<_, String>(0),
163 )
164 .ok()
165 .and_then(|v| v.parse::<usize>().ok());
166
167 if let Some(override_dim) = crate::constants::embedding_dim_from_runtime() {
168 if db_dim != Some(override_dim) {
169 conn.execute(
170 "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('dim', ?1)",
171 rusqlite::params![override_dim.to_string()],
172 )?;
173 }
174 return Ok(());
175 }
176
177 match db_dim {
178 Some(dim) => crate::constants::set_active_embedding_dim(dim),
179 None => {
180 conn.execute(
181 "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('dim', ?1)",
182 rusqlite::params![crate::constants::embedding_dim().to_string()],
183 )?;
184 }
185 }
186 Ok(())
187}
188
189fn insert_default_schema_meta(conn: &Connection) -> Result<(), AppError> {
190 conn.execute(
191 "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', ?1)",
192 rusqlite::params![crate::constants::CURRENT_SCHEMA_VERSION.to_string()],
193 )?;
194 conn.execute(
195 "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('model', ?1)",
196 rusqlite::params![crate::constants::SQLITE_GRAPHRAG_VERSION],
197 )?;
198 conn.execute(
199 "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('dim', ?1)",
200 rusqlite::params![crate::constants::embedding_dim().to_string()],
201 )?;
202 conn.execute(
203 "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('created_at', CAST(unixepoch() AS TEXT))",
204 [],
205 )?;
206 conn.execute(
207 "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('sqlite-graphrag_version', ?1)",
208 rusqlite::params![crate::constants::SQLITE_GRAPHRAG_VERSION],
209 )?;
210 Ok(())
211}
212
213#[allow(unused_variables)]
220fn apply_secure_permissions(path: &Path) {
221 #[cfg(unix)]
222 {
223 use std::os::unix::fs::PermissionsExt;
224 let candidates = [
225 path.to_path_buf(),
226 path.with_extension(format!(
227 "{}-wal",
228 path.extension()
229 .and_then(|e| e.to_str())
230 .unwrap_or("sqlite")
231 )),
232 path.with_extension(format!(
233 "{}-shm",
234 path.extension()
235 .and_then(|e| e.to_str())
236 .unwrap_or("sqlite")
237 )),
238 ];
239 for file in candidates.iter() {
240 if file.exists() {
241 if let Ok(meta) = std::fs::metadata(file) {
242 let mut perms = meta.permissions();
243 perms.set_mode(0o600);
244 let _ = std::fs::set_permissions(file, perms);
245 }
246 }
247 }
248 }
249 #[cfg(windows)]
250 {
251 tracing::debug!(target: "storage",
252 path = %path.display(),
253 "skipping Unix mode 0o600 on Windows; NTFS DACL default is private-to-user"
254 );
255 }
256}
257
258pub fn open_ro(path: &Path) -> Result<Connection, AppError> {
260 let conn = Connection::open_with_flags(
261 path,
262 rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_URI,
263 )?;
264 conn.execute_batch("PRAGMA foreign_keys = ON;")?;
265 adopt_embedding_dim(&conn);
268 Ok(conn)
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
282 #[serial_test::serial(env)]
283 fn open_rw_adopts_schema_meta_dim() {
284 let dir = tempfile::tempdir().expect("tempdir");
285 let db = dir.path().join("g43.sqlite");
286 {
287 let conn = Connection::open(&db).expect("create seed db");
288 conn.execute_batch(
289 "CREATE TABLE schema_meta (key TEXT PRIMARY KEY, value TEXT);
290 INSERT INTO schema_meta VALUES ('dim', '128');",
291 )
292 .expect("seed schema_meta");
293 }
294 std::env::remove_var("SQLITE_GRAPHRAG_EMBEDDING_DIM");
295 let _conn = open_rw(&db).expect("open_rw");
296 let adopted = crate::constants::embedding_dim();
297 crate::constants::set_active_embedding_dim(crate::constants::DEFAULT_EMBEDDING_DIM);
300 assert_eq!(adopted, 128, "open_rw must adopt the recorded db dim (G43)");
301 }
302
303 #[test]
306 #[serial_test::serial(env)]
307 fn open_ro_adopts_schema_meta_dim() {
308 let dir = tempfile::tempdir().expect("tempdir");
309 let db = dir.path().join("g43-ro.sqlite");
310 {
311 let conn = Connection::open(&db).expect("create seed db");
312 conn.execute_batch(
313 "CREATE TABLE schema_meta (key TEXT PRIMARY KEY, value TEXT);
314 INSERT INTO schema_meta VALUES ('dim', '256');",
315 )
316 .expect("seed schema_meta");
317 }
318 std::env::remove_var("SQLITE_GRAPHRAG_EMBEDDING_DIM");
319 let _conn = open_ro(&db).expect("open_ro");
320 let adopted = crate::constants::embedding_dim();
321 crate::constants::set_active_embedding_dim(crate::constants::DEFAULT_EMBEDDING_DIM);
322 assert_eq!(adopted, 256, "open_ro must adopt the recorded db dim (G43)");
323 }
324
325 #[test]
328 #[serial_test::serial(env)]
329 fn env_override_wins_over_schema_meta_dim() {
330 let dir = tempfile::tempdir().expect("tempdir");
331 let db = dir.path().join("g43-env.sqlite");
332 {
333 let conn = Connection::open(&db).expect("create seed db");
334 conn.execute_batch(
335 "CREATE TABLE schema_meta (key TEXT PRIMARY KEY, value TEXT);
336 INSERT INTO schema_meta VALUES ('dim', '128');",
337 )
338 .expect("seed schema_meta");
339 }
340 let _conn = open_rw(&db).expect("open_rw");
343 let adopted = crate::constants::embedding_dim();
344 crate::constants::set_active_embedding_dim(crate::constants::DEFAULT_EMBEDDING_DIM);
345 assert_eq!(
346 adopted, 128,
347 "schema_meta dim is adopted when no CLI/XDG override is active"
348 );
349 }
350
351 #[test]
354 #[serial_test::serial(env)]
355 fn open_rw_on_virgin_db_is_a_noop() {
356 let dir = tempfile::tempdir().expect("tempdir");
357 let db = dir.path().join("g43-virgin.sqlite");
358 std::env::remove_var("SQLITE_GRAPHRAG_EMBEDDING_DIM");
359 crate::constants::set_active_embedding_dim(crate::constants::DEFAULT_EMBEDDING_DIM);
360 let _conn = open_rw(&db).expect("open_rw on virgin db must not fail");
361 assert_eq!(
362 crate::constants::embedding_dim(),
363 crate::constants::DEFAULT_EMBEDDING_DIM,
364 "virgin db must keep the compiled default (G43)"
365 );
366 }
367}