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