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 editors_stmt = tx.prepare_cached(
155                        r#"
156                        SELECT items.id, item_path.path
157                        FROM items
158                        LEFT JOIN item_path
159                            ON items.id = item_path.item_id
160                        WHERE items.kind = ?;
161                        "#,
162                    )?;
163
164                    let editors_iter = editors_stmt.query_map(
165                        [SerializedItemKind::Editor.to_string()],
166                        |row| {
167                            let id: usize = row.get(0)?;
168
169                            let buf: Vec<u8> = row.get(1)?;
170                            let path: PathBuf = OsStr::from_bytes(&buf).into();
171
172                            Ok(SerializedItem::Editor(id, path))
173                        },
174                    )?;
175
176                    let mut terminals_stmt = tx.prepare_cached(
177                        r#"
178                        SELECT items.id
179                        FROM items
180                        WHERE items.kind = ?;
181                        "#,
182                    )?;
183                    let terminals_iter = terminals_stmt.query_map(
184                        [SerializedItemKind::Terminal.to_string()],
185                        |row| {
186                            let id: usize = row.get(0)?;
187
188                            Ok(SerializedItem::Terminal(id))
189                        },
190                    )?;
191
192                    let mut search_stmt = tx.prepare_cached(
193                        r#"
194                        SELECT items.id, item_query.query
195                        FROM items
196                        LEFT JOIN item_query
197                            ON items.id = item_query.item_id
198                        WHERE items.kind = ?;
199                        "#,
200                    )?;
201                    let searches_iter = search_stmt.query_map(
202                        [SerializedItemKind::ProjectSearch.to_string()],
203                        |row| {
204                            let id: usize = row.get(0)?;
205                            let query = row.get(1)?;
206
207                            Ok(SerializedItem::ProjectSearch(id, query))
208                        },
209                    )?;
210
211                    #[cfg(debug_assertions)]
212                    let tmp =
213                        searches_iter.collect::<Vec<Result<SerializedItem, rusqlite::Error>>>();
214                    #[cfg(debug_assertions)]
215                    debug_assert!(tmp.len() == 0 || tmp.len() == 1);
216                    #[cfg(debug_assertions)]
217                    let searches_iter = tmp.into_iter();
218
219                    let mut diagnostic_stmt = tx.prepare_cached(
220                        r#"
221                        SELECT items.id
222                        FROM items
223                        WHERE items.kind = ?;
224                        "#,
225                    )?;
226
227                    let diagnostics_iter = diagnostic_stmt.query_map(
228                        [SerializedItemKind::Diagnostics.to_string()],
229                        |row| {
230                            let id: usize = row.get(0)?;
231
232                            Ok(SerializedItem::Diagnostics(id))
233                        },
234                    )?;
235
236                    #[cfg(debug_assertions)]
237                    let tmp =
238                        diagnostics_iter.collect::<Vec<Result<SerializedItem, rusqlite::Error>>>();
239                    #[cfg(debug_assertions)]
240                    debug_assert!(tmp.len() == 0 || tmp.len() == 1);
241                    #[cfg(debug_assertions)]
242                    let diagnostics_iter = tmp.into_iter();
243
244                    let res = editors_iter
245                        .chain(terminals_iter)
246                        .chain(diagnostics_iter)
247                        .chain(searches_iter)
248                        .collect::<Result<HashSet<SerializedItem>, rusqlite::Error>>()?;
249
250                    let mut delete_stmt = tx.prepare_cached(
251                        r#"
252                        DELETE FROM items;
253                        DELETE FROM item_path;
254                        DELETE FROM item_query;
255                        "#,
256                    )?;
257
258                    delete_stmt.execute([])?;
259
260                    res
261                };
262
263                tx.commit()?;
264
265                Ok(result)
266            })
267            .unwrap_or(Ok(HashSet::default()))
268    }
269}
270
271#[cfg(test)]
272mod test {
273    use anyhow::Result;
274
275    use super::*;
276
277    #[test]
278    fn test_items_round_trip() -> Result<()> {
279        let db = Db::open_in_memory();
280
281        let mut items = vec![
282            SerializedItem::Editor(0, PathBuf::from("/tmp/test.txt")),
283            SerializedItem::Terminal(1),
284            SerializedItem::ProjectSearch(2, "Test query!".to_string()),
285            SerializedItem::Diagnostics(3),
286        ]
287        .into_iter()
288        .collect::<HashSet<_>>();
289
290        for item in items.iter() {
291            dbg!("Inserting... ");
292            db.write_item(item.clone())?;
293        }
294
295        assert_eq!(items, db.take_items()?);
296
297        // Check that it's empty, as expected
298        assert_eq!(HashSet::default(), db.take_items()?);
299
300        for item in items.iter() {
301            db.write_item(item.clone())?;
302        }
303
304        items.remove(&SerializedItem::ProjectSearch(2, "Test query!".to_string()));
305        db.delete_item(2)?;
306
307        assert_eq!(items, db.take_items()?);
308
309        Ok(())
310    }
311}