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 .set_abort_divergent(false)
62 .run(conn)
63 .map_err(|e| AppError::Internal(anyhow::anyhow!("migration failed: {e}")))?;
64 conn.execute_batch(&format!(
65 "PRAGMA user_version = {};",
66 crate::constants::SCHEMA_USER_VERSION
67 ))?;
68 Ok(())
69}
70
71pub fn ensure_db_ready(paths: &AppPaths) -> Result<(), AppError> {
85 register_vec_extension();
86 paths.ensure_dirs()?;
87
88 let db_existed = paths.db.exists();
89
90 if !db_existed {
91 tracing::info!(target: "storage",
92 path = %paths.db.display(),
93 schema_version = crate::constants::CURRENT_SCHEMA_VERSION,
94 "creating database (auto-init)"
95 );
96 }
97
98 let mut conn = open_rw(&paths.db)?;
99
100 if !db_existed {
101 apply_init_pragmas(&conn)?;
102 }
103
104 let current_user_version: i64 = conn
105 .query_row("PRAGMA user_version", [], |row| row.get(0))
106 .unwrap_or(0);
107 let target_user_version = crate::constants::SCHEMA_USER_VERSION;
108
109 if current_user_version < target_user_version {
110 if db_existed {
111 tracing::warn!(target: "storage",
112 from = current_user_version,
113 to = target_user_version,
114 path = %paths.db.display(),
115 "auto-migrating database schema"
116 );
117 }
118 crate::migrations::runner()
126 .set_abort_divergent(false)
127 .run(&mut conn)
128 .map_err(|e| AppError::Internal(anyhow::anyhow!("auto-migration failed: {e}")))?;
129 conn.execute_batch(&format!("PRAGMA user_version = {target_user_version};"))?;
130
131 if !db_existed {
132 insert_default_schema_meta(&conn)?;
133 }
134
135 ensure_wal_mode(&conn)?;
140 }
141
142 crate::commands::migrate::ensure_v013_tables_exist(&conn)?;
146
147 sync_embedding_dim_meta(&conn)?;
153
154 Ok(())
155}
156
157fn sync_embedding_dim_meta(conn: &Connection) -> Result<(), AppError> {
167 let db_dim: Option<usize> = conn
168 .query_row(
169 "SELECT value FROM schema_meta WHERE key = 'dim'",
170 [],
171 |row| row.get::<_, String>(0),
172 )
173 .ok()
174 .and_then(|v| v.parse::<usize>().ok());
175
176 if let Some(override_dim) = crate::constants::embedding_dim_from_runtime() {
177 if db_dim != Some(override_dim) {
178 conn.execute(
179 "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('dim', ?1)",
180 rusqlite::params![override_dim.to_string()],
181 )?;
182 }
183 return Ok(());
184 }
185
186 match db_dim {
187 Some(dim) => crate::constants::set_active_embedding_dim(dim),
188 None => {
189 conn.execute(
190 "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('dim', ?1)",
191 rusqlite::params![crate::constants::embedding_dim().to_string()],
192 )?;
193 }
194 }
195 Ok(())
196}
197
198fn insert_default_schema_meta(conn: &Connection) -> Result<(), AppError> {
199 conn.execute(
200 "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', ?1)",
201 rusqlite::params![crate::constants::CURRENT_SCHEMA_VERSION.to_string()],
202 )?;
203 conn.execute(
204 "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('model', ?1)",
205 rusqlite::params![crate::constants::SQLITE_GRAPHRAG_VERSION],
206 )?;
207 conn.execute(
208 "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('dim', ?1)",
209 rusqlite::params![crate::constants::embedding_dim().to_string()],
210 )?;
211 conn.execute(
212 "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('created_at', CAST(unixepoch() AS TEXT))",
213 [],
214 )?;
215 conn.execute(
216 "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('sqlite-graphrag_version', ?1)",
217 rusqlite::params![crate::constants::SQLITE_GRAPHRAG_VERSION],
218 )?;
219 Ok(())
220}
221
222#[allow(unused_variables)]
229fn apply_secure_permissions(path: &Path) {
230 #[cfg(unix)]
231 {
232 use std::os::unix::fs::PermissionsExt;
233 let candidates = [
234 path.to_path_buf(),
235 path.with_extension(format!(
236 "{}-wal",
237 path.extension()
238 .and_then(|e| e.to_str())
239 .unwrap_or("sqlite")
240 )),
241 path.with_extension(format!(
242 "{}-shm",
243 path.extension()
244 .and_then(|e| e.to_str())
245 .unwrap_or("sqlite")
246 )),
247 ];
248 for file in candidates.iter() {
249 if file.exists() {
250 if let Ok(meta) = std::fs::metadata(file) {
251 let mut perms = meta.permissions();
252 perms.set_mode(0o600);
253 let _ = std::fs::set_permissions(file, perms);
254 }
255 }
256 }
257 }
258 #[cfg(windows)]
259 {
260 tracing::debug!(target: "storage",
261 path = %path.display(),
262 "skipping Unix mode 0o600 on Windows; NTFS DACL default is private-to-user"
263 );
264 }
265}
266
267pub fn open_ro(path: &Path) -> Result<Connection, AppError> {
269 let conn = Connection::open_with_flags(
270 path,
271 rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_URI,
272 )?;
273 conn.execute_batch("PRAGMA foreign_keys = ON;")?;
274 adopt_embedding_dim(&conn);
277 Ok(conn)
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283
284 #[test]
291 #[serial_test::serial(env)]
292 fn open_rw_adopts_schema_meta_dim() {
293 let dir = tempfile::tempdir().expect("tempdir");
294 let db = dir.path().join("g43.sqlite");
295 {
296 let conn = Connection::open(&db).expect("create seed db");
297 conn.execute_batch(
298 "CREATE TABLE schema_meta (key TEXT PRIMARY KEY, value TEXT);
299 INSERT INTO schema_meta VALUES ('dim', '128');",
300 )
301 .expect("seed schema_meta");
302 }
303 std::env::remove_var("SQLITE_GRAPHRAG_EMBEDDING_DIM");
304 let _conn = open_rw(&db).expect("open_rw");
305 let adopted = crate::constants::embedding_dim();
306 crate::constants::set_active_embedding_dim(crate::constants::DEFAULT_EMBEDDING_DIM);
309 assert_eq!(adopted, 128, "open_rw must adopt the recorded db dim (G43)");
310 }
311
312 #[test]
315 #[serial_test::serial(env)]
316 fn open_ro_adopts_schema_meta_dim() {
317 let dir = tempfile::tempdir().expect("tempdir");
318 let db = dir.path().join("g43-ro.sqlite");
319 {
320 let conn = Connection::open(&db).expect("create seed db");
321 conn.execute_batch(
322 "CREATE TABLE schema_meta (key TEXT PRIMARY KEY, value TEXT);
323 INSERT INTO schema_meta VALUES ('dim', '256');",
324 )
325 .expect("seed schema_meta");
326 }
327 std::env::remove_var("SQLITE_GRAPHRAG_EMBEDDING_DIM");
328 let _conn = open_ro(&db).expect("open_ro");
329 let adopted = crate::constants::embedding_dim();
330 crate::constants::set_active_embedding_dim(crate::constants::DEFAULT_EMBEDDING_DIM);
331 assert_eq!(adopted, 256, "open_ro must adopt the recorded db dim (G43)");
332 }
333
334 #[test]
337 #[serial_test::serial(env)]
338 fn env_override_wins_over_schema_meta_dim() {
339 let dir = tempfile::tempdir().expect("tempdir");
340 let db = dir.path().join("g43-env.sqlite");
341 {
342 let conn = Connection::open(&db).expect("create seed db");
343 conn.execute_batch(
344 "CREATE TABLE schema_meta (key TEXT PRIMARY KEY, value TEXT);
345 INSERT INTO schema_meta VALUES ('dim', '128');",
346 )
347 .expect("seed schema_meta");
348 }
349 let _conn = open_rw(&db).expect("open_rw");
352 let adopted = crate::constants::embedding_dim();
353 crate::constants::set_active_embedding_dim(crate::constants::DEFAULT_EMBEDDING_DIM);
354 assert_eq!(
355 adopted, 128,
356 "schema_meta dim is adopted when no CLI/XDG override is active"
357 );
358 }
359
360 #[test]
363 #[serial_test::serial(env)]
364 fn open_rw_on_virgin_db_is_a_noop() {
365 let dir = tempfile::tempdir().expect("tempdir");
366 let db = dir.path().join("g43-virgin.sqlite");
367 std::env::remove_var("SQLITE_GRAPHRAG_EMBEDDING_DIM");
368 crate::constants::set_active_embedding_dim(crate::constants::DEFAULT_EMBEDDING_DIM);
369 let _conn = open_rw(&db).expect("open_rw on virgin db must not fail");
370 assert_eq!(
371 crate::constants::embedding_dim(),
372 crate::constants::DEFAULT_EMBEDDING_DIM,
373 "virgin db must keep the compiled default (G43)"
374 );
375 }
376}