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}