1#![cfg(feature = "connection")]
3
4use crate::model::{DbRecord, Schema};
5use deadpool_sqlite::rusqlite;
6use deadpool_sqlite::rusqlite::Row;
7use deadpool_sqlite::rusqlite::types::Value;
8use deadpool_sqlite::{Config, Object, Pool, Runtime};
9use std::path::Path;
10use std::rc::Rc;
11use thiserror::Error;
12use tracing::instrument;
13
14const MAX_BIND_PARAMS: usize = 32766;
17
18macro_rules! try_query_db_records {
20 ($stmt:ident, $values:expr) => {{
21 let rows = $stmt
22 .query_map($values, |row| DbRecord::try_from(row))
23 .inspect_err(|e| tracing::error!("Couldn't execute the query: {e}"))?;
24
25 let mut records = vec![];
26 for maybe_record in rows {
27 let record = maybe_record
28 .inspect_err(|e| tracing::error!("Couldn't get the mapped record: {e}"))?;
29 records.push(record);
30 }
31
32 records
33 }};
34}
35
36#[derive(Debug, Error)]
38#[error(transparent)]
39pub struct DatabaseError(DbErrorRepr);
40
41impl<E> From<E> for DatabaseError
45where
46 E: Into<DbErrorRepr>,
47{
48 fn from(error: E) -> Self {
49 Self(error.into())
50 }
51}
52
53#[derive(Error, Debug)]
55enum DbErrorRepr {
56 #[error("error creating the DB pool: {0}")]
58 CreatePool(#[from] deadpool_sqlite::CreatePoolError),
59
60 #[error("error interacting with the DB pool: {0}")]
64 PoolInteract(String),
65
66 #[error("error getting connection from the DB pool: {0}")]
68 Pool(#[from] deadpool_sqlite::PoolError),
69
70 #[error("error in rusqlite: {0}")]
72 Rusqlite(#[from] rusqlite::Error),
73
74 #[error(
77 "The caller passed too many parameters to bind (got {0}, max allowed is {MAX_BIND_PARAMS})"
78 )]
79 TooManyBindParameters(usize),
80
81 #[error("Couldn't activate the WAL; journaling mode is {0}")]
83 WalActivation(String),
84}
85
86impl From<deadpool_sqlite::InteractError> for DbErrorRepr {
87 fn from(error: deadpool_sqlite::InteractError) -> Self {
88 Self::PoolInteract(error.to_string())
89 }
90}
91
92pub type Result<T> = std::result::Result<T, DatabaseError>;
94
95impl TryFrom<&Row<'_>> for DbRecord {
96 type Error = rusqlite::Error;
97
98 fn try_from(row: &Row) -> std::result::Result<Self, Self::Error> {
99 Ok(Self {
100 id: row.get(0)?,
101 url: row.get(1)?,
102 client_datetime: row.get(2)?,
103 client_datetime_raw: row.get(3)?,
104 timestamp_flora: row.get(4)?,
105 title: row.get(5)?,
106 language: row.get(6)?,
107 web_archive_serialized: row.get(7)?,
108 })
109 }
110}
111
112#[derive(Debug, Clone)]
114pub struct Database {
115 pool: Pool,
117}
118
119impl Database {
120 #[instrument(skip(path))]
125 async fn new(path: impl AsRef<Path>) -> Result<Self> {
126 let cfg = Config::new(path.as_ref());
127 let pool = cfg.create_pool(Runtime::Tokio1)?;
128 Ok(Self { pool })
129 }
130
131 #[instrument(skip(self))]
138 async fn get_conn_array(&self) -> Result<Object> {
139 let conn = self.pool.get().await?;
140 conn.interact(|conn| {
141 tracing::debug!("Loading rusqlite::vtab::array");
142 rusqlite::vtab::array::load_module(conn)
143 })
144 .await?
145 .inspect(|()| tracing::debug!("Loaded rusqlite::vtab::array"))?;
146
147 Ok(conn)
148 }
149
150 #[instrument(skip(self))]
152 async fn create_table(&self, schema: &Schema) -> Result<()> {
153 let create_table_query = format!("CREATE TABLE IF NOT EXISTS {schema};");
154 let conn = self.pool.get().await?;
155 let _rows: usize = conn
156 .interact(move |conn| {
157 let mut stmt = conn.prepare(&create_table_query)?;
158 stmt.execute(())
159 })
160 .await??;
161
162 Ok(())
163 }
164
165 #[instrument(skip(self))]
167 async fn create_indexes(&self, schema: &Schema, columns: &[&str]) -> Result<()> {
168 let table_name = schema.table_name();
169 let conn = self.pool.get().await?;
170
171 for column in columns {
172 let idx_name = format!("idx_{column}");
173 let query =
174 format!("CREATE INDEX IF NOT EXISTS {idx_name} ON {table_name} ({column});");
175 tracing::debug!("Query: {query}");
176
177 tracing::info!("Creating index '{idx_name}' on '{table_name}'");
178 conn.interact(move |conn| {
179 let mut stmt = conn.prepare(&query)?;
180 stmt.execute([])
181 })
182 .await??;
183 }
184
185 Ok(())
186 }
187
188 #[instrument(skip(self))]
196 pub async fn activate_wal(&self) -> Result<()> {
197 const SUCCESS_VALUE: &str = "wal";
198
199 let conn = self.pool.get().await?;
200
201 tracing::debug!("Enabling the WAL.");
202
203 let journal_mode: String = conn
204 .interact(move |conn| {
205 conn.pragma_update_and_check(None, "journal_mode", "WAL", |v| v.get(0))
206 })
207 .await??;
208
209 if journal_mode.as_str() == SUCCESS_VALUE {
210 Ok(())
211 } else {
212 Err(DbErrorRepr::WalActivation(journal_mode).into())
213 }
214 }
215
216 #[instrument(skip(path))]
221 pub async fn new_with_records_table(path: impl AsRef<Path>) -> Result<Self> {
222 let db = Self::new(path).await?;
223 let schema = DbRecord::schema();
224
225 db.activate_wal().await?;
226 db.create_table(&schema).await?;
227 db.create_indexes(&schema, &["url"]).await?;
228
229 Ok(db)
230 }
231
232 #[instrument]
237 pub async fn count(&self) -> Result<i64> {
238 let query = r"SELECT count(*) FROM records;";
239 let conn = self.pool.get().await?;
240
241 let record_count = conn
242 .interact(move |conn| {
243 let mut stmt = conn.prepare(query)?;
244 stmt.query_one([], |row| row.get(0))
245 })
246 .await??;
247
248 Ok(record_count)
249 }
250
251 pub async fn insert(&self, record: DbRecord) -> Result<i64> {
258 let query = r"INSERT INTO records
259 VALUES (:id, :url, :client_datetime, :client_datetime_raw, :timestamp_flora, :title,
260 :language, :web_archive_serialized)
261 RETURNING id;";
262
263 let conn = self.pool.get().await?;
264
265 let row_id = conn
266 .interact(move |conn| {
267 let mut stmt = conn.prepare(query)?;
268 stmt.query_one(
269 rusqlite::named_params! {
270 ":id": record.id,
271 ":url": record.url,
272 ":client_datetime": record.client_datetime,
273 ":client_datetime_raw": record.client_datetime_raw,
274 ":timestamp_flora": record.timestamp_flora,
275 ":title": record.title,
276 ":language": record.language,
277 ":web_archive_serialized": record.web_archive_serialized,
278 },
279 |row| row.get(0),
280 )
281 })
282 .await??;
283
284 Ok(row_id)
285 }
286
287 #[instrument(skip(self))]
292 pub async fn delete(&self, id: i64) -> Result<usize> {
293 const QUERY: &str = r"DELETE FROM records WHERE id = ?1;";
294
295 let conn = self.pool.get().await?;
296 let rows_changed = conn
297 .interact(move |conn| {
298 let mut stmt = conn.prepare(QUERY)?;
299 stmt.execute([id])
300 })
301 .await??;
302
303 Ok(rows_changed)
304 }
305
306 pub async fn get_record(&self, id: i64) -> Result<DbRecord> {
311 let query = r"SELECT id, url, client_datetime, client_datetime_raw, timestamp_flora, title,
312 language, web_archive_serialized
313 FROM records WHERE id = ?1;";
314
315 let conn = self.pool.get().await?;
316 let result = conn
317 .interact(move |conn| {
318 let mut stmt = conn.prepare(query)?;
319 stmt.query_row([id], |row| {
320 let record = row.try_into()?;
321 Ok(record)
322 })
323 })
324 .await??;
325
326 Ok(result)
327 }
328
329 #[instrument]
334 pub async fn get_many_records(&self, ids: &[i64]) -> Result<Vec<DbRecord>> {
335 if ids.len() > MAX_BIND_PARAMS {
336 return Err(DbErrorRepr::TooManyBindParameters(ids.len()).into());
337 }
338
339 tracing::debug!("Getting records.");
340
341 let query = r"SELECT id, url, client_datetime, client_datetime_raw, timestamp_flora, title,
342 language, web_archive_serialized
343 FROM records
344 WHERE id IN rarray(?1);";
345
346 let conn = self
347 .get_conn_array()
348 .await
349 .inspect_err(|e| tracing::error!("Couldn't get a connection from the pool: {e}"))?;
350
351 let values = ids.iter().copied().map(Value::from).collect::<Vec<Value>>();
352
353 let records = conn
354 .interact(|conn| -> Result<Vec<DbRecord>> {
355 let values = Rc::new(values);
356
357 let mut stmt = conn
358 .prepare(query)
359 .inspect_err(|e| tracing::error!("Couldn't prepare the query: {e}"))?;
360
361 let records = try_query_db_records!(stmt, [values]);
362 Ok(records)
363 })
364 .await
365 .inspect_err(|e| {
366 tracing::error!("Couldn't interact with the database connection: {e}");
367 })??;
368
369 tracing::debug!("Got {} records", records.len());
370
371 Ok(records)
372 }
373
374 #[instrument(skip(self))]
380 pub async fn get_all_records(&self) -> Result<Vec<DbRecord>> {
381 tracing::debug!("Getting all records");
382
383 let query = r"SELECT * FROM records";
384 let conn = self.pool.get().await?;
385 let records = conn
386 .interact(move |conn| -> Result<Vec<DbRecord>> {
387 let mut stmt = conn.prepare(query)?;
388 let records = try_query_db_records!(stmt, []);
389 Ok(records)
390 })
391 .await??;
392
393 Ok(records)
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400 use crate::model::tests::test_record_params;
401 use anyhow::Result;
402 use chrono::{DateTime, FixedOffset};
403 use omry_archiving::record::Record;
404 use pretty_assertions::assert_eq;
405 use serde::Serialize;
406 use std::fs::File;
407 use std::ops::Deref;
408 use tempfile::NamedTempFile;
409
410 struct TempDb {
413 #[allow(dead_code)]
414 temp_db_file: NamedTempFile<File>,
415 db: Database,
416 }
417
418 impl TempDb {
419 async fn new() -> Result<Self> {
420 let temp_db_file = NamedTempFile::with_suffix(".db")?;
421 let db = Database::new_with_records_table(temp_db_file.path()).await?;
422 Ok(Self { temp_db_file, db })
423 }
424 }
425
426 impl Deref for TempDb {
427 type Target = Database;
428
429 fn deref(&self) -> &Self::Target {
430 &self.db
431 }
432 }
433
434 #[derive(Debug, Serialize)]
436 struct DbRecordDef {
437 id: Option<i64>,
438 url: String,
439 client_datetime: DateTime<FixedOffset>,
440 client_datetime_raw: Option<String>,
441 timestamp_flora: i64,
442 title: String,
443 language: Option<String>,
444 web_archive_serialized: Vec<u8>,
445 }
446
447 impl From<DbRecord> for DbRecordDef {
448 fn from(value: DbRecord) -> DbRecordDef {
449 let DbRecord {
450 id,
451 url,
452 client_datetime,
453 client_datetime_raw,
454 timestamp_flora,
455 title,
456 language,
457 web_archive_serialized,
458 } = value;
459
460 Self {
461 id,
462 url,
463 client_datetime,
464 client_datetime_raw,
465 timestamp_flora,
466 title,
467 language,
468 web_archive_serialized,
469 }
470 }
471 }
472
473 fn test_db_record() -> Result<DbRecord> {
474 let record = Record::new(test_record_params()?);
475 Ok(record.try_into()?)
476 }
477
478 #[tokio::test]
479 async fn can_create_table() -> Result<()> {
480 let db = TempDb::new().await?;
481 assert!(db.create_table(&DbRecord::schema()).await.is_ok());
482 Ok(())
484 }
485
486 #[tokio::test]
487 async fn can_add_record() -> Result<()> {
488 let db = TempDb::new().await?;
489 let record = test_db_record()?;
490 let result = db.insert(record).await?;
491 assert_eq!(result, 1);
492 Ok(())
493 }
494
495 #[tokio::test]
496 async fn can_get_record() -> Result<()> {
497 let db = TempDb::new().await?;
498 let record = test_db_record()?;
499 assert!(record.id.is_none());
500 db.insert(record).await?;
501
502 let expected_id = 1i64;
503 let retrieved_record = db.get_record(expected_id).await?;
504
505 let retrieved_record: DbRecordDef = retrieved_record.into();
506 insta::assert_ron_snapshot!(retrieved_record, {
507 ".client_datetime" => "[client_datetime]",
508 ".timestamp_flora" => "[timestamp]"
509 });
510
511 Ok(())
512 }
513
514 #[tokio::test]
515 async fn can_get_many_records() -> Result<()> {
516 let db = TempDb::new().await?;
517
518 let num_records_to_insert = 7;
519 let mut record_ids = Vec::with_capacity(num_records_to_insert);
520
521 for _ in 0..num_records_to_insert {
522 let record = test_db_record()?;
523 let record_id = db.insert(record).await?;
524 record_ids.push(record_id);
525 }
526
527 let selected_records = db
528 .get_many_records(&record_ids)
529 .await?
530 .into_iter()
531 .map(DbRecordDef::from)
532 .collect::<Vec<_>>();
533
534 assert_eq!(selected_records.len(), num_records_to_insert);
535
536 insta::assert_ron_snapshot!(selected_records, {
537 "[].client_datetime" => "[client_datetime]",
538 "[].timestamp_flora" => "[timestamp]"
539 });
540
541 Ok(())
542 }
543
544 #[tokio::test]
545 async fn passing_too_many_bind_parameters_returns_error() -> Result<()> {
546 let db = TempDb::new().await?;
547
548 let num_records_to_insert = MAX_BIND_PARAMS + 1;
549 db.insert(test_db_record()?).await?;
550
551 let mut ids = Vec::with_capacity(num_records_to_insert);
552 for index in 0..num_records_to_insert {
553 ids.push(i64::try_from(index + 1)?);
554 }
555
556 let result = db.get_many_records(&ids).await;
557 assert!(result.is_err());
558
559 #[allow(clippy::unwrap_used)]
561 let error = result.unwrap_err();
562 insta::assert_snapshot!(error);
563
564 Ok(())
565 }
566
567 #[tokio::test]
568 async fn can_get_count() -> Result<()> {
569 let db = TempDb::new().await?;
570
571 let num_records_to_insert = 7;
572 let mut record_ids = Vec::with_capacity(num_records_to_insert);
573
574 for _ in 0..num_records_to_insert {
575 let record = test_db_record()?;
576 let record_id = db.insert(record).await?;
577 record_ids.push(record_id);
578 }
579
580 let count: usize = db.count().await?.try_into()?;
581
582 assert_eq!(count, num_records_to_insert);
583 Ok(())
584 }
585
586 #[tokio::test]
587 async fn can_delete_record() -> Result<()> {
588 let db = TempDb::new().await?;
589 let record = test_db_record()?;
590 let id = db.insert(record).await?;
591 let rows_changed = db.delete(id).await?;
592 assert_eq!(rows_changed, 1);
593 Ok(())
594 }
595
596 #[tokio::test]
597 async fn can_get_all_records() -> Result<()> {
598 let db = TempDb::new().await?;
599
600 let num_records_to_insert = 7;
601 let mut record_ids = Vec::with_capacity(num_records_to_insert);
602
603 for _ in 0..num_records_to_insert {
604 let record = test_db_record()?;
605 let record_id = db.insert(record).await?;
606 record_ids.push(record_id);
607 }
608
609 let selected_records = db
610 .get_all_records()
611 .await?
612 .into_iter()
613 .map(DbRecordDef::from)
614 .collect::<Vec<_>>();
615
616 assert_eq!(selected_records.len(), num_records_to_insert);
617
618 insta::assert_ron_snapshot!(selected_records, {
619 "[].client_datetime" => "[client_datetime]",
620 "[].timestamp_flora" => "[timestamp]",
621 "[].web_archive_serialized" => "[data]"
622 });
623
624 Ok(())
625 }
626}