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