mod schema;
use async_trait::async_trait;
use origin_domain::{AppError, Result};
use origin_storage::{Record, Storage, StorageKey};
use rusqlite::{Connection, OptionalExtension, params};
use std::path::Path;
use std::sync::{Arc, Mutex};
use time::OffsetDateTime;
use time::format_description::well_known::Rfc3339;
#[derive(Debug, Clone)]
pub struct SqliteStorage {
connection: Arc<Mutex<Connection>>,
}
impl SqliteStorage {
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
let connection = Connection::open(path.as_ref()).map_err(to_storage_error)?;
Self::from_connection(connection)
}
pub fn in_memory() -> Result<Self> {
let connection = Connection::open_in_memory().map_err(to_storage_error)?;
Self::from_connection(connection)
}
fn from_connection(connection: Connection) -> Result<Self> {
schema::apply(&connection)?;
Ok(Self {
connection: Arc::new(Mutex::new(connection)),
})
}
pub async fn prune_expired(&self, now: OffsetDateTime) -> Result<usize> {
let now = encode_time(now)?;
self.with_connection(move |connection| {
let removed = connection
.execute(
"DELETE FROM records WHERE expires_at IS NOT NULL AND expires_at <= ?1",
params![now],
)
.map_err(to_storage_error)?;
Ok(removed)
})
.await
}
async fn with_connection<T, F>(&self, operation: F) -> Result<T>
where
T: Send + 'static,
F: FnOnce(&Connection) -> Result<T> + Send + 'static,
{
let connection = self.connection.clone();
tokio::task::spawn_blocking(move || {
let guard = connection
.lock()
.map_err(|_| AppError::storage("sqlite connection poisoned"))?;
operation(&guard)
})
.await
.map_err(|error| AppError::storage(format!("storage task failed: {error}")))?
}
}
#[async_trait]
impl Storage for SqliteStorage {
async fn get(&self, key: &StorageKey) -> Result<Option<Record>> {
let (namespace, name) = split(key);
self.with_connection(move |connection| {
connection
.query_row(
"SELECT value, stored_at, expires_at FROM records \
WHERE namespace = ?1 AND key = ?2",
params![namespace, name],
|row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, Option<String>>(2)?,
))
},
)
.optional()
.map_err(to_storage_error)?
.map(|(value, stored_at, expires_at)| {
let mut record = Record::new(value, decode_time(&stored_at)?);
record.expires_at = expires_at.as_deref().map(decode_time).transpose()?;
Ok(record)
})
.transpose()
})
.await
}
async fn put(&self, key: &StorageKey, record: Record) -> Result<()> {
let (namespace, name) = split(key);
let stored_at = encode_time(record.stored_at)?;
let expires_at = record.expires_at.map(encode_time).transpose()?;
let value = record.value;
self.with_connection(move |connection| {
connection
.execute(
"INSERT INTO records (namespace, key, value, stored_at, expires_at) \
VALUES (?1, ?2, ?3, ?4, ?5) \
ON CONFLICT(namespace, key) DO UPDATE SET \
value = excluded.value, \
stored_at = excluded.stored_at, \
expires_at = excluded.expires_at",
params![namespace, name, value, stored_at, expires_at],
)
.map_err(to_storage_error)?;
Ok(())
})
.await
}
async fn delete(&self, key: &StorageKey) -> Result<()> {
let (namespace, name) = split(key);
self.with_connection(move |connection| {
connection
.execute(
"DELETE FROM records WHERE namespace = ?1 AND key = ?2",
params![namespace, name],
)
.map_err(to_storage_error)?;
Ok(())
})
.await
}
async fn keys(&self, namespace: &str) -> Result<Vec<StorageKey>> {
let namespace = namespace.to_owned();
self.with_connection(move |connection| {
let mut statement = connection
.prepare("SELECT key FROM records WHERE namespace = ?1")
.map_err(to_storage_error)?;
let keys = statement
.query_map(params![namespace], |row| row.get::<_, String>(0))
.map_err(to_storage_error)?
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(to_storage_error)?
.into_iter()
.map(|key| StorageKey::new(&namespace, key))
.collect();
Ok(keys)
})
.await
}
async fn clear(&self, namespace: &str) -> Result<()> {
let namespace = namespace.to_owned();
self.with_connection(move |connection| {
connection
.execute(
"DELETE FROM records WHERE namespace = ?1",
params![namespace],
)
.map_err(to_storage_error)?;
Ok(())
})
.await
}
async fn clear_prefix(&self, prefix: &str) -> Result<usize> {
let pattern = format!("{}*", glob_escape(prefix));
self.with_connection(move |connection| {
let removed = connection
.execute(
"DELETE FROM records WHERE namespace GLOB ?1",
params![pattern],
)
.map_err(to_storage_error)?;
Ok(removed)
})
.await
}
}
fn glob_escape(value: &str) -> String {
let mut escaped = String::with_capacity(value.len());
for character in value.chars() {
match character {
'*' | '?' | '[' | ']' => {
escaped.push('[');
escaped.push(character);
escaped.push(']');
}
other => escaped.push(other),
}
}
escaped
}
fn split(key: &StorageKey) -> (String, String) {
(key.namespace().to_owned(), key.key().to_owned())
}
fn encode_time(value: OffsetDateTime) -> Result<String> {
value
.format(&Rfc3339)
.map_err(|error| AppError::storage(format!("cannot format timestamp: {error}")))
}
fn decode_time(value: &str) -> Result<OffsetDateTime> {
OffsetDateTime::parse(value, &Rfc3339)
.map_err(|error| AppError::storage(format!("cannot parse timestamp {value:?}: {error}")))
}
fn to_storage_error(error: rusqlite::Error) -> AppError {
AppError::storage(error.to_string())
}