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