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#[derive(serde::Serialize)]
20pub struct Element {
21 index: i64,
23
24 ulid: ulid::Ulid,
26 value: String,
27}
28
29#[derive(Debug, thiserror::Error)]
30#[error(transparent)]
31pub struct PublicError(#[from] ErrorRepr);
32
33#[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 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 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 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 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}