Skip to main content

cratestack_client_store_sqlite/
store.rs

1//! The `SqliteStateStore` handle and `ClientStateStore` impl.
2
3use 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}