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