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}