1#![cfg(feature = "connection")]
3
4use deadpool_sqlite::rusqlite;
5use deadpool_sqlite::rusqlite::Row;
6use deadpool_sqlite::rusqlite::types::Value;
7use deadpool_sqlite::{Config, Object, Pool, Runtime};
8use rusqlite_migration::{M, Migrations};
9use std::path::Path;
10use std::rc::Rc;
11use thiserror::Error;
12use tracing::instrument;
13
14use crate::model::{DbRecord, DbRecordMeta};
15
16const MAX_BIND_PARAMS: usize = 32766;
19
20const SELECT_ALL_LIMIT: i64 = 50;
22
23macro_rules! try_query_db_records {
25 ($stmt:ident, $values:expr, $type:ty) => {{
26 $stmt
27 .query_map($values, |row| <$type>::try_from(row))
28 .inspect_err(|e| tracing::error!("Couldn't execute the query: {e}"))?
29 .collect::<std::result::Result<Vec<$type>, rusqlite::Error>>()?
30 }};
31
32 ($stmt:ident, $values:expr) => {
33 try_query_db_records!($stmt, $values, DbRecord)
34 };
35}
36
37const MIGRATIONS_SLICE: &[M<'_>] = &[
38 M::up(
39 r"CREATE TABLE IF NOT EXISTS records(id INTEGER NOT NULL PRIMARY KEY,
40url TEXT NOT NULL,
41client_datetime TEXT NOT NULL,
42client_datetime_raw TEXT,
43timestamp_flora INTEGER NOT NULL,
44title TEXT NOT NULL,
45language TEXT,
46web_archive_serialized BLOB NOT NULL);",
47 ),
48 M::up("ALTER TABLE records ADD COLUMN updated_at INTEGER;"),
49 M::up("CREATE INDEX timestamp ON records(timestamp_flora);"),
50];
51const MIGRATIONS: Migrations<'_> = Migrations::from_slice(MIGRATIONS_SLICE);
52
53#[derive(Debug, Error)]
55#[error(transparent)]
56pub struct DatabaseError(DbErrorRepr);
57
58impl<E> From<E> for DatabaseError
62where
63 E: Into<DbErrorRepr>,
64{
65 fn from(error: E) -> Self {
66 Self(error.into())
67 }
68}
69
70#[derive(Error, Debug)]
72enum DbErrorRepr {
73 #[error("error creating the DB pool: {0}")]
75 CreatePool(#[from] deadpool_sqlite::CreatePoolError),
76
77 #[error("error interacting with the DB pool: {0}")]
81 PoolInteract(String),
82
83 #[error("error getting connection from the DB pool: {0}")]
85 Pool(#[from] deadpool_sqlite::PoolError),
86
87 #[error("error in rusqlite: {0}")]
89 Rusqlite(#[from] rusqlite::Error),
90
91 #[error(
94 "The caller passed too many parameters to bind (got {0}, max allowed is {MAX_BIND_PARAMS})"
95 )]
96 TooManyBindParameters(usize),
97
98 #[error("Couldn't activate the WAL; journaling mode is {0}")]
100 WalActivation(String),
101
102 #[error("error performing migration: {0}")]
103 Migration(#[from] rusqlite_migration::Error),
104
105 #[error("error performing record update: {0}")]
106 Update(String),
107
108 #[error("error converting int: {0}")]
109 IntConversion(#[from] std::num::TryFromIntError),
110}
111
112impl From<deadpool_sqlite::InteractError> for DbErrorRepr {
113 fn from(error: deadpool_sqlite::InteractError) -> Self {
114 Self::PoolInteract(error.to_string())
115 }
116}
117
118pub type Result<T> = std::result::Result<T, DatabaseError>;
120
121impl TryFrom<&Row<'_>> for DbRecord {
122 type Error = rusqlite::Error;
123
124 fn try_from(row: &Row) -> std::result::Result<Self, Self::Error> {
125 Ok(Self {
126 id: row.get(0)?,
127 url: row.get(1)?,
128 client_datetime: row.get(2)?,
129 client_datetime_raw: row.get(3)?,
130 timestamp_flora: row.get(4)?,
131 title: row.get(5)?,
132 language: row.get(6)?,
133 document: row.get(7)?,
134 updated_at: row.get(8)?,
135 })
136 }
137}
138
139impl TryFrom<&Row<'_>> for DbRecordMeta {
140 type Error = rusqlite::Error;
141
142 fn try_from(row: &Row) -> std::result::Result<Self, Self::Error> {
143 Ok(Self {
144 id: row.get(0)?,
145 url: row.get(1)?,
146 client_datetime: row.get(2)?,
147 timestamp_flora: row.get(3)?,
148 title: row.get(4)?,
149 language: row.get(5)?,
150 updated_at: row.get(6)?,
151 })
152 }
153}
154#[derive(Debug, Clone)]
156pub struct Database {
157 pool: Pool,
159}
160
161impl Database {
162 #[instrument(skip(path))]
167 async fn new(path: impl AsRef<Path>) -> Result<Self> {
168 let cfg = Config::new(path.as_ref());
169 let pool = cfg.create_pool(Runtime::Tokio1)?;
170 Ok(Self { pool })
171 }
172
173 #[instrument(skip(self))]
183 async fn get_conn_array(&self) -> Result<Object> {
184 let conn = self.pool.get().await?;
185 conn.interact(|conn| {
186 tracing::debug!("Loading rusqlite::vtab::array");
187 rusqlite::vtab::array::load_module(conn)
188 })
189 .await?
190 .inspect(|()| tracing::debug!("Loaded rusqlite::vtab::array"))?;
191
192 Ok(conn)
193 }
194
195 #[instrument(skip(self))]
197 async fn create_table(&self) -> Result<()> {
198 let conn = self.pool.get().await?;
199 conn.interact(move |conn| MIGRATIONS.to_latest(conn))
200 .await??;
201
202 Ok(())
203 }
204
205 #[instrument(skip(self))]
213 async fn activate_wal(&self) -> Result<()> {
214 const SUCCESS_VALUE: &str = "wal";
215
216 let conn = self.pool.get().await?;
217
218 tracing::debug!("Enabling the WAL.");
219
220 let journal_mode: String = conn
221 .interact(move |conn| {
222 conn.pragma_update_and_check(None, "journal_mode", "WAL", |v| v.get(0))
223 })
224 .await??;
225
226 if journal_mode.as_str() == SUCCESS_VALUE {
227 Ok(())
228 } else {
229 Err(DbErrorRepr::WalActivation(journal_mode).into())
230 }
231 }
232
233 pub async fn optimize(&self) -> Result<()> {
240 const MASK: &str = "0x10002";
242 let conn = self.pool.get().await?;
243 tracing::trace!("running OPTIMIZE");
244
245 conn.interact(move |conn| conn.pragma_update(None, "optimize", MASK))
246 .await??;
247 Ok(())
248 }
249
250 #[instrument(skip(path))]
255 pub async fn new_with_records_table(path: impl AsRef<Path>) -> Result<Self> {
256 let db = Self::new(path).await?;
257 db.activate_wal().await?;
258 db.create_table().await?;
259 db.optimize().await?;
260 Ok(db)
261 }
262
263 #[instrument]
268 pub async fn count(&self) -> Result<i64> {
269 let query = r"SELECT count(*) FROM records;";
270 let conn = self.pool.get().await?;
271
272 let record_count = conn
273 .interact(move |conn| {
274 let mut stmt = conn.prepare(query)?;
275 stmt.query_one([], |row| row.get(0))
276 })
277 .await??;
278
279 Ok(record_count)
280 }
281
282 #[instrument(skip_all, fields(record_url = record.url))]
289 pub async fn insert(&self, record: DbRecord) -> Result<i64> {
290 let query = r"INSERT INTO records
291 VALUES (:id, :url, :client_datetime, :client_datetime_raw, :timestamp_flora, :title,
292 :language, :web_archive_serialized, :updated_at)
293 RETURNING id;";
294
295 let DbRecord {
296 id,
297 url,
298 client_datetime,
299 client_datetime_raw,
300 timestamp_flora,
301 title,
302 language,
303 document,
304 updated_at,
305 } = record;
306
307 let conn = self.pool.get().await?;
308 let row_id = conn
309 .interact(move |conn| {
310 let mut stmt = conn.prepare(query)?;
311 stmt.query_one(
312 rusqlite::named_params! {
313 ":id": id,
314 ":url": url,
315 ":client_datetime": client_datetime,
316 ":client_datetime_raw": client_datetime_raw,
317 ":timestamp_flora": timestamp_flora,
318 ":title": title,
319 ":language": language,
320 ":web_archive_serialized": document,
321 ":updated_at": updated_at,
322 },
323 |row| row.get(0),
324 )
325 })
326 .await?
327 .inspect_err(|e| tracing::error!("Error on insert: {e}"))?;
328
329 Ok(row_id)
330 }
331
332 #[instrument(skip(self))]
341 pub async fn delete(&self, id: i64) -> Result<usize> {
342 const QUERY: &str = r"DELETE FROM records WHERE id = ?1;";
343
344 let conn = self.pool.get().await?;
345 let rows_changed = conn
346 .interact(move |conn| {
347 let mut stmt = conn.prepare(QUERY)?;
348 stmt.execute([id])
349 })
350 .await??;
351
352 Ok(rows_changed)
353 }
354
355 pub async fn get_record(&self, id: i64) -> Result<DbRecord> {
360 let query = r"SELECT id, url, client_datetime, client_datetime_raw, timestamp_flora, title,
361 language, web_archive_serialized, updated_at
362 FROM records WHERE id = ?1;";
363
364 let conn = self.pool.get().await?;
365 let result = conn
366 .interact(move |conn| {
367 let mut stmt = conn.prepare(query)?;
368 stmt.query_row([id], |row| {
369 let record = row.try_into()?;
370 Ok(record)
371 })
372 })
373 .await??;
374
375 Ok(result)
376 }
377
378 #[instrument]
383 pub async fn get_many_records(&self, ids: &[i64]) -> Result<Vec<DbRecord>> {
384 if ids.len() > MAX_BIND_PARAMS {
385 return Err(DbErrorRepr::TooManyBindParameters(ids.len()).into());
386 }
387
388 tracing::debug!("Getting records.");
389
390 let query = r"SELECT id, url, client_datetime, client_datetime_raw, timestamp_flora, title,
391 language, web_archive_serialized, updated_at
392 FROM records
393 WHERE id IN rarray(?1);";
394
395 let conn = self
396 .get_conn_array()
397 .await
398 .inspect_err(|e| tracing::error!("Couldn't get a connection from the pool: {e}"))?;
399
400 let values = ids.iter().copied().map(Value::from).collect::<Vec<Value>>();
401
402 let records = conn
403 .interact(|conn| -> Result<Vec<DbRecord>> {
404 let values = Rc::new(values);
405
406 let mut stmt = conn
407 .prepare(query)
408 .inspect_err(|e| tracing::error!("Couldn't prepare the query: {e}"))?;
409
410 let records = try_query_db_records!(stmt, [values]);
411 Ok(records)
412 })
413 .await
414 .inspect_err(|e| {
415 tracing::error!("Couldn't interact with the database connection: {e}");
416 })??;
417
418 tracing::debug!("Got {} records", records.len());
419
420 Ok(records)
421 }
422
423 #[instrument(skip(self))]
429 pub async fn get_all_records(&self, start_row: Option<i64>) -> Result<Vec<DbRecord>> {
430 tracing::debug!("Getting all records");
431
432 let query = r"SELECT * FROM records
434WHERE id NOT IN (SELECT id FROM records
435 ORDER BY id ASC LIMIT ?1)
436ORDER BY id ASC LIMIT ?2;";
437
438 let start_row = start_row.unwrap_or_default();
440 let conn = self.pool.get().await?;
441 let records = conn
442 .interact(move |conn| -> Result<Vec<DbRecord>> {
443 let mut stmt = conn.prepare(query)?;
444 let records = try_query_db_records!(stmt, [start_row, SELECT_ALL_LIMIT]);
445 Ok(records)
446 })
447 .await??;
448
449 Ok(records)
450 }
451
452 #[instrument(skip(self))]
460 pub async fn get_all_meta(&self, start_row: Option<i64>) -> Result<Vec<DbRecordMeta>> {
461 tracing::debug!("Getting metadata for all records");
462
463 let query = r"
464SELECT id, url, client_datetime, timestamp_flora, title, language, updated_at
465FROM records
466WHERE id NOT IN (SELECT id FROM records
467 ORDER BY id ASC LIMIT ?1)
468ORDER BY id ASC LIMIT ?2;";
469
470 let start_row = start_row.unwrap_or_default();
471 let conn = self.pool.get().await?;
472 let records = conn
473 .interact(move |conn| -> Result<Vec<DbRecordMeta>> {
474 let mut stmt = conn.prepare(query)?;
475 let records =
476 try_query_db_records!(stmt, [start_row, SELECT_ALL_LIMIT], DbRecordMeta);
477 Ok(records)
478 })
479 .await??;
480
481 Ok(records)
482 }
483
484 #[instrument(skip(self))]
490 pub async fn get_all_ids(&self) -> Result<Vec<i64>> {
491 tracing::debug!("Getting all record IDs");
492
493 let query = r"SELECT id FROM records;";
494
495 let conn = self.pool.get().await?;
496 let ids = conn
497 .interact(move |conn| -> Result<Vec<i64>> {
498 let mut stmt = conn.prepare(query)?;
499 let rows = stmt.query_map([], |row| row.get(0))?;
500 let mut ids = vec![];
501 for row in rows {
502 ids.push(row?);
503 }
504
505 Ok(ids)
506 })
507 .await??;
508
509 Ok(ids)
510 }
511
512 #[instrument(skip(self))]
514 async fn get_ids_by_recent(&self, limit: i64) -> Result<Vec<i64>> {
515 let query = r"SELECT id FROM records
519ORDER BY timestamp_flora DESC
520LIMIT ?1;";
521
522 let conn = self.pool.get().await?;
523 let ids = conn
524 .interact(move |conn| -> Result<Vec<i64>> {
525 let mut stmt = conn.prepare(query)?;
526 let rows = stmt.query_map([limit], |row| row.get(0))?;
527 let mut ids = vec![];
528 for row in rows {
529 ids.push(row?);
530 }
531
532 Ok(ids)
533 })
534 .await??;
535
536 Ok(ids)
537 }
538
539 #[allow(clippy::missing_errors_doc)]
544 #[instrument(skip(self))]
545 pub async fn most_recent_meta(&self, start_idx: Option<usize>) -> Result<Vec<DbRecordMeta>> {
546 tracing::trace!("Selecting recent RecordMeta");
547
548 let limit = start_idx.unwrap_or_default();
550 let prev_ids = self.get_ids_by_recent(limit.try_into()?).await?;
551 let prev_ids = prev_ids
552 .iter()
553 .copied()
554 .map(Value::from)
555 .collect::<Vec<Value>>();
556
557 let query = r"
558SELECT id, url, client_datetime, timestamp_flora, title, language, updated_at
559FROM records
560WHERE id NOT IN rarray(?1)
561ORDER BY timestamp_flora DESC LIMIT ?2;";
562
563 let conn = self.get_conn_array().await?;
564 let records = conn
565 .interact(move |conn| -> Result<Vec<DbRecordMeta>> {
566 let ids = Rc::new(prev_ids);
567 let mut stmt = conn.prepare(query)?;
568 let records = try_query_db_records!(
569 stmt,
570 rusqlite::params![ids, SELECT_ALL_LIMIT],
571 DbRecordMeta
572 );
573 Ok(records)
574 })
575 .await??;
576
577 Ok(records)
578 }
579
580 #[instrument(skip_all, fields(record_id = record.id))]
585 pub async fn update_record_data(&self, record: DbRecord) -> Result<i64> {
586 let query = r"UPDATE records
587SET web_archive_serialized = :document, updated_at = :updated_at
588WHERE records.id = :id
589RETURNING id;";
590
591 let DbRecord {
592 id,
593 document,
594 updated_at,
595 ..
596 } = record;
597 let id = id.ok_or_else(|| DbErrorRepr::Update("missing id".into()))?;
598 let updated_at =
599 updated_at.ok_or_else(|| DbErrorRepr::Update("missing updated_at".into()))?;
600
601 let conn = self.pool.get().await?;
602 let row_id = conn
603 .interact(move |conn| {
604 let mut stmt = conn.prepare(query)?;
605 stmt.query_one(
606 rusqlite::named_params! {
607 ":id": id,
608 ":document": document,
609 ":updated_at": updated_at,
610 },
611 |row| row.get(0),
612 )
613 })
614 .await?
615 .inspect_err(|e| tracing::error!("Error on update: {e}"))?;
616 Ok(row_id)
617 }
618}
619
620#[cfg(test)]
621mod tests {
622 use super::*;
623 use crate::model::tests::test_record_params;
624 use anyhow::{Context, Result};
625 use chrono::{DateTime, FixedOffset, Utc};
626 use omry_archiving::record::Record;
627 use pretty_assertions::assert_eq;
628 use serde::Serialize;
629 use std::fs::File;
630 use std::ops::Deref;
631 use tempfile::NamedTempFile;
632
633 struct TempDb {
636 #[allow(dead_code)]
637 temp_db_file: NamedTempFile<File>,
638 db: Database,
639 }
640
641 impl TempDb {
642 async fn new() -> Result<Self> {
643 let temp_db_file = NamedTempFile::with_suffix(".db")?;
644 let db = Database::new_with_records_table(temp_db_file.path()).await?;
645 Ok(Self { temp_db_file, db })
646 }
647 }
648
649 impl Deref for TempDb {
650 type Target = Database;
651
652 fn deref(&self) -> &Self::Target {
653 &self.db
654 }
655 }
656
657 #[derive(Debug, Serialize)]
659 struct DbRecordDef {
660 id: Option<i64>,
661 url: String,
662 client_datetime: DateTime<FixedOffset>,
663 client_datetime_raw: Option<String>,
664 timestamp_flora: i64,
665 title: String,
666 language: Option<String>,
667 document: Vec<u8>,
668 updated_at: Option<i64>,
669 }
670
671 impl From<DbRecord> for DbRecordDef {
672 fn from(value: DbRecord) -> DbRecordDef {
673 let DbRecord {
674 id,
675 url,
676 client_datetime,
677 client_datetime_raw,
678 timestamp_flora,
679 title,
680 language,
681 document,
682 updated_at,
683 } = value;
684
685 Self {
686 id,
687 url,
688 client_datetime,
689 client_datetime_raw,
690 timestamp_flora,
691 title,
692 language,
693 document,
694 updated_at,
695 }
696 }
697 }
698
699 #[derive(Debug, Serialize)]
700 struct DbRecordMetaDef {
701 id: i64,
702 url: String,
703 client_datetime: DateTime<FixedOffset>,
704 timestamp_flora: i64,
705 title: String,
706 language: Option<String>,
707 updated_at: Option<i64>,
708 }
709
710 impl From<DbRecordMeta> for DbRecordMetaDef {
711 fn from(value: DbRecordMeta) -> Self {
712 let DbRecordMeta {
713 id,
714 url,
715 client_datetime,
716 timestamp_flora,
717 title,
718 language,
719 updated_at,
720 } = value;
721
722 Self {
723 id,
724 url,
725 client_datetime,
726 timestamp_flora,
727 title,
728 language,
729 updated_at,
730 }
731 }
732 }
733
734 #[allow(dead_code, reason = "Used in snapshot.")]
735 #[derive(Debug)]
736 struct RowInfo {
737 name: String,
738 type_: String,
739 notnull: bool,
740 }
741
742 impl TryFrom<&Row<'_>> for RowInfo {
743 type Error = rusqlite::Error;
744
745 fn try_from(row: &Row) -> std::result::Result<Self, Self::Error> {
746 let (name, type_, notnull) = (row.get(0)?, row.get(1)?, row.get(2)?);
747 Ok(Self {
748 name,
749 type_,
750 notnull,
751 })
752 }
753 }
754
755 fn test_db_record() -> Result<DbRecord> {
756 let record = Record::new(test_record_params()?);
757 Ok(record.try_into()?)
758 }
759
760 #[tokio::test]
761 async fn can_create_table() -> Result<()> {
762 let db = TempDb::new().await?;
763 let query = r#"SELECT name, type, "notnull" FROM pragma_table_info('records');"#;
764 let conn = db.pool.get().await?;
765 let row_infos = conn
766 .interact(move |conn| -> Result<Vec<RowInfo>> {
767 let mut stmt = conn.prepare(query)?;
768 let rows = stmt.query_map([], |row| RowInfo::try_from(row))?;
769 let mut values = vec![];
770 for row in rows {
771 values.push(row?);
772 }
773 Ok(values)
774 })
775 .await
776 .map_err(|e| anyhow::anyhow!("{e}"))??;
777 insta::assert_debug_snapshot!(row_infos);
778 Ok(())
779 }
780
781 #[tokio::test]
782 async fn can_add_record() -> Result<()> {
783 let db = TempDb::new().await?;
784 let record = test_db_record()?;
785 let result = db.insert(record).await?;
786 assert_eq!(result, 1);
787 Ok(())
788 }
789
790 #[tokio::test]
791 async fn can_get_record() -> Result<()> {
792 let db = TempDb::new().await?;
793 let record = test_db_record()?;
794 assert!(record.id.is_none());
795 db.insert(record).await?;
796
797 let expected_id = 1i64;
798 let retrieved_record = db.get_record(expected_id).await?;
799
800 let retrieved_record: DbRecordDef = retrieved_record.into();
801 insta::assert_ron_snapshot!(retrieved_record, {
802 ".client_datetime" => "[client_datetime]",
803 ".timestamp_flora" => "[timestamp]"
804 });
805
806 Ok(())
807 }
808
809 #[tokio::test]
810 async fn can_get_many_records() -> Result<()> {
811 let db = TempDb::new().await?;
812
813 let num_records_to_insert = 7;
814 let mut record_ids = Vec::with_capacity(num_records_to_insert);
815
816 for _ in 0..num_records_to_insert {
817 let record = test_db_record()?;
818 let record_id = db.insert(record).await?;
819 record_ids.push(record_id);
820 }
821
822 let selected_records = db
823 .get_many_records(&record_ids)
824 .await?
825 .into_iter()
826 .map(DbRecordDef::from)
827 .collect::<Vec<_>>();
828
829 assert_eq!(selected_records.len(), num_records_to_insert);
830
831 insta::assert_ron_snapshot!(selected_records, {
832 "[].client_datetime" => "[client_datetime]",
833 "[].timestamp_flora" => "[timestamp]"
834 });
835
836 Ok(())
837 }
838
839 #[tokio::test]
840 async fn passing_too_many_bind_parameters_returns_error() -> Result<()> {
841 let db = TempDb::new().await?;
842
843 let num_records_to_insert = MAX_BIND_PARAMS + 1;
844 db.insert(test_db_record()?).await?;
845
846 let mut ids = Vec::with_capacity(num_records_to_insert);
847 for index in 0..num_records_to_insert {
848 ids.push(i64::try_from(index + 1)?);
849 }
850
851 let result = db.get_many_records(&ids).await;
852 assert!(result.is_err());
853
854 #[allow(clippy::unwrap_used)]
856 let error = result.unwrap_err();
857 insta::assert_snapshot!(error);
858
859 Ok(())
860 }
861
862 #[tokio::test]
863 async fn can_get_count() -> Result<()> {
864 let db = TempDb::new().await?;
865
866 let num_records_to_insert = 7;
867 let mut record_ids = Vec::with_capacity(num_records_to_insert);
868
869 for _ in 0..num_records_to_insert {
870 let record = test_db_record()?;
871 let record_id = db.insert(record).await?;
872 record_ids.push(record_id);
873 }
874
875 let count: usize = db.count().await?.try_into()?;
876
877 assert_eq!(count, num_records_to_insert);
878 Ok(())
879 }
880
881 #[tokio::test]
882 async fn can_delete_record() -> Result<()> {
883 let db = TempDb::new().await?;
884 let record = test_db_record()?;
885 let id = db.insert(record).await?;
886 let rows_changed = db.delete(id).await?;
887 assert_eq!(rows_changed, 1);
888 Ok(())
889 }
890
891 #[tokio::test]
892 async fn can_get_all_records() -> Result<()> {
893 let db = TempDb::new().await?;
894
895 let num_records_to_insert = (SELECT_ALL_LIMIT + 5).try_into()?;
897 for _ in 0..num_records_to_insert {
898 let record = test_db_record()?;
899 let _record_id = db.insert(record).await?;
900 }
901
902 let mut db_recs = Vec::with_capacity(num_records_to_insert);
903 let mut start_row = None;
904 loop {
905 let mut selected_records = db
906 .get_all_records(start_row)
907 .await?
908 .into_iter()
909 .map(DbRecordDef::from)
910 .collect::<Vec<_>>();
911
912 if selected_records.is_empty() {
913 break;
914 }
915
916 db_recs.append(&mut selected_records);
917 start_row = Some(db_recs.len().try_into()?);
918 }
919
920 assert_eq!(db_recs.len(), num_records_to_insert);
921
922 insta::assert_ron_snapshot!(db_recs, {
923 "[].client_datetime" => "[client_datetime]",
924 "[].timestamp_flora" => "[timestamp]",
925 "[].document" => "[data]"
926 });
927
928 Ok(())
929 }
930
931 #[tokio::test]
934 async fn can_get_all_records_after_deleting_some() -> Result<()> {
935 let db = TempDb::new().await?;
936
937 let num_records_to_insert = (SELECT_ALL_LIMIT + 5).try_into()?;
939 let mut record_ids = Vec::with_capacity(num_records_to_insert);
940
941 for _ in 0..num_records_to_insert {
942 let record = test_db_record()?;
943 let record_id = db.insert(record).await?;
944 record_ids.push(record_id);
945 }
946
947 let idx_to_delete = [3, 7, 11];
951 for idx in idx_to_delete {
952 let id = record_ids.remove(idx);
953 db.delete(id).await?;
954 }
955
956 let mut db_recs = Vec::with_capacity(num_records_to_insert);
957 let mut start_row = None;
958 loop {
959 let mut selected_records = db
960 .get_all_records(start_row)
961 .await?
962 .into_iter()
963 .map(DbRecordDef::from)
964 .collect::<Vec<_>>();
965
966 if selected_records.is_empty() {
967 break;
968 }
969
970 db_recs.append(&mut selected_records);
971 start_row = Some(db_recs.len().try_into()?);
972 }
973
974 assert_eq!(db_recs.len(), record_ids.len());
975 insta::assert_ron_snapshot!(db_recs, {
976 "[].client_datetime" => "[client_datetime]",
977 "[].timestamp_flora" => "[timestamp]",
978 "[].document" => "[data]"
979 });
980
981 Ok(())
982 }
983
984 #[tokio::test]
985 async fn can_get_all_meta() -> Result<()> {
986 let db = TempDb::new().await?;
987
988 let num_records_to_insert = (SELECT_ALL_LIMIT + 1).try_into()?;
989 let mut record_ids = Vec::with_capacity(num_records_to_insert);
990
991 for _ in 0..num_records_to_insert {
992 let record = test_db_record()?;
993 let record_id = db.insert(record).await?;
994 record_ids.push(record_id);
995 }
996
997 let mut start_row = None;
998 let mut db_recs = Vec::with_capacity(num_records_to_insert);
999 loop {
1000 let mut selected_recs = db
1001 .get_all_meta(start_row)
1002 .await?
1003 .into_iter()
1004 .map(DbRecordMetaDef::from)
1005 .collect::<Vec<_>>();
1006
1007 if selected_recs.is_empty() {
1008 break;
1009 }
1010
1011 db_recs.append(&mut selected_recs);
1012 start_row = Some(db_recs.len().try_into()?);
1013 }
1014
1015 assert_eq!(db_recs.len(), num_records_to_insert);
1016
1017 insta::assert_ron_snapshot!(db_recs, {
1018 "[].client_datetime" => "[client_datetime]",
1019 "[].timestamp_flora" => "[timestamp]",
1020 });
1021
1022 Ok(())
1023 }
1024
1025 #[tokio::test]
1026 async fn can_get_all_ids() -> Result<()> {
1027 let db = TempDb::new().await?;
1028
1029 let num_records_to_insert = 7;
1030 let mut record_ids = Vec::with_capacity(num_records_to_insert);
1031
1032 for _ in 0..num_records_to_insert {
1033 let record = test_db_record()?;
1034 let record_id = db.insert(record).await?;
1035 record_ids.push(record_id);
1036 }
1037
1038 let db_ids = db.get_all_ids().await?;
1039 assert_eq!(db_ids, record_ids);
1040 Ok(())
1041 }
1042
1043 #[tokio::test]
1044 async fn can_get_most_recent_meta() -> Result<()> {
1045 let db = TempDb::new().await?;
1046
1047 let num_recs_to_insert = (SELECT_ALL_LIMIT + 5).try_into()?;
1048 let ts_incr = 5;
1049 let mut record_ids = Vec::with_capacity(num_recs_to_insert);
1050
1051 let mut cur_timestamp = Utc::now().timestamp();
1052 for _ in 0..num_recs_to_insert {
1053 let mut rec = test_db_record()?;
1054 rec.timestamp_flora = cur_timestamp;
1055 let record_id = db.insert(rec).await?;
1056 record_ids.push(record_id);
1057 cur_timestamp += ts_incr;
1058 }
1059
1060 let mut recs = Vec::with_capacity(num_recs_to_insert);
1061 let mut start_idx = None;
1062 loop {
1063 let mut selected_recs = db
1064 .most_recent_meta(start_idx)
1065 .await?
1066 .into_iter()
1067 .map(DbRecordMetaDef::from)
1068 .collect::<Vec<_>>();
1069
1070 if selected_recs.is_empty() {
1071 break;
1072 }
1073
1074 recs.append(&mut selected_recs);
1075 start_idx = Some(recs.len());
1076 }
1077
1078 assert_eq!(recs.len(), num_recs_to_insert);
1079 let timestamps = recs.iter().map(|r| r.timestamp_flora).collect::<Vec<_>>();
1080 assert!(
1081 recs.is_sorted_by(|a, b| a.timestamp_flora > b.timestamp_flora),
1082 "{timestamps:#?}"
1083 );
1084 Ok(())
1085 }
1086
1087 #[tokio::test]
1088 async fn can_update_record() -> Result<()> {
1089 let db = TempDb::new().await?;
1090 let record = test_db_record()?;
1091 let id = db.insert(record).await.context("error inserting record")?;
1092
1093 let mut record = db.get_record(id).await.context("error getting record")?;
1095 record.updated_at = Some(Utc::now().timestamp());
1096 let updated_id = db
1097 .update_record_data(record)
1098 .await
1099 .context("error updating record data")?;
1100 assert_eq!(id, updated_id);
1101 Ok(())
1102 }
1103}