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,
70 dim: usize,
71 namespace: String,
73 status: String,
74 elapsed_ms: u64,
76}
77
78pub fn run(
85 args: InitArgs,
86 backends: crate::cli::BackendChoice,
87 embedding_model: Option<&str>,
88) -> Result<(), AppError> {
89 let start = std::time::Instant::now();
90 let paths = AppPaths::resolve(args.db.as_deref())?;
91 paths.ensure_dirs()?;
92
93 let namespace = crate::namespace::resolve_namespace(args.namespace.as_deref())?;
94
95 let mut conn = open_rw(&paths.db)?;
96
97 apply_init_pragmas(&conn)?;
98
99 crate::storage::connection::run_migrations_with_foreign_keys_off(
106 &mut conn,
107 "migration failed",
108 )?;
109
110 conn.execute_batch(&format!(
111 "PRAGMA user_version = {};",
112 crate::constants::SCHEMA_USER_VERSION
113 ))?;
114
115 ensure_wal_mode(&conn)?;
117
118 let schema_version = latest_schema_version(&conn)?;
119
120 conn.execute(
121 "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('schema_version', ?1)",
122 rusqlite::params![schema_version],
123 )?;
124 conn.execute(
125 "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('model', ?1)",
126 rusqlite::params![crate::constants::SQLITE_GRAPHRAG_VERSION],
127 )?;
128 conn.execute(
133 "INSERT OR IGNORE INTO schema_meta (key, value) VALUES ('dim', ?1)",
134 rusqlite::params![crate::constants::embedding_dim().to_string()],
135 )?;
136 conn.execute(
137 "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('created_at', CAST(unixepoch() AS TEXT))",
138 [],
139 )?;
140 conn.execute(
141 "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('sqlite-graphrag_version', ?1)",
142 rusqlite::params![crate::constants::SQLITE_GRAPHRAG_VERSION],
143 )?;
144 conn.execute(
146 "INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('namespace_initial', ?1)",
147 rusqlite::params![namespace],
148 )?;
149
150 output::emit_progress_i18n(
151 "Validating embedding backend...",
152 "Validando backend de embedding...",
153 );
154
155 let (dim, status) = match crate::embedder::embed_passage_with_embedding_choice(
162 &paths.models,
163 "smoke test",
164 backends,
165 ) {
166 Ok((v, _backend)) => (v.len(), "ok"),
167 Err(crate::errors::AppError::Validation(msg)) => {
168 return Err(crate::errors::AppError::Validation(msg))
169 }
170 Err(e) => {
171 tracing::warn!(target: "init", error = %e, "embedding smoke test failed; init continues without LLM validation");
172 (crate::constants::embedding_dim(), "ok_no_embedding")
173 }
174 };
175
176 output::emit_json(&InitResponse {
177 db_path: paths.db.display().to_string(),
178 schema_version,
179 model: embedding_model.unwrap_or("none").to_string(),
180 dim,
181 namespace,
182 status: status.to_string(),
183 elapsed_ms: start.elapsed().as_millis() as u64,
184 })?;
185
186 Ok(())
187}
188
189fn latest_schema_version(conn: &rusqlite::Connection) -> Result<u32, AppError> {
190 match conn.query_row(
191 "SELECT version FROM refinery_schema_history ORDER BY version DESC LIMIT 1",
192 [],
193 |row| row.get::<_, i64>(0),
194 ) {
195 Ok(version) => Ok(version.max(0) as u32),
196 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(0),
197 Err(err) => Err(AppError::Database(err)),
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn init_response_serializes_all_fields() {
207 let resp = InitResponse {
208 db_path: "/tmp/test.sqlite".to_string(),
209 schema_version: 6,
210 model: "qwen/qwen3-embedding-8b".to_string(),
211 dim: crate::constants::DEFAULT_EMBEDDING_DIM,
212 namespace: "global".to_string(),
213 status: "ok".to_string(),
214 elapsed_ms: 100,
215 };
216 let json = serde_json::to_value(&resp).expect("serialization failed");
217 assert_eq!(json["db_path"], "/tmp/test.sqlite");
218 assert_eq!(json["schema_version"], 6);
219 assert_eq!(json["model"], "qwen/qwen3-embedding-8b");
223 assert_ne!(json["model"], crate::constants::SQLITE_GRAPHRAG_VERSION);
224 assert_eq!(json["dim"], crate::constants::DEFAULT_EMBEDDING_DIM);
225 assert_eq!(json["namespace"], "global");
226 assert_eq!(json["status"], "ok");
227 assert!(json["elapsed_ms"].is_number());
228 }
229
230 #[test]
231 fn latest_schema_version_returns_zero_for_empty_db() {
232 let conn = rusqlite::Connection::open_in_memory().expect("failed to open in-memory db");
233 conn.execute_batch("CREATE TABLE refinery_schema_history (version INTEGER NOT NULL);")
234 .expect("failed to create table");
235
236 let version = latest_schema_version(&conn).expect("latest_schema_version failed");
237 assert_eq!(version, 0u32, "empty db must return schema_version 0");
238 }
239
240 #[test]
241 fn latest_schema_version_returns_max_version() {
242 let conn = rusqlite::Connection::open_in_memory().expect("failed to open in-memory db");
243 conn.execute_batch(
244 "CREATE TABLE refinery_schema_history (version INTEGER NOT NULL);
245 INSERT INTO refinery_schema_history VALUES (1);
246 INSERT INTO refinery_schema_history VALUES (3);
247 INSERT INTO refinery_schema_history VALUES (2);",
248 )
249 .expect("failed to populate table");
250
251 let version = latest_schema_version(&conn).expect("latest_schema_version failed");
252 assert_eq!(version, 3u32, "must return the highest version present");
253 }
254
255 #[test]
256 fn init_default_dim_matches_the_registered_setting_default() {
257 let registered = crate::config::SETTING_KEYS
263 .iter()
264 .find(|entry| entry.key == "embedding.dim")
265 .and_then(|entry| entry.default)
266 .expect("embedding.dim must be registered with a literal default");
267 assert_eq!(
268 registered.parse::<usize>().ok(),
269 Some(crate::constants::DEFAULT_EMBEDDING_DIM),
270 "config doctor would advertise {registered} while init stamps {}",
271 crate::constants::DEFAULT_EMBEDDING_DIM
272 );
273 }
274
275 #[test]
276 fn init_default_dim_is_inside_the_accepted_range() {
277 assert!(
280 crate::constants::EMBEDDING_DIM_RANGE
281 .contains(&crate::constants::DEFAULT_EMBEDDING_DIM),
282 "default dim must sit inside EMBEDDING_DIM_RANGE"
283 );
284 }
285
286 #[test]
287 fn init_response_namespace_aligned_with_schema() {
288 let resp = InitResponse {
290 db_path: "/tmp/x.sqlite".to_string(),
291 schema_version: 6,
292 model: "none".to_string(),
293 dim: crate::constants::DEFAULT_EMBEDDING_DIM,
294 namespace: "my-project".to_string(),
295 status: "ok".to_string(),
296 elapsed_ms: 0,
297 };
298 let json = serde_json::to_value(&resp).expect("serialization failed");
299 assert_eq!(json["namespace"], "my-project");
300 }
301}