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: &[&str]) -> 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, &["url"]).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 RETURNING id;";
225
226 let conn = self.pool.get().await?;
227
228 let row_id = conn
229 .interact(move |conn| {
230 let mut stmt = conn.prepare(query)?;
231 stmt.query_one(
232 rusqlite::named_params! {
233 ":id": record.id,
234 ":url": record.url,
235 ":client_datetime": record.client_datetime,
236 ":client_datetime_raw": record.client_datetime_raw,
237 ":timestamp_flora": record.timestamp_flora,
238 ":title": record.title,
239 ":language": record.language,
240 ":web_archive_serialized": record.web_archive_serialized,
241 },
242 |row| row.get(0),
243 )
244 })
245 .await??;
246
247 Ok(row_id)
248 }
249
250 #[instrument(skip(self))]
255 pub async fn delete(&self, id: i64) -> Result<usize> {
256 const QUERY: &str = r"DELETE FROM records WHERE id = ?1;";
257
258 let conn = self.pool.get().await?;
259 let rows_changed = conn
260 .interact(move |conn| {
261 let mut stmt = conn.prepare(QUERY)?;
262 stmt.execute([id])
263 })
264 .await??;
265
266 Ok(rows_changed)
267 }
268
269 pub async fn get_record(&self, id: i64) -> Result<DbRecord> {
274 let query = r"SELECT id, url, client_datetime, client_datetime_raw, timestamp_flora, title,
275 language, web_archive_serialized
276 FROM records WHERE id = ?1;";
277
278 let conn = self.pool.get().await?;
279 let result = conn
280 .interact(move |conn| {
281 let mut stmt = conn.prepare(query)?;
282 stmt.query_row([id], |row| {
283 let record = row.try_into()?;
284 Ok(record)
285 })
286 })
287 .await??;
288
289 Ok(result)
290 }
291
292 #[instrument]
297 pub async fn get_many_records(&self, ids: &[i64]) -> Result<Vec<DbRecord>> {
298 if ids.len() > MAX_BIND_PARAMS {
299 return Err(DatabaseErrorRepr::TooManyBindParameters(ids.len()).into());
300 }
301
302 tracing::debug!("Getting records.");
303
304 let query = r"SELECT id, url, client_datetime, client_datetime_raw, timestamp_flora, title,
305 language, web_archive_serialized
306 FROM records
307 WHERE id IN rarray(?1);";
308
309 let conn = self
310 .get_conn_array()
311 .await
312 .inspect_err(|e| tracing::error!("Couldn't get a connection from the pool: {e}"))?;
313
314 let values = ids.iter().copied().map(Value::from).collect::<Vec<Value>>();
315
316 let records = conn
317 .interact(|conn| -> Result<Vec<DbRecord>> {
318 let values = Rc::new(values);
319
320 let mut stmt = conn
321 .prepare(query)
322 .inspect_err(|e| tracing::error!("Couldn't prepare the query: {e}"))?;
323
324 let records = try_query_db_records!(stmt, [values]);
325 Ok(records)
326 })
327 .await
328 .inspect_err(|e| {
329 tracing::error!("Couldn't interact with the database connection: {e}");
330 })??;
331
332 tracing::debug!("Got {} records", records.len());
333
334 Ok(records)
335 }
336
337 #[instrument(skip(self))]
343 pub async fn get_all_records(&self) -> Result<Vec<DbRecord>> {
344 tracing::debug!("Getting all records");
345
346 let query = r"SELECT * FROM records";
347 let conn = self.pool.get().await?;
348 let records = conn
349 .interact(move |conn| -> Result<Vec<DbRecord>> {
350 let mut stmt = conn.prepare(query)?;
351 let records = try_query_db_records!(stmt, []);
352 Ok(records)
353 })
354 .await??;
355
356 Ok(records)
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363 use crate::model::tests::test_record_params;
364 use anyhow::Result;
365 use chrono::{DateTime, FixedOffset};
366 use omry_archiving::record::Record;
367 use pretty_assertions::assert_eq;
368 use serde::Serialize;
369 use std::fs::File;
370 use std::ops::Deref;
371 use tempfile::NamedTempFile;
372
373 struct TempDb {
376 #[allow(dead_code)]
377 temp_db_file: NamedTempFile<File>,
378 db: Database,
379 }
380
381 impl TempDb {
382 async fn new() -> Result<Self> {
383 let temp_db_file = NamedTempFile::with_suffix(".db")?;
384 let db = Database::new_with_records_table(temp_db_file.path()).await?;
385 Ok(Self { temp_db_file, db })
386 }
387 }
388
389 impl Deref for TempDb {
390 type Target = Database;
391
392 fn deref(&self) -> &Self::Target {
393 &self.db
394 }
395 }
396
397 #[derive(Debug, Serialize)]
399 struct DbRecordDef {
400 id: Option<i64>,
401 url: String,
402 client_datetime: DateTime<FixedOffset>,
403 client_datetime_raw: Option<String>,
404 timestamp_flora: i64,
405 title: String,
406 language: Option<String>,
407 web_archive_serialized: Vec<u8>,
408 }
409
410 impl From<DbRecord> for DbRecordDef {
411 fn from(value: DbRecord) -> DbRecordDef {
412 let DbRecord {
413 id,
414 url,
415 client_datetime,
416 client_datetime_raw,
417 timestamp_flora,
418 title,
419 language,
420 web_archive_serialized,
421 } = value;
422
423 Self {
424 id,
425 url,
426 client_datetime,
427 client_datetime_raw,
428 timestamp_flora,
429 title,
430 language,
431 web_archive_serialized,
432 }
433 }
434 }
435
436 fn test_db_record() -> Result<DbRecord> {
437 let record = Record::new(test_record_params()?);
438 Ok(record.try_into()?)
439 }
440
441 #[tokio::test]
442 async fn can_create_table() -> Result<()> {
443 let db = TempDb::new().await?;
444 assert!(db.create_table(&DbRecord::schema()).await.is_ok());
445 Ok(())
447 }
448
449 #[tokio::test]
450 async fn can_add_record() -> Result<()> {
451 let db = TempDb::new().await?;
452 let record = test_db_record()?;
453 let result = db.insert(record).await?;
454 assert_eq!(result, 1);
455 Ok(())
456 }
457
458 #[tokio::test]
459 async fn can_get_record() -> Result<()> {
460 let db = TempDb::new().await?;
461 let record = test_db_record()?;
462 assert!(record.id.is_none());
463 db.insert(record).await?;
464
465 let expected_id = 1i64;
466 let retrieved_record = db.get_record(expected_id).await?;
467
468 let retrieved_record: DbRecordDef = retrieved_record.into();
469 insta::assert_ron_snapshot!(retrieved_record, {
470 ".client_datetime" => "[client_datetime]",
471 ".timestamp_flora" => "[timestamp]"
472 });
473
474 Ok(())
475 }
476
477 #[tokio::test]
478 async fn can_get_many_records() -> Result<()> {
479 let db = TempDb::new().await?;
480
481 let num_records_to_insert = 7;
482 let mut record_ids = Vec::with_capacity(num_records_to_insert);
483
484 for _ in 0..num_records_to_insert {
485 let record = test_db_record()?;
486 let record_id = db.insert(record).await?;
487 record_ids.push(record_id);
488 }
489
490 let selected_records = db
491 .get_many_records(&record_ids)
492 .await?
493 .into_iter()
494 .map(DbRecordDef::from)
495 .collect::<Vec<_>>();
496
497 assert_eq!(selected_records.len(), num_records_to_insert);
498
499 insta::assert_ron_snapshot!(selected_records, {
500 "[].client_datetime" => "[client_datetime]",
501 "[].timestamp_flora" => "[timestamp]"
502 });
503
504 Ok(())
505 }
506
507 #[tokio::test]
508 async fn passing_too_many_bind_parameters_returns_error() -> Result<()> {
509 let db = TempDb::new().await?;
510
511 let num_records_to_insert = MAX_BIND_PARAMS + 1;
512 db.insert(test_db_record()?).await?;
513
514 let mut ids = Vec::with_capacity(num_records_to_insert);
515 for index in 0..num_records_to_insert {
516 ids.push(i64::try_from(index + 1)?);
517 }
518
519 let result = db.get_many_records(&ids).await;
520 assert!(result.is_err());
521
522 #[allow(clippy::unwrap_used)]
524 let error = result.unwrap_err();
525 insta::assert_snapshot!(error);
526
527 Ok(())
528 }
529
530 #[tokio::test]
531 async fn can_get_count() -> Result<()> {
532 let db = TempDb::new().await?;
533
534 let num_records_to_insert = 7;
535 let mut record_ids = Vec::with_capacity(num_records_to_insert);
536
537 for _ in 0..num_records_to_insert {
538 let record = test_db_record()?;
539 let record_id = db.insert(record).await?;
540 record_ids.push(record_id);
541 }
542
543 let count: usize = db.count().await?.try_into()?;
544
545 assert_eq!(count, num_records_to_insert);
546 Ok(())
547 }
548
549 #[tokio::test]
550 async fn can_delete_record() -> Result<()> {
551 let db = TempDb::new().await?;
552 let record = test_db_record()?;
553 let id = db.insert(record).await?;
554 let rows_changed = db.delete(id).await?;
555 assert_eq!(rows_changed, 1);
556 Ok(())
557 }
558
559 #[tokio::test]
560 async fn can_get_all_records() -> Result<()> {
561 let db = TempDb::new().await?;
562
563 let num_records_to_insert = 7;
564 let mut record_ids = Vec::with_capacity(num_records_to_insert);
565
566 for _ in 0..num_records_to_insert {
567 let record = test_db_record()?;
568 let record_id = db.insert(record).await?;
569 record_ids.push(record_id);
570 }
571
572 let selected_records = db
573 .get_all_records()
574 .await?
575 .into_iter()
576 .map(DbRecordDef::from)
577 .collect::<Vec<_>>();
578
579 assert_eq!(selected_records.len(), num_records_to_insert);
580
581 insta::assert_ron_snapshot!(selected_records, {
582 "[].client_datetime" => "[client_datetime]",
583 "[].timestamp_flora" => "[timestamp]",
584 "[].web_archive_serialized" => "[data]"
585 });
586
587 Ok(())
588 }
589}