claw_core/engine.rs
1//! Main engine entry point for claw-core.
2//!
3//! [`ClawEngine`] is the primary handle through which callers interact with the
4//! embedded SQLite database. It owns the SQLx connection pool, an internal LRU
5//! cache for [`MemoryRecord`]s, applies migrations on startup (when
6//! `auto_migrate` is enabled), and exposes methods for interacting with the
7//! various store modules.
8
9use std::path::Path;
10use std::sync::Arc;
11
12use sqlx::SqlitePool;
13use tokio::sync::Mutex;
14use uuid::Uuid;
15
16use crate::cache::{CacheStats, ClawCache};
17use crate::config::ClawConfig;
18use crate::error::{ClawError, ClawResult};
19use crate::snapshot::{SnapshotManifest, SnapshotMeta, Snapshotter};
20use crate::store::memory::{ListOptions, MemoryRecord, MemoryStore, MemoryType};
21use crate::store::session_lifecycle::{Session, SessionLifecycleStore};
22use crate::store::tool_output::{ToolOutputRecord, ToolOutputStore};
23
24/// Database-level statistics for a [`ClawEngine`] instance.
25///
26/// Retrieve via [`ClawEngine::db_stats`].
27///
28/// # Example
29///
30/// ```rust,no_run
31/// # use claw_core::ClawEngine;
32/// # async fn example() -> claw_core::ClawResult<()> {
33/// # let engine = ClawEngine::open_default().await?;
34/// let stats = engine.db_stats().await?;
35/// println!("memories: {}", stats.memory_count);
36/// # Ok(())
37/// # }
38/// ```
39#[derive(Debug, Clone)]
40pub struct DbStats {
41 /// Total number of memory records in the database.
42 pub memory_count: u64,
43 /// Total number of sessions in the database.
44 pub session_count: u64,
45 /// Total number of tool-output records in the database.
46 pub tool_output_count: u64,
47}
48
49/// Comprehensive runtime statistics for a [`ClawEngine`] instance.
50///
51/// Retrieve via [`ClawEngine::stats`].
52///
53/// # Example
54///
55/// ```rust,no_run
56/// # use claw_core::ClawEngine;
57/// # async fn example() -> claw_core::ClawResult<()> {
58/// # let engine = ClawEngine::open_default().await?;
59/// let s = engine.stats().await?;
60/// println!("hit rate (last 1 000 ops): {:.1}%", s.cache_hit_rate * 100.0);
61/// # Ok(())
62/// # }
63/// ```
64#[derive(Debug, Clone)]
65pub struct ClawStats {
66 /// Total number of memory records currently stored.
67 pub total_memories: u64,
68 /// Cache hit rate over the most recent 1 000 lookup operations (0.0 – 1.0).
69 pub cache_hit_rate: f64,
70 /// Timestamp of the most recently created snapshot, or `None` if no
71 /// snapshot has been taken during this engine session.
72 pub last_snapshot_at: Option<chrono::DateTime<chrono::Utc>>,
73 /// Size of the main database file in bytes.
74 pub db_size_bytes: u64,
75 /// Size of the WAL file in bytes (0 if WAL is not enabled or not present).
76 pub wal_size_bytes: u64,
77}
78
79/// The main entry point for claw-core.
80///
81/// Construct a [`ClawEngine`] via [`ClawEngine::open`] or
82/// [`ClawEngine::open_default`], passing a validated [`ClawConfig`]. The engine
83/// holds the underlying SQLx [`SqlitePool`], an internal LRU memory cache, and
84/// serves as the root accessor for all store, transaction, and snapshot APIs.
85///
86/// # Example
87///
88/// ```rust,no_run
89/// use claw_core::{ClawEngine, ClawConfig};
90///
91/// # async fn example() -> claw_core::ClawResult<()> {
92/// let config = ClawConfig::builder()
93/// .db_path("/tmp/my_agent.db")
94/// .build()?;
95/// let engine = ClawEngine::open(config).await?;
96/// engine.close().await;
97/// # Ok(())
98/// # }
99/// ```
100#[derive(Debug)]
101pub struct ClawEngine {
102 /// Validated runtime configuration.
103 pub(crate) config: ClawConfig,
104 /// SQLx connection pool backed by SQLite.
105 pub(crate) pool: SqlitePool,
106 /// In-memory LRU cache for [`MemoryRecord`]s.
107 cache: Arc<Mutex<ClawCache<Uuid, MemoryRecord>>>,
108 /// Running cache statistics (includes rolling hit-rate window).
109 stats: Arc<Mutex<CacheStats>>,
110 /// Timestamp of the last successful snapshot taken during this session.
111 last_snapshot_at: Arc<Mutex<Option<chrono::DateTime<chrono::Utc>>>>,
112}
113
114impl ClawEngine {
115 /// Open (or create) the database at the path specified in `config`.
116 ///
117 /// When `config.auto_migrate` is `true`, any pending embedded migrations
118 /// are applied before the engine is returned.
119 ///
120 /// # Errors
121 ///
122 /// Returns a [`ClawError`] if the pool cannot be created or migrations fail.
123 ///
124 /// # Example
125 ///
126 /// ```rust,no_run
127 /// # use claw_core::{ClawEngine, ClawConfig};
128 /// # async fn example() -> claw_core::ClawResult<()> {
129 /// let config = ClawConfig::builder()
130 /// .db_path("/tmp/claw.db")
131 /// .build()?;
132 /// let engine = ClawEngine::open(config).await?;
133 /// engine.close().await;
134 /// # Ok(())
135 /// # }
136 /// ```
137 pub async fn open(config: ClawConfig) -> ClawResult<Self> {
138 use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions};
139 use std::str::FromStr;
140
141 let db_url = format!("sqlite:{}", config.db_path.display());
142
143 let journal_mode = match config.journal_mode {
144 crate::config::JournalMode::WAL => SqliteJournalMode::Wal,
145 crate::config::JournalMode::Delete => SqliteJournalMode::Delete,
146 crate::config::JournalMode::Truncate => SqliteJournalMode::Truncate,
147 };
148
149 let connect_options = SqliteConnectOptions::from_str(&db_url)
150 .map_err(|e| ClawError::Config(format!("invalid database URL: {e}")))?
151 .create_if_missing(true)
152 .journal_mode(journal_mode);
153
154 let pool = SqlitePoolOptions::new()
155 .max_connections(config.max_connections)
156 .connect_with(connect_options)
157 .await?;
158
159 // When the "encryption" feature is enabled and a key is provided,
160 // apply `PRAGMA key` immediately after connecting. This requires a
161 // SQLCipher build of SQLite (not the default bundled libsqlite3).
162 #[cfg(feature = "encryption")]
163 if let Some(key) = &config.encryption_key {
164 let hex: String = key.iter().map(|b| format!("{b:02x}")).collect();
165 sqlx::query(&format!("PRAGMA key = \"x'{hex}'\""))
166 .execute(&pool)
167 .await?;
168 }
169
170 // Estimate ~512 bytes per MemoryRecord; minimum 64 entries.
171 let cache_cap = ((config.cache_size_mb * 1024 * 1024) / 512).max(64);
172 let cache = Arc::new(Mutex::new(ClawCache::new(cache_cap)?));
173 let stats = Arc::new(Mutex::new(CacheStats::new()));
174
175 let engine = ClawEngine {
176 config,
177 pool,
178 cache,
179 stats,
180 last_snapshot_at: Arc::new(Mutex::new(None)),
181 };
182
183 if engine.config.auto_migrate {
184 engine.migrate().await?;
185 }
186
187 Ok(engine)
188 }
189
190 /// Open the database using the default [`ClawConfig`].
191 ///
192 /// The database is created at `$XDG_DATA_HOME/clawdb/claw.db` (or
193 /// platform equivalent). All migrations are applied automatically.
194 ///
195 /// # Errors
196 ///
197 /// Returns a [`ClawError`] if the pool cannot be created or migrations fail.
198 ///
199 /// # Example
200 ///
201 /// ```rust,no_run
202 /// use claw_core::ClawEngine;
203 ///
204 /// # async fn example() -> claw_core::ClawResult<()> {
205 /// let engine = ClawEngine::open_default().await?;
206 /// engine.close().await;
207 /// # Ok(())
208 /// # }
209 /// ```
210 pub async fn open_default() -> ClawResult<Self> {
211 ClawEngine::open(ClawConfig::default()).await
212 }
213
214 /// Apply any pending embedded SQL migrations.
215 ///
216 /// # Errors
217 ///
218 /// Returns [`ClawError::Migration`] if a migration step fails.
219 pub async fn migrate(&self) -> ClawResult<()> {
220 crate::schema::migrations::run_migrations(&self.pool).await
221 }
222
223 /// Return a reference to the underlying [`SqlitePool`].
224 pub fn pool(&self) -> &SqlitePool {
225 &self.pool
226 }
227
228 /// Return a reference to the engine's [`ClawConfig`].
229 pub fn config(&self) -> &ClawConfig {
230 &self.config
231 }
232
233 /// Close the connection pool, waiting for in-flight queries to complete.
234 pub async fn close(self) {
235 self.pool.close().await;
236 }
237
238 // ── Memory API ────────────────────────────────────────────────────────────
239
240 /// Insert a new [`MemoryRecord`] into the database.
241 ///
242 /// The record is also inserted into the in-memory LRU cache.
243 ///
244 /// # Errors
245 ///
246 /// Returns a [`ClawError`] if the SQL execution fails.
247 ///
248 /// # Example
249 ///
250 /// ```rust,no_run
251 /// # use claw_core::{ClawEngine, MemoryRecord, MemoryType};
252 /// # async fn example() -> claw_core::ClawResult<()> {
253 /// # let engine = ClawEngine::open_default().await?;
254 /// let record = MemoryRecord::new("hello world", MemoryType::Semantic, vec![], None);
255 /// let id = engine.insert_memory(&record).await?;
256 /// # Ok(())
257 /// # }
258 /// ```
259 #[tracing::instrument(skip(self, record), fields(memory_id = %record.id))]
260 pub async fn insert_memory(&self, record: &MemoryRecord) -> ClawResult<Uuid> {
261 MemoryStore::new(&self.pool).insert(record).await?;
262 let mut cache = self.cache.lock().await;
263 let mut stats = self.stats.lock().await;
264 cache.insert(record.id, record.clone());
265 stats.insert_count += 1;
266 Ok(record.id)
267 }
268
269 /// Retrieve a [`MemoryRecord`] by its UUID.
270 ///
271 /// Results are served from the in-memory LRU cache when available.
272 ///
273 /// # Errors
274 ///
275 /// Returns [`ClawError::NotFound`] if no record with the given `id` exists.
276 ///
277 /// # Example
278 ///
279 /// ```rust,no_run
280 /// # use claw_core::{ClawEngine, MemoryRecord, MemoryType};
281 /// # async fn example() -> claw_core::ClawResult<()> {
282 /// # let engine = ClawEngine::open_default().await?;
283 /// let record = MemoryRecord::new("hello", MemoryType::Semantic, vec![], None);
284 /// let id = engine.insert_memory(&record).await?;
285 /// let fetched = engine.get_memory(id).await?;
286 /// assert_eq!(fetched.content, "hello");
287 /// # Ok(())
288 /// # }
289 /// ```
290 #[tracing::instrument(skip(self), fields(memory_id = %id))]
291 pub async fn get_memory(&self, id: Uuid) -> ClawResult<MemoryRecord> {
292 {
293 let mut cache = self.cache.lock().await;
294 let mut stats = self.stats.lock().await;
295 if let Some(record) = cache.get(&id) {
296 stats.record_hit();
297 return Ok(record.clone());
298 }
299 stats.record_miss();
300 }
301
302 let record = MemoryStore::new(&self.pool).get(id).await?;
303 let mut cache = self.cache.lock().await;
304 cache.insert(record.id, record.clone());
305 Ok(record)
306 }
307
308 /// Update the content of an existing [`MemoryRecord`].
309 ///
310 /// The `updated_at` timestamp is set to the current UTC time. The cache
311 /// entry is invalidated so the next read fetches fresh data.
312 ///
313 /// # Errors
314 ///
315 /// Returns [`ClawError::NotFound`] if no record with the given `id` exists.
316 ///
317 /// # Example
318 ///
319 /// ```rust,no_run
320 /// # use claw_core::{ClawEngine, MemoryRecord, MemoryType};
321 /// # async fn example() -> claw_core::ClawResult<()> {
322 /// # let engine = ClawEngine::open_default().await?;
323 /// let record = MemoryRecord::new("old content", MemoryType::Semantic, vec![], None);
324 /// let id = engine.insert_memory(&record).await?;
325 /// engine.update_memory(id, "new content").await?;
326 /// # Ok(())
327 /// # }
328 /// ```
329 #[tracing::instrument(skip(self), fields(memory_id = %id))]
330 pub async fn update_memory(&self, id: Uuid, content: &str) -> ClawResult<()> {
331 let updated_at = chrono::Utc::now();
332 MemoryStore::new(&self.pool)
333 .update_content(id, content, updated_at)
334 .await?;
335 let mut cache = self.cache.lock().await;
336 cache.invalidate(&id);
337 Ok(())
338 }
339
340 /// Delete a [`MemoryRecord`] from the database by its UUID.
341 ///
342 /// The cache entry is also invalidated.
343 ///
344 /// # Errors
345 ///
346 /// Returns [`ClawError::NotFound`] if no record with the given `id` exists.
347 ///
348 /// # Example
349 ///
350 /// ```rust,no_run
351 /// # use claw_core::{ClawEngine, MemoryRecord, MemoryType};
352 /// # async fn example() -> claw_core::ClawResult<()> {
353 /// # let engine = ClawEngine::open_default().await?;
354 /// let record = MemoryRecord::new("to delete", MemoryType::Episodic, vec![], None);
355 /// let id = engine.insert_memory(&record).await?;
356 /// engine.delete_memory(id).await?;
357 /// # Ok(())
358 /// # }
359 /// ```
360 #[tracing::instrument(skip(self), fields(memory_id = %id))]
361 pub async fn delete_memory(&self, id: Uuid) -> ClawResult<()> {
362 MemoryStore::new(&self.pool).delete(id).await?;
363 let mut cache = self.cache.lock().await;
364 cache.invalidate(&id);
365 Ok(())
366 }
367
368 /// List all [`MemoryRecord`]s, optionally filtered by [`MemoryType`].
369 ///
370 /// Results are ordered by `created_at` ascending.
371 ///
372 /// # Errors
373 ///
374 /// Returns a [`ClawError`] if the query fails.
375 ///
376 /// # Example
377 ///
378 /// ```rust,no_run
379 /// # use claw_core::{ClawEngine, MemoryType};
380 /// # async fn example() -> claw_core::ClawResult<()> {
381 /// # let engine = ClawEngine::open_default().await?;
382 /// let all = engine.list_memories(None).await?;
383 /// let semantic = engine.list_memories(Some(MemoryType::Semantic)).await?;
384 /// # Ok(())
385 /// # }
386 /// ```
387 #[tracing::instrument(skip(self))]
388 pub async fn list_memories(
389 &self,
390 type_filter: Option<MemoryType>,
391 ) -> ClawResult<Vec<MemoryRecord>> {
392 MemoryStore::new(&self.pool)
393 .list(type_filter.as_ref())
394 .await
395 }
396
397 /// List memories with keyset pagination.
398 ///
399 /// # Errors
400 ///
401 /// Returns a [`ClawError`] if the query fails.
402 ///
403 /// # Example
404 ///
405 /// ```rust,no_run
406 /// # use claw_core::{ClawEngine, ListOptions, MemoryType};
407 /// # async fn example() -> claw_core::ClawResult<()> {
408 /// # let engine = ClawEngine::open_default().await?;
409 /// let (page, cursor) = engine.list_memories_paginated(
410 /// Some(MemoryType::Semantic),
411 /// ListOptions { limit: 50, cursor: None },
412 /// ).await?;
413 /// # Ok(())
414 /// # }
415 /// ```
416 #[tracing::instrument(skip(self))]
417 pub async fn list_memories_paginated(
418 &self,
419 type_filter: Option<MemoryType>,
420 opts: ListOptions,
421 ) -> ClawResult<(Vec<MemoryRecord>, Option<String>)> {
422 MemoryStore::new(&self.pool)
423 .list_paginated(type_filter.as_ref(), &opts)
424 .await
425 }
426
427 /// Search for [`MemoryRecord`]s whose tag list contains `tag`.
428 ///
429 /// # Errors
430 ///
431 /// Returns a [`ClawError`] if the query fails.
432 ///
433 /// # Example
434 ///
435 /// ```rust,no_run
436 /// # use claw_core::ClawEngine;
437 /// # async fn example() -> claw_core::ClawResult<()> {
438 /// # let engine = ClawEngine::open_default().await?;
439 /// let results = engine.search_by_tag("important").await?;
440 /// # Ok(())
441 /// # }
442 /// ```
443 #[tracing::instrument(skip(self))]
444 pub async fn search_by_tag(&self, tag: &str) -> ClawResult<Vec<MemoryRecord>> {
445 MemoryStore::new(&self.pool).search_by_tag(tag).await
446 }
447
448 /// Full-text search over all [`MemoryRecord`] contents using SQLite FTS5.
449 ///
450 /// The `query` string follows FTS5 query syntax (e.g. `"hello world"` for
451 /// phrase search, `hello AND world` for AND search).
452 ///
453 /// # Errors
454 ///
455 /// Returns a [`ClawError`] if the query fails.
456 ///
457 /// # Example
458 ///
459 /// ```rust,no_run
460 /// # use claw_core::ClawEngine;
461 /// # async fn example() -> claw_core::ClawResult<()> {
462 /// # let engine = ClawEngine::open_default().await?;
463 /// let results = engine.fts_search("hello world").await?;
464 /// # Ok(())
465 /// # }
466 /// ```
467 #[tracing::instrument(skip(self))]
468 pub async fn fts_search(&self, query: &str) -> ClawResult<Vec<MemoryRecord>> {
469 MemoryStore::new(&self.pool).fts_search(query).await
470 }
471
472 /// Expire all [`MemoryRecord`]s whose TTL has elapsed.
473 ///
474 /// Returns the number of records deleted. The cache is cleared if any
475 /// records were deleted, since we do not know which IDs were affected.
476 ///
477 /// # Errors
478 ///
479 /// Returns a [`ClawError`] if the underlying deletion fails.
480 ///
481 /// # Example
482 ///
483 /// ```rust,no_run
484 /// # use claw_core::ClawEngine;
485 /// # async fn example() -> claw_core::ClawResult<()> {
486 /// # let engine = ClawEngine::open_default().await?;
487 /// let expired = engine.expire_ttl_memories().await?;
488 /// println!("deleted {expired} expired records");
489 /// # Ok(())
490 /// # }
491 /// ```
492 pub async fn expire_ttl_memories(&self) -> ClawResult<u64> {
493 let deleted = MemoryStore::new(&self.pool).expire_ttl().await?;
494 if deleted > 0 {
495 let mut cache = self.cache.lock().await;
496 cache.clear();
497 }
498 Ok(deleted)
499 }
500
501 // ── Session API ───────────────────────────────────────────────────────────
502
503 /// Start a new session and return its unique ID.
504 ///
505 /// # Errors
506 ///
507 /// Returns a [`ClawError`] if the SQL execution fails.
508 ///
509 /// # Example
510 ///
511 /// ```rust,no_run
512 /// # use claw_core::ClawEngine;
513 /// # async fn example() -> claw_core::ClawResult<()> {
514 /// # let engine = ClawEngine::open_default().await?;
515 /// let session_id = engine.start_session().await?;
516 /// # Ok(())
517 /// # }
518 /// ```
519 pub async fn start_session(&self) -> ClawResult<String> {
520 SessionLifecycleStore::new(&self.pool).start().await
521 }
522
523 /// Mark the session identified by `session_id` as ended.
524 ///
525 /// # Errors
526 ///
527 /// Returns [`ClawError::NotFound`] if the session does not exist.
528 ///
529 /// # Example
530 ///
531 /// ```rust,no_run
532 /// # use claw_core::ClawEngine;
533 /// # async fn example() -> claw_core::ClawResult<()> {
534 /// # let engine = ClawEngine::open_default().await?;
535 /// let sid = engine.start_session().await?;
536 /// engine.end_session(&sid).await?;
537 /// # Ok(())
538 /// # }
539 /// ```
540 pub async fn end_session(&self, session_id: &str) -> ClawResult<()> {
541 SessionLifecycleStore::new(&self.pool).end(session_id).await
542 }
543
544 /// Retrieve the [`Session`] record for `session_id`.
545 ///
546 /// # Errors
547 ///
548 /// Returns [`ClawError::NotFound`] if the session does not exist.
549 ///
550 /// # Example
551 ///
552 /// ```rust,no_run
553 /// # use claw_core::ClawEngine;
554 /// # async fn example() -> claw_core::ClawResult<()> {
555 /// # let engine = ClawEngine::open_default().await?;
556 /// let sid = engine.start_session().await?;
557 /// let session = engine.get_session(&sid).await?;
558 /// assert!(session.ended_at.is_none());
559 /// # Ok(())
560 /// # }
561 /// ```
562 pub async fn get_session(&self, session_id: &str) -> ClawResult<Session> {
563 SessionLifecycleStore::new(&self.pool).get(session_id).await
564 }
565
566 /// List all sessions, ordered by `started_at` descending.
567 ///
568 /// # Errors
569 ///
570 /// Returns a [`ClawError`] if the query fails.
571 ///
572 /// # Example
573 ///
574 /// ```rust,no_run
575 /// # use claw_core::ClawEngine;
576 /// # async fn example() -> claw_core::ClawResult<()> {
577 /// # let engine = ClawEngine::open_default().await?;
578 /// let sessions = engine.list_sessions().await?;
579 /// # Ok(())
580 /// # }
581 /// ```
582 pub async fn list_sessions(&self) -> ClawResult<Vec<Session>> {
583 SessionLifecycleStore::new(&self.pool).list().await
584 }
585
586 // ── Tool Output API ───────────────────────────────────────────────────────
587
588 /// Record a tool-output entry.
589 ///
590 /// # Errors
591 ///
592 /// Returns a [`ClawError`] if the SQL execution fails.
593 ///
594 /// # Example
595 ///
596 /// ```rust,no_run
597 /// # use claw_core::{ClawEngine, ToolOutput};
598 /// # use uuid::Uuid;
599 /// # async fn example() -> claw_core::ClawResult<()> {
600 /// # let engine = ClawEngine::open_default().await?;
601 /// let output = ToolOutput {
602 /// id: Uuid::new_v4(),
603 /// session_id: "sess-1".to_string(),
604 /// tool_name: "my_tool".to_string(),
605 /// output: serde_json::json!({"result": 42}),
606 /// success: true,
607 /// created_at: chrono::Utc::now(),
608 /// };
609 /// engine.record_tool_output(&output).await?;
610 /// # Ok(())
611 /// # }
612 /// ```
613 pub async fn record_tool_output(&self, output: &ToolOutputRecord) -> ClawResult<()> {
614 ToolOutputStore::new(&self.pool).insert(output).await
615 }
616
617 /// List all tool-output records for a given `session_id`.
618 ///
619 /// # Errors
620 ///
621 /// Returns a [`ClawError`] if the query fails.
622 ///
623 /// # Example
624 ///
625 /// ```rust,no_run
626 /// # use claw_core::ClawEngine;
627 /// # async fn example() -> claw_core::ClawResult<()> {
628 /// # let engine = ClawEngine::open_default().await?;
629 /// let outputs = engine.list_tool_outputs("sess-1").await?;
630 /// # Ok(())
631 /// # }
632 /// ```
633 pub async fn list_tool_outputs(&self, session_id: &str) -> ClawResult<Vec<ToolOutputRecord>> {
634 ToolOutputStore::new(&self.pool)
635 .get_by_session(session_id)
636 .await
637 }
638
639 // ── Transaction API ───────────────────────────────────────────────────────
640
641 /// Begin a new [`crate::transaction::ClawTransaction`] against this engine.
642 ///
643 /// # Errors
644 ///
645 /// Returns [`ClawError::Transaction`] if the pool cannot start a transaction.
646 ///
647 /// # Example
648 ///
649 /// ```rust,no_run
650 /// # use claw_core::{ClawEngine, MemoryRecord, MemoryType};
651 /// # async fn example() -> claw_core::ClawResult<()> {
652 /// # let engine = ClawEngine::open_default().await?;
653 /// let mut tx = engine.transaction().await?;
654 /// let r = MemoryRecord::new("hello", MemoryType::Semantic, vec![], None);
655 /// tx.insert_memory(&r).await?;
656 /// tx.commit().await?;
657 /// # Ok(())
658 /// # }
659 /// ```
660 pub async fn transaction(&self) -> ClawResult<crate::transaction::ClawTransaction<'_>> {
661 crate::transaction::ClawTransaction::begin(self).await
662 }
663
664 /// Begin a new [`crate::transaction::ClawTransaction`] against this engine.
665 ///
666 /// This is an alias for [`ClawEngine::transaction`].
667 ///
668 /// # Errors
669 ///
670 /// Returns [`ClawError::Transaction`] if the pool cannot start a transaction.
671 pub async fn begin_transaction(&self) -> ClawResult<crate::transaction::ClawTransaction<'_>> {
672 crate::transaction::ClawTransaction::begin(self).await
673 }
674
675 // ── Snapshot API ──────────────────────────────────────────────────────────
676
677 /// Create a snapshot of the current database state.
678 ///
679 /// Requires `config.snapshot_dir` to be set. A WAL checkpoint is performed
680 /// before the file copy to ensure all committed data is in the main DB file.
681 ///
682 /// # Errors
683 ///
684 /// Returns [`ClawError::Config`] if no snapshot directory is configured, or
685 /// [`ClawError::Snapshot`] if the snapshot file cannot be written.
686 ///
687 /// # Example
688 ///
689 /// ```rust,no_run
690 /// # use claw_core::{ClawEngine, ClawConfig};
691 /// # async fn example() -> claw_core::ClawResult<()> {
692 /// # let config = ClawConfig::builder()
693 /// # .db_path("/tmp/claw.db")
694 /// # .snapshot_dir("/tmp/snaps")
695 /// # .build()?;
696 /// # let engine = ClawEngine::open(config).await?;
697 /// let meta = engine.snapshot_create().await?;
698 /// println!("snapshot at {}", meta.path.display());
699 /// # Ok(())
700 /// # }
701 /// ```
702 #[tracing::instrument(skip(self))]
703 pub async fn snapshot_create(&self) -> ClawResult<SnapshotMeta> {
704 let snap_dir = self.config.snapshot_dir.as_ref().ok_or_else(|| {
705 ClawError::Config("snapshot_dir must be set to use snapshot_create".to_string())
706 })?;
707 // Flush WAL pages into the main DB file before copying.
708 sqlx::query("PRAGMA wal_checkpoint(FULL)")
709 .execute(&self.pool)
710 .await?;
711 let snapshotter = Snapshotter::new(snap_dir)?;
712 let meta = snapshotter.take(&self.config.db_path)?;
713 *self.last_snapshot_at.lock().await = Some(meta.created_at);
714 Ok(meta)
715 }
716
717 /// Restore the database from a snapshot file.
718 ///
719 /// This method validates the snapshot is a genuine SQLite 3 file, closes
720 /// the connection pool, replaces the live database, removes stale WAL/SHM
721 /// sidecars, re-opens the pool, re-runs migrations, and clears the LRU
722 /// cache.
723 ///
724 /// # Errors
725 ///
726 /// Returns [`ClawError::Snapshot`] if the snapshot is invalid or the copy
727 /// fails, or a database error if the pool cannot be re-opened.
728 ///
729 /// # Example
730 ///
731 /// ```rust,no_run
732 /// # use claw_core::{ClawEngine, ClawConfig};
733 /// # use std::path::Path;
734 /// # async fn example() -> claw_core::ClawResult<()> {
735 /// # let config = ClawConfig::builder()
736 /// # .db_path("/tmp/claw.db")
737 /// # .snapshot_dir("/tmp/snaps")
738 /// # .build()?;
739 /// # let mut engine = ClawEngine::open(config).await?;
740 /// engine.restore(Path::new("/tmp/snaps/snapshot.db")).await?;
741 /// # Ok(())
742 /// # }
743 /// ```
744 #[tracing::instrument(skip(self), fields(snapshot = %snapshot_path.display()))]
745 pub async fn restore(&mut self, snapshot_path: &Path) -> ClawResult<()> {
746 use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
747 use std::str::FromStr;
748
749 // 1. Validate SQLite magic bytes.
750 Self::validate_sqlite_magic(snapshot_path)?;
751
752 // 2. Close the existing connection pool.
753 self.pool.close().await;
754
755 // 3. Overwrite the live database with the snapshot.
756 std::fs::copy(snapshot_path, &self.config.db_path)
757 .map_err(|e| ClawError::Snapshot(format!("failed to restore snapshot: {e}")))?;
758
759 // 4. Remove stale WAL / SHM sidecar files.
760 let db_name = self
761 .config
762 .db_path
763 .file_name()
764 .unwrap_or_default()
765 .to_string_lossy()
766 .into_owned();
767 let db_parent = self
768 .config
769 .db_path
770 .parent()
771 .unwrap_or(std::path::Path::new("."));
772 for suffix in &["-wal", "-shm"] {
773 let sidecar = db_parent.join(format!("{db_name}{suffix}"));
774 if sidecar.exists() {
775 let _ = std::fs::remove_file(&sidecar);
776 }
777 }
778
779 // 5. Re-open the connection pool.
780 let db_url = format!("sqlite:{}", self.config.db_path.display());
781 let connect_options = SqliteConnectOptions::from_str(&db_url)
782 .map_err(|e| ClawError::Config(format!("invalid database URL: {e}")))?
783 .create_if_missing(false);
784 self.pool = SqlitePoolOptions::new()
785 .max_connections(self.config.max_connections)
786 .connect_with(connect_options)
787 .await?;
788
789 // Re-apply encryption key if required.
790 #[cfg(feature = "encryption")]
791 if let Some(key) = &self.config.encryption_key {
792 let hex: String = key.iter().map(|b| format!("{b:02x}")).collect();
793 sqlx::query(&format!("PRAGMA key = \"x'{hex}'\""))
794 .execute(&self.pool)
795 .await?;
796 }
797
798 // 6. Apply pending migrations.
799 self.migrate().await?;
800
801 // 7. Clear stale LRU cache entries.
802 self.cache.lock().await.clear();
803
804 tracing::info!(
805 snapshot = %snapshot_path.display(),
806 db = %self.config.db_path.display(),
807 "database restored from snapshot"
808 );
809 Ok(())
810 }
811
812 /// Load the [`SnapshotManifest`] from the configured snapshot directory.
813 ///
814 /// Returns an empty manifest if no snapshot has been taken yet.
815 ///
816 /// # Errors
817 ///
818 /// Returns [`ClawError::Config`] if no `snapshot_dir` is configured.
819 pub fn snapshot_manifest(&self) -> ClawResult<SnapshotManifest> {
820 let snap_dir = self
821 .config
822 .snapshot_dir
823 .as_ref()
824 .ok_or_else(|| ClawError::Config("snapshot_dir must be set".to_string()))?;
825 Snapshotter::new(snap_dir)?.load_manifest()
826 }
827
828 /// Rotate the SQLCipher encryption key using `PRAGMA rekey`.
829 ///
830 /// Only available with the `encryption` Cargo feature.
831 ///
832 /// # Errors
833 ///
834 /// Returns a [`ClawError`] if the PRAGMA execution fails.
835 #[cfg(feature = "encryption")]
836 pub async fn rotate_key(&self, _old_key: [u8; 32], new_key: [u8; 32]) -> ClawResult<()> {
837 let hex: String = new_key.iter().map(|b| format!("{b:02x}")).collect();
838 sqlx::query(&format!("PRAGMA rekey = \"x'{hex}'\""))
839 .execute(&self.pool)
840 .await?;
841 Ok(())
842 }
843
844 // ── Cache & Stats API ─────────────────────────────────────────────────────
845
846 /// Return a snapshot of the current in-memory cache statistics.
847 ///
848 /// # Example
849 ///
850 /// ```rust,no_run
851 /// # use claw_core::ClawEngine;
852 /// # async fn example() -> claw_core::ClawResult<()> {
853 /// # let engine = ClawEngine::open_default().await?;
854 /// let stats = engine.cache_stats().await;
855 /// println!("hits: {}, misses: {}", stats.hit_count, stats.miss_count);
856 /// # Ok(())
857 /// # }
858 /// ```
859 pub async fn cache_stats(&self) -> CacheStats {
860 self.stats.lock().await.clone()
861 }
862
863 /// Return comprehensive runtime statistics.
864 ///
865 /// Reports the total memory count, rolling cache hit rate (last 1000 ops),
866 /// last snapshot timestamp, and on-disk database/WAL sizes.
867 ///
868 /// # Errors
869 ///
870 /// Returns a [`ClawError`] if the memory count query fails.
871 ///
872 /// # Example
873 ///
874 /// ```rust,no_run
875 /// # use claw_core::ClawEngine;
876 /// # async fn example() -> claw_core::ClawResult<()> {
877 /// # let engine = ClawEngine::open_default().await?;
878 /// let s = engine.stats().await?;
879 /// println!("hit rate: {:.1}%", s.cache_hit_rate * 100.0);
880 /// # Ok(())
881 /// # }
882 /// ```
883 #[tracing::instrument(skip(self))]
884 pub async fn stats(&self) -> ClawResult<ClawStats> {
885 let (total_memories,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM memories")
886 .fetch_one(&self.pool)
887 .await?;
888 let cache_hit_rate = self.stats.lock().await.rolling_hit_rate();
889 let last_snapshot_at = *self.last_snapshot_at.lock().await;
890 let db_size_bytes = std::fs::metadata(&self.config.db_path)
891 .map(|m| m.len())
892 .unwrap_or(0);
893 let wal_path = {
894 let p = self.config.db_path.to_string_lossy();
895 std::path::PathBuf::from(format!("{p}-wal"))
896 };
897 let wal_size_bytes = std::fs::metadata(&wal_path).map(|m| m.len()).unwrap_or(0);
898 Ok(ClawStats {
899 total_memories: total_memories as u64,
900 cache_hit_rate,
901 last_snapshot_at,
902 db_size_bytes,
903 wal_size_bytes,
904 })
905 }
906
907 /// Return database-level statistics.
908 ///
909 /// # Errors
910 ///
911 /// Returns a [`ClawError`] if any of the count queries fail.
912 ///
913 /// # Example
914 ///
915 /// ```rust,no_run
916 /// # use claw_core::ClawEngine;
917 /// # async fn example() -> claw_core::ClawResult<()> {
918 /// # let engine = ClawEngine::open_default().await?;
919 /// let stats = engine.db_stats().await?;
920 /// println!("memories: {}", stats.memory_count);
921 /// # Ok(())
922 /// # }
923 /// ```
924 pub async fn db_stats(&self) -> ClawResult<DbStats> {
925 let (mc,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM memories")
926 .fetch_one(&self.pool)
927 .await?;
928 let (sc,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM sessions")
929 .fetch_one(&self.pool)
930 .await?;
931 let (tc,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM tool_output")
932 .fetch_one(&self.pool)
933 .await?;
934 Ok(DbStats {
935 memory_count: mc as u64,
936 session_count: sc as u64,
937 tool_output_count: tc as u64,
938 })
939 }
940
941 // ── Helpers ───────────────────────────────────────────────────────────────
942
943 fn validate_sqlite_magic(path: &Path) -> ClawResult<()> {
944 use std::io::Read;
945 const SQLITE_MAGIC: &[u8; 16] = b"SQLite format 3\0";
946 let mut header = [0u8; 16];
947 let mut file = std::fs::File::open(path)
948 .map_err(|e| ClawError::Snapshot(format!("cannot open snapshot: {e}")))?;
949 file.read_exact(&mut header)
950 .map_err(|e| ClawError::Snapshot(format!("cannot read snapshot header: {e}")))?;
951 if &header != SQLITE_MAGIC {
952 return Err(ClawError::Snapshot(
953 "file does not have a valid SQLite 3 header".to_string(),
954 ));
955 }
956 Ok(())
957 }
958}