cratestack_client_store_sqlite/
store.rs1use std::fs;
4use std::path::{Path, PathBuf};
5use std::sync::Mutex;
6
7use chrono::Utc;
8use cratestack_core::{ClientStateStore, CoolError, PersistedClientState, RequestJournalEntry};
9use rusqlite::{Connection, params};
10
11use crate::bootstrap::{SQLITE_SCHEMA_VERSION, bootstrap, sqlite_error};
12use crate::ops::{load_state, save_state};
13
14pub struct SqliteStateStore {
15 connection: Mutex<Connection>,
16 path: PathBuf,
17}
18
19impl SqliteStateStore {
20 pub fn open(path: impl Into<PathBuf>) -> Result<Self, CoolError> {
21 let path = path.into();
22 if let Some(parent) = path.parent() {
23 fs::create_dir_all(parent).map_err(|error| {
24 CoolError::Internal(format!(
25 "failed to create SQLite state directory {}: {error}",
26 parent.display()
27 ))
28 })?;
29 }
30 let connection = Connection::open(&path).map_err(|error| {
31 CoolError::Internal(format!(
32 "failed to open SQLite state store {}: {error}",
33 path.display()
34 ))
35 })?;
36 bootstrap(&connection, SQLITE_SCHEMA_VERSION)?;
37
38 Ok(Self {
39 connection: Mutex::new(connection),
40 path,
41 })
42 }
43
44 pub fn path(&self) -> &Path {
45 &self.path
46 }
47}
48
49impl ClientStateStore for SqliteStateStore {
50 fn load(&self) -> Result<PersistedClientState, CoolError> {
51 let connection = self.connection.lock().map_err(|error| {
52 CoolError::Internal(format!("failed to lock SQLite state store: {error}"))
53 })?;
54 load_state(&connection)
55 }
56
57 fn save(&self, state: &PersistedClientState) -> Result<(), CoolError> {
58 let mut connection = self.connection.lock().map_err(|error| {
59 CoolError::Internal(format!("failed to lock SQLite state store: {error}"))
60 })?;
61 save_state(&mut connection, state)
62 }
63
64 fn append_request_journal(&self, entry: &RequestJournalEntry) -> Result<(), CoolError> {
65 let mut connection = self.connection.lock().map_err(|error| {
66 CoolError::Internal(format!("failed to lock SQLite state store: {error}"))
67 })?;
68 let transaction = connection.transaction().map_err(sqlite_error)?;
69 transaction
70 .execute(
71 "INSERT INTO request_journal (method, path, status_code, content_type, recorded_at) VALUES (?1, ?2, ?3, ?4, ?5)",
72 params![
73 &entry.method,
74 &entry.path,
75 entry.status_code,
76 &entry.content_type,
77 entry.recorded_at.to_rfc3339(),
78 ],
79 )
80 .map_err(sqlite_error)?;
81 transaction
82 .execute(
83 "UPDATE state_meta SET state_version = state_version + 1, updated_at = ?1 WHERE singleton = 1",
84 params![Utc::now().to_rfc3339()],
85 )
86 .map_err(sqlite_error)?;
87 transaction.commit().map_err(sqlite_error)
88 }
89}