1use crate::error::{Cookie as _, Error, Result};
2use std::time::SystemTime;
3
4use camino::Utf8PathBuf;
5use sql_peas::StatementHandle;
6
7pub struct Log {
8 inner: sql_peas::Connection,
9 get_handle: StatementHandle,
10 insert_handle: StatementHandle,
11 iter_handle: StatementHandle,
12}
13
14pub struct LogCursor<'a> {
15 inner: sql_peas::Cursor<'a>,
16}
17
18#[derive(serde::Serialize)]
22pub struct Element {
23 index: i64,
25
26 ulid: ulid::Ulid,
28 value: String,
29}
30
31impl Iterator for LogCursor<'_> {
32 type Item = Result<Element>;
33
34 fn next(&mut self) -> Option<Self::Item> {
35 let row = match self.inner.next()? {
36 Err(e) => return Some(Err(e.into())),
37 Ok(x) => x,
38 };
39 let index = row.read(0);
40 let ulid = row.read::<&str, _>(1);
41 let ulid = match ulid::Ulid::from_string(ulid) {
42 Err(e) => return Some(Err(e.into())),
43 Ok(x) => x,
44 };
45 let value = row.read::<&str, _>(2).to_owned();
46 Some(Ok(Element { index, ulid, value }))
47 }
48}
49
50const LOG_USER_VERSION: u32 = 756530437;
51const LOG_USER_VERSION_I64: i64 = LOG_USER_VERSION as i64;
52const LOG_TABLE_NAME: &str = "sql_hummus_0_log";
53
54impl Log {
55 pub fn open_default() -> Result<(Log, Utf8PathBuf)> {
61 let dirs = directories::ProjectDirs::from("", "ReactorScram", "sql-hummus")
62 .ok_or(Error::from("directories::ProjectDirs failed"))?;
63 let dir = dirs.data_local_dir();
64 std::fs::create_dir_all(dir).cookie("create_dir_all() for default data dir failed")?;
65 let path = dir.join("default-log.db").try_into()?;
66 let log = Log::new(&path)?;
67 Ok((log, path))
68 }
69
70 pub fn new<P: AsRef<std::path::Path>>(p: P) -> Result<Self> {
71 let mut inner = sql_peas::Connection::open(p)?;
72
73 inner.execute("PRAGMA trusted_schema = 0;")?;
75 inner.execute("PRAGMA foreign_keys = 1;")?;
76
77 let needs_setup = {
78 let handle = inner.prepare("PRAGMA user_version")?;
79 let stmt = inner.borrow_statement(handle)?;
80 let mut rows = stmt.iter();
82 let row = rows
83 .next()
84 .ok_or("Expected one row from PRAGMA user_version")??;
85 let needs_setup = match row.read(0) {
86 0 => true,
87 LOG_USER_VERSION_I64 => false,
88 _ => {
89 return Err("PRAGMA user_version looks like a non-KV file")?;
90 }
91 };
92
93 inner.drop_statement(handle)?;
94 needs_setup
95 };
96
97 if needs_setup {
98 inner.execute(format!("CREATE TABLE IF NOT EXISTS {LOG_TABLE_NAME} (idx INTEGER PRIMARY KEY NOT NULL, ulid TEXT NOT NULL UNIQUE, value TEXT)"))?;
102 inner.execute(format!("PRAGMA user_version = {LOG_USER_VERSION}"))?;
103 }
104
105 let get_handle = inner.prepare(format!(
106 "SELECT idx, ulid, value from {LOG_TABLE_NAME} WHERE idx = ?"
107 ))?;
108 let insert_handle = inner.prepare(format!(
109 "INSERT INTO {LOG_TABLE_NAME} (ulid, value) VALUES (?, ?) RETURNING idx"
110 ))?;
111 let iter_handle = inner.prepare(format!(
112 "SELECT idx, ulid, value FROM {LOG_TABLE_NAME} ORDER BY ulid"
113 ))?;
114
115 Ok(Self {
116 inner,
117 get_handle,
118 insert_handle,
119 iter_handle,
120 })
121 }
122
123 pub fn clear(&self) -> Result<()> {
124 self.inner
125 .execute(format!("DELETE FROM {LOG_TABLE_NAME}"))?;
126 Ok(())
127 }
128
129 pub fn get_by_index(&self, index: i64) -> Result<Option<Element>> {
130 let stmt = self.inner.borrow_statement(self.get_handle)?;
131 stmt.bind((1, index))?;
132 if stmt.next()? != sql_peas::State::Row {
133 return Ok(None);
134 }
135 if index != stmt.read(0)? {
136 Err("Log::get didn't get the same index back from the DB")?;
137 }
138 let ulid = ulid::Ulid::from_string(&stmt.read::<String, _>(1)?)?;
139 let value = stmt.read(2)?;
140 Ok(Some(Element { index, ulid, value }))
141 }
142
143 pub fn iter<'a>(&'a self) -> Result<LogCursor<'a>> {
144 let stmt = self.inner.borrow_statement(self.iter_handle)?;
145 let cursor = stmt.iter();
146 Ok(LogCursor { inner: cursor })
147 }
148
149 pub(crate) fn push_inner(&self, ts: &str, value: &str) -> Result<i64> {
150 let stmt = self.inner.borrow_statement(self.insert_handle).cookie(
151 "KPQL7L2G
152 ",
153 )?;
154 stmt.bind((1, ts)).cookie("7RMQCY4N")?;
155 stmt.bind((2, value)).cookie("AGRZOZBD")?;
156 if stmt.next().cookie("WEMNOGO7")? != sql_peas::State::Row {
157 Err("We didn't get State::Row during Log::insert")?;
158 }
159 let index = stmt.read(0).cookie("MEJYJBDW")?;
160 if stmt.next().cookie("QOV7GD47")? != sql_peas::State::Done {
161 Err("We didn't get State::Done during Log::insert")?;
162 }
163 Ok(index)
164 }
165
166 pub fn push_with_time<S: AsRef<str>>(&self, ts: SystemTime, value: S) -> Result<i64> {
168 let ulid = ulid::Ulid::from_datetime(ts);
169 self.push_inner(ulid.to_string().as_str(), value.as_ref())
170 }
171
172 pub fn push<S: AsRef<str>>(&self, value: S) -> Result<i64> {
173 let ulid = ulid::Ulid::generate();
174 self.push_inner(ulid.to_string().as_str(), value.as_ref())
175 .cookie("6JEUVGMI")
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn simple() -> Result<()> {
185 let path = "sql_hummus_test_temp_IVTIQYBF.db";
186
187 let cxn = Log::new(path).unwrap();
188 cxn.clear()?;
189
190 assert_eq!(cxn.push_inner("01KX54X6X3G98K9A24P1HBA8GY", "test 1")?, 1);
191 assert_eq!(cxn.push_inner("01KX54X8MW2N2FA4ERGGXVNTWH", "test 2")?, 2);
192
193 let mut cursor = cxn.iter()?;
194
195 let line = cursor.next().unwrap().unwrap();
196 assert_eq!(line.index, 1);
197 assert_eq!(line.ulid.to_string(), "01KX54X6X3G98K9A24P1HBA8GY");
198 assert_eq!(line.value, "test 1");
199
200 let line = cursor.next().unwrap().unwrap();
201 assert_eq!(line.index, 2);
202 assert_eq!(line.ulid.to_string(), "01KX54X8MW2N2FA4ERGGXVNTWH");
203 assert_eq!(line.value, "test 2");
204
205 Ok(())
206 }
207}