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(DatabaseErrorRepr);
40
41impl<E> From<E> for DatabaseError
45where
46 E: Into<DatabaseErrorRepr>,
47{
48 fn from(error: E) -> Self {
49 Self(error.into())
50 }
51}
52
53#[derive(Error, Debug)]
55enum DatabaseErrorRepr {
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
82impl From<deadpool_sqlite::InteractError> for DatabaseErrorRepr {
83 fn from(error: deadpool_sqlite::InteractError) -> Self {
84 Self::PoolInteract(error.to_string())
85 }
86}
87
88pub type Result<T> = std::result::Result<T, DatabaseError>;
90
91impl TryFrom<&Row<'_>> for DbRecord {
92 type Error = rusqlite::Error;
93
94 fn try_from(row: &Row) -> std::result::Result<Self, Self::Error> {
95 Ok(Self {
96 id: row.get(0)?,
97 url: row.get(1)?,
98 client_datetime: row.get(2)?,
99 client_datetime_raw: row.get(3)?,
100 timestamp_flora: row.get(4)?,
101 title: row.get(5)?,
102 language: row.get(6)?,
103 web_archive_serialized: row.get(7)?,
104 })
105 }
106}
107
108#[derive(Debug, Clone)]
110pub struct Database {
111 pool: Pool,
113}
114
115impl Database {
116 #[instrument(skip(path))]
121 async fn new(path: impl AsRef<Path>) -> Result<Self> {
122 let cfg = Config::new(path.as_ref());
123 let pool = cfg.create_pool(Runtime::Tokio1)?;
124 Ok(Self { pool })
125 }
126
127 #[instrument(skip(self))]
134 async fn get_conn_array(&self) -> Result<Object> {
135 let conn = self.pool.get().await?;
136 conn.interact(|conn| {
137 tracing::debug!("Loading rusqlite::vtab::array");
138 rusqlite::vtab::array::load_module(conn)
139 })
140 .await?
141 .inspect(|()| tracing::debug!("Loaded rusqlite::vtab::array"))?;
142
143 Ok(conn)
144 }
145
146 #[instrument(skip(self))]
148 async fn create_table(&self, schema: &Schema) -> Result<()> {
149 let create_table_query = format!("CREATE TABLE IF NOT EXISTS {schema};");
150 let conn = self.pool.get().await?;
151 let _rows: usize = conn
152 .interact(move |conn| {
153 let mut stmt = conn.prepare(&create_table_query)?;
154 stmt.execute(())
155 })
156 .await??;
157
158 Ok(())
159 }
160
161 #[instrument(skip(self))]
163 async fn create_indexes(&self, schema: &Schema, columns: &[&str]) -> Result<()> {
164 let table_name = schema.table_name();
165 let conn = self.pool.get().await?;
166
167 for column in columns {
168 let idx_name = format!("idx_{column}");
169 let query =
170 format!("CREATE INDEX IF NOT EXISTS {idx_name} ON {table_name} ({column});");
171 tracing::debug!("Query: {query}");
172
173 tracing::info!("Creating index '{idx_name}' on '{table_name}'");
174 conn.interact(move |conn| {
175 let mut stmt = conn.prepare(&query)?;
176 stmt.execute([])
177 })
178 .await??;
179 }
180
181 Ok(())
182 }
183
184 #[instrument(skip(path))]
189 pub async fn new_with_records_table(path: impl AsRef<Path>) -> Result<Self> {
190 let db = Self::new(path).await?;
191 let schema = DbRecord::schema();
192 db.create_table(&schema).await?;
193 db.create_indexes(&schema, &["url"]).await?;
194 Ok(db)
195 }
196
197 #[instrument]
202 pub async fn count(&self) -> Result<i64> {
203 let query = r"SELECT count(*) FROM records;";
204 let conn = self.pool.get().await?;
205
206 let record_count = conn
207 .interact(move |conn| {
208 let mut stmt = conn.prepare(query)?;
209 stmt.query_one([], |row| row.get(0))
210 })
211 .await??;
212
213 Ok(record_count)
214 }
215
216 pub async fn insert(&self, record: DbRecord) -> Result<i64> {
223 let query = r"INSERT INTO records
224 VALUES (:id, :url, :client_datetime, :client_datetime_raw, :timestamp_flora, :title,
225 :language, :web_archive_serialized)
226 RETURNING id;";
227
228 let conn = self.pool.get().await?;
229
230 let row_id = conn
231 .interact(move |conn| {
232 let mut stmt = conn.prepare(query)?;
233 stmt.query_one(
234 rusqlite::named_params! {
235 ":id": record.id,
236 ":url": record.url,
237 ":client_datetime": record.client_datetime,
238 ":client_datetime_raw": record.client_datetime_raw,
239 ":timestamp_flora": record.timestamp_flora,
240 ":title": record.title,
241 ":language": record.language,
242 ":web_archive_serialized": record.web_archive_serialized,
243 },
244 |row| row.get(0),
245 )
246 })
247 .await??;
248
249 Ok(row_id)
250 }
251
252 #[instrument(skip(self))]
257 pub async fn delete(&self, id: i64) -> Result<usize> {
258 const QUERY: &str = r"DELETE FROM records WHERE id = ?1;";
259
260 let conn = self.pool.get().await?;
261 let rows_changed = conn
262 .interact(move |conn| {
263 let mut stmt = conn.prepare(QUERY)?;
264 stmt.execute([id])
265 })
266 .await??;
267
268 Ok(rows_changed)
269 }
270
271 pub async fn get_record(&self, id: i64) -> Result<DbRecord> {
276 let query = r"SELECT id, url, client_datetime, client_datetime_raw, timestamp_flora, title,
277 language, web_archive_serialized
278 FROM records WHERE id = ?1;";
279
280 let conn = self.pool.get().await?;
281 let result = conn
282 .interact(move |conn| {
283 let mut stmt = conn.prepare(query)?;
284 stmt.query_row([id], |row| {
285 let record = row.try_into()?;
286 Ok(record)
287 })
288 })
289 .await??;
290
291 Ok(result)
292 }
293
294 #[instrument]
299 pub async fn get_many_records(&self, ids: &[i64]) -> Result<Vec<DbRecord>> {
300 if ids.len() > MAX_BIND_PARAMS {
301 return Err(DatabaseErrorRepr::TooManyBindParameters(ids.len()).into());
302 }
303
304 tracing::debug!("Getting records.");
305
306 let query = r"SELECT id, url, client_datetime, client_datetime_raw, timestamp_flora, title,
307 language, web_archive_serialized
308 FROM records
309 WHERE id IN rarray(?1);";
310
311 let conn = self
312 .get_conn_array()
313 .await
314 .inspect_err(|e| tracing::error!("Couldn't get a connection from the pool: {e}"))?;
315
316 let values = ids.iter().copied().map(Value::from).collect::<Vec<Value>>();
317
318 let records = conn
319 .interact(|conn| -> Result<Vec<DbRecord>> {
320 let values = Rc::new(values);
321
322 let mut stmt = conn
323 .prepare(query)
324 .inspect_err(|e| tracing::error!("Couldn't prepare the query: {e}"))?;
325
326 let records = try_query_db_records!(stmt, [values]);
327 Ok(records)
328 })
329 .await
330 .inspect_err(|e| {
331 tracing::error!("Couldn't interact with the database connection: {e}");
332 })??;
333
334 tracing::debug!("Got {} records", records.len());
335
336 Ok(records)
337 }
338
339 #[instrument(skip(self))]
345 pub async fn get_all_records(&self) -> Result<Vec<DbRecord>> {
346 tracing::debug!("Getting all records");
347
348 let query = r"SELECT * FROM records";
349 let conn = self.pool.get().await?;
350 let records = conn
351 .interact(move |conn| -> Result<Vec<DbRecord>> {
352 let mut stmt = conn.prepare(query)?;
353 let records = try_query_db_records!(stmt, []);
354 Ok(records)
355 })
356 .await??;
357
358 Ok(records)
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365 use crate::model::tests::test_record_params;
366 use anyhow::Result;
367 use chrono::{DateTime, FixedOffset};
368 use omry_archiving::record::Record;
369 use pretty_assertions::assert_eq;
370 use serde::Serialize;
371 use std::fs::File;
372 use std::ops::Deref;
373 use tempfile::NamedTempFile;
374
375 struct TempDb {
378 #[allow(dead_code)]
379 temp_db_file: NamedTempFile<File>,
380 db: Database,
381 }
382
383 impl TempDb {
384 async fn new() -> Result<Self> {
385 let temp_db_file = NamedTempFile::with_suffix(".db")?;
386 let db = Database::new_with_records_table(temp_db_file.path()).await?;
387 Ok(Self { temp_db_file, db })
388 }
389 }
390
391 impl Deref for TempDb {
392 type Target = Database;
393
394 fn deref(&self) -> &Self::Target {
395 &self.db
396 }
397 }
398
399 #[derive(Debug, Serialize)]
401 struct DbRecordDef {
402 id: Option<i64>,
403 url: String,
404 client_datetime: DateTime<FixedOffset>,
405 client_datetime_raw: Option<String>,
406 timestamp_flora: i64,
407 title: String,
408 language: Option<String>,
409 web_archive_serialized: Vec<u8>,
410 }
411
412 impl From<DbRecord> for DbRecordDef {
413 fn from(value: DbRecord) -> DbRecordDef {
414 let DbRecord {
415 id,
416 url,
417 client_datetime,
418 client_datetime_raw,
419 timestamp_flora,
420 title,
421 language,
422 web_archive_serialized,
423 } = value;
424
425 Self {
426 id,
427 url,
428 client_datetime,
429 client_datetime_raw,
430 timestamp_flora,
431 title,
432 language,
433 web_archive_serialized,
434 }
435 }
436 }
437
438 fn test_db_record() -> Result<DbRecord> {
439 let record = Record::new(test_record_params()?);
440 Ok(record.try_into()?)
441 }
442
443 #[tokio::test]
444 async fn can_create_table() -> Result<()> {
445 let db = TempDb::new().await?;
446 assert!(db.create_table(&DbRecord::schema()).await.is_ok());
447 Ok(())
449 }
450
451 #[tokio::test]
452 async fn can_add_record() -> Result<()> {
453 let db = TempDb::new().await?;
454 let record = test_db_record()?;
455 let result = db.insert(record).await?;
456 assert_eq!(result, 1);
457 Ok(())
458 }
459
460 #[tokio::test]
461 async fn can_get_record() -> Result<()> {
462 let db = TempDb::new().await?;
463 let record = test_db_record()?;
464 assert!(record.id.is_none());
465 db.insert(record).await?;
466
467 let expected_id = 1i64;
468 let retrieved_record = db.get_record(expected_id).await?;
469
470 let retrieved_record: DbRecordDef = retrieved_record.into();
471 insta::assert_ron_snapshot!(retrieved_record, {
472 ".client_datetime" => "[client_datetime]",
473 ".timestamp_flora" => "[timestamp]"
474 });
475
476 Ok(())
477 }
478
479 #[tokio::test]
480 async fn can_get_many_records() -> Result<()> {
481 let db = TempDb::new().await?;
482
483 let num_records_to_insert = 7;
484 let mut record_ids = Vec::with_capacity(num_records_to_insert);
485
486 for _ in 0..num_records_to_insert {
487 let record = test_db_record()?;
488 let record_id = db.insert(record).await?;
489 record_ids.push(record_id);
490 }
491
492 let selected_records = db
493 .get_many_records(&record_ids)
494 .await?
495 .into_iter()
496 .map(DbRecordDef::from)
497 .collect::<Vec<_>>();
498
499 assert_eq!(selected_records.len(), num_records_to_insert);
500
501 insta::assert_ron_snapshot!(selected_records, {
502 "[].client_datetime" => "[client_datetime]",
503 "[].timestamp_flora" => "[timestamp]"
504 });
505
506 Ok(())
507 }
508
509 #[tokio::test]
510 async fn passing_too_many_bind_parameters_returns_error() -> Result<()> {
511 let db = TempDb::new().await?;
512
513 let num_records_to_insert = MAX_BIND_PARAMS + 1;
514 db.insert(test_db_record()?).await?;
515
516 let mut ids = Vec::with_capacity(num_records_to_insert);
517 for index in 0..num_records_to_insert {
518 ids.push(i64::try_from(index + 1)?);
519 }
520
521 let result = db.get_many_records(&ids).await;
522 assert!(result.is_err());
523
524 #[allow(clippy::unwrap_used)]
526 let error = result.unwrap_err();
527 insta::assert_snapshot!(error);
528
529 Ok(())
530 }
531
532 #[tokio::test]
533 async fn can_get_count() -> Result<()> {
534 let db = TempDb::new().await?;
535
536 let num_records_to_insert = 7;
537 let mut record_ids = Vec::with_capacity(num_records_to_insert);
538
539 for _ in 0..num_records_to_insert {
540 let record = test_db_record()?;
541 let record_id = db.insert(record).await?;
542 record_ids.push(record_id);
543 }
544
545 let count: usize = db.count().await?.try_into()?;
546
547 assert_eq!(count, num_records_to_insert);
548 Ok(())
549 }
550
551 #[tokio::test]
552 async fn can_delete_record() -> Result<()> {
553 let db = TempDb::new().await?;
554 let record = test_db_record()?;
555 let id = db.insert(record).await?;
556 let rows_changed = db.delete(id).await?;
557 assert_eq!(rows_changed, 1);
558 Ok(())
559 }
560
561 #[tokio::test]
562 async fn can_get_all_records() -> Result<()> {
563 let db = TempDb::new().await?;
564
565 let num_records_to_insert = 7;
566 let mut record_ids = Vec::with_capacity(num_records_to_insert);
567
568 for _ in 0..num_records_to_insert {
569 let record = test_db_record()?;
570 let record_id = db.insert(record).await?;
571 record_ids.push(record_id);
572 }
573
574 let selected_records = db
575 .get_all_records()
576 .await?
577 .into_iter()
578 .map(DbRecordDef::from)
579 .collect::<Vec<_>>();
580
581 assert_eq!(selected_records.len(), num_records_to_insert);
582
583 insta::assert_ron_snapshot!(selected_records, {
584 "[].client_datetime" => "[client_datetime]",
585 "[].timestamp_flora" => "[timestamp]",
586 "[].web_archive_serialized" => "[data]"
587 });
588
589 Ok(())
590 }
591}