use rusqlite::OptionalExtension;
use crate::common::{Error, SizeT, check_constraint_violation};
use crate::rusqlite::{Connection, Transaction};
const SQL_CREATE_TBL: &str = "CREATE TABLE data (key TEXT PRIMARY KEY NOT NULL, value TEXT NOT NULL) WITHOUT ROWID;";
const SQL_CREATE_NOC: &str = "CREATE TABLE data (key TEXT PRIMARY KEY NOT NULL COLLATE NOCASE, value TEXT NOT NULL) WITHOUT ROWID;";
const SQL_COUNT_KEYS: &str = "SELECT COUNT(*) FROM data;";
const SQL_INSERT_KEY: &str = "INSERT INTO data (key, value) VALUES (?1, ?2);";
const SQL_UPSERT_KEY: &str = "INSERT INTO data (key, value) VALUES (?1, ?2) ON CONFLICT(key) DO UPDATE SET value = excluded.value;";
const SQL_EXISTS_KEY: &str = "SELECT 1 FROM data WHERE key = ? LIMIT 1;";
const SQL_EXISTS_NOC: &str = "SELECT 1 FROM data WHERE key COLLATE NOCASE = ? LIMIT 1;";
const SQL_LOOKUP_KEY: &str = "SELECT value FROM data WHERE key = ? LIMIT 1;";
const SQL_LOOKUP_NOC: &str = "SELECT value FROM data WHERE key COLLATE NOCASE = ? LIMIT 1;";
const SQL_QUERY_KEYS: &str = "SELECT key, value FROM data;";
const SQL_DELETE_KEY: &str = "DELETE FROM data WHERE key = ?;";
const SQL_DELETE_NOC: &str = "DELETE FROM data WHERE key COLLATE NOCASE = ?;";
const SQL_DELETE_ALL: &str = "DELETE FROM data;";
pub struct SQLiteMap {
connection: Connection,
}
impl SQLiteMap {
#[inline]
pub fn new() -> Result<Self, Error> {
Ok(Self { connection: Self::initialize_connection(false)? })
}
#[inline]
pub fn with_nocase() -> Result<Self, Error> {
Ok(Self { connection: Self::initialize_connection(true)? })
}
#[inline]
fn initialize_connection(no_case: bool) -> Result<Connection, Error> {
let connection = Connection::open_in_memory()?;
connection.pragma_update(None, "journal_mode", "OFF")?;
connection.pragma_update(None, "synchronous", "OFF")?;
connection.pragma_update(None, "temp_store", "MEMORY")?;
if !no_case {
connection.execute(SQL_CREATE_TBL, [])?;
} else {
connection.execute(SQL_CREATE_NOC, [])?;
}
Ok(connection)
}
#[inline]
pub fn transaction(&mut self) -> Result<SQLiteMapTransaction<'_>, Error> {
SQLiteMapTransaction::from(&mut self.connection)
}
#[inline]
pub fn insert(&mut self, key: &str, value: &str) -> Result<bool, Error> {
let mut insert = self.connection.prepare_cached(SQL_INSERT_KEY)?;
match insert.execute([key, value]) {
Ok(_) => Ok(true),
Err(error) => check_constraint_violation(error),
}
}
#[inline]
pub fn update(&mut self, key: &str, value: &str) -> Result<(), Error> {
let mut update = self.connection.prepare_cached(SQL_UPSERT_KEY)?;
update.execute([key, value])?;
Ok(())
}
#[inline]
pub fn contains(&self, key: &str) -> Result<bool, Error> {
let mut contains = self.connection.prepare_cached(SQL_EXISTS_KEY)?;
Ok(contains.exists([key])?)
}
#[inline]
pub fn contains_nocase(&self, key: &str) -> Result<bool, Error> {
let mut contains = self.connection.prepare_cached(SQL_EXISTS_NOC)?;
Ok(contains.exists([key])?)
}
#[inline]
pub fn get(&self, key: &str) -> Result<Option<String>, Error> {
let mut get = self.connection.prepare_cached(SQL_LOOKUP_KEY)?;
Ok(get.query_one([key], |row| row.get(0)).optional()?)
}
#[inline]
pub fn get_nocase(&self, key: &str) -> Result<Option<String>, Error> {
let mut get = self.connection.prepare_cached(SQL_LOOKUP_NOC)?;
Ok(get.query_one([key], |row| row.get(0)).optional()?)
}
#[inline]
pub fn remove(&mut self, key: &str) -> Result<bool, Error> {
let mut contains = self.connection.prepare_cached(SQL_DELETE_KEY)?;
Ok(contains.execute([key])? != 0)
}
#[inline]
pub fn remove_nocase(&mut self, key: &str) -> Result<bool, Error> {
let mut contains = self.connection.prepare_cached(SQL_DELETE_NOC)?;
Ok(contains.execute([key])? != 0)
}
#[inline]
pub fn for_each<F>(&self, mut callback: F) -> Result<(), Error>
where
F: FnMut(&str, &str),
{
let mut iter = self.connection.prepare_cached(SQL_QUERY_KEYS)?;
let mut result = iter.query([])?;
while let Some(current_item) = result.next()? {
let key: String = current_item.get(0)?;
let value: String = current_item.get(1)?;
callback(&key, &value);
}
Ok(())
}
#[inline]
pub fn find<P>(&self, predicate: P) -> Result<Option<(String, String)>, Error>
where
P: Fn(&str, &str) -> bool,
{
let mut iter = self.connection.prepare_cached(SQL_QUERY_KEYS)?;
let mut result = iter.query([])?;
while let Some(current_item) = result.next()? {
let key: String = current_item.get(0)?;
let value: String = current_item.get(1)?;
if predicate(&key, &value) {
return Ok(Some((key, value)));
}
}
Ok(None)
}
#[inline]
pub fn len(&self) -> Result<SizeT, Error> {
let mut query_count = self.connection.prepare_cached(SQL_COUNT_KEYS)?;
let count: i64 = query_count.query_one([], |row| row.get(0))?;
Ok(count.try_into().unwrap_or_default())
}
#[inline]
pub fn is_empty(&self) -> Result<bool, Error> {
Ok(self.len()? == 0)
}
#[inline]
pub fn clear(&mut self) -> Result<(), Error> {
let mut clear = self.connection.prepare_cached(SQL_DELETE_ALL)?;
clear.execute([])?;
Ok(())
}
}
impl Default for SQLiteMap {
#[inline]
fn default() -> Self {
Self::new().expect("Failed to create SQLiteMap instance!")
}
}
pub struct SQLiteMapTransaction<'a> {
transaction: Transaction<'a>,
}
impl<'a> SQLiteMapTransaction<'a> {
#[inline]
fn from(connection: &'a mut Connection) -> Result<Self, Error> {
let mut transaction = connection.transaction()?;
transaction.set_drop_behavior(rusqlite::DropBehavior::Commit);
Ok(Self { transaction })
}
#[inline]
pub fn commit(self) {}
#[inline]
pub fn insert(&mut self, key: &str, value: &str) -> Result<bool, Error> {
let mut insert = self.transaction.prepare_cached(SQL_INSERT_KEY)?;
match insert.execute([key, value]) {
Ok(_) => Ok(true),
Err(error) => check_constraint_violation(error),
}
}
#[inline]
pub fn update(&mut self, key: &str, value: &str) -> Result<(), Error> {
let mut update = self.transaction.prepare_cached(SQL_UPSERT_KEY)?;
update.execute([key, value])?;
Ok(())
}
#[inline]
pub fn contains(&self, key: &str) -> Result<bool, Error> {
let mut contains = self.transaction.prepare_cached(SQL_EXISTS_KEY)?;
Ok(contains.exists([key])?)
}
#[inline]
pub fn contains_nocase(&self, key: &str) -> Result<bool, Error> {
let mut contains = self.transaction.prepare_cached(SQL_EXISTS_NOC)?;
Ok(contains.exists([key])?)
}
#[inline]
pub fn get(&self, key: &str) -> Result<Option<String>, Error> {
let mut get = self.transaction.prepare_cached(SQL_LOOKUP_KEY)?;
Ok(get.query_one([key], |row| row.get(0)).optional()?)
}
#[inline]
pub fn get_nocase(&self, key: &str) -> Result<Option<String>, Error> {
let mut get = self.transaction.prepare_cached(SQL_LOOKUP_NOC)?;
Ok(get.query_one([key], |row| row.get(0)).optional()?)
}
#[inline]
pub fn remove(&mut self, key: &str) -> Result<bool, Error> {
let mut contains = self.transaction.prepare_cached(SQL_DELETE_KEY)?;
Ok(contains.execute([key])? != 0)
}
#[inline]
pub fn remove_nocase(&mut self, key: &str) -> Result<bool, Error> {
let mut contains = self.transaction.prepare_cached(SQL_DELETE_NOC)?;
Ok(contains.execute([key])? != 0)
}
#[inline]
pub fn for_each<F>(&self, mut callback: F) -> Result<(), Error>
where
F: FnMut(&str, &str),
{
let mut iter = self.transaction.prepare_cached(SQL_QUERY_KEYS)?;
let mut result = iter.query([])?;
while let Some(current_item) = result.next()? {
let key: String = current_item.get(0)?;
let value: String = current_item.get(1)?;
callback(&key, &value);
}
Ok(())
}
#[inline]
pub fn find<P>(&self, predicate: P) -> Result<Option<(String, String)>, Error>
where
P: Fn(&str, &str) -> bool,
{
let mut iter = self.transaction.prepare_cached(SQL_QUERY_KEYS)?;
let mut result = iter.query([])?;
while let Some(current_item) = result.next()? {
let key: String = current_item.get(0)?;
let value: String = current_item.get(1)?;
if predicate(&key, &value) {
return Ok(Some((key, value)));
}
}
Ok(None)
}
#[inline]
pub fn len(&self) -> Result<SizeT, Error> {
let mut query_count = self.transaction.prepare_cached(SQL_COUNT_KEYS)?;
let count: i64 = query_count.query_one([], |row| row.get(0))?;
Ok(count.try_into().unwrap_or_default())
}
#[inline]
pub fn is_empty(&self) -> Result<bool, Error> {
Ok(self.len()? == 0)
}
#[inline]
pub fn clear(&mut self) -> Result<(), Error> {
let mut clear = self.transaction.prepare_cached(SQL_DELETE_ALL)?;
clear.execute([])?;
Ok(())
}
}