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