items.rs

  1use std::{ffi::OsStr, os::unix::prelude::OsStrExt, path::PathBuf, sync::Arc};
  2
  3use anyhow::Result;
  4use rusqlite::{
  5    named_params, params,
  6    types::{FromSql, FromSqlError, FromSqlResult, ValueRef},
  7};
  8
  9use super::Db;
 10
 11pub(crate) const ITEMS_M_1: &str = "
 12CREATE TABLE items(
 13    id INTEGER PRIMARY KEY,
 14    kind TEXT
 15) STRICT;
 16CREATE TABLE item_path(
 17    item_id INTEGER PRIMARY KEY,
 18    path BLOB
 19) STRICT;
 20CREATE TABLE item_query(
 21    item_id INTEGER PRIMARY KEY,
 22    query TEXT
 23) STRICT;
 24";
 25
 26#[derive(PartialEq, Eq, Hash, Debug)]
 27pub enum SerializedItemKind {
 28    Editor,
 29    Terminal,
 30    ProjectSearch,
 31    Diagnostics,
 32}
 33
 34#[derive(Clone, Debug, PartialEq, Eq)]
 35pub enum SerializedItem {
 36    Editor(usize, PathBuf),
 37    Terminal(usize),
 38    ProjectSearch(usize, String),
 39    Diagnostics(usize),
 40}
 41
 42impl FromSql for SerializedItemKind {
 43    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
 44        match value {
 45            ValueRef::Null => Err(FromSqlError::InvalidType),
 46            ValueRef::Integer(_) => Err(FromSqlError::InvalidType),
 47            ValueRef::Real(_) => Err(FromSqlError::InvalidType),
 48            ValueRef::Text(bytes) => {
 49                let str = std::str::from_utf8(bytes).map_err(|_| FromSqlError::InvalidType)?;
 50                match str {
 51                    "Editor" => Ok(SerializedItemKind::Editor),
 52                    "Terminal" => Ok(SerializedItemKind::Terminal),
 53                    "ProjectSearch" => Ok(SerializedItemKind::ProjectSearch),
 54                    "Diagnostics" => Ok(SerializedItemKind::Diagnostics),
 55                    _ => Err(FromSqlError::InvalidType),
 56                }
 57            }
 58            ValueRef::Blob(_) => Err(FromSqlError::InvalidType),
 59        }
 60    }
 61}
 62
 63impl SerializedItem {
 64    fn kind(&self) -> SerializedItemKind {
 65        match self {
 66            SerializedItem::Editor(_, _) => SerializedItemKind::Editor,
 67            SerializedItem::Terminal(_) => SerializedItemKind::Terminal,
 68            SerializedItem::ProjectSearch(_, _) => SerializedItemKind::ProjectSearch,
 69            SerializedItem::Diagnostics(_) => SerializedItemKind::Diagnostics,
 70        }
 71    }
 72
 73    fn id(&self) -> usize {
 74        match self {
 75            SerializedItem::Editor(id, _)
 76            | SerializedItem::Terminal(id)
 77            | SerializedItem::ProjectSearch(id, _)
 78            | SerializedItem::Diagnostics(id) => *id,
 79        }
 80    }
 81}
 82
 83impl Db {
 84    fn write_item(&self, serialized_item: SerializedItem) -> Result<()> {
 85        let mut lock = self.connection.lock();
 86        let tx = lock.transaction()?;
 87
 88        // Serialize the item
 89        let id = serialized_item.id();
 90        {
 91            let kind = format!("{:?}", serialized_item.kind());
 92
 93            let mut stmt =
 94                tx.prepare_cached("INSERT OR REPLACE INTO items(id, kind) VALUES ((?), (?))")?;
 95
 96            stmt.execute(params![id, kind])?;
 97        }
 98
 99        // Serialize item data
100        match &serialized_item {
101            SerializedItem::Editor(_, path) => {
102                let mut stmt = tx.prepare_cached(
103                    "INSERT OR REPLACE INTO item_path(item_id, path) VALUES ((?), (?))",
104                )?;
105
106                let path_bytes = path.as_os_str().as_bytes();
107                stmt.execute(params![id, path_bytes])?;
108            }
109            SerializedItem::ProjectSearch(_, query) => {
110                let mut stmt = tx.prepare_cached(
111                    "INSERT OR REPLACE INTO item_query(item_id, query) VALUES ((?), (?))",
112                )?;
113
114                stmt.execute(params![id, query])?;
115            }
116            _ => {}
117        }
118
119        tx.commit()?;
120
121        Ok(())
122    }
123
124    fn delete_item(&self, item_id: usize) -> Result<()> {
125        let lock = self.connection.lock();
126
127        let mut stmt = lock.prepare_cached(
128            "
129            DELETE FROM items WHERE id = (:id);
130            DELETE FROM item_path WHERE id = (:id);
131            DELETE FROM item_query WHERE id = (:id);
132        ",
133        )?;
134
135        stmt.execute(named_params! {":id": item_id})?;
136
137        Ok(())
138    }
139
140    fn take_items(&self) -> Result<Vec<SerializedItem>> {
141        let mut lock = self.connection.lock();
142
143        let tx = lock.transaction()?;
144
145        // When working with transactions in rusqlite, need to make this kind of scope
146        // To make the borrow stuff work correctly. Don't know why, rust is wild.
147        let result = {
148            let mut read_stmt = tx.prepare_cached(
149                "
150                    SELECT items.id, items.kind, item_path.path, item_query.query
151                    FROM items
152                    LEFT JOIN item_path
153                        ON items.id = item_path.item_id
154                    LEFT JOIN item_query
155                        ON items.id = item_query.item_id
156                    ORDER BY items.id
157            ",
158            )?;
159
160            let result = read_stmt
161                .query_map([], |row| {
162                    let id: usize = row.get(0)?;
163                    let kind: SerializedItemKind = row.get(1)?;
164
165                    match kind {
166                        SerializedItemKind::Editor => {
167                            let buf: Vec<u8> = row.get(2)?;
168                            let path: PathBuf = OsStr::from_bytes(&buf).into();
169
170                            Ok(SerializedItem::Editor(id, path))
171                        }
172                        SerializedItemKind::Terminal => Ok(SerializedItem::Terminal(id)),
173                        SerializedItemKind::ProjectSearch => {
174                            let query: Arc<str> = row.get(3)?;
175                            Ok(SerializedItem::ProjectSearch(id, query.to_string()))
176                        }
177                        SerializedItemKind::Diagnostics => Ok(SerializedItem::Diagnostics(id)),
178                    }
179                })?
180                .collect::<Result<Vec<SerializedItem>, rusqlite::Error>>()?;
181
182            let mut delete_stmt = tx.prepare_cached(
183                "DELETE FROM items;
184                DELETE FROM item_path;
185                DELETE FROM item_query;",
186            )?;
187
188            delete_stmt.execute([])?;
189
190            result
191        };
192
193        tx.commit()?;
194
195        Ok(result)
196    }
197}
198
199#[cfg(test)]
200mod test {
201    use anyhow::Result;
202
203    use super::*;
204
205    #[test]
206    fn test_items_round_trip() -> Result<()> {
207        let db = Db::open_in_memory()?;
208
209        let mut items = vec![
210            SerializedItem::Editor(0, PathBuf::from("/tmp/test.txt")),
211            SerializedItem::Terminal(1),
212            SerializedItem::ProjectSearch(2, "Test query!".to_string()),
213            SerializedItem::Diagnostics(3),
214        ];
215
216        for item in items.iter() {
217            db.write_item(item.clone())?;
218        }
219
220        assert_eq!(items, db.take_items()?);
221
222        // Check that it's empty, as expected
223        assert_eq!(Vec::<SerializedItem>::new(), db.take_items()?);
224
225        for item in items.iter() {
226            db.write_item(item.clone())?;
227        }
228
229        items.remove(2);
230        db.delete_item(2)?;
231
232        assert_eq!(items, db.take_items()?);
233
234        Ok(())
235    }
236}