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_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}