sqlite_graphrag/commands/
pending.rs1use clap::{Args, Subcommand};
15use serde::Serialize;
16
17use crate::errors::AppError;
18use crate::output::emit_json_compact;
19use crate::paths::AppPaths;
20use crate::storage::connection::open_rw;
21use crate::storage::pending_memories::{self, PendingMemory, PendingStatus};
22
23#[derive(Debug, Args)]
24#[command(after_long_help = "EXAMPLES:\n \
25 # List all entries currently waiting for embedding (Stage A done, Stage B pending)\n \
26 sqlite-graphrag pending list --status validated --json\n\n \
27 # Show the full record of pending_id 42\n \
28 sqlite-graphrag pending show 42 --json\n\n \
29 # Clean up entries abandoned for >24h (86400 seconds)\n \
30 sqlite-graphrag pending cleanup --staged-cleanup-after 86400 --yes")]
31pub struct PendingArgs {
33 #[command(subcommand)]
35 pub cmd: PendingCmd,
36}
37
38#[derive(Debug, Subcommand)]
40pub enum PendingCmd {
41 List(PendingListArgs),
43 Show(PendingShowArgs),
45 Cleanup(PendingCleanupArgs),
47}
48
49#[derive(Debug, Args)]
51pub struct PendingListArgs {
52 #[arg(long, value_enum)]
55 pub status: Option<PendingStatusArg>,
56 #[arg(long, default_value_t = 100)]
58 pub limit: usize,
59 #[arg(long)]
63 pub db: Option<String>,
64 #[arg(long, hide = true)]
66 pub json: bool,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
71#[value(rename_all = "snake_case")]
72pub enum PendingStatusArg {
73 Validated,
75 EmbeddingInProgress,
77 EmbeddingDone,
79 Committed,
81 Abandoned,
83 Failed,
85}
86
87impl From<PendingStatusArg> for PendingStatus {
88 fn from(value: PendingStatusArg) -> Self {
89 match value {
90 PendingStatusArg::Validated => Self::Validated,
91 PendingStatusArg::EmbeddingInProgress => Self::EmbeddingInProgress,
92 PendingStatusArg::EmbeddingDone => Self::EmbeddingDone,
93 PendingStatusArg::Committed => Self::Committed,
94 PendingStatusArg::Abandoned => Self::Abandoned,
95 PendingStatusArg::Failed => Self::Failed,
96 }
97 }
98}
99
100#[derive(Debug, Args)]
102pub struct PendingShowArgs {
103 pub pending_id: i64,
105 #[arg(long)]
109 pub db: Option<String>,
110 #[arg(long, hide = true)]
112 pub json: bool,
113}
114
115#[derive(Debug, Args)]
117pub struct PendingCleanupArgs {
118 #[arg(long, default_value_t = 86400)]
120 pub staged_cleanup_after: u64,
121 #[arg(long)]
123 pub yes: bool,
124 #[arg(long)]
126 pub dry_run: bool,
127 #[arg(long)]
130 pub db: Option<String>,
131 #[arg(long, hide = true)]
133 pub json: bool,
134}
135
136#[derive(Serialize)]
137struct PendingListEntry {
138 pending_id: i64,
139 name: String,
140 namespace: String,
141 memory_type: String,
142 status: String,
143 attempt_count: i32,
144 last_error: Option<String>,
145 embedding_dim: Option<i32>,
146 created_at: i64,
147 updated_at: i64,
148}
149
150impl From<&PendingMemory> for PendingListEntry {
151 fn from(p: &PendingMemory) -> Self {
152 Self {
153 pending_id: p.pending_id,
154 name: p.name.clone(),
155 namespace: p.namespace.clone(),
156 memory_type: p.memory_type.clone(),
157 status: p.status.as_str().to_string(),
158 attempt_count: p.attempt_count,
159 last_error: p.last_error.clone(),
160 embedding_dim: p.embedding_dim,
161 created_at: p.created_at,
162 updated_at: p.updated_at,
163 }
164 }
165}
166
167#[derive(Serialize)]
168struct PendingListOutput {
169 action: &'static str,
170 filter_status: Option<String>,
171 count: usize,
172 entries: Vec<PendingListEntry>,
173 elapsed_ms: u64,
174}
175
176#[derive(Serialize)]
177struct PendingShowOutput {
178 action: &'static str,
179 entry: PendingMemory,
180 elapsed_ms: u64,
181}
182
183#[derive(Serialize)]
184struct PendingCleanupOutput {
185 action: &'static str,
186 dry_run: bool,
187 staged_cleanup_after_secs: u64,
188 candidates: usize,
189 removed: usize,
190 elapsed_ms: u64,
191 yes: bool,
192}
193
194pub fn run(args: PendingArgs) -> Result<(), AppError> {
196 match args.cmd {
197 PendingCmd::List(a) => run_list(a),
198 PendingCmd::Show(a) => run_show(a),
199 PendingCmd::Cleanup(a) => run_cleanup(a),
200 }
201}
202
203fn open_conn(db_override: Option<&str>) -> Result<(AppPaths, rusqlite::Connection), AppError> {
204 let paths = AppPaths::resolve(db_override)?;
209 let conn = open_rw(&paths.db)?;
210 Ok((paths, conn))
211}
212
213fn run_list(args: PendingListArgs) -> Result<(), AppError> {
214 let start = std::time::Instant::now();
215 let (_paths, conn) = open_conn(args.db.as_deref())?;
216
217 let entries: Vec<PendingMemory> = if let Some(status) = args.status {
220 pending_memories::list_by_status(&conn, status.into(), args.limit)?
221 } else {
222 let mut all = Vec::new();
223 for status in [
224 PendingStatus::EmbeddingInProgress,
225 PendingStatus::EmbeddingDone,
226 PendingStatus::Validated,
227 PendingStatus::Abandoned,
228 PendingStatus::Failed,
229 ] {
230 let mut bucket = pending_memories::list_by_status(&conn, status, args.limit)?;
231 all.append(&mut bucket);
232 }
233 all.truncate(args.limit);
234 all
235 };
236
237 let count = entries.len();
238 let entries_out: Vec<PendingListEntry> = entries.iter().map(PendingListEntry::from).collect();
239 let output = PendingListOutput {
240 action: "pending_list",
241 filter_status: args.status.map(|s| {
242 match s {
243 PendingStatusArg::Validated => "validated",
244 PendingStatusArg::EmbeddingInProgress => "embedding_in_progress",
245 PendingStatusArg::EmbeddingDone => "embedding_done",
246 PendingStatusArg::Committed => "committed",
247 PendingStatusArg::Abandoned => "abandoned",
248 PendingStatusArg::Failed => "failed",
249 }
250 .to_string()
251 }),
252 count,
253 entries: entries_out,
254 elapsed_ms: start.elapsed().as_millis() as u64,
255 };
256 emit_json_compact(&output)
257}
258
259fn run_show(args: PendingShowArgs) -> Result<(), AppError> {
260 let start = std::time::Instant::now();
261 let (_paths, conn) = open_conn(args.db.as_deref())?;
262 let entry = pending_memories::find_by_id(&conn, args.pending_id)?.ok_or_else(|| {
263 AppError::NotFound(format!(
264 "pending_id {} not found in pending_memories",
265 args.pending_id
266 ))
267 })?;
268 let output = PendingShowOutput {
269 action: "pending_show",
270 entry,
271 elapsed_ms: start.elapsed().as_millis() as u64,
272 };
273 emit_json_compact(&output)
274}
275
276fn run_cleanup(args: PendingCleanupArgs) -> Result<(), AppError> {
277 let start = std::time::Instant::now();
278 let (_paths, conn) = open_conn(args.db.as_deref())?;
279
280 let candidates = pending_memories::list_by_status(&conn, PendingStatus::Abandoned, 100_000)?
282 .into_iter()
283 .filter(|p| {
284 let now = chrono::Utc::now().timestamp();
285 now - p.updated_at >= args.staged_cleanup_after as i64
286 })
287 .count();
288
289 let removed = if args.dry_run {
290 0
291 } else {
292 pending_memories::cleanup_older_than(&conn, args.staged_cleanup_after as i64)?
293 };
294
295 let output = PendingCleanupOutput {
296 action: if args.dry_run {
297 "pending_cleanup_dry_run"
298 } else {
299 "pending_cleanup"
300 },
301 dry_run: args.dry_run,
302 staged_cleanup_after_secs: args.staged_cleanup_after,
303 candidates,
304 removed,
305 elapsed_ms: start.elapsed().as_millis() as u64,
306 yes: args.yes,
307 };
308 emit_json_compact(&output)
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn status_arg_round_trip_all_variants() {
317 for arg in [
318 PendingStatusArg::Validated,
319 PendingStatusArg::EmbeddingInProgress,
320 PendingStatusArg::EmbeddingDone,
321 PendingStatusArg::Committed,
322 PendingStatusArg::Abandoned,
323 PendingStatusArg::Failed,
324 ] {
325 let status: PendingStatus = arg.into();
326 assert_eq!(status.as_str(), arg_to_str(arg));
327 }
328 }
329
330 fn arg_to_str(arg: PendingStatusArg) -> &'static str {
331 match arg {
332 PendingStatusArg::Validated => "validated",
333 PendingStatusArg::EmbeddingInProgress => "embedding_in_progress",
334 PendingStatusArg::EmbeddingDone => "embedding_done",
335 PendingStatusArg::Committed => "committed",
336 PendingStatusArg::Abandoned => "abandoned",
337 PendingStatusArg::Failed => "failed",
338 }
339 }
340}