Skip to main content

sql_hummus/
log.rs

1use std::time::SystemTime;
2
3use sql_peas::StatementHandle;
4
5pub struct Log {
6    inner: sql_peas::Connection,
7    get_handle: StatementHandle,
8    insert_handle: StatementHandle,
9    iter_handle: StatementHandle,
10}
11
12pub struct LogCursor<'a> {
13    inner: sql_peas::Cursor<'a>,
14}
15
16/// A single log entry within the log database
17///
18/// Called "Element" to match `Vec`
19#[derive(serde::Serialize)]
20pub struct Element {
21    /// Incrementing index within the file, e.g. 1, 2, 3, 4...
22    index: i64,
23
24    /// ULID recorded at some point before the database transaction committed
25    ulid: ulid::Ulid,
26    value: String,
27}
28
29#[derive(Debug, thiserror::Error)]
30#[error(transparent)]
31pub struct PublicError(#[from] ErrorRepr);
32
33// FIXME: I'm not 100% about this error setup. If you have gdb, it's redundant. If you don't have gdb, it's nice.
34#[derive(Debug, thiserror::Error)]
35#[error("kind={kind}; cookies={cookies:?}")]
36struct ErrorRepr {
37    cookies: Vec<&'static str>,
38    #[source]
39    kind: ErrorKind,
40}
41
42#[derive(Debug, thiserror::Error)]
43pub enum ErrorKind {
44    #[error("SQLite error: {0}")]
45    Sqlite(#[from] sql_peas::Error),
46    #[error("Error: {0}")]
47    Other(&'static str),
48    #[error("ULID decode error: {0}")]
49    UlidDecode(#[from] ulid::DecodeError),
50}
51
52impl<T> From<T> for PublicError
53where
54    ErrorKind: From<T>,
55{
56    fn from(value: T) -> Self {
57        ErrorRepr {
58            cookies: vec![],
59            kind: ErrorKind::from(value),
60        }
61        .into()
62    }
63}
64
65trait Cookie<T> {
66    fn cookie(self, s: &'static str) -> Result<T>;
67}
68
69impl<T, E> Cookie<T> for std::result::Result<T, E>
70where
71    PublicError: From<E>,
72{
73    fn cookie(self, s: &'static str) -> Result<T> {
74        match self {
75            Ok(x) => Ok(x),
76            Err(e) => {
77                let mut e = PublicError::from(e);
78                e.0.cookies.push(s);
79                Err(e)
80            }
81        }
82    }
83}
84
85type Result<T> = std::result::Result<T, PublicError>;
86
87impl Iterator for LogCursor<'_> {
88    type Item = Result<Element>;
89
90    fn next(&mut self) -> Option<Self::Item> {
91        let row = match self.inner.next()? {
92            Err(e) => return Some(Err(e.into())),
93            Ok(x) => x,
94        };
95        let index = row.read(0);
96        let ulid = row.read::<&str, _>(1);
97        let ulid = match ulid::Ulid::from_string(ulid) {
98            Err(e) => return Some(Err(e.into())),
99            Ok(x) => x,
100        };
101        let value = row.read::<&str, _>(2).to_owned();
102        Some(Ok(Element { index, ulid, value }))
103    }
104}
105
106const LOG_USER_VERSION: u32 = 756530437;
107const LOG_USER_VERSION_I64: i64 = LOG_USER_VERSION as i64;
108const LOG_TABLE_NAME: &str = "sql_hummus_0_log";
109
110impl Log {
111    pub fn new<P: AsRef<std::path::Path>>(p: P) -> Result<Self> {
112        let mut inner = sql_peas::Connection::open(p)?;
113
114        // Set some pragmas where new apps need different defaults than SQLite's defaults
115        inner.execute("PRAGMA trusted_schema = 0;")?;
116        inner.execute("PRAGMA foreign_keys = 1;")?;
117
118        let needs_setup = {
119            let handle = inner.prepare("PRAGMA user_version")?;
120            let stmt = inner.borrow_statement(handle)?;
121            // FIXME: de-dupe single row read up into SQLite
122            let mut rows = stmt.iter();
123            let row = rows.next().ok_or(ErrorKind::Other(
124                "Expected one row from PRAGMA user_version",
125            ))??;
126            let needs_setup = match row.read(0) {
127                0 => true,
128                LOG_USER_VERSION_I64 => false,
129                _ => {
130                    return Err(ErrorKind::Other(
131                        "PRAGMA user_version looks like a non-KV file",
132                    ))?;
133                }
134            };
135
136            inner.drop_statement(handle)?;
137            needs_setup
138        };
139
140        if needs_setup {
141            // FIXME: On further reflection, the logs could be implemented as a layer on top of the key-value store, if I combine features a little bit and decide that storing local time is the user's business, which is probably right.
142            // But I'll keep the original design for now.
143
144            inner.execute(format!("CREATE TABLE IF NOT EXISTS {LOG_TABLE_NAME} (idx INTEGER PRIMARY KEY NOT NULL, ulid TEXT NOT NULL UNIQUE, value TEXT)"))?;
145            inner.execute(format!("PRAGMA user_version = {LOG_USER_VERSION}"))?;
146        }
147
148        let get_handle = inner.prepare(format!(
149            "SELECT idx, ulid, value from {LOG_TABLE_NAME} WHERE idx = ?"
150        ))?;
151        let insert_handle = inner.prepare(format!(
152            "INSERT INTO {LOG_TABLE_NAME} (ulid, value) VALUES (?, ?) RETURNING idx"
153        ))?;
154        let iter_handle = inner.prepare(format!(
155            "SELECT idx, ulid, value FROM {LOG_TABLE_NAME} ORDER BY ulid"
156        ))?;
157
158        Ok(Self {
159            inner,
160            get_handle,
161            insert_handle,
162            iter_handle,
163        })
164    }
165
166    pub fn clear(&self) -> Result<()> {
167        self.inner
168            .execute(format!("DELETE FROM {LOG_TABLE_NAME}"))?;
169        Ok(())
170    }
171
172    pub fn get_by_index(&self, index: i64) -> Result<Option<Element>> {
173        let stmt = self.inner.borrow_statement(self.get_handle)?;
174        stmt.bind((1, index))?;
175        if stmt.next()? != sql_peas::State::Row {
176            return Ok(None);
177        }
178        if index != stmt.read(0)? {
179            Err(ErrorKind::Other(
180                "Log::get didn't get the same index back from the DB",
181            ))?;
182        }
183        let ulid = ulid::Ulid::from_string(&stmt.read::<String, _>(1)?)?;
184        let value = stmt.read(2)?;
185        Ok(Some(Element { index, ulid, value }))
186    }
187
188    pub fn iter<'a>(&'a self) -> Result<LogCursor<'a>> {
189        let stmt = self.inner.borrow_statement(self.iter_handle)?;
190        let cursor = stmt.iter();
191        Ok(LogCursor { inner: cursor })
192    }
193
194    pub(crate) fn push_inner(&self, ts: &str, value: &str) -> Result<i64> {
195        let stmt = self.inner.borrow_statement(self.insert_handle).cookie(
196            "KPQL7L2G
197        ",
198        )?;
199        stmt.bind((1, ts)).cookie("7RMQCY4N")?;
200        stmt.bind((2, value)).cookie("AGRZOZBD")?;
201        if stmt.next().cookie("WEMNOGO7")? != sql_peas::State::Row {
202            Err(ErrorKind::Other(
203                "We didn't get State::Row during Log::insert",
204            ))?;
205        }
206        let index = stmt.read(0).cookie("MEJYJBDW")?;
207        if stmt.next().cookie("QOV7GD47")? != sql_peas::State::Done {
208            Err(ErrorKind::Other(
209                "We didn't get sql_peas::State::Done during Log::insert",
210            ))?;
211        }
212        Ok(index)
213    }
214
215    /// Insert with user-specified time in case the user does store their local time inside the line.
216    pub fn push_with_time<S: AsRef<str>>(&self, ts: SystemTime, value: S) -> Result<i64> {
217        let ulid = ulid::Ulid::from_datetime(ts);
218        self.push_inner(ulid.to_string().as_str(), value.as_ref())
219    }
220
221    pub fn push<S: AsRef<str>>(&self, value: S) -> Result<i64> {
222        let ulid = ulid::Ulid::generate();
223        self.push_inner(ulid.to_string().as_str(), value.as_ref())
224            .cookie("6JEUVGMI")
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn simple() -> Result<()> {
234        let path = "sql_hummus_test_temp_IVTIQYBF.db";
235
236        let cxn = Log::new(path).unwrap();
237        cxn.clear()?;
238
239        assert_eq!(cxn.push_inner("01KX54X6X3G98K9A24P1HBA8GY", "test 1")?, 1);
240        assert_eq!(cxn.push_inner("01KX54X8MW2N2FA4ERGGXVNTWH", "test 2")?, 2);
241
242        let mut cursor = cxn.iter()?;
243
244        let line = cursor.next().unwrap().unwrap();
245        assert_eq!(line.index, 1);
246        assert_eq!(line.ulid.to_string(), "01KX54X6X3G98K9A24P1HBA8GY");
247        assert_eq!(line.value, "test 1");
248
249        let line = cursor.next().unwrap().unwrap();
250        assert_eq!(line.index, 2);
251        assert_eq!(line.ulid.to_string(), "01KX54X8MW2N2FA4ERGGXVNTWH");
252        assert_eq!(line.value, "test 2");
253
254        Ok(())
255    }
256}