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}