sqlite_graphrag/commands/
init.rs1use crate::errors::AppError;
4use crate::output;
5use crate::paths::AppPaths;
6use crate::pragmas::{apply_init_pragmas, ensure_wal_mode};
7use crate::storage::connection::open_rw;
8use serde::Serialize;
9
10#[derive(Copy, Clone, Debug, PartialEq, Eq, clap::ValueEnum)]
16pub enum EmbeddingModelChoice {
17 #[value(name = "multilingual-e5-small")]
19 MultilingualE5Small,
20}
21
22#[derive(clap::Args)]
23#[command(after_long_help = "EXAMPLES:\n \
24 # Initialize a new database in the current directory\n \
25 sqlite-graphrag init\n\n \
26 # Initialize with a specific namespace\n \
27 sqlite-graphrag init --namespace my-project\n\n \
28 # Initialize at a custom database path\n \
29 sqlite-graphrag init --db /path/to/graphrag.sqlite")]
30pub struct InitArgs {
32 #[arg(long)]
38 pub db: Option<String>,
39 #[arg(long, value_enum)]
42 pub model: Option<EmbeddingModelChoice>,
43 #[arg(long)]
46 pub force: bool,
47 #[arg(long)]
50 pub namespace: Option<String>,
51 #[arg(long, hide = true, help = "No-op; JSON is always emitted on stdout")]
53 pub json: bool,
54}
55
56#[derive(Serialize)]
57struct InitResponse {
58 db_path: String,
59 schema_version: u32,
62 model: String,
63 dim: usize,
64 namespace: String,
66 status: String,
67 elapsed_ms: u64,
69}
70
71pub fn run(
73 args: InitArgs,
74 llm_backend: crate::cli::LlmBackendChoice,
75 embedding_backend: crate::cli::EmbeddingBackendChoice,
76) -> Result<(), AppError> {
77 let start = std::time::Instant::now();
78 let paths = AppPaths::resolve(args.db.as_deref())?;
79 paths.ensure_dirs()?;
80
81 let namespace = crate::namespace::resolve_namespace(args.namespace.as_deref())?;
82
83 let mut conn = open_rw(&paths.db)?;
84
85 apply_init_pragmas(&conn)?;
86
87 crate::migrations::runner()
88 .run(&mut conn)
89 .map_err(|e| AppError::Internal(anyhow::anyhow!("migration failed: {e}")))?;
90
91 conn.execute_batch(&format!(
92 "PRAGMA user_version = {};",
93 crate::constants::SCHEMA_USER_VERSION
94 ))?;
95
96 ensure_wal_mode(&conn)?;
98
99 let schema_version = latest_schema_version(&conn)?;
100
101 conn.execute(
102 "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', ?1)",
103 rusqlite::params![schema_version],
104 )?;
105 conn.execute(
106 "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('model', ?1)",
107 rusqlite::params![crate::constants::SQLITE_GRAPHRAG_VERSION],
108 )?;
109 conn.execute(
114 "INSERT OR IGNORE INTO schema_meta (key, value) VALUES ('dim', ?1)",
115 rusqlite::params![crate::constants::embedding_dim().to_string()],
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 conn.execute(
127 "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('namespace_initial', ?1)",
128 rusqlite::params![namespace],
129 )?;
130
131 output::emit_progress_i18n(
132 "Validating embedding backend...",
133 "Validando backend de embedding...",
134 );
135
136 let (dim, status) = match crate::embedder::embed_passage_with_embedding_choice(
143 &paths.models,
144 "smoke test",
145 embedding_backend,
146 llm_backend,
147 ) {
148 Ok((v, _backend)) => (v.len(), "ok"),
149 Err(crate::errors::AppError::Validation(msg)) => {
150 return Err(crate::errors::AppError::Validation(msg))
151 }
152 Err(e) => {
153 tracing::warn!(target: "init", error = %e, "embedding smoke test failed; init continues without LLM validation");
154 (crate::constants::embedding_dim(), "ok_no_embedding")
155 }
156 };
157
158 output::emit_json(&InitResponse {
159 db_path: paths.db.display().to_string(),
160 schema_version,
161 model: crate::constants::SQLITE_GRAPHRAG_VERSION.to_string(),
162 dim,
163 namespace,
164 status: status.to_string(),
165 elapsed_ms: start.elapsed().as_millis() as u64,
166 })?;
167
168 Ok(())
169}
170
171fn latest_schema_version(conn: &rusqlite::Connection) -> Result<u32, AppError> {
172 match conn.query_row(
173 "SELECT version FROM refinery_schema_history ORDER BY version DESC LIMIT 1",
174 [],
175 |row| row.get::<_, i64>(0),
176 ) {
177 Ok(version) => Ok(version.max(0) as u32),
178 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(0),
179 Err(err) => Err(AppError::Database(err)),
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn init_response_serializes_all_fields() {
189 let resp = InitResponse {
190 db_path: "/tmp/test.sqlite".to_string(),
191 schema_version: 6,
192 model: crate::constants::SQLITE_GRAPHRAG_VERSION.to_string(),
193 dim: crate::constants::DEFAULT_EMBEDDING_DIM,
194 namespace: "global".to_string(),
195 status: "ok".to_string(),
196 elapsed_ms: 100,
197 };
198 let json = serde_json::to_value(&resp).expect("serialization failed");
199 assert_eq!(json["db_path"], "/tmp/test.sqlite");
200 assert_eq!(json["schema_version"], 6);
201 assert_eq!(json["model"], crate::constants::SQLITE_GRAPHRAG_VERSION);
202 assert_eq!(json["dim"], crate::constants::DEFAULT_EMBEDDING_DIM);
203 assert_eq!(json["namespace"], "global");
204 assert_eq!(json["status"], "ok");
205 assert!(json["elapsed_ms"].is_number());
206 }
207
208 #[test]
209 fn latest_schema_version_returns_zero_for_empty_db() {
210 let conn = rusqlite::Connection::open_in_memory().expect("failed to open in-memory db");
211 conn.execute_batch("CREATE TABLE refinery_schema_history (version INTEGER NOT NULL);")
212 .expect("failed to create table");
213
214 let version = latest_schema_version(&conn).expect("latest_schema_version failed");
215 assert_eq!(version, 0u32, "empty db must return schema_version 0");
216 }
217
218 #[test]
219 fn latest_schema_version_returns_max_version() {
220 let conn = rusqlite::Connection::open_in_memory().expect("failed to open in-memory db");
221 conn.execute_batch(
222 "CREATE TABLE refinery_schema_history (version INTEGER NOT NULL);
223 INSERT INTO refinery_schema_history VALUES (1);
224 INSERT INTO refinery_schema_history VALUES (3);
225 INSERT INTO refinery_schema_history VALUES (2);",
226 )
227 .expect("failed to populate table");
228
229 let version = latest_schema_version(&conn).expect("latest_schema_version failed");
230 assert_eq!(version, 3u32, "must return the highest version present");
231 }
232
233 #[test]
234 fn init_default_dim_matches_the_registered_setting_default() {
235 let registered = crate::config::SETTING_KEYS
241 .iter()
242 .find(|entry| entry.key == "embedding.dim")
243 .and_then(|entry| entry.default)
244 .expect("embedding.dim must be registered with a literal default");
245 assert_eq!(
246 registered.parse::<usize>().ok(),
247 Some(crate::constants::DEFAULT_EMBEDDING_DIM),
248 "config doctor would advertise {registered} while init stamps {}",
249 crate::constants::DEFAULT_EMBEDDING_DIM
250 );
251 }
252
253 #[test]
254 fn init_default_dim_is_inside_the_accepted_range() {
255 assert!(
258 crate::constants::EMBEDDING_DIM_RANGE
259 .contains(&crate::constants::DEFAULT_EMBEDDING_DIM),
260 "default dim must sit inside EMBEDDING_DIM_RANGE"
261 );
262 }
263
264 #[test]
265 fn init_response_namespace_aligned_with_schema() {
266 let resp = InitResponse {
268 db_path: "/tmp/x.sqlite".to_string(),
269 schema_version: 6,
270 model: crate::constants::SQLITE_GRAPHRAG_VERSION.to_string(),
271 dim: crate::constants::DEFAULT_EMBEDDING_DIM,
272 namespace: "my-project".to_string(),
273 status: "ok".to_string(),
274 elapsed_ms: 0,
275 };
276 let json = serde_json::to_value(&resp).expect("serialization failed");
277 assert_eq!(json["namespace"], "my-project");
278 }
279}