items.rs

  1use std::{ffi::OsStr, fmt::Display, hash::Hash, os::unix::prelude::OsStrExt, path::PathBuf};
  2
  3use anyhow::Result;
  4use collections::HashSet;
  5use rusqlite::{named_params, params};
  6
  7use super::Db;
  8
  9pub(crate) const ITEMS_M_1: &str = "
 10CREATE TABLE items(
 11    id INTEGER PRIMARY KEY,
 12    kind TEXT
 13) STRICT;
 14CREATE TABLE item_path(
 15    item_id INTEGER PRIMARY KEY,
 16    path BLOB
 17) STRICT;
 18CREATE TABLE item_query(
 19    item_id INTEGER PRIMARY KEY,
 20    query TEXT
 21) STRICT;
 22";
 23
 24#[derive(PartialEq, Eq, Hash, Debug)]
 25pub enum SerializedItemKind {
 26    Editor,
 27    Terminal,
 28    ProjectSearch,
 29    Diagnostics,
 30}
 31
 32impl Display for SerializedItemKind {
 33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 34        f.write_str(&format!("{:?}", self))
 35    }
 36}
 37
 38#[derive(Clone, Debug, PartialEq, Eq, Hash)]
 39pub enum SerializedItem {
 40    Editor(usize, PathBuf),
 41    Terminal(usize),
 42    ProjectSearch(usize, String),
 43    Diagnostics(usize),
 44}
 45
 46impl SerializedItem {
 47    fn kind(&self) -> SerializedItemKind {
 48        match self {
 49            SerializedItem::Editor(_, _) => SerializedItemKind::Editor,
 50            SerializedItem::Terminal(_) => SerializedItemKind::Terminal,
 51            SerializedItem::ProjectSearch(_, _) => SerializedItemKind::ProjectSearch,
 52            SerializedItem::Diagnostics(_) => SerializedItemKind::Diagnostics,
 53        }
 54    }
 55
 56    fn id(&self) -> usize {
 57        match self {
 58            SerializedItem::Editor(id, _)
 59            | SerializedItem::Terminal(id)
 60            | SerializedItem::ProjectSearch(id, _)
 61            | SerializedItem::Diagnostics(id) => *id,
 62        }
 63    }
 64}
 65
 66impl Db {
 67    fn write_item(&self, serialized_item: SerializedItem) -> Result<()> {
 68        self.real()
 69            .map(|db| {
 70                let mut lock = db.connection.lock();
 71                let tx = lock.transaction()?;
 72
 73                // Serialize the item
 74                let id = serialized_item.id();
 75                {
 76                    let mut stmt = tx.prepare_cached(
 77                        "INSERT OR REPLACE INTO items(id, kind) VALUES ((?), (?))",
 78                    )?;
 79
 80                    dbg!("inserting item");
 81                    stmt.execute(params![id, serialized_item.kind().to_string()])?;
 82                }
 83
 84                // Serialize item data
 85                match &serialized_item {
 86                    SerializedItem::Editor(_, path) => {
 87                        dbg!("inserting path");
 88                        let mut stmt = tx.prepare_cached(
 89                            "INSERT OR REPLACE INTO item_path(item_id, path) VALUES ((?), (?))",
 90                        )?;
 91
 92                        let path_bytes = path.as_os_str().as_bytes();
 93                        stmt.execute(params![id, path_bytes])?;
 94                    }
 95                    SerializedItem::ProjectSearch(_, query) => {
 96                        dbg!("inserting query");
 97                        let mut stmt = tx.prepare_cached(
 98                            "INSERT OR REPLACE INTO item_query(item_id, query) VALUES ((?), (?))",
 99                        )?;
100
101                        stmt.execute(params![id, query])?;
102                    }
103                    _ => {}
104                }
105
106                tx.commit()?;
107
108                let mut stmt = lock.prepare_cached("SELECT id, kind FROM items")?;
109                let _ = stmt
110                    .query_map([], |row| {
111                        let zero: usize = row.get(0)?;
112                        let one: String = row.get(1)?;
113
114                        dbg!(zero, one);
115                        Ok(())
116                    })?
117                    .collect::<Vec<Result<(), _>>>();
118
119                Ok(())
120            })
121            .unwrap_or(Ok(()))
122    }
123
124    fn delete_item(&self, item_id: usize) -> Result<()> {
125        self.real()
126            .map(|db| {
127                let lock = db.connection.lock();
128
129                let mut stmt = lock.prepare_cached(
130                    r#"
131                    DELETE FROM items WHERE id = (:id);
132                    DELETE FROM item_path WHERE id = (:id);
133                    DELETE FROM item_query WHERE id = (:id);
134                    "#,
135                )?;
136
137                stmt.execute(named_params! {":id": item_id})?;
138
139                Ok(())
140            })
141            .unwrap_or(Ok(()))
142    }
143
144    fn take_items(&self) -> Result<HashSet<SerializedItem>> {
145        self.real()
146            .map(|db| {
147                let mut lock = db.connection.lock();
148
149                let tx = lock.transaction()?;
150
151        // When working with transactions in rusqlite, need to make this kind of scope
152        // To make the borrow stuff work correctly. Don't know why, rust is wild.
153        let result = {
154            let mut read_editors = tx
155                .prepare_cached(
156                    r#"
157                        SELECT items.id, item_path.path
158                        FROM items
159                        LEFT JOIN item_path
160	            ON items.id = item_path.item_id
161                    WHERE items.kind = "Editor";
162                "#r,
163                )?
164                .query_map([], |row| {
165                    let buf: Vec<u8> = row.get(2)?;
166                    let path: PathBuf = OsStr::from_bytes(&buf).into();
167
168                    Ok(SerializedItem::Editor(id, path))
169                })?;
170
171            let mut read_stmt = tx.prepare_cached(
172                "
173                    SELECT items.id, items.kind, item_path.path, item_query.query
174                    FROM items
175                    LEFT JOIN item_path
176                        ON items.id = item_path.item_id
177                    LEFT JOIN item_query
178                        ON items.id = item_query.item_id
179                    WHERE 
180                    ORDER BY items.id;
181            ",
182            )?;
183
184                    let editors_iter = editors_stmt.query_map(
185                        [SerializedItemKind::Editor.to_string()],
186                        |row| {
187                            let id: usize = row.get(0)?;
188
189                            let buf: Vec<u8> = row.get(1)?;
190                            let path: PathBuf = OsStr::from_bytes(&buf).into();
191
192                            Ok(SerializedItem::Editor(id, path))
193                        },
194                    )?;
195
196                    let mut terminals_stmt = tx.prepare_cached(
197                        r#"
198                        SELECT items.id
199                        FROM items
200                        WHERE items.kind = ?;
201                        "#,
202                    )?;
203                    let terminals_iter = terminals_stmt.query_map(
204                        [SerializedItemKind::Terminal.to_string()],
205                        |row| {
206                            let id: usize = row.get(0)?;
207
208                            Ok(SerializedItem::Terminal(id))
209                        },
210                    )?;
211
212                    let mut search_stmt = tx.prepare_cached(
213                        r#"
214                        SELECT items.id, item_query.query
215                        FROM items
216                        LEFT JOIN item_query
217                            ON items.id = item_query.item_id
218                        WHERE items.kind = ?;
219                        "#,
220                    )?;
221                    let searches_iter = search_stmt.query_map(
222                        [SerializedItemKind::ProjectSearch.to_string()],
223                        |row| {
224                            let id: usize = row.get(0)?;
225                            let query = row.get(1)?;
226
227                            Ok(SerializedItem::ProjectSearch(id, query))
228                        },
229                    )?;
230
231                    #[cfg(debug_assertions)]
232                    let tmp =
233                        searches_iter.collect::<Vec<Result<SerializedItem, rusqlite::Error>>>();
234                    #[cfg(debug_assertions)]
235                    debug_assert!(tmp.len() == 0 || tmp.len() == 1);
236                    #[cfg(debug_assertions)]
237                    let searches_iter = tmp.into_iter();
238
239                    let mut diagnostic_stmt = tx.prepare_cached(
240                        r#"
241                        SELECT items.id
242                        FROM items
243                        WHERE items.kind = ?;
244                        "#,
245                    )?;
246
247                    let diagnostics_iter = diagnostic_stmt.query_map(
248                        [SerializedItemKind::Diagnostics.to_string()],
249                        |row| {
250                            let id: usize = row.get(0)?;
251
252                            Ok(SerializedItem::Diagnostics(id))
253                        },
254                    )?;
255
256                    #[cfg(debug_assertions)]
257                    let tmp =
258                        diagnostics_iter.collect::<Vec<Result<SerializedItem, rusqlite::Error>>>();
259                    #[cfg(debug_assertions)]
260                    debug_assert!(tmp.len() == 0 || tmp.len() == 1);
261                    #[cfg(debug_assertions)]
262                    let diagnostics_iter = tmp.into_iter();
263
264                    let res = editors_iter
265                        .chain(terminals_iter)
266                        .chain(diagnostics_iter)
267                        .chain(searches_iter)
268                        .collect::<Result<HashSet<SerializedItem>, rusqlite::Error>>()?;
269
270                    let mut delete_stmt = tx.prepare_cached(
271                        r#"
272                        DELETE FROM items;
273                        DELETE FROM item_path;
274                        DELETE FROM item_query;
275                        "#,
276                    )?;
277
278                    delete_stmt.execute([])?;
279
280                    res
281                };
282
283                tx.commit()?;
284
285                Ok(result)
286            })
287            .unwrap_or(Ok(HashSet::default()))
288    }
289}
290
291#[cfg(test)]
292mod test {
293    use anyhow::Result;
294
295    use super::*;
296
297    #[test]
298    fn test_items_round_trip() -> Result<()> {
299        let db = Db::open_in_memory();
300
301        let mut items = vec![
302            SerializedItem::Editor(0, PathBuf::from("/tmp/test.txt")),
303            SerializedItem::Terminal(1),
304            SerializedItem::ProjectSearch(2, "Test query!".to_string()),
305            SerializedItem::Diagnostics(3),
306        ]
307        .into_iter()
308        .collect::<HashSet<_>>();
309
310        for item in items.iter() {
311            dbg!("Inserting... ");
312            db.write_item(item.clone())?;
313        }
314
315        assert_eq!(items, db.take_items()?);
316
317        // Check that it's empty, as expected
318        assert_eq!(HashSet::default(), db.take_items()?);
319
320        for item in items.iter() {
321            db.write_item(item.clone())?;
322        }
323
324        items.remove(&SerializedItem::ProjectSearch(2, "Test query!".to_string()));
325        db.delete_item(2)?;
326
327        assert_eq!(items, db.take_items()?);
328
329        Ok(())
330    }
331}