1use crate::model::{DbRecord, Schema};
2use deadpool_sqlite::rusqlite;
3use deadpool_sqlite::rusqlite::Row;
4use deadpool_sqlite::rusqlite::types::Value;
5use deadpool_sqlite::{Config, Pool, Runtime};
6use std::path::Path;
7use std::rc::Rc;
8use thiserror::Error;
9use tracing::instrument;
10
11const MAX_BIND_PARAMS: usize = 32766;
12
13#[derive(Debug, Error)]
15#[error(transparent)]
16pub struct DatabaseError(DatabaseErrorRepr);
17
18impl<E> From<E> for DatabaseError
22where
23 E: Into<DatabaseErrorRepr>,
24{
25 fn from(error: E) -> Self {
26 Self(error.into())
27 }
28}
29
30#[derive(Error, Debug)]
32enum DatabaseErrorRepr {
33 #[error("error creating the DB pool: {0}")]
34 CreatePool(#[from] deadpool_sqlite::CreatePoolError),
35
36 #[error("error interacting with the DB pool: {0}")]
39 PoolInteract(String),
40
41 #[error("error getting connection from the DB pool: {0}")]
42 Pool(#[from] deadpool_sqlite::PoolError),
43
44 #[error("error in rusqlite: {0}")]
45 Rusqlite(#[from] rusqlite::Error),
46
47 #[error(
48 "The caller passed too many parameters to bind (got {0}, max allowed is {MAX_BIND_PARAMS})"
49 )]
50 TooManyBindParameters(usize),
51}
52
53impl From<deadpool_sqlite::InteractError> for DatabaseErrorRepr {
54 fn from(error: deadpool_sqlite::InteractError) -> Self {
55 Self::PoolInteract(error.to_string())
56 }
57}
58
59pub type Result<T> = std::result::Result<T, DatabaseError>;
60
61impl TryFrom<&Row<'_>> for DbRecord {
62 type Error = rusqlite::Error;
63
64 fn try_from(row: &Row) -> std::result::Result<Self, Self::Error> {
65 Ok(Self {
66 id: row.get(0)?,
67 url: row.get(1)?,
68 client_datetime: row.get(2)?,
69 client_datetime_raw: row.get(3)?,
70 timestamp_flora: row.get(4)?,
71 title: row.get(5)?,
72 language: row.get(6)?,
73 web_archive_serialized: row.get(7)?,
74 })
75 }
76}
77
78#[derive(Debug, Clone)]
80pub struct Database {
81 pool: Pool,
82}
83
84impl Database {
85 #[instrument(skip(path))]
90 async fn new(path: impl AsRef<Path>) -> Result<Self> {
91 let cfg = Config::new(path.as_ref());
92 let pool = cfg.create_pool(Runtime::Tokio1)?;
93
94 let conn = pool.get().await?;
96 conn.interact(|conn| rusqlite::vtab::array::load_module(conn))
97 .await??;
98
99 Ok(Self { pool })
100 }
101
102 #[instrument(skip(self))]
103 async fn create_table(&self, schema: &Schema) -> Result<()> {
104 let create_table_query = format!("CREATE TABLE IF NOT EXISTS {schema};");
105
106 let conn = self.pool.get().await?;
107
108 let _rows: usize = conn
109 .interact(move |conn| {
110 let mut stmt = conn.prepare(&create_table_query)?;
111 stmt.execute(())
112 })
113 .await??;
114
115 Ok(())
116 }
117
118 #[instrument(skip(path))]
123 pub async fn new_with_records_table(path: impl AsRef<Path>) -> Result<Self> {
124 let db = Self::new(path).await?;
125 db.create_table(&DbRecord::schema()).await?;
126 Ok(db)
127 }
128
129 #[instrument]
134 pub async fn count(&self) -> Result<i64> {
135 let query = r"SELECT count(*) FROM records;";
136 let conn = self.pool.get().await?;
137
138 let record_count = conn
139 .interact(move |conn| {
140 let mut stmt = conn.prepare(query)?;
141 stmt.query_one([], |row| row.get(0))
142 })
143 .await??;
144
145 Ok(record_count)
146 }
147
148 pub async fn insert(&self, record: DbRecord) -> Result<i64> {
155 let query = r"INSERT INTO records
156 VALUES (:id, :url, :client_datetime, :client_datetime_raw, :timestamp_flora, :title,
157 :language, :web_archive_serialized)";
158 let conn = self.pool.get().await?;
159
160 let row_id = conn
161 .interact(move |conn| {
162 let mut stmt = conn.prepare(query)?;
163 stmt.insert(rusqlite::named_params! {
164 ":id": record.id,
165 ":url": record.url,
166 ":client_datetime": record.client_datetime,
167 ":client_datetime_raw": record.client_datetime_raw,
168 ":timestamp_flora": record.timestamp_flora,
169 ":title": record.title,
170 ":language": record.language,
171 ":web_archive_serialized": record.web_archive_serialized,
172 })
173 })
174 .await??;
175
176 Ok(row_id)
177 }
178
179 #[instrument(skip(self))]
184 pub async fn delete(&self, id: i64) -> Result<usize> {
185 const QUERY: &str = r"DELETE FROM records WHERE id = ?1;";
186
187 let conn = self.pool.get().await?;
188 let rows_changed = conn
189 .interact(move |conn| {
190 let mut stmt = conn.prepare(QUERY)?;
191 stmt.execute([id])
192 })
193 .await??;
194
195 Ok(rows_changed)
196 }
197
198 pub async fn get_record(&self, id: i64) -> Result<DbRecord> {
203 let query = r"SELECT id, url, client_datetime, client_datetime_raw, timestamp_flora, title,
204 language, web_archive_serialized
205 FROM records WHERE id = ?1;";
206
207 let conn = self.pool.get().await?;
208 let result = conn
209 .interact(move |conn| {
210 let mut stmt = conn.prepare(query)?;
211 stmt.query_row([id], |row| {
212 let record = row.try_into()?;
213 Ok(record)
214 })
215 })
216 .await??;
217
218 Ok(result)
219 }
220
221 #[instrument]
226 pub async fn get_many_records(&self, ids: &[i64]) -> Result<Vec<DbRecord>> {
227 if ids.len() > MAX_BIND_PARAMS {
228 return Err(DatabaseErrorRepr::TooManyBindParameters(ids.len()).into());
229 }
230
231 tracing::debug!("Getting records.");
232
233 let query = r"SELECT id, url, client_datetime, client_datetime_raw, timestamp_flora, title,
234 language, web_archive_serialized
235 FROM records
236 WHERE id IN rarray(?1);";
237
238 let conn = self
239 .pool
240 .get()
241 .await
242 .inspect_err(|e| tracing::error!("Couldn't get a connection from the pool: {e}"))?;
243
244 let values = ids.iter().copied().map(Value::from).collect::<Vec<Value>>();
245
246 let records = conn
247 .interact(|conn| -> Result<Vec<DbRecord>> {
248 let values = Rc::new(values);
249
250 let mut stmt = conn
251 .prepare(query)
252 .inspect_err(|e| tracing::error!("Couldn't prepare the query: {e}"))?;
253
254 let rows = stmt
255 .query_map([values], |row| DbRecord::try_from(row))
256 .inspect_err(|e| tracing::error!("Couldn't execute the query: {e}"))?;
257
258 let mut records = vec![];
259 for maybe_record in rows {
260 let record = maybe_record
261 .inspect_err(|e| tracing::error!("Couldn't get the mapped record: {e}"))?;
262 records.push(record);
263 }
264
265 Ok(records)
266 })
267 .await
268 .inspect_err(|e| {
269 tracing::error!("Couldn't interact with the database connection: {e}");
270 })??;
271
272 tracing::debug!("Got {} records", records.len());
273
274 Ok(records)
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use crate::model::tests::test_record_params;
282 use anyhow::Result;
283 use chrono::{DateTime, FixedOffset};
284 use omry_archiving::record::Record;
285 use pretty_assertions::assert_eq;
286 use serde::Serialize;
287 use std::fs::File;
288 use std::ops::Deref;
289 use tempfile::NamedTempFile;
290
291 struct TempDb {
294 #[allow(dead_code)]
295 temp_db_file: NamedTempFile<File>,
296 db: Database,
297 }
298
299 impl TempDb {
300 async fn new() -> Result<Self> {
301 let temp_db_file = NamedTempFile::with_suffix(".db")?;
302 let db = Database::new_with_records_table(temp_db_file.path()).await?;
303 Ok(Self { temp_db_file, db })
304 }
305 }
306
307 impl Deref for TempDb {
308 type Target = Database;
309
310 fn deref(&self) -> &Self::Target {
311 &self.db
312 }
313 }
314
315 #[derive(Debug, Serialize)]
317 struct DbRecordDef {
318 id: Option<i64>,
319 url: String,
320 client_datetime: DateTime<FixedOffset>,
321 client_datetime_raw: Option<String>,
322 timestamp_flora: i64,
323 title: String,
324 language: Option<String>,
325 web_archive_serialized: Vec<u8>,
326 }
327
328 impl From<DbRecord> for DbRecordDef {
329 fn from(value: DbRecord) -> DbRecordDef {
330 let DbRecord {
331 id,
332 url,
333 client_datetime,
334 client_datetime_raw,
335 timestamp_flora,
336 title,
337 language,
338 web_archive_serialized,
339 } = value;
340
341 Self {
342 id,
343 url,
344 client_datetime,
345 client_datetime_raw,
346 timestamp_flora,
347 title,
348 language,
349 web_archive_serialized,
350 }
351 }
352 }
353
354 fn test_db_record() -> Result<DbRecord> {
355 let record = Record::new(test_record_params()?);
356 Ok(record.try_into()?)
357 }
358
359 #[tokio::test]
360 async fn can_create_table() -> Result<()> {
361 let db = TempDb::new().await?;
362 assert!(db.create_table(&DbRecord::schema()).await.is_ok());
363 Ok(())
365 }
366
367 #[tokio::test]
368 async fn can_add_record() -> Result<()> {
369 let db = TempDb::new().await?;
370 let record = test_db_record()?;
371 let result = db.insert(record).await?;
372 assert_eq!(result, 1);
373 Ok(())
374 }
375
376 #[tokio::test]
377 async fn can_get_record() -> Result<()> {
378 let db = TempDb::new().await?;
379 let record = test_db_record()?;
380 assert!(record.id.is_none());
381 db.insert(record).await?;
382
383 let expected_id = 1i64;
384 let retrieved_record = db.get_record(expected_id).await?;
385
386 let retrieved_record: DbRecordDef = retrieved_record.into();
387 insta::assert_ron_snapshot!(retrieved_record, {
388 ".client_datetime" => "[client_datetime]",
389 ".timestamp_flora" => "[timestamp]"
390 });
391
392 Ok(())
393 }
394
395 #[tokio::test]
396 async fn can_get_many_records() -> Result<()> {
397 let db = TempDb::new().await?;
398
399 let num_records_to_insert = 7;
400 let mut record_ids = Vec::with_capacity(num_records_to_insert);
401
402 for _ in 0..num_records_to_insert {
403 let record = test_db_record()?;
404 let record_id = db.insert(record).await?;
405 record_ids.push(record_id);
406 }
407
408 let selected_records = db
409 .get_many_records(&record_ids)
410 .await?
411 .into_iter()
412 .map(DbRecordDef::from)
413 .collect::<Vec<_>>();
414
415 assert_eq!(selected_records.len(), num_records_to_insert);
416
417 insta::assert_ron_snapshot!(selected_records, {
418 "[].client_datetime" => "[client_datetime]",
419 "[].timestamp_flora" => "[timestamp]"
420 });
421
422 Ok(())
423 }
424
425 #[tokio::test]
426 async fn passing_too_many_bind_parameters_returns_error() -> Result<()> {
427 let db = TempDb::new().await?;
428
429 let num_records_to_insert = MAX_BIND_PARAMS + 1;
430 db.insert(test_db_record()?).await?;
431
432 let mut ids = Vec::with_capacity(num_records_to_insert);
433 for index in 0..num_records_to_insert {
434 ids.push(i64::try_from(index + 1)?);
435 }
436
437 let result = db.get_many_records(&ids).await;
438 assert!(result.is_err());
439
440 #[allow(clippy::unwrap_used)]
442 let error = result.unwrap_err();
443 insta::assert_snapshot!(error);
444
445 Ok(())
446 }
447
448 #[tokio::test]
449 async fn can_get_count() -> Result<()> {
450 let db = TempDb::new().await?;
451
452 let num_records_to_insert = 7;
453 let mut record_ids = Vec::with_capacity(num_records_to_insert);
454
455 for _ in 0..num_records_to_insert {
456 let record = test_db_record()?;
457 let record_id = db.insert(record).await?;
458 record_ids.push(record_id);
459 }
460
461 let count: usize = db.count().await?.try_into()?;
462
463 assert_eq!(count, num_records_to_insert);
464 Ok(())
465 }
466
467 #[tokio::test]
468 async fn can_delete_record() -> Result<()> {
469 let db = TempDb::new().await?;
470 let record = test_db_record()?;
471 let id = db.insert(record).await?;
472 let rows_changed = db.delete(id).await?;
473 assert_eq!(rows_changed, 1);
474 Ok(())
475 }
476}