Skip to main content

omry_db/
db.rs

1//! Database abstractions.
2#![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
16/// Maximum number of 'bound' (programmatically populated)
17/// parameters allowed in a query.
18const MAX_BIND_PARAMS: usize = 32766;
19
20/// Limit used when selecting all records.
21const SELECT_ALL_LIMIT: i64 = 50;
22
23/// Tries to query for rows in `records` using the given statement, and convert them to [`DbRecord`].
24macro_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/// Errors that may occur when interacting with database APIs.
62#[derive(Debug, Error)]
63#[error(transparent)]
64pub struct DatabaseError(DbErrorRepr);
65
66// Implement this manually so we can still conveniently use ? with errors
67// that map onto variants of DatabaseErrorRepr
68// (using #[from] would conflict with this).
69impl<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/// Database errors.
79#[derive(Error, Debug)]
80enum DbErrorRepr {
81    /// Error when creating the database connection pool.
82    #[error("error creating the DB pool: {0}")]
83    CreatePool(#[from] deadpool_sqlite::CreatePoolError),
84
85    /// Error when interacting with the database pool.
86    // This error has a string payload and a manual implementation of [`From`]
87    // because `deadpool_sqlite::InteractError` isn't Sync.
88    #[error("error interacting with the DB pool: {0}")]
89    PoolInteract(String),
90
91    /// Error when getting a new connection from the database pool.
92    #[error("error getting connection from the DB pool: {0}")]
93    Pool(#[from] deadpool_sqlite::PoolError),
94
95    /// Any unspecified error returned by [`rusqlite`].
96    #[error("error in rusqlite: {0}")]
97    Rusqlite(#[from] rusqlite::Error),
98
99    /// Error returned when a query function was given too many
100    /// parameters to bind.
101    #[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 activating write-ahead logging.
107    #[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
123/// Alias for results returned by the database APIs.
124pub 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/// Abstraction for flora's primary datastore (SQLite).
160#[derive(Debug, Clone)]
161pub struct Database {
162    /// The connection pool.
163    pool: Pool,
164}
165
166impl Database {
167    /// Connects to the SQLite database at the given URL and creates a new `Database`.
168    ///
169    /// # Errors
170    /// Returns an instance of [`DatabaseError`] on error.
171    #[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    /// Gets a connection, and calls [`rusqlite::vtab::array::load_module`] with it.
179    ///
180    /// This method should be called to get any connection that will be used
181    /// to perform queries using the `array` extension of [`rusqlite`].
182    ///
183    /// See <https://docs.rs/rusqlite/latest/rusqlite/vtab/array/index.html>.
184    #[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    /// Creates a table to store the records.
198    #[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    /// Enables the WAL
208    ///
209    /// See <https://sqlite.org/wal.html>
210    ///
211    /// # Errors
212    ///
213    /// Returns [`DatabaseError`] if WAL activation fails.
214    #[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    /// Initializes the database, activates WAL, creates a 'records' table, and creates indexes.
236    ///
237    /// # Errors
238    /// Returns [`DatabaseError`] if the operation fails.
239    #[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    /// Queries the database to get a count of records it contains.
248    ///
249    /// # Errors
250    /// Returns [`DatabaseError`] if the operation fails.
251    #[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    /// Inserts the given record into the `Database`.
267    ///
268    /// Returns the ID of the inserted row.
269    ///
270    /// # Errors
271    /// Returns [`DatabaseError`] if the insert operation fails.
272    #[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    /// Deletes the record corresponding to the given id.
317    ///
318    /// Returns the number of documents affected
319    /// (should be 1 if a document matching the id was deleted,
320    /// and 0 if no matching document was found in the DB).
321    ///
322    /// # Errors
323    /// Returns [`DatabaseError`] if the operation fails.
324    #[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    /// Get a record from SQLite given an id.
340    ///
341    /// # Errors
342    /// Returns [`DatabaseError`] if the operation fails.
343    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    /// Get multiple records from SQLite given their IDs.
363    ///
364    /// # Errors
365    /// Returns [`DatabaseError`] if the operation fails.
366    #[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    /// Selects all rows in the `records` table, without any pagination.
408    ///
409    /// # Errors
410    ///
411    /// Returns [`DatabaseError`] if the operation fails.
412    #[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        // pagination adapted from <https://gist.github.com/ssokolow/262503>
417        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        // start at 0 if we aren't given an explicit start row
423        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    /// Selects all rows in the `records` table, without any pagination,
437    /// and returns their metadata (all fields except serialized data
438    /// of the page content).
439    ///
440    /// # Errors
441    ///
442    /// Returns [`DatabaseError`] if the operation fails.
443    #[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    /// Returns all record IDs in SQLite.
469    ///
470    /// # Errors
471    ///
472    /// Returns [`DatabaseError`] if the operation fails.
473    #[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    /// Returns all record IDs in descending order.
497    #[instrument(skip(self))]
498    async fn get_all_ids_by_recent(&self) -> Result<Vec<i64>> {
499        // order by timestamp because in rare cases IDs might not
500        // be monotonically increasing
501        // (see <https://sqlite.org/autoinc.html>)
502        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    /// Similar to [`Database::get_all_meta`], except orders records by
523    /// descending order of `timestamp_flora`.
524    ///
525    /// Returns an empty list if the given index is out of bounds.
526    #[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            // all IDs we haven't yet seen will be to the left of this point
541            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    /// Updates the record by ID.
576    ///
577    /// # Errors
578    /// Returns [`DatabaseError`] if the update operation fails.
579    #[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    /// Data structure to keep the temporary file holding the database alive,
629    /// because it will get unlinked on drop.
630    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    /// Wrapper for [`DbRecord`] that supports [`Serialize`] for redacting snapshots.
653    #[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        // At this point, we know this is definitely an error.
850        #[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        // Insert more than the limit to test pagination.
891        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    /// This tests that we can still get all records
927    /// after deleting some from the middle (causing IDs to become incontiguous).
928    #[tokio::test]
929    async fn can_get_all_records_after_deleting_some() -> Result<()> {
930        let db = TempDb::new().await?;
931
932        // Insert more than the limit to test pagination.
933        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        // remove IDs from `record_ids` to assert that
943        // `record_ids` length matches the number of records
944        // we get back in the end
945        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        // workaround for DbRecord not being `Clone`
1089        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}