1use std::path::Path;
2use std::sync::Arc;
3
4use r2d2::{CustomizeConnection, Pool};
5use r2d2_sqlite::SqliteConnectionManager;
6use rusqlite::TransactionBehavior;
7
8use crate::entry::ContextEntry;
9use crate::storage::schema::{migrate, row_to_entry};
10use crate::traits::ContextStorage;
11
12pub mod schema;
14pub mod searcher;
16
17pub use searcher::SqliteSearcher;
18
19pub fn open_storage(
26 db_path: &Path,
27 max_entries: usize,
28) -> crate::Result<(SqliteStorage, SqliteSearcher)> {
29 let storage = SqliteStorage::open(db_path, max_entries)?;
30 let searcher = SqliteSearcher::new(storage.pool());
31 Ok((storage, searcher))
32}
33
34#[derive(Debug)]
35struct PragmaCustomizer;
36
37impl CustomizeConnection<rusqlite::Connection, rusqlite::Error> for PragmaCustomizer {
38 fn on_acquire(
39 &self,
40 conn: &mut rusqlite::Connection,
41 ) -> std::result::Result<(), rusqlite::Error> {
42 conn.execute_batch(
43 "PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000; PRAGMA foreign_keys=ON;",
44 )?;
45 Ok(())
46 }
47}
48
49pub struct SqliteStorage {
51 pool: Arc<Pool<SqliteConnectionManager>>,
52 max_entries: usize,
53}
54
55impl SqliteStorage {
56 pub fn open(db_path: &Path, max_entries: usize) -> crate::Result<Self> {
66 let manager = SqliteConnectionManager::file(db_path);
67 let mut builder = Pool::builder().connection_customizer(Box::new(PragmaCustomizer));
68
69 if db_path == Path::new(":memory:") {
72 builder = builder.max_size(1);
73 } else {
74 builder = builder.max_size(4);
75 }
76
77 let pool = builder.build(manager)?;
78
79 let conn = pool.get()?;
80 migrate(&conn)?;
81
82 Ok(Self {
83 pool: Arc::new(pool),
84 max_entries,
85 })
86 }
87
88 #[must_use]
91 pub fn pool(&self) -> Arc<Pool<SqliteConnectionManager>> {
92 Arc::clone(&self.pool)
93 }
94
95 pub fn schema_version(&self) -> crate::Result<i64> {
101 let conn = self.pool.get()?;
102 let version = conn.query_row(
103 "SELECT COALESCE(MAX(version), 0) FROM schema_version",
104 [],
105 |row| row.get(0),
106 )?;
107 Ok(version)
108 }
109
110 pub fn checkpoint(&self) -> crate::Result<()> {
118 let conn = self.pool.get()?;
119 conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE);")?;
120 Ok(())
121 }
122}
123
124impl ContextStorage for SqliteStorage {
125 fn save(&self, entry: &ContextEntry) -> crate::Result<()> {
126 let mut conn = self.pool.get()?;
127
128 let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
129
130 let exists: bool = tx.query_row(
133 "SELECT EXISTS(SELECT 1 FROM entries WHERE id = ?1)",
134 [&entry.id],
135 |r| r.get(0),
136 )?;
137
138 if !exists {
139 let current_count: i64 =
140 tx.query_row("SELECT COUNT(*) FROM entries", [], |r| r.get(0))?;
141
142 let current_count = usize::try_from(current_count).unwrap_or(usize::MAX);
143 if current_count >= self.max_entries {
144 tx.execute(
145 "DELETE FROM entries WHERE id = (\
146 SELECT id FROM entries ORDER BY timestamp ASC LIMIT 1)",
147 [],
148 )?;
149 }
150 }
151
152 let metadata_json = entry
153 .metadata
154 .as_ref()
155 .map(serde_json::to_string)
156 .transpose()
157 .map_err(|e| crate::Error::InvalidEntry(format!("metadata is not valid JSON: {e}")))?;
158
159 tx.execute(
160 "INSERT OR REPLACE INTO entries (
161 id,
162 content,
163 timestamp,
164 kind,
165 scope,
166 session_id,
167 token_count,
168 metadata
169 ) VALUES (
170 ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8
171 )",
172 rusqlite::params![
173 entry.id,
174 entry.content,
175 entry.timestamp,
176 entry.kind,
177 entry.scope,
178 entry.session_id,
179 entry
180 .token_count
181 .map(|v| i64::try_from(v).unwrap_or(i64::MAX)),
182 metadata_json,
183 ],
184 )?;
185
186 tx.commit()?;
187
188 Ok(())
189 }
190
191 fn get_top_k(&self, k: usize) -> crate::Result<Vec<ContextEntry>> {
192 let conn = self.pool.get()?;
193 let mut stmt = conn.prepare(
194 "SELECT
195 id,
196 content,
197 timestamp,
198 kind,
199 scope,
200 session_id,
201 token_count,
202 metadata
203 FROM entries
204 ORDER BY timestamp DESC
205 LIMIT ?1",
206 )?;
207
208 let entries = stmt
209 .query_map([i64::try_from(k).unwrap_or(i64::MAX)], row_to_entry)?
210 .collect::<std::result::Result<Vec<_>, _>>()?;
211
212 Ok(entries)
213 }
214
215 fn get_all(&self) -> crate::Result<Vec<ContextEntry>> {
216 let conn = self.pool.get()?;
217 let mut stmt = conn.prepare(
218 "SELECT
219 id,
220 content,
221 timestamp,
222 kind,
223 scope,
224 session_id,
225 token_count,
226 metadata
227 FROM entries
228 ORDER BY timestamp DESC",
229 )?;
230
231 let entries = stmt
232 .query_map([], row_to_entry)?
233 .collect::<std::result::Result<Vec<_>, _>>()?;
234
235 Ok(entries)
236 }
237
238 fn delete(&self, id: &str) -> crate::Result<bool> {
239 let conn = self.pool.get()?;
240 let changes = conn.execute("DELETE FROM entries WHERE id = ?1", [id])?;
241 Ok(changes > 0)
242 }
243
244 fn clear(&self) -> crate::Result<usize> {
245 let conn = self.pool.get()?;
246 let changes = conn.execute("DELETE FROM entries", [])?;
247 Ok(changes)
248 }
249
250 fn clear_scope(&self, scope: &str) -> crate::Result<usize> {
251 let conn = self.pool.get()?;
252 let changes = conn.execute("DELETE FROM entries WHERE scope = ?1", [scope])?;
253 Ok(changes)
254 }
255
256 fn count(&self) -> crate::Result<usize> {
257 let conn = self.pool.get()?;
258 let count: i64 = conn.query_row("SELECT COUNT(*) FROM entries", [], |r| r.get(0))?;
259 Ok(usize::try_from(count).unwrap_or(usize::MAX))
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use std::fs;
266 use std::path::Path;
267 use std::time::{SystemTime, UNIX_EPOCH};
268
269 use rusqlite::Connection;
270
271 use crate::engine::MATCH_ALL_QUERY;
272 use crate::entry::{kind, ContextEntry};
273 use crate::storage::{open_storage, SqliteStorage};
274 use crate::traits::{ContextStorage, Searcher};
275
276 fn temp_db_path(name: &str) -> std::path::PathBuf {
277 let nanos = SystemTime::now()
278 .duration_since(UNIX_EPOCH)
279 .expect("clock drift before unix epoch")
280 .as_nanos();
281 std::env::temp_dir().join(format!("cf-storage-{name}-{nanos}.db"))
282 }
283
284 fn make_entry(id: &str, content: &str, timestamp: i64, kind: &str) -> ContextEntry {
285 ContextEntry {
286 id: id.into(),
287 content: content.into(),
288 timestamp,
289 kind: kind.to_owned(),
290 scope: None,
291 session_id: None,
292 token_count: None,
293 metadata: None,
294 }
295 }
296
297 fn make_scoped_entry(id: &str, content: &str, timestamp: i64, scope: &str) -> ContextEntry {
298 ContextEntry {
299 id: id.into(),
300 content: content.into(),
301 timestamp,
302 kind: kind::MANUAL.to_owned(),
303 scope: Some(scope.to_owned()),
304 session_id: None,
305 token_count: None,
306 metadata: None,
307 }
308 }
309
310 #[test]
311 fn checkpoint_runs_without_error() {
312 let dir = tempfile::tempdir().unwrap();
313 let storage = SqliteStorage::open(dir.path().join("test.db").as_path(), 100).unwrap();
314 assert!(storage.checkpoint().is_ok());
315 }
316
317 #[test]
318 fn test_save_and_count() {
319 let (storage, _) = open_storage(Path::new(":memory:"), 100).unwrap();
320 let entry = make_entry("e1", "hello world", 1000, kind::MANUAL);
321 storage.save(&entry).unwrap();
322 assert_eq!(storage.count().unwrap(), 1);
323 }
324
325 #[test]
326 fn test_save_and_get_top_k() {
327 let (storage, _) = open_storage(Path::new(":memory:"), 100).unwrap();
328 storage
329 .save(&make_entry("e1", "first", 100, kind::MANUAL))
330 .unwrap();
331 storage
332 .save(&make_entry("e2", "second", 200, kind::SNAPSHOT))
333 .unwrap();
334 storage
335 .save(&make_entry("e3", "third", 300, kind::SUMMARY))
336 .unwrap();
337
338 let top2 = storage.get_top_k(2).unwrap();
339 assert_eq!(top2.len(), 2);
340 assert_eq!(top2[0].id, "e3"); assert_eq!(top2[1].id, "e2");
342 }
343
344 #[test]
345 fn test_save_and_get_all() {
346 let (storage, _) = open_storage(Path::new(":memory:"), 100).unwrap();
347 storage
348 .save(&make_entry("e1", "first", 100, kind::MANUAL))
349 .unwrap();
350 storage
351 .save(&make_entry("e2", "second", 200, kind::MANUAL))
352 .unwrap();
353 storage
354 .save(&make_entry("e3", "third", 300, kind::MANUAL))
355 .unwrap();
356
357 let all = storage.get_all().unwrap();
358 assert_eq!(all.len(), 3);
359 assert_eq!(all[0].id, "e3");
361 assert_eq!(all[1].id, "e2");
362 assert_eq!(all[2].id, "e1");
363 }
364
365 #[test]
366 fn test_delete() {
367 let (storage, _) = open_storage(Path::new(":memory:"), 100).unwrap();
368 storage
369 .save(&make_entry("e1", "hello", 1000, kind::MANUAL))
370 .unwrap();
371
372 assert!(storage.delete("e1").unwrap());
373 assert!(!storage.delete("nonexistent").unwrap());
374 assert_eq!(storage.count().unwrap(), 0);
375 }
376
377 #[test]
378 fn test_clear() {
379 let (storage, _) = open_storage(Path::new(":memory:"), 100).unwrap();
380 storage
381 .save(&make_entry("e1", "a", 100, kind::MANUAL))
382 .unwrap();
383 storage
384 .save(&make_entry("e2", "b", 200, kind::MANUAL))
385 .unwrap();
386 storage
387 .save(&make_entry("e3", "c", 300, kind::MANUAL))
388 .unwrap();
389
390 let cleared = storage.clear().unwrap();
391 assert_eq!(cleared, 3);
392 assert_eq!(storage.count().unwrap(), 0);
393 }
394
395 #[test]
396 fn test_clear_scope() {
397 let (storage, _) = open_storage(Path::new(":memory:"), 100).unwrap();
398 storage
399 .save(&make_scoped_entry("e1", "a", 100, "scope-a"))
400 .unwrap();
401 storage
402 .save(&make_scoped_entry("e2", "b", 200, "scope-a"))
403 .unwrap();
404 storage
405 .save(&make_scoped_entry("e3", "c", 300, "scope-b"))
406 .unwrap();
407
408 let cleared = storage.clear_scope("scope-a").unwrap();
409 assert_eq!(cleared, 2);
410 assert_eq!(storage.count().unwrap(), 1);
411
412 let all = storage.get_all().unwrap();
413 assert_eq!(all[0].id, "e3");
414 }
415
416 #[test]
417 fn test_clear_scope_no_match() {
418 let (storage, _) = open_storage(Path::new(":memory:"), 100).unwrap();
419 storage
420 .save(&make_scoped_entry("e1", "a", 100, "scope-a"))
421 .unwrap();
422
423 let cleared = storage.clear_scope("scope-z").unwrap();
424 assert_eq!(cleared, 0);
425 assert_eq!(storage.count().unwrap(), 1);
426 }
427
428 #[test]
429 fn test_lru_eviction() {
430 let (storage, _) = open_storage(Path::new(":memory:"), 2).unwrap();
431 storage
432 .save(&make_entry("e1", "oldest", 100, kind::MANUAL))
433 .unwrap();
434 storage
435 .save(&make_entry("e2", "middle", 200, kind::MANUAL))
436 .unwrap();
437 storage
438 .save(&make_entry("e3", "newest", 300, kind::MANUAL))
439 .unwrap();
440
441 assert_eq!(storage.count().unwrap(), 2);
442
443 let all = storage.get_all().unwrap();
444 let ids: Vec<&str> = all.iter().map(|e| e.id.as_str()).collect();
445 assert!(
446 !ids.contains(&"e1"),
447 "oldest entry should have been evicted"
448 );
449 assert!(ids.contains(&"e2"));
450 assert!(ids.contains(&"e3"));
451 }
452
453 #[test]
454 fn test_fts_search() {
455 let (storage, searcher) = open_storage(Path::new(":memory:"), 100).unwrap();
456 storage
457 .save(&make_entry(
458 "e1",
459 "rust programming language",
460 100,
461 kind::MANUAL,
462 ))
463 .unwrap();
464 storage
465 .save(&make_entry("e2", "python scripting", 200, kind::MANUAL))
466 .unwrap();
467 storage
468 .save(&make_entry("e3", "rust borrow checker", 300, kind::MANUAL))
469 .unwrap();
470
471 let results = searcher.search("rust", None, 5).unwrap();
472 assert_eq!(results.len(), 2);
473 assert!(
475 results[0].score >= results[1].score,
476 "results should be ordered by descending score"
477 );
478 }
479
480 #[test]
481 fn test_fts_search_no_results() {
482 let (storage, searcher) = open_storage(Path::new(":memory:"), 100).unwrap();
483 storage
484 .save(&make_entry("e1", "hello world", 100, kind::MANUAL))
485 .unwrap();
486
487 let results = searcher.search("nonexistent", None, 5).unwrap();
488 assert!(results.is_empty());
489 }
490
491 #[test]
492 fn test_fts_search_scoped() {
493 let (storage, searcher) = open_storage(Path::new(":memory:"), 100).unwrap();
494 storage
495 .save(&make_scoped_entry("e1", "rust programming", 100, "a"))
496 .unwrap();
497 storage
498 .save(&make_scoped_entry("e2", "rust borrow checker", 200, "b"))
499 .unwrap();
500
501 let results_a = searcher.search("rust", Some("a"), 5).unwrap();
502 assert_eq!(results_a.len(), 1);
503 assert_eq!(results_a[0].entry.id, "e1");
504
505 let results_b = searcher.search("rust", Some("b"), 5).unwrap();
506 assert_eq!(results_b.len(), 1);
507 assert_eq!(results_b[0].entry.id, "e2");
508
509 let results_all = searcher.search("rust", None, 5).unwrap();
510 assert_eq!(results_all.len(), 2);
511 }
512
513 #[test]
514 fn test_v2_migration_idempotent() {
515 let storage1 = SqliteStorage::open(Path::new(":memory:"), 100).unwrap();
516 let conn = storage1.pool().get().unwrap();
517 crate::storage::schema::migrate(&conn).unwrap();
519 }
520
521 #[test]
522 fn test_v1_to_v3_migration() {
523 let db_path = temp_db_path("v1-to-v3");
524
525 {
526 let conn = Connection::open(&db_path).unwrap();
527 conn.execute_batch(crate::storage::schema::SCHEMA_V1)
528 .unwrap();
529 conn.execute_batch(
530 "CREATE TABLE IF NOT EXISTS schema_version (id INTEGER PRIMARY KEY CHECK(id = 1), version INTEGER NOT NULL)",
531 )
532 .unwrap();
533 conn.execute(
534 "INSERT OR REPLACE INTO schema_version (id, version) VALUES (1, 1)",
535 [],
536 )
537 .unwrap();
538
539 conn.execute(
540 "INSERT INTO entries (id, content, timestamp, kind, token_count) VALUES (?1, ?2, ?3, ?4, ?5)",
541 rusqlite::params!["m1", "manual entry", 100_i64, "Manual", 2_i64],
542 )
543 .unwrap();
544 conn.execute(
545 "INSERT INTO entries (id, content, timestamp, kind, token_count) VALUES (?1, ?2, ?3, ?4, ?5)",
546 rusqlite::params!["p1", "precompact entry", 200_i64, "PreCompact", 3_i64],
547 )
548 .unwrap();
549 conn.execute(
550 "INSERT INTO entries (id, content, timestamp, kind, token_count) VALUES (?1, ?2, ?3, ?4, ?5)",
551 rusqlite::params!["a1", "auto entry", 300_i64, "Auto", 4_i64],
552 )
553 .unwrap();
554 }
555
556 let storage = SqliteStorage::open(&db_path, 100).unwrap();
557 let conn = storage.pool().get().unwrap();
558
559 let version: i64 = conn
560 .query_row(
561 "SELECT COALESCE(MAX(version), 0) FROM schema_version",
562 [],
563 |r| r.get(0),
564 )
565 .unwrap();
566 assert_eq!(version, 3);
567
568 let mut kinds: Vec<String> = conn
570 .prepare("SELECT kind FROM entries ORDER BY timestamp")
571 .unwrap()
572 .query_map([], |r| r.get(0))
573 .unwrap()
574 .collect::<rusqlite::Result<Vec<_>>>()
575 .unwrap();
576 kinds.sort();
577 assert_eq!(kinds, vec!["manual", "snapshot", "summary"]);
578
579 let tags: i64 = conn
580 .query_row("SELECT COUNT(*) FROM tags", [], |r| r.get(0))
581 .unwrap();
582 assert_eq!(tags, 0, "tags table should exist but be empty");
583
584 let entry_tags: i64 = conn
585 .query_row("SELECT COUNT(*) FROM entry_tags", [], |r| r.get(0))
586 .unwrap();
587 assert_eq!(entry_tags, 0, "entry_tags table should exist but be empty");
588
589 let runtime_configs_exists: i64 = conn
591 .query_row(
592 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='runtime_configs'",
593 [],
594 |r| r.get(0),
595 )
596 .unwrap();
597 assert_eq!(runtime_configs_exists, 0);
598
599 let _ = fs::remove_file(&db_path);
600 }
601
602 #[test]
603 fn test_v2_to_v3_migration() {
604 let db_path = temp_db_path("v2-to-v3");
605
606 {
607 let conn = Connection::open(&db_path).unwrap();
608
609 conn.execute_batch(crate::storage::schema::SCHEMA_V1)
612 .unwrap();
613 conn.execute_batch(
614 "CREATE TABLE IF NOT EXISTS schema_version (id INTEGER PRIMARY KEY CHECK(id = 1), version INTEGER NOT NULL)",
615 )
616 .unwrap();
617 conn.execute(
618 "INSERT OR REPLACE INTO schema_version (id, version) VALUES (1, 1)",
619 [],
620 )
621 .unwrap();
622 conn.execute_batch(crate::storage::schema::SCHEMA_V2)
623 .unwrap();
624
625 conn.execute(
626 "INSERT INTO entries (
627 id, content, timestamp, kind, token_count, session_id,
628 compaction_count, compaction_trigger, runtime, model, cwd,
629 git_branch, git_sha, turn_id, agent_type, agent_id
630 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)",
631 rusqlite::params![
632 "a1",
633 "auto entry with runtime metadata",
634 300_i64,
635 "Auto",
636 4_i64,
637 "session-abc",
638 2_i64,
639 "matcher:threshold",
640 "codex",
641 "gpt-5.3-codex",
642 "/workspace/context-forge",
643 "feature/schema-v2",
644 "abc123def",
645 "turn-77",
646 "coder",
647 "agent-main",
648 ],
649 )
650 .unwrap();
651 conn.execute(
652 "INSERT INTO entries (id, content, timestamp, kind, token_count) VALUES (?1, ?2, ?3, ?4, ?5)",
653 rusqlite::params!["m1", "manual entry", 100_i64, "Manual", 2_i64],
654 )
655 .unwrap();
656 }
657
658 let storage = SqliteStorage::open(&db_path, 100).unwrap();
659
660 let all = storage.get_all().unwrap();
662 assert_eq!(all.len(), 2);
663
664 let auto_entry = all.iter().find(|e| e.id == "a1").unwrap();
666 let manual_entry = all.iter().find(|e| e.id == "m1").unwrap();
667 assert_eq!(auto_entry.kind, "summary");
668 assert_eq!(manual_entry.kind, "manual");
669
670 let metadata = auto_entry
672 .metadata
673 .as_ref()
674 .expect("metadata should be present for migrated v2 entry");
675 assert_eq!(metadata["runtime"], "codex");
676 assert_eq!(metadata["model"], "gpt-5.3-codex");
677 assert_eq!(metadata["cwd"], "/workspace/context-forge");
678 assert_eq!(metadata["git_branch"], "feature/schema-v2");
679 assert_eq!(metadata["git_sha"], "abc123def");
680 assert_eq!(metadata["compaction_trigger"], "matcher:threshold");
681 assert_eq!(metadata["turn_id"], "turn-77");
682 assert_eq!(metadata["agent_type"], "coder");
683 assert_eq!(metadata["agent_id"], "agent-main");
684
685 assert_eq!(auto_entry.session_id.as_deref(), Some("session-abc"));
687 assert_eq!(auto_entry.token_count, Some(4));
688
689 let (_, searcher) = open_storage(&db_path, 100).unwrap();
691 let results = searcher.search("runtime metadata", None, 10).unwrap();
692 assert!(
693 results.iter().any(|r| r.entry.id == "a1"),
694 "FTS search should still find the migrated entry's content"
695 );
696
697 let conn = storage.pool().get().unwrap();
699 let runtime_configs_exists: i64 = conn
700 .query_row(
701 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='runtime_configs'",
702 [],
703 |r| r.get(0),
704 )
705 .unwrap();
706 assert_eq!(runtime_configs_exists, 0);
707
708 let version: i64 = conn
709 .query_row(
710 "SELECT COALESCE(MAX(version), 0) FROM schema_version",
711 [],
712 |r| r.get(0),
713 )
714 .unwrap();
715 assert_eq!(version, 3);
716
717 let _ = fs::remove_file(&db_path);
718 }
719
720 #[test]
721 fn test_new_entry_with_scope_and_metadata() {
722 let (storage, _) = open_storage(Path::new(":memory:"), 100).unwrap();
723
724 let metadata = serde_json::json!({"runtime": "codex", "model": "gpt-5.3-codex"});
725 let entry = ContextEntry {
726 id: "v3-entry".into(),
727 content: "entry with scope and metadata".into(),
728 timestamp: 1_700_000_100,
729 kind: kind::SUMMARY.to_owned(),
730 scope: Some("project:homelab-rs".into()),
731 session_id: Some("session-123".into()),
732 token_count: Some(6),
733 metadata: Some(metadata.clone()),
734 };
735
736 storage.save(&entry).unwrap();
737
738 let all = storage.get_all().unwrap();
739 assert_eq!(all.len(), 1);
740 let got = &all[0];
741 assert_eq!(got.id, entry.id);
742 assert_eq!(got.content, entry.content);
743 assert_eq!(got.timestamp, entry.timestamp);
744 assert_eq!(got.kind, entry.kind);
745 assert_eq!(got.scope, entry.scope);
746 assert_eq!(got.session_id, entry.session_id);
747 assert_eq!(got.token_count, entry.token_count);
748 assert_eq!(got.metadata, Some(metadata));
749 }
750
751 #[test]
752 fn test_insert_or_replace() {
753 let (storage, _) = open_storage(Path::new(":memory:"), 100).unwrap();
754 storage
755 .save(&make_entry("e1", "original content", 100, kind::MANUAL))
756 .unwrap();
757 storage
758 .save(&make_entry("e1", "updated content", 200, kind::SUMMARY))
759 .unwrap();
760
761 assert_eq!(storage.count().unwrap(), 1);
762
763 let all = storage.get_all().unwrap();
764 assert_eq!(all[0].content, "updated content");
765 }
766
767 #[test]
768 fn test_search_match_all_query() {
769 let (storage, searcher) = open_storage(Path::new(":memory:"), 100).unwrap();
770 storage
771 .save(&make_entry("e1", "first entry", 100, kind::MANUAL))
772 .unwrap();
773 storage
774 .save(&make_entry("e2", "second entry", 200, kind::SNAPSHOT))
775 .unwrap();
776 storage
777 .save(&make_entry("e3", "third entry", 300, kind::SUMMARY))
778 .unwrap();
779
780 let results = searcher.search(MATCH_ALL_QUERY, None, 10).unwrap();
781 assert_eq!(results.len(), 3);
782
783 assert_eq!(results[0].entry.id, "e3");
785 assert_eq!(results[1].entry.id, "e2");
786 assert_eq!(results[2].entry.id, "e1");
787
788 for r in &results {
790 assert!((r.score - 1.0).abs() < f64::EPSILON);
791 }
792 }
793
794 #[test]
795 fn test_search_match_all_query_scoped() {
796 let (storage, searcher) = open_storage(Path::new(":memory:"), 100).unwrap();
797 storage
798 .save(&make_scoped_entry("e1", "first entry", 100, "a"))
799 .unwrap();
800 storage
801 .save(&make_scoped_entry("e2", "second entry", 200, "b"))
802 .unwrap();
803
804 let results_a = searcher.search(MATCH_ALL_QUERY, Some("a"), 10).unwrap();
805 assert_eq!(results_a.len(), 1);
806 assert_eq!(results_a[0].entry.id, "e1");
807
808 let results_all = searcher.search(MATCH_ALL_QUERY, None, 10).unwrap();
809 assert_eq!(results_all.len(), 2);
810 }
811}