use std::{collections::HashMap, fs::metadata, path::Path};
use rusqlite::{CachedStatement, Connection, Error, OpenFlags, Result, Row, blob::Blob};
use crate::error::table::{TableConnectError, TableError};
pub trait Table: Sized {
fn from_row(row: &Row) -> Result<Self>;
fn get(db: &'_ Connection) -> Result<CachedStatement<'_>, TableError>;
fn extract(item: Result<Result<Self, Error>, Error>) -> Result<Self, TableError> {
match item {
Ok(Ok(row)) => Ok(row),
Err(why) | Ok(Err(why)) => Err(TableError::QueryError(why)),
}
}
fn stream<F, E>(db: &Connection, callback: F) -> Result<(), E>
where
E: From<TableError>,
F: FnMut(Result<Self, TableError>) -> Result<(), E>,
{
stream_table_callback::<Self, F, E>(db, callback)
}
fn get_blob<'a>(
&self,
db: &'a Connection,
table: &str,
column: &str,
rowid: i64,
) -> Option<Blob<'a>> {
db.blob_open(rusqlite::MAIN_DB, table, column, rowid, true)
.ok()
}
fn has_blob(&self, db: &Connection, table: &str, column: &str, rowid: i64) -> bool {
let sql = std::format!(
"SELECT ({column} IS NOT NULL) AS not_null
FROM {table}
WHERE rowid = ?1",
);
db.query_row(&sql, [rowid], |row| row.get(0))
.ok()
.is_some_and(|v: i32| v != 0)
}
}
fn stream_table_callback<T, F, E>(db: &Connection, mut callback: F) -> Result<(), E>
where
T: Table + Sized,
E: From<TableError>,
F: FnMut(Result<T, TableError>) -> Result<(), E>,
{
let mut stmt = T::get(db).map_err(E::from)?;
let rows = stmt
.query_map([], |row| Ok(T::from_row(row)))
.map_err(TableError::from)
.map_err(E::from)?;
for row_result in rows {
let item_result = T::extract(row_result);
callback(item_result)?;
}
Ok(())
}
pub trait Cacheable {
type K;
type V;
fn cache(db: &Connection) -> Result<HashMap<Self::K, Self::V>, TableError>;
}
pub fn get_connection(path: &Path) -> Result<Connection, TableError> {
if path.exists() && path.is_file() {
return match Connection::open_with_flags(
path,
OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
) {
Ok(res) => Ok(res),
Err(why) => Err(TableError::CannotConnect(TableConnectError::Permissions(
why,
))),
};
}
if path.exists() && !path.is_file() {
return Err(TableError::CannotConnect(TableConnectError::NotAFile(
path.to_path_buf(),
)));
}
Err(TableError::CannotConnect(TableConnectError::DoesNotExist(
path.to_path_buf(),
)))
}
pub fn get_db_size(path: &Path) -> Result<u64, TableError> {
Ok(metadata(path)?.len())
}
pub const HANDLE: &str = "handle";
pub const MESSAGE: &str = "message";
pub const CHAT: &str = "chat";
pub const ATTACHMENT: &str = "attachment";
pub const CHAT_MESSAGE_JOIN: &str = "chat_message_join";
pub const MESSAGE_ATTACHMENT_JOIN: &str = "message_attachment_join";
pub const CHAT_HANDLE_JOIN: &str = "chat_handle_join";
pub const RECENTLY_DELETED: &str = "chat_recoverable_message_join";
pub const MESSAGE_PAYLOAD: &str = "payload_data";
pub const MESSAGE_SUMMARY_INFO: &str = "message_summary_info";
pub const ATTRIBUTED_BODY: &str = "attributedBody";
pub const STICKER_USER_INFO: &str = "sticker_user_info";
pub const ATTRIBUTION_INFO: &str = "attribution_info";
pub const PROPERTIES: &str = "properties";
pub const ME: &str = "Me";
pub const YOU: &str = "You";
pub const UNKNOWN: &str = "Unknown";
pub const DEFAULT_PATH_MACOS: &str = "Library/Messages/chat.db";
pub const DEFAULT_PATH_IOS: &str = "3d/3d0d7e5fb2ce288813306e4d4636395e047a3d28";
pub const ORPHANED: &str = "orphaned";
pub const FITNESS_RECEIVER: &str = "$(kIMTranscriptPluginBreadcrumbTextReceiverIdentifier)";
pub const ATTACHMENTS_DIR: &str = "attachments";
#[cfg(test)]
mod tests {
use rusqlite::{CachedStatement, Connection, Result, Row};
use crate::error::table::TableError;
use super::Table;
struct TestRow(i64);
impl Table for TestRow {
fn from_row(row: &Row) -> Result<Self> {
Ok(Self(row.get(0)?))
}
fn get(db: &'_ Connection) -> Result<CachedStatement<'_>, TableError> {
Ok(db.prepare_cached("SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3")?)
}
}
#[derive(Debug)]
enum StreamError {
Table(TableError),
Stop,
}
impl From<TableError> for StreamError {
fn from(err: TableError) -> Self {
Self::Table(err)
}
}
#[test]
fn stream_propagates_callback_errors() {
let db = Connection::open_in_memory().unwrap();
let mut seen = vec![];
let result = TestRow::stream(&db, |row| {
let row = row.map_err(StreamError::from)?;
seen.push(row.0);
if row.0 == 2 {
return Err(StreamError::Stop);
}
Ok(())
});
assert!(matches!(result, Err(StreamError::Stop)));
assert_eq!(seen, vec![1, 2]);
}
#[test]
fn stream_converts_setup_errors() {
struct BrokenTable;
impl Table for BrokenTable {
fn from_row(_row: &Row) -> Result<Self> {
Ok(Self)
}
fn get(_db: &'_ Connection) -> Result<CachedStatement<'_>, TableError> {
Err(TableError::CannotRead(std::io::Error::other("boom")))
}
}
let db = Connection::open_in_memory().unwrap();
let result = BrokenTable::stream(&db, |_| Ok::<(), StreamError>(()));
assert!(matches!(
result,
Err(StreamError::Table(TableError::CannotRead(_)))
));
}
}