1use std::collections::HashMap;
28use std::path::Path;
29
30use anyhow::{Context, Result};
31use rusqlite::{Connection, OptionalExtension};
32
33pub struct BbtDb {
35 conn: Connection,
36}
37
38impl BbtDb {
39 pub fn open(path: &Path) -> Result<Self> {
41 let uri = format!("file:{}?mode=ro", path.display());
42 let conn = Connection::open_with_flags(
43 &uri,
44 rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_URI,
45 )
46 .with_context(|| format!("Failed to open BBT database: {}", path.display()))?;
47 Ok(Self { conn })
48 }
49
50 pub fn citekey_for_item_key(&self, item_key: &str) -> Result<Option<String>> {
52 let mut stmt = self
53 .conn
54 .prepare_cached("SELECT citationKey FROM citationkey WHERE itemKey = ?1")?;
55 let result = stmt
56 .query_row([item_key], |row| row.get::<_, String>(0))
57 .optional()?;
58 Ok(result)
59 }
60
61 pub fn item_key_for_citekey(&self, citekey: &str) -> Result<Option<String>> {
63 let mut stmt = self
64 .conn
65 .prepare_cached("SELECT itemKey FROM citationkey WHERE citationKey = ?1")?;
66 let result = stmt
67 .query_row([citekey], |row| row.get::<_, String>(0))
68 .optional()?;
69 Ok(result)
70 }
71
72 pub fn all_citekeys(&self) -> Result<HashMap<String, String>> {
77 let mut stmt = self.conn.prepare(
78 "SELECT citationKey, itemKey FROM citationkey WHERE citationKey IS NOT NULL",
79 )?;
80 let rows = stmt.query_map([], |row| {
81 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
82 })?;
83 let mut map = HashMap::new();
84 for row in rows {
85 let (ck, ik) = row?;
86 map.insert(ck, ik);
87 }
88 Ok(map)
89 }
90}
91
92pub fn citekey_from_zotero_sqlite(conn: &Connection, item_key: &str) -> Result<Option<String>> {
102 let mut stmt = conn.prepare_cached(
103 "SELECT iv.value FROM items i
104 JOIN itemData id ON i.itemID = id.itemID
105 JOIN fields f ON id.fieldID = f.fieldID
106 JOIN itemDataValues iv ON id.valueID = iv.valueID
107 WHERE f.fieldName = 'citationKey' AND i.key = ?1",
108 )?;
109 let result = stmt
110 .query_row([item_key], |row| row.get::<_, String>(0))
111 .optional()?;
112 Ok(result)
113}
114
115pub fn item_key_from_zotero_sqlite(conn: &Connection, citekey: &str) -> Result<Option<String>> {
117 let mut stmt = conn.prepare_cached(
118 "SELECT i.key FROM items i
119 JOIN itemData id ON i.itemID = id.itemID
120 JOIN fields f ON id.fieldID = f.fieldID
121 JOIN itemDataValues iv ON id.valueID = iv.valueID
122 WHERE f.fieldName = 'citationKey' AND iv.value = ?1
123 AND i.libraryID = 1
124 AND i.itemID NOT IN (SELECT itemID FROM deletedItems)",
125 )?;
126 let result = stmt
127 .query_row([citekey], |row| row.get::<_, String>(0))
128 .optional()?;
129 Ok(result)
130}
131
132pub fn all_citekeys_from_zotero_sqlite(conn: &Connection) -> Result<HashMap<String, String>> {
134 let mut stmt = conn.prepare(
135 "SELECT iv.value, i.key FROM items i
136 JOIN itemData id ON i.itemID = id.itemID
137 JOIN fields f ON id.fieldID = f.fieldID
138 JOIN itemDataValues iv ON id.valueID = iv.valueID
139 WHERE f.fieldName = 'citationKey'
140 AND i.libraryID = 1
141 AND i.itemID NOT IN (SELECT itemID FROM deletedItems)",
142 )?;
143 let rows = stmt.query_map([], |row| {
144 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
145 })?;
146 let mut map = HashMap::new();
147 for row in rows {
148 let (ck, ik) = row?;
149 map.insert(ck, ik);
150 }
151 Ok(map)
152}
153
154#[cfg(test)]
157mod tests {
158 use super::*;
159 use rusqlite::Connection;
160
161 fn test_bbt_db() -> BbtDb {
163 let conn = Connection::open_in_memory().unwrap();
164 conn.execute_batch(
165 "CREATE TABLE citationkey (
166 itemID INTEGER,
167 itemKey TEXT,
168 libraryID INTEGER,
169 citationKey TEXT,
170 pinned INTEGER
171 );
172 INSERT INTO citationkey VALUES (1, 'ABC12345', 1, 'demilloHintsTestData1978', 0);
173 INSERT INTO citationkey VALUES (2, 'DEF67890', 1, 'jiaAnalysisSurvey2011', 1);
174 INSERT INTO citationkey VALUES (3, 'GHI11111', 1, NULL, 0);",
175 )
176 .unwrap();
177 BbtDb { conn }
178 }
179
180 #[test]
181 fn citekey_for_item_key_found() {
182 let db = test_bbt_db();
183 let ck = db.citekey_for_item_key("ABC12345").unwrap();
184 assert_eq!(ck, Some("demilloHintsTestData1978".into()));
185 }
186
187 #[test]
188 fn citekey_for_item_key_not_found() {
189 let db = test_bbt_db();
190 let ck = db.citekey_for_item_key("ZZZZZZZZ").unwrap();
191 assert_eq!(ck, None);
192 }
193
194 #[test]
195 fn item_key_for_citekey_found() {
196 let db = test_bbt_db();
197 let ik = db.item_key_for_citekey("jiaAnalysisSurvey2011").unwrap();
198 assert_eq!(ik, Some("DEF67890".into()));
199 }
200
201 #[test]
202 fn item_key_for_citekey_not_found() {
203 let db = test_bbt_db();
204 let ik = db.item_key_for_citekey("nonexistent2099").unwrap();
205 assert_eq!(ik, None);
206 }
207
208 #[test]
209 fn all_citekeys_excludes_null() {
210 let db = test_bbt_db();
211 let map = db.all_citekeys().unwrap();
212 assert_eq!(map.len(), 2); assert_eq!(map["demilloHintsTestData1978"], "ABC12345");
214 assert_eq!(map["jiaAnalysisSurvey2011"], "DEF67890");
215 }
216}
217
218#[cfg(test)]
219mod zotero_sqlite_tests {
220 use super::*;
221 use crate::test_helpers::test_zotero_db;
222
223 #[test]
224 fn citekey_from_zotero_sqlite_found() {
225 let zdb = test_zotero_db();
226 let ck = citekey_from_zotero_sqlite(zdb.conn(), "ABC12345").unwrap();
227 assert_eq!(ck, Some("demilloHintsTestData1978".into()));
228 }
229
230 #[test]
231 fn citekey_from_zotero_sqlite_not_found() {
232 let zdb = test_zotero_db();
233 let ck = citekey_from_zotero_sqlite(zdb.conn(), "ZZZZZZZZ").unwrap();
234 assert_eq!(ck, None);
235 }
236
237 #[test]
238 fn item_key_from_zotero_sqlite_found() {
239 let zdb = test_zotero_db();
240 let ik = item_key_from_zotero_sqlite(zdb.conn(), "demilloHintsTestData1978").unwrap();
241 assert_eq!(ik, Some("ABC12345".into()));
242 }
243
244 #[test]
245 fn item_key_from_zotero_sqlite_not_found() {
246 let zdb = test_zotero_db();
247 let ik = item_key_from_zotero_sqlite(zdb.conn(), "nonexistent2099").unwrap();
248 assert_eq!(ik, None);
249 }
250
251 #[test]
252 fn all_citekeys_from_zotero_sqlite_returns_all() {
253 let zdb = test_zotero_db();
254 let map = all_citekeys_from_zotero_sqlite(zdb.conn()).unwrap();
255 assert_eq!(map.len(), 2);
256 assert_eq!(map["demilloHintsTestData1978"], "ABC12345");
257 assert_eq!(map["artTesting2020"], "DEF67890");
258 }
259
260 #[test]
261 fn all_citekeys_from_zotero_sqlite_values_are_item_keys() {
262 let zdb = test_zotero_db();
263 let map = all_citekeys_from_zotero_sqlite(zdb.conn()).unwrap();
264 for (ck, ik) in &map {
266 assert!(!ck.is_empty(), "Citekey should not be empty");
267 assert!(!ik.is_empty(), "Item key should not be empty");
268 assert_eq!(ik.len(), 8, "Item key should be 8 chars: {ik}");
269 }
270 }
271}