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