sqlite_graphrag/commands/enrich/
mod.rs1mod args;
28mod drain_parallel;
29mod drain_serial;
30mod events;
31mod extraction;
32mod postprocess;
35mod predicates;
36mod prompts;
37mod queue;
38mod queue_ops;
39mod run;
40mod scan;
41mod scan_ec;
42mod quality_sample;
43mod scheduler;
44mod schemas;
45mod status;
46
47pub(crate) const DEFAULT_RATE_LIMIT_WAIT: u64 = 60;
48pub(crate) const DEFAULT_BODY_ENRICH_MIN_CHARS: usize = 500;
49pub(crate) const DEFAULT_BODY_ENRICH_MAX_CHARS: usize = 2000;
50
51pub use args::{EnrichArgs, EnrichMode, EnrichOperation, ReEmbedTarget};
52pub use queue::{cleanup_queue_entry, DeadItem, DeadSummary, EnrichStatus, WaitingItem};
53pub use run::run;
54
55use crate::errors::AppError;
56use queue::{enqueue_candidate_with_priority, open_queue_db, PRIORITY_HOT};
57
58pub fn enqueue_priority_entity_descriptions(
61 paths: &crate::paths::AppPaths,
62 namespace: &str,
63 entity_names: &[String],
64) -> Result<usize, AppError> {
65 let _ = namespace; let queue_path = crate::paths::sidecar_path(&paths.db, ".enrich-queue.sqlite");
67 let queue = open_queue_db(&queue_path)?;
68 let mut n = 0usize;
69 for name in entity_names {
70 enqueue_candidate_with_priority(
71 &queue,
72 name,
73 "entity",
74 "EntityDescriptions",
75 PRIORITY_HOT,
76 );
77 n += 1;
78 }
79 Ok(n)
80}
81
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86 use super::events::{enrich_operation_cli_name, is_sqlite_interrupt, scan_operation_with_deadline};
87 use super::schemas::{
88 BINDINGS_SCHEMA, BODY_ENRICH_SCHEMA, ENTITY_DESCRIPTION_SCHEMA,
89 };
90 use crate::errors::AppError;
91 use rusqlite::{Connection, ErrorCode};
92 use std::time::{Duration, Instant};
93
94 #[test]
95 fn bindings_schema_is_valid_json() {
96 let _: serde_json::Value =
97 serde_json::from_str(BINDINGS_SCHEMA).expect("BINDINGS_SCHEMA must be valid JSON");
98 }
99
100 #[test]
101 fn entity_description_schema_is_valid_json() {
102 let _: serde_json::Value = serde_json::from_str(ENTITY_DESCRIPTION_SCHEMA)
103 .expect("ENTITY_DESCRIPTION_SCHEMA must be valid JSON");
104 }
105
106 #[test]
107 fn body_enrich_schema_is_valid_json() {
108 let _: serde_json::Value = serde_json::from_str(BODY_ENRICH_SCHEMA)
109 .expect("BODY_ENRICH_SCHEMA must be valid JSON");
110 }
111
112 #[test]
115 fn enrich_operation_cli_name_pair_ops_are_kebab_case() {
116 assert_eq!(
117 enrich_operation_cli_name(&EnrichOperation::EntityConnect),
118 "entity-connect"
119 );
120 assert_eq!(
121 enrich_operation_cli_name(&EnrichOperation::CrossDomainBridges),
122 "cross-domain-bridges"
123 );
124 assert_eq!(
125 enrich_operation_cli_name(&EnrichOperation::EntityDescriptions),
126 "entity-descriptions"
127 );
128 }
129
130 #[test]
131 fn is_sqlite_interrupt_detects_operation_interrupted() {
132 let ffi_err = rusqlite::ffi::Error {
133 code: ErrorCode::OperationInterrupted,
134 extended_code: 9,
135 };
136 let err = rusqlite::Error::SqliteFailure(ffi_err, Some("interrupted".into()));
137 assert!(is_sqlite_interrupt(&err));
138
139 let busy = rusqlite::ffi::Error {
140 code: ErrorCode::DatabaseBusy,
141 extended_code: 5,
142 };
143 let busy_err = rusqlite::Error::SqliteFailure(busy, None);
144 assert!(!is_sqlite_interrupt(&busy_err));
145 }
146
147 #[test]
148 fn scan_deadline_already_elapsed_returns_timeout() {
149 use clap::Parser;
151 let cli = crate::cli::Cli::try_parse_from([
152 "sqlite-graphrag",
153 "enrich",
154 "--operation",
155 "entity-connect",
156 "--mode",
157 "openrouter",
158 "--openrouter-model",
159 "test/model",
160 "--dry-run",
161 "--limit",
162 "1",
163 ])
164 .expect("parse enrich args");
165 let Some(crate::cli::Commands::Enrich(args)) = cli.command else {
166 panic!("expected Commands::Enrich");
167 };
168 let conn = Connection::open_in_memory().unwrap();
169 let past = Instant::now() - Duration::from_secs(1);
170 let err = scan_operation_with_deadline(&conn, "global", &args, Some(past))
171 .expect_err("elapsed deadline must Timeout");
172 match err {
173 AppError::Timeout { .. } => {}
174 other => panic!("expected Timeout, got {other:?}"),
175 }
176 }
177
178 #[test]
179 fn interrupt_handle_maps_long_query_to_sqlite_interrupt() {
180 let conn = Connection::open_in_memory().unwrap();
183 let handle = conn.get_interrupt_handle();
184 let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
185 let stop_w = std::sync::Arc::clone(&stop);
186 let watchdog = std::thread::spawn(move || {
187 std::thread::sleep(Duration::from_millis(30));
188 if !stop_w.load(std::sync::atomic::Ordering::Relaxed) {
189 handle.interrupt();
190 }
191 });
192 let result = conn.query_row(
193 "WITH RECURSIVE t(x) AS (
194 SELECT 1
195 UNION ALL
196 SELECT x + 1 FROM t WHERE x < 500000000
197 )
198 SELECT COUNT(*) FROM t",
199 [],
200 |r| r.get::<_, i64>(0),
201 );
202 stop.store(true, std::sync::atomic::Ordering::Relaxed);
203 let _ = watchdog.join();
204 let err = result.expect_err("recursive CTE must be interrupted");
205 assert!(
206 is_sqlite_interrupt(&err),
207 "expected SQLITE_INTERRUPT, got {err:?}"
208 );
209 }
210}