1use async_trait::async_trait;
4use rusqlite::params;
5use std::path::Path;
6use std::sync::{Arc, Mutex};
7
8use super::{BaselineStore, StorageHealth};
9use crate::error::StoreError;
10use crate::models::{
11 BaselineRecord, BaselineSource, BaselineSummary, BaselineVersion, ListBaselinesQuery,
12 ListBaselinesResponse, PaginationInfo,
13};
14
15#[derive(Debug)]
21pub struct SqliteStore {
22 _path: std::path::PathBuf,
24
25 conn: Arc<Mutex<rusqlite::Connection>>,
27}
28
29impl SqliteStore {
30 pub fn new<P: AsRef<Path>>(path: P) -> Result<Self, StoreError> {
32 let path = path.as_ref().to_path_buf();
33
34 if let Some(parent) = path.parent().filter(|p| !p.exists()) {
36 std::fs::create_dir_all(parent)?;
37 }
38
39 let conn = rusqlite::Connection::open(&path)?;
40
41 let store = Self {
42 _path: path,
43 conn: Arc::new(Mutex::new(conn)),
44 };
45
46 store.initialize()?;
47 Ok(store)
48 }
49
50 pub fn in_memory() -> Result<Self, StoreError> {
52 let conn = rusqlite::Connection::open_in_memory()?;
53
54 let store = Self {
55 _path: std::path::PathBuf::from(":memory:"),
56 conn: Arc::new(Mutex::new(conn)),
57 };
58
59 store.initialize()?;
60 Ok(store)
61 }
62
63 fn initialize(&self) -> Result<(), StoreError> {
65 let conn = self
66 .conn
67 .lock()
68 .map_err(|e| StoreError::LockError(e.to_string()))?;
69
70 conn.execute_batch(
71 r#"
72 -- Baselines table
73 CREATE TABLE IF NOT EXISTS baselines (
74 id TEXT PRIMARY KEY,
75 project TEXT NOT NULL,
76 benchmark TEXT NOT NULL,
77 version TEXT NOT NULL,
78 git_ref TEXT,
79 git_sha TEXT,
80 receipt TEXT NOT NULL,
81 metadata TEXT NOT NULL DEFAULT '{}',
82 tags TEXT NOT NULL DEFAULT '[]',
83 source TEXT NOT NULL DEFAULT 'upload',
84 content_hash TEXT NOT NULL,
85 deleted INTEGER NOT NULL DEFAULT 0,
86 created_at TEXT NOT NULL,
87 updated_at TEXT NOT NULL,
88
89 UNIQUE(project, benchmark, version)
90 );
91
92 -- Indexes for common queries
93 CREATE INDEX IF NOT EXISTS idx_baselines_project_benchmark
94 ON baselines(project, benchmark);
95 CREATE INDEX IF NOT EXISTS idx_baselines_project_git_ref
96 ON baselines(project, git_ref);
97 CREATE INDEX IF NOT EXISTS idx_baselines_created_at
98 ON baselines(created_at DESC);
99 "#,
100 )?;
101
102 Ok(())
103 }
104
105 fn parse_tags(tags_json: &str) -> Vec<String> {
107 serde_json::from_str(tags_json).unwrap_or_default()
108 }
109
110 fn serialize_tags(tags: &[String]) -> String {
112 serde_json::to_string(tags).unwrap_or_else(|_| "[]".to_string())
113 }
114
115 fn parse_metadata(metadata_json: &str) -> std::collections::BTreeMap<String, String> {
117 serde_json::from_str(metadata_json).unwrap_or_default()
118 }
119
120 fn serialize_metadata(metadata: &std::collections::BTreeMap<String, String>) -> String {
122 serde_json::to_string(metadata).unwrap_or_else(|_| "{}".to_string())
123 }
124
125 fn row_to_record(row: &rusqlite::Row) -> Result<BaselineRecord, rusqlite::Error> {
127 let schema = crate::models::BASELINE_SCHEMA_V1.to_string();
132 let source_str: String = row.get(9)?;
133 let source = match source_str.as_str() {
134 "upload" => BaselineSource::Upload,
135 "promote" => BaselineSource::Promote,
136 "migrate" => BaselineSource::Migrate,
137 "rollback" => BaselineSource::Rollback,
138 _ => BaselineSource::Upload,
139 };
140
141 let receipt_json: String = row.get(6)?;
142 let receipt: perfgate_types::RunReceipt =
143 serde_json::from_str(&receipt_json).map_err(|e| {
144 rusqlite::Error::FromSqlConversionFailure(
145 6,
146 rusqlite::types::Type::Text,
147 Box::new(std::io::Error::new(
148 std::io::ErrorKind::InvalidData,
149 format!("Failed to parse receipt JSON: {}", e),
150 )),
151 )
152 })?;
153
154 let created_at_str: String = row.get(12)?;
155 let updated_at_str: String = row.get(13)?;
156
157 Ok(BaselineRecord {
158 schema,
159 id: row.get(0)?,
160 project: row.get(1)?,
161 benchmark: row.get(2)?,
162 version: row.get(3)?,
163 git_ref: row.get(4)?,
164 git_sha: row.get(5)?,
165 receipt,
166 metadata: Self::parse_metadata(&row.get::<_, String>(7)?),
167 tags: Self::parse_tags(&row.get::<_, String>(8)?),
168 created_at: chrono::DateTime::parse_from_rfc3339(&created_at_str)
169 .map(|dt| dt.with_timezone(&chrono::Utc))
170 .unwrap_or_else(|_| chrono::Utc::now()),
171 updated_at: chrono::DateTime::parse_from_rfc3339(&updated_at_str)
172 .map(|dt| dt.with_timezone(&chrono::Utc))
173 .unwrap_or_else(|_| chrono::Utc::now()),
174 content_hash: row.get(10)?,
175 source,
176 deleted: row.get::<_, i64>(11)? != 0,
177 })
178 }
179}
180
181#[async_trait]
182impl BaselineStore for SqliteStore {
183 async fn create(&self, record: &BaselineRecord) -> Result<(), StoreError> {
184 let conn = self
185 .conn
186 .lock()
187 .map_err(|e| StoreError::LockError(e.to_string()))?;
188
189 let receipt_json = serde_json::to_string(&record.receipt)?;
190 let metadata_json = Self::serialize_metadata(&record.metadata);
191 let tags_json = Self::serialize_tags(&record.tags);
192 let source_str = match record.source {
193 BaselineSource::Upload => "upload",
194 BaselineSource::Promote => "promote",
195 BaselineSource::Migrate => "migrate",
196 BaselineSource::Rollback => "rollback",
197 };
198
199 let result = conn.execute(
200 r#"
201 INSERT INTO baselines (
202 id, project, benchmark, version, git_ref, git_sha,
203 receipt, metadata, tags, source, content_hash,
204 deleted, created_at, updated_at
205 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)
206 "#,
207 params![
208 record.id,
209 record.project,
210 record.benchmark,
211 record.version,
212 record.git_ref,
213 record.git_sha,
214 receipt_json,
215 metadata_json,
216 tags_json,
217 source_str,
218 record.content_hash,
219 if record.deleted { 1i64 } else { 0i64 },
220 record.created_at.to_rfc3339(),
221 record.updated_at.to_rfc3339(),
222 ],
223 );
224
225 match result {
226 Ok(_) => Ok(()),
227 Err(rusqlite::Error::SqliteFailure(err, _)) => {
228 if err.code == rusqlite::ErrorCode::ConstraintViolation {
229 Err(StoreError::AlreadyExists(format!(
230 "project={}, benchmark={}, version={}",
231 record.project, record.benchmark, record.version
232 )))
233 } else {
234 Err(StoreError::SqliteError(rusqlite::Error::SqliteFailure(
235 err, None,
236 )))
237 }
238 }
239 Err(e) => Err(StoreError::SqliteError(e)),
240 }
241 }
242
243 async fn get(
244 &self,
245 project: &str,
246 benchmark: &str,
247 version: &str,
248 ) -> Result<Option<BaselineRecord>, StoreError> {
249 let conn = self
250 .conn
251 .lock()
252 .map_err(|e| StoreError::LockError(e.to_string()))?;
253
254 let mut stmt = conn.prepare(
255 r#"
256 SELECT id, project, benchmark, version, git_ref, git_sha,
257 receipt, metadata, tags, source, content_hash, deleted,
258 created_at, updated_at
259 FROM baselines
260 WHERE project = ?1 AND benchmark = ?2 AND version = ?3 AND deleted = 0
261 "#,
262 )?;
263
264 let result = stmt.query_row(params![project, benchmark, version], Self::row_to_record);
265
266 match result {
267 Ok(record) => Ok(Some(record)),
268 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
269 Err(e) => Err(StoreError::SqliteError(e)),
270 }
271 }
272
273 async fn get_latest(
274 &self,
275 project: &str,
276 benchmark: &str,
277 ) -> Result<Option<BaselineRecord>, StoreError> {
278 let conn = self
279 .conn
280 .lock()
281 .map_err(|e| StoreError::LockError(e.to_string()))?;
282
283 let mut stmt = conn.prepare(
284 r#"
285 SELECT id, project, benchmark, version, git_ref, git_sha,
286 receipt, metadata, tags, source, content_hash, deleted,
287 created_at, updated_at
288 FROM baselines
289 WHERE project = ?1 AND benchmark = ?2 AND deleted = 0
290 ORDER BY created_at DESC
291 LIMIT 1
292 "#,
293 )?;
294
295 let result = stmt.query_row(params![project, benchmark], Self::row_to_record);
296
297 match result {
298 Ok(record) => Ok(Some(record)),
299 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
300 Err(e) => Err(StoreError::SqliteError(e)),
301 }
302 }
303
304 async fn list(
305 &self,
306 project: &str,
307 query: &ListBaselinesQuery,
308 ) -> Result<ListBaselinesResponse, StoreError> {
309 let conn = self
310 .conn
311 .lock()
312 .map_err(|e| StoreError::LockError(e.to_string()))?;
313
314 let mut conditions = Vec::new();
316 let mut params: Vec<Box<dyn rusqlite::ToSql>> = vec![Box::new(project.to_string())];
317
318 if let Some(ref b) = query.benchmark {
319 conditions.push("benchmark = ?".to_string());
320 params.push(Box::new(b.clone()));
321 }
322 if let Some(ref p) = query.benchmark_prefix {
323 conditions.push("benchmark LIKE ? || '%'".to_string());
324 params.push(Box::new(p.clone()));
325 }
326 if let Some(ref r) = query.git_ref {
327 conditions.push("git_ref = ?".to_string());
328 params.push(Box::new(r.clone()));
329 }
330 if let Some(ref s) = query.git_sha {
331 conditions.push("git_sha = ?".to_string());
332 params.push(Box::new(s.clone()));
333 }
334
335 let where_clause = if conditions.is_empty() {
337 String::new()
338 } else {
339 format!(" AND {}", conditions.join(" AND "))
340 };
341
342 let count_sql = format!(
344 "SELECT COUNT(*) FROM baselines WHERE project = ? AND deleted = 0{}",
345 where_clause
346 );
347
348 let count_param_count = params.len();
350 let count_params: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();
351 let total: u64 = conn.query_row(&count_sql, &count_params[..count_param_count], |row| {
352 row.get(0)
353 })?;
354
355 let sql = format!(
357 r#"
358 SELECT id, project, benchmark, version, git_ref, git_sha,
359 receipt, metadata, tags, source, content_hash, deleted,
360 created_at, updated_at
361 FROM baselines
362 WHERE project = ? AND deleted = 0{}
363 ORDER BY created_at DESC
364 LIMIT ? OFFSET ?
365 "#,
366 where_clause
367 );
368
369 params.push(Box::new(query.limit as i64));
371 params.push(Box::new(query.offset as i64));
372
373 let mut stmt = conn.prepare(&sql)?;
374 let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();
375 let records = stmt
376 .query_map(¶m_refs[..], Self::row_to_record)?
377 .collect::<Result<Vec<_>, _>>()?;
378
379 let baselines: Vec<BaselineSummary> = records
380 .into_iter()
381 .map(|r| {
382 let mut summary: BaselineSummary = r.clone().into();
383 if query.include_receipt {
384 summary.receipt = Some(r.receipt);
385 }
386 summary
387 })
388 .collect();
389
390 let has_more = (query.offset + baselines.len() as u64) < total;
391
392 Ok(ListBaselinesResponse {
393 baselines,
394 pagination: PaginationInfo {
395 total,
396 limit: query.limit,
397 offset: query.offset,
398 has_more,
399 },
400 })
401 }
402
403 async fn update(&self, record: &BaselineRecord) -> Result<(), StoreError> {
404 let conn = self
405 .conn
406 .lock()
407 .map_err(|e| StoreError::LockError(e.to_string()))?;
408
409 let receipt_json = serde_json::to_string(&record.receipt)?;
410 let metadata_json = Self::serialize_metadata(&record.metadata);
411 let tags_json = Self::serialize_tags(&record.tags);
412 let source_str = match record.source {
413 BaselineSource::Upload => "upload",
414 BaselineSource::Promote => "promote",
415 BaselineSource::Migrate => "migrate",
416 BaselineSource::Rollback => "rollback",
417 };
418
419 let rows_affected = conn.execute(
420 r#"
421 UPDATE baselines SET
422 git_ref = ?1, git_sha = ?2, receipt = ?3,
423 metadata = ?4, tags = ?5, source = ?6,
424 content_hash = ?7, deleted = ?8, updated_at = ?9
425 WHERE project_id = ?10 AND benchmark = ?11 AND version = ?12
426 "#,
427 params![
428 record.git_ref,
429 record.git_sha,
430 receipt_json,
431 metadata_json,
432 tags_json,
433 source_str,
434 record.content_hash,
435 if record.deleted { 1i64 } else { 0i64 },
436 record.updated_at.to_rfc3339(),
437 record.project,
438 record.benchmark,
439 record.version,
440 ],
441 )?;
442
443 if rows_affected == 0 {
444 return Err(StoreError::NotFound(format!(
445 "project={}, benchmark={}, version={}",
446 record.project, record.benchmark, record.version
447 )));
448 }
449
450 Ok(())
451 }
452
453 async fn delete(
454 &self,
455 project: &str,
456 benchmark: &str,
457 version: &str,
458 ) -> Result<bool, StoreError> {
459 let conn = self
460 .conn
461 .lock()
462 .map_err(|e| StoreError::LockError(e.to_string()))?;
463
464 let rows_affected = conn.execute(
465 r#"
466 UPDATE baselines SET deleted = 1, updated_at = ?1
467 WHERE project = ?2 AND benchmark = ?3 AND version = ?4 AND deleted = 0
468 "#,
469 params![chrono::Utc::now().to_rfc3339(), project, benchmark, version,],
470 )?;
471
472 Ok(rows_affected > 0)
473 }
474
475 async fn hard_delete(
476 &self,
477 project: &str,
478 benchmark: &str,
479 version: &str,
480 ) -> Result<bool, StoreError> {
481 let conn = self
482 .conn
483 .lock()
484 .map_err(|e| StoreError::LockError(e.to_string()))?;
485
486 let rows_affected = conn.execute(
487 "DELETE FROM baselines WHERE project = ?1 AND benchmark = ?2 AND version = ?3",
488 params![project, benchmark, version],
489 )?;
490
491 Ok(rows_affected > 0)
492 }
493
494 async fn list_versions(
495 &self,
496 project: &str,
497 benchmark: &str,
498 ) -> Result<Vec<BaselineVersion>, StoreError> {
499 let conn = self
500 .conn
501 .lock()
502 .map_err(|e| StoreError::LockError(e.to_string()))?;
503
504 let mut stmt = conn.prepare(
505 r#"
506 SELECT version, git_ref, git_sha, source, created_at
507 FROM baselines
508 WHERE project = ?1 AND benchmark = ?2 AND deleted = 0
509 ORDER BY created_at DESC
510 "#,
511 )?;
512
513 let versions: Vec<BaselineVersion> = stmt
514 .query_map(params![project, benchmark], |row| {
515 let source_str: String = row.get(3)?;
516 let source = match source_str.as_str() {
517 "upload" => BaselineSource::Upload,
518 "promote" => BaselineSource::Promote,
519 "migrate" => BaselineSource::Migrate,
520 "rollback" => BaselineSource::Rollback,
521 _ => BaselineSource::Upload,
522 };
523
524 let created_at_str: String = row.get(4)?;
525 let created_at = chrono::DateTime::parse_from_rfc3339(&created_at_str)
526 .map(|dt| dt.with_timezone(&chrono::Utc))
527 .unwrap_or_else(|_| chrono::Utc::now());
528
529 Ok(BaselineVersion {
530 version: row.get(0)?,
531 git_ref: row.get(1)?,
532 git_sha: row.get(2)?,
533 created_at,
534 created_by: None,
535 is_current: false,
536 source,
537 })
538 })?
539 .collect::<Result<Vec<_>, _>>()?;
540
541 let mut versions = versions;
543 if let Some(first) = versions.first_mut() {
544 first.is_current = true;
545 }
546
547 Ok(versions)
548 }
549
550 async fn health_check(&self) -> Result<StorageHealth, StoreError> {
551 let conn = self
552 .conn
553 .lock()
554 .map_err(|e| StoreError::LockError(e.to_string()))?;
555
556 match conn.query_row("SELECT 1", [], |_| Ok(())) {
557 Ok(_) => Ok(StorageHealth::Healthy),
558 Err(_) => Ok(StorageHealth::Unhealthy),
559 }
560 }
561
562 fn backend_type(&self) -> &'static str {
563 "sqlite"
564 }
565}
566
567#[cfg(test)]
568mod tests {
569 use super::*;
570 use crate::models::BaselineSource;
571 use perfgate_types::{BenchMeta, HostInfo, RunMeta, RunReceipt, Stats, ToolInfo, U64Summary};
572 use std::collections::BTreeMap;
573 use tempfile::tempdir;
574
575 fn create_test_receipt(name: &str) -> RunReceipt {
576 RunReceipt {
577 schema: "perfgate.run.v1".to_string(),
578 tool: ToolInfo {
579 name: "perfgate".to_string(),
580 version: "0.3.0".to_string(),
581 },
582 run: RunMeta {
583 id: "test-run-id".to_string(),
584 started_at: "2026-01-01T00:00:00Z".to_string(),
585 ended_at: "2026-01-01T00:01:00Z".to_string(),
586 host: HostInfo {
587 os: "linux".to_string(),
588 arch: "x86_64".to_string(),
589 cpu_count: Some(8),
590 memory_bytes: Some(16 * 1024 * 1024 * 1024),
591 hostname_hash: None,
592 },
593 },
594 bench: BenchMeta {
595 name: name.to_string(),
596 cwd: None,
597 command: vec!["./bench.sh".to_string()],
598 repeat: 5,
599 warmup: 1,
600 work_units: None,
601 timeout_ms: None,
602 },
603 samples: vec![],
604 stats: Stats {
605 wall_ms: U64Summary {
606 median: 100,
607 min: 90,
608 max: 110,
609 },
610 cpu_ms: None,
611 page_faults: None,
612 ctx_switches: None,
613 max_rss_kb: None,
614 binary_bytes: None,
615 throughput_per_s: None,
616 },
617 }
618 }
619
620 fn create_test_record(project: &str, benchmark: &str, version: &str) -> BaselineRecord {
621 BaselineRecord::new(
622 project.to_string(),
623 benchmark.to_string(),
624 version.to_string(),
625 create_test_receipt(benchmark),
626 Some("refs/heads/main".to_string()),
627 Some("abc123".to_string()),
628 BTreeMap::new(),
629 vec!["test".to_string()],
630 BaselineSource::Upload,
631 )
632 }
633
634 #[tokio::test(flavor = "multi_thread")]
635 async fn test_in_memory_database() {
636 let store = SqliteStore::in_memory().unwrap();
637
638 let record = create_test_record("my-project", "my-bench", "v1.0.0");
639 store.create(&record).await.unwrap();
640
641 let retrieved = store.get("my-project", "my-bench", "v1.0.0").await.unwrap();
642
643 assert!(retrieved.is_some());
644 let retrieved = retrieved.unwrap();
645 assert_eq!(retrieved.project, "my-project");
646 assert_eq!(retrieved.benchmark, "my-bench");
647 assert_eq!(retrieved.version, "v1.0.0");
648 }
649
650 #[tokio::test(flavor = "multi_thread")]
651 async fn test_persistent_database() {
652 let dir = tempdir().unwrap();
653 let db_path = dir.path().join("test.db");
654
655 {
657 let store = SqliteStore::new(&db_path).unwrap();
658 let record = create_test_record("my-project", "my-bench", "v1.0.0");
659 store.create(&record).await.unwrap();
660 }
661
662 {
664 let store = SqliteStore::new(&db_path).unwrap();
665 let retrieved = store.get("my-project", "my-bench", "v1.0.0").await.unwrap();
666
667 assert!(retrieved.is_some());
668 }
669 }
670
671 #[tokio::test(flavor = "multi_thread")]
672 async fn test_create_duplicate_fails() {
673 let store = SqliteStore::in_memory().unwrap();
674 let record = create_test_record("my-project", "my-bench", "v1.0.0");
675
676 store.create(&record).await.unwrap();
677
678 let result = store.create(&record).await;
680 assert!(result.is_err());
681 }
682
683 #[tokio::test(flavor = "multi_thread")]
684 async fn test_get_latest() {
685 let store = SqliteStore::in_memory().unwrap();
686
687 let record1 = create_test_record("my-project", "my-bench", "v1.0.0");
688 store.create(&record1).await.unwrap();
689
690 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
692
693 let record2 = create_test_record("my-project", "my-bench", "v1.1.0");
694 store.create(&record2).await.unwrap();
695
696 let latest = store.get_latest("my-project", "my-bench").await.unwrap();
697
698 assert!(latest.is_some());
699 let latest = latest.unwrap();
700 assert_eq!(latest.version, "v1.1.0");
701 }
702
703 #[tokio::test(flavor = "multi_thread")]
704 async fn test_list_with_filters() {
705 let store = SqliteStore::in_memory().unwrap();
706
707 store
708 .create(&create_test_record("my-project", "bench-a", "v1.0.0"))
709 .await
710 .unwrap();
711 store
712 .create(&create_test_record("my-project", "bench-b", "v1.0.0"))
713 .await
714 .unwrap();
715 store
716 .create(&create_test_record("my-project", "bench-a", "v2.0.0"))
717 .await
718 .unwrap();
719
720 let query = ListBaselinesQuery::default();
722 let result = store.list("my-project", &query).await.unwrap();
723 assert_eq!(result.baselines.len(), 3);
724
725 let query = ListBaselinesQuery {
727 benchmark: Some("bench-a".to_string()),
728 ..Default::default()
729 };
730 let result = store.list("my-project", &query).await.unwrap();
731 assert_eq!(result.baselines.len(), 2);
732
733 let query = ListBaselinesQuery {
735 limit: 2,
736 offset: 0,
737 ..Default::default()
738 };
739 let result = store.list("my-project", &query).await.unwrap();
740 assert_eq!(result.baselines.len(), 2);
741 assert!(result.pagination.has_more);
742 }
743
744 #[tokio::test(flavor = "multi_thread")]
745 async fn test_delete() {
746 let store = SqliteStore::in_memory().unwrap();
747 let record = create_test_record("my-project", "my-bench", "v1.0.0");
748
749 store.create(&record).await.unwrap();
750
751 let deleted = store
753 .delete("my-project", "my-bench", "v1.0.0")
754 .await
755 .unwrap();
756 assert!(deleted);
757
758 let retrieved = store.get("my-project", "my-bench", "v1.0.0").await.unwrap();
760 assert!(retrieved.is_none());
761 }
762
763 #[tokio::test(flavor = "multi_thread")]
764 async fn test_list_versions() {
765 let store = SqliteStore::in_memory().unwrap();
766
767 store
768 .create(&create_test_record("my-project", "my-bench", "v1.0.0"))
769 .await
770 .unwrap();
771 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
772 store
773 .create(&create_test_record("my-project", "my-bench", "v1.1.0"))
774 .await
775 .unwrap();
776
777 let versions = store.list_versions("my-project", "my-bench").await.unwrap();
778
779 assert_eq!(versions.len(), 2);
780 assert!(versions[0].is_current);
781 assert!(!versions[1].is_current);
782 }
783
784 #[tokio::test(flavor = "multi_thread")]
785 async fn test_health_check() {
786 let store = SqliteStore::in_memory().unwrap();
787 let health = store.health_check().await.unwrap();
788 assert_eq!(health, StorageHealth::Healthy);
789 }
790
791 #[test]
792 fn test_backend_type() {
793 let store = SqliteStore::in_memory().unwrap();
794 assert_eq!(store.backend_type(), "sqlite");
795 }
796}