use std::fmt;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use auditor::domain::{RecordAdd, RecordUpdate};
use sqlx::{QueryBuilder, Sqlite, SqlitePool, sqlite::SqliteJournalMode};
const BULK_SIZE: usize = 16384;
fn is_path_valid(path: &Path) -> bool {
path.to_str().is_some_and(|s| !s.is_empty()) && path.try_exists().is_ok()
}
#[derive(Clone)]
pub(crate) struct Database {
db_pool: SqlitePool,
}
impl Database {
#[tracing::instrument(name = "Initializing sqlite database connection", level = "debug")]
pub(crate) async fn new<S: AsRef<str> + fmt::Debug>(path: S) -> Result<Database, sqlx::Error> {
if !is_path_valid(&PathBuf::from(path.as_ref())) {
tracing::error!("Invalid path for database: {:?}", path);
return Err(sqlx::Error::Io(std::io::Error::from(
std::io::ErrorKind::Other,
)));
};
let db_pool = SqlitePool::connect_with(
sqlx::sqlite::SqliteConnectOptions::from_str(path.as_ref())?
.journal_mode(SqliteJournalMode::Wal)
.create_if_missing(true),
)
.await?;
tracing::debug!("Migrating database");
sqlx::migrate!().run(&db_pool).await?;
Ok(Database { db_pool })
}
#[tracing::instrument(
name = "Inserting record into database",
level = "debug",
skip(self, record),
fields(record_id = %record.record_id)
)]
pub(crate) async fn insert(&self, record: &RecordAdd) -> Result<(), sqlx::Error> {
let record = bincode::serialize(record).expect("Should never fail on a record");
sqlx::query!(
r#"INSERT OR IGNORE INTO inserts (record) VALUES ($1)"#,
record
)
.execute(&self.db_pool)
.await?;
Ok(())
}
#[tracing::instrument(
name = "Bulk inserting records into database",
level = "debug",
skip(self, records)
)]
pub(crate) async fn insert_many(&self, records: &[RecordAdd]) -> Result<(), sqlx::Error> {
for chunk in records.chunks(BULK_SIZE) {
let mut query_builder: QueryBuilder<Sqlite> =
QueryBuilder::new("INSERT OR IGNORE INTO inserts (record) ");
let blobs = chunk
.iter()
.map(|r| bincode::serialize(&r).expect("Should never fail on a record"));
query_builder.push_values(blobs, |mut b, blob| {
b.push_bind(blob);
});
query_builder.build().execute(&self.db_pool).await?;
}
Ok(())
}
#[tracing::instrument(
name = "Updating record in database",
level = "debug",
skip(self, record),
fields(record_id = %record.record_id)
)]
pub(crate) async fn update(&self, record: &RecordUpdate) -> Result<(), sqlx::Error> {
let record = bincode::serialize(record).expect("Should never fail on a record");
sqlx::query!(r#"INSERT INTO updates (record) VALUES ($1)"#, record)
.execute(&self.db_pool)
.await?;
Ok(())
}
#[allow(dead_code)]
#[tracing::instrument(name = "Deleting records from database", level = "debug", skip(self))]
pub(crate) async fn delete_inserts_le(&self, rowid: i64) -> Result<(), sqlx::Error> {
sqlx::query!(r#"DELETE FROM inserts WHERE rowid<=$1"#, rowid)
.execute(&self.db_pool)
.await?;
Ok(())
}
#[tracing::instrument(name = "Deleting record from database", level = "debug", skip(self))]
pub(crate) async fn delete_insert(&self, rowid: i64) -> Result<(), sqlx::Error> {
sqlx::query!(r#"DELETE FROM inserts WHERE rowid=$1"#, rowid)
.execute(&self.db_pool)
.await?;
Ok(())
}
#[tracing::instrument(name = "Deleting record from database", level = "debug", skip(self))]
pub(crate) async fn delete_update(&self, rowid: i64) -> Result<(), sqlx::Error> {
sqlx::query!(r#"DELETE FROM updates WHERE rowid=$1"#, rowid)
.execute(&self.db_pool)
.await?;
Ok(())
}
#[tracing::instrument(
name = "Getting insert records from database",
level = "debug",
skip(self)
)]
pub(crate) async fn get_inserts(&self) -> Result<Vec<(i64, RecordAdd)>, sqlx::Error> {
struct Row {
rowid: i64,
record: Vec<u8>,
}
let rows: Vec<Row> = sqlx::query_as!(
Row,
r#"SELECT rowid, record FROM inserts ORDER BY rowid ASC"#
)
.fetch_all(&self.db_pool)
.await?;
let records = rows
.into_iter()
.map(|Row { rowid, record }| {
(rowid, bincode::deserialize::<RecordAdd>(&record).unwrap())
})
.collect();
Ok(records)
}
#[tracing::instrument(
name = "Getting update records from database",
level = "debug",
skip(self)
)]
pub(crate) async fn get_updates(&self) -> Result<Vec<(i64, RecordUpdate)>, sqlx::Error> {
struct Row {
rowid: i64,
record: Vec<u8>,
}
let rows: Vec<Row> = sqlx::query_as!(
Row,
r#"SELECT rowid, record FROM updates ORDER BY rowid ASC"#
)
.fetch_all(&self.db_pool)
.await?;
let records = rows
.into_iter()
.map(|Row { rowid, record }| {
(
rowid,
bincode::deserialize::<RecordUpdate>(&record).unwrap(),
)
})
.collect();
Ok(records)
}
#[allow(dead_code)]
#[tracing::instrument(
name = "Getting update records from database",
level = "debug",
skip(self)
)]
pub(crate) async fn get_updates_le(
&self,
rowid: i64,
) -> Result<Vec<(i64, RecordUpdate)>, sqlx::Error> {
struct Row {
rowid: i64,
record: Vec<u8>,
}
let rows: Vec<Row> = sqlx::query_as!(
Row,
r#"SELECT rowid, record FROM updates WHERE rowid<=$1 ORDER BY rowid ASC"#,
rowid
)
.fetch_all(&self.db_pool)
.await?;
let records = rows
.into_iter()
.map(|Row { rowid, record }| {
(
rowid,
bincode::deserialize::<RecordUpdate>(&record).unwrap(),
)
})
.collect();
Ok(records)
}
#[tracing::instrument(name = "Getting highest update id", level = "debug", skip(self))]
pub(crate) async fn get_last_update_rowid(&self) -> Result<Option<i64>, sqlx::Error> {
struct Row {
id: Option<i64>,
}
let row = sqlx::query_as!(Row, r#"SELECT max(rowid) as id FROM updates"#)
.fetch_one(&self.db_pool)
.await?;
Ok(row.id)
}
#[allow(dead_code)]
#[tracing::instrument(name = "Closing database connection", level = "debug", skip(self))]
pub(crate) async fn close(&self) {
self.db_pool.close().await
}
}
#[cfg(test)]
mod tests {
use super::*;
use auditor::domain::{Record, RecordTest};
use fake::{Fake, Faker};
fn record<T: TryFrom<RecordTest>>() -> T
where
<T as TryFrom<RecordTest>>::Error: std::fmt::Debug,
{
T::try_from(Faker.fake::<RecordTest>()).unwrap()
}
#[tokio::test]
async fn insert_get() {
let db = Database::new("sqlite://:memory:").await.unwrap();
let rec = record();
db.insert(&rec).await.unwrap();
let mut res = db.get_inserts().await.unwrap();
let (_, res) = res.pop().unwrap();
assert_eq!(Record::from(res), Record::from(rec));
}
#[tokio::test]
async fn update_get() {
let db = Database::new("sqlite://:memory:").await.unwrap();
let rec = record();
db.update(&rec).await.unwrap();
let mut res = db.get_updates().await.unwrap();
let (_, res) = res.pop().unwrap();
assert_eq!(Record::from(res), Record::from(rec));
}
#[tokio::test]
async fn insert_many_get() {
let db = Database::new("sqlite://:memory:").await.unwrap();
let recs: Vec<RecordAdd> = (0..10).map(|_| record()).collect();
db.insert_many(&recs).await.unwrap();
let res = db.get_inserts().await.unwrap();
assert_eq!(res.len(), 10);
assert_eq!(recs.len(), 10);
res.into_iter()
.map(|(_, r)| r)
.zip(recs)
.for_each(|(a, b)| assert_eq!(Record::from(a), Record::from(b)));
}
#[tokio::test]
async fn update_get_le() {
let db = Database::new("sqlite://:memory:").await.unwrap();
let recs: Vec<_> = (0..10).map(|_| record()).collect();
for r in recs.iter() {
db.update(r).await.unwrap()
}
let res = db.get_updates_le(5).await.unwrap();
assert_eq!(res.len(), 5);
assert_eq!(recs.len(), 10);
res.into_iter()
.map(|(_, r)| r)
.zip(recs)
.for_each(|(a, b)| assert_eq!(Record::from(a), Record::from(b)));
}
#[tokio::test]
async fn insert_many_delete() {
let db = Database::new("sqlite://:memory:").await.unwrap();
let recs: Vec<RecordAdd> = (0..10).map(|_| record()).collect();
db.insert_many(&recs).await.unwrap();
db.delete_inserts_le(5).await.unwrap();
let res = db.get_inserts().await.unwrap();
assert_eq!(res.len(), 5);
res.into_iter()
.map(|(_, r)| r)
.zip(recs.into_iter().skip(5))
.for_each(|(a, b)| assert_eq!(Record::from(a), Record::from(b)));
}
#[tokio::test]
async fn update_delete() {
let db = Database::new("sqlite://:memory:").await.unwrap();
let mut recs: Vec<_> = (0..10).map(|_| record()).collect();
for r in recs.iter() {
db.update(r).await.unwrap()
}
db.delete_update(5).await.unwrap();
let res = db.get_updates().await.unwrap();
recs.remove(4);
assert_eq!(res.len(), 9);
res.into_iter()
.map(|(_, r)| r)
.zip(recs)
.for_each(|(a, b)| assert_eq!(Record::from(a), Record::from(b)));
}
#[tokio::test]
async fn update_rowid() {
let db = Database::new("sqlite://:memory:").await.unwrap();
let recs: Vec<_> = (0..10).map(|_| record()).collect();
for r in recs.iter() {
db.update(r).await.unwrap()
}
let rowid = db.get_last_update_rowid().await.unwrap().unwrap();
assert_eq!(rowid, 10);
}
}