123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397 |
- #![doc = include_str!("../README.md")]
- mod meta;
- pub mod model;
- pub mod query;
- use meta::Metaschema;
- use model::Entity;
- pub use microrm_macros::{make_index, Entity, Modelable};
- pub use query::{QueryInterface, WithID};
- #[macro_export]
- macro_rules! value_list {
- ( $( $element:expr ),* ) => {
- [ $( ($element) as &dyn $crate::model::Modelable ),* ]
- }
- }
- // no need to show the re-exports in the documentation
- #[doc(hidden)]
- pub mod re_export {
- pub use lazy_static;
- pub use serde;
- pub use serde_json;
- pub use sqlite;
- }
- #[derive(Debug)]
- pub enum DBError {
- ConnectFailure,
- EarlyFailure(sqlite::Error),
- NoSchema,
- DifferentSchema,
- DropFailure,
- CreateFailure,
- SanityCheckFailure,
- }
- #[derive(PartialEq, Debug)]
- pub enum CreateMode {
- /// The database must exist and have a valid schema already
- MustExist,
- /// It's fine if the database doesn't exist, but it must have a valid schema if it does
- AllowNewDatabase,
- /// Nuke the contents if need be, just get the database
- AllowSchemaUpdate,
- }
- impl std::fmt::Display for DBError {
- fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
- fmt.write_fmt(format_args!("Database error: {:?}", self))
- }
- }
- impl std::error::Error for DBError {}
- /// SQLite database connection
- pub struct DB {
- conn: sqlite::Connection,
- schema_hash: String,
- schema: model::SchemaModel,
- }
- impl DB {
- pub fn new(schema: model::SchemaModel, path: &str, mode: CreateMode) -> Result<Self, DBError> {
- Self::from_connection(
- sqlite::Connection::open(path).map_err(|_| DBError::ConnectFailure)?,
- schema,
- mode,
- )
- }
- /// Mostly for use in tests, but may be useful in some applications as well.
- pub fn new_in_memory(schema: model::SchemaModel) -> Result<Self, DBError> {
- Self::from_connection(
- sqlite::Connection::open(":memory:").map_err(|_| DBError::ConnectFailure)?,
- schema,
- CreateMode::AllowNewDatabase,
- )
- }
- /// Get a query interface for this DB connection
- pub fn query_interface(&self) -> query::QueryInterface {
- query::QueryInterface::new(self)
- }
- pub fn recreate_schema(&self) -> Result<(), DBError> {
- self.create_schema()
- }
- fn from_connection(
- conn: sqlite::Connection,
- schema: model::SchemaModel,
- mode: CreateMode,
- ) -> Result<Self, DBError> {
- let sig = Self::calculate_schema_hash(&schema);
- let ret = Self {
- conn,
- schema_hash: sig,
- schema: schema.add::<meta::Metaschema>(),
- };
- ret.check_schema(mode)?;
- Ok(ret)
- }
- fn calculate_schema_hash(schema: &model::SchemaModel) -> String {
- use sha2::Digest;
- let mut hasher = sha2::Sha256::new();
- schema
- .drop()
- .iter()
- .map(|sql| hasher.update(sql.as_bytes()))
- .count();
- schema
- .create()
- .iter()
- .map(|sql| hasher.update(sql.as_bytes()))
- .count();
- base64::encode(hasher.finalize())
- }
- fn check_schema(&self, mode: CreateMode) -> Result<(), DBError> {
- let mut has_metaschema = false;
- self.conn
- .iterate(
- format!(
- "SELECT * FROM \"sqlite_master\" WHERE \"type\"='table' AND \"name\"='{}'",
- Metaschema::table_name()
- ),
- |_row| {
- has_metaschema = true;
- true
- },
- )
- .map_err(DBError::EarlyFailure)?;
- if !has_metaschema && mode != CreateMode::MustExist {
- return self.create_schema();
- } else if !has_metaschema && mode == CreateMode::MustExist {
- return Err(DBError::NoSchema);
- }
- let qi = query::QueryInterface::new(self);
- let hash = qi.get_one_by(meta::MetaschemaColumns::Key, "schema_hash");
- if hash.is_none() {
- if mode == CreateMode::MustExist {
- return Err(DBError::NoSchema);
- }
- return self.create_schema();
- } else if hash.unwrap().value != self.schema_hash {
- if mode != CreateMode::AllowSchemaUpdate {
- return Err(DBError::DifferentSchema);
- }
- self.drop_schema()?;
- return self.create_schema();
- }
- Ok(())
- }
- fn drop_schema(&self) -> Result<(), DBError> {
- for ds in self.schema.drop() {
- self.conn.execute(ds).map_err(|_| DBError::DropFailure)?;
- }
- Ok(())
- }
- fn create_schema(&self) -> Result<(), DBError> {
- for cs in self.schema.create() {
- self.conn.execute(cs).map_err(|_| DBError::CreateFailure)?;
- }
- let qi = query::QueryInterface::new(self);
- let add_result = qi.add(&meta::Metaschema {
- key: "schema_hash".to_string(),
- value: self.schema_hash.clone(),
- });
- assert!(add_result.is_some());
- let sanity_check = qi.get_one_by(meta::MetaschemaColumns::Key, "schema_hash");
- assert!(sanity_check.is_some());
- assert_eq!(sanity_check.unwrap().value, self.schema_hash);
- Ok(())
- }
- }
- /// Add support for multi-threading to a `DB`.
- ///
- /// This is a thread-local cache that carefully maintains the property that no
- /// element of the cache will ever be accessed in any way from another thread. The only
- /// way to maintain this property is to leak all data, so this is best used
- /// in lightly-threaded programs (or at least a context where threads are long-lived).
- /// All cached values are assumed to use interior mutability where needed to maintain state.
- ///
- /// This approach ensures that all items can live for the provided lifetime `'l`.
- pub struct DBPool<'a> {
- // normally DB is not Send because the raw sqlite ptr is not Send
- // however we assume sqlite is operating in serialized mode, which means
- // that it is in fact both `Send` and `Sync`
- db: &'a DB,
- // we carefully maintain the invariant here that only the thread with the given `ThreadId`
- // accesses the QueryInterface part of the pair, which means that despite the fact that
- // QueryInterface is neither Send nor Sync can be dismissed in this Send and Sync container
- qi: std::sync::RwLock<Vec<(std::thread::ThreadId, &'a QueryInterface<'a>)>>,
- }
- impl<'a> DBPool<'a> {
- pub fn new(db: &'a DB) -> Self {
- Self {
- db,
- qi: std::sync::RwLock::new(Vec::new()),
- }
- }
- /// Get a query interface from this DB pool for the current thread
- pub fn query_interface(&self) -> &query::QueryInterface<'a> {
- let guard = self.qi.read().expect("Couldn't acquire read lock");
- let current_id = std::thread::current().id();
- if let Some(res) = guard
- .iter()
- .find_map(|x| if x.0 == current_id { Some(x.1) } else { None })
- {
- return res;
- }
- drop(guard);
- let mut guard = self.qi.write().expect("Couldn't acquire write lock");
- guard.push((current_id, Box::leak(Box::new(self.db.query_interface()))));
- drop(guard);
- self.query_interface()
- }
- }
- /// We carefully implement `DBPool` so that it is `Send`.
- unsafe impl<'a> Send for DBPool<'a> {}
- /// We carefully implement `DBPool` so that it is `Sync`.
- unsafe impl<'a> Sync for DBPool<'a> {}
- #[cfg(test)]
- mod pool_test {
- trait IsSend: Send {}
- impl IsSend for super::DB {}
- impl<'a> IsSend for super::DBPool<'a> {}
- // we make sure that DBPool is send / sync safe
- trait IsSendAndSync: Send + Sync {}
- impl<'a> IsSendAndSync for super::DBPool<'a> {}
- }
- #[cfg(test)]
- mod test {
- use super::DB;
- #[derive(serde::Serialize, serde::Deserialize, crate::Entity)]
- #[microrm_internal]
- pub struct S1 {
- an_id: i32,
- }
- fn simple_schema() -> super::model::SchemaModel {
- super::model::SchemaModel::new().add::<S1>()
- }
- #[test]
- fn in_memory_schema() {
- let _db = DB::new_in_memory(simple_schema());
- drop(_db);
- }
- #[derive(serde::Serialize, serde::Deserialize, crate::Entity)]
- #[microrm_internal]
- pub struct S2 {
- #[microrm_foreign]
- parent_id: S1ID,
- }
- #[test]
- fn simple_foreign_key() {
- let db = DB::new_in_memory(super::model::SchemaModel::new().add::<S1>().add::<S2>())
- .expect("Can't connect to in-memory DB");
- let qi = db.query_interface();
- let id = qi.add(&S1 { an_id: -1 }).expect("Can't add S1");
- let child_id = qi.add(&S2 { parent_id: id }).expect("Can't add S2");
- qi.get_one_by_id(child_id).expect("Can't get S2 instance");
- }
- microrm_macros::make_index_internal!(S2ParentIndex, S2Columns::ParentId);
- }
- #[cfg(test)]
- mod test2 {
- #[derive(Debug, crate::Entity, serde::Serialize, serde::Deserialize)]
- #[microrm_internal]
- pub struct KVStore {
- pub key: String,
- pub value: String,
- }
- // the !KVStoreIndex here means a type representing a unique index named KVStoreIndex
- microrm_macros::make_index_internal!(!KVStoreIndex, KVStoreColumns::Key);
- #[test]
- fn dump_test() {
- let schema = crate::model::SchemaModel::new()
- .add::<KVStore>()
- .index::<KVStoreIndex>();
- // dump the schema in case you want to inspect it manually
- for create_sql in schema.create() {
- println!("{};", create_sql);
- }
- let db = crate::DB::new_in_memory(schema).unwrap();
- let qi = db.query_interface();
- qi.add(&KVStore {
- key: "a_key".to_string(),
- value: "a_value".to_string(),
- });
- // because KVStoreIndex indexes key, this is a logarithmic lookup
- let qr = qi.get_one_by(KVStoreColumns::Key, "a_key");
- assert_eq!(qr.is_some(), true);
- assert_eq!(qr.as_ref().unwrap().key, "a_key");
- assert_eq!(qr.as_ref().unwrap().value, "a_value");
- }
- }
- #[cfg(test)]
- mod delete_test {
- #[derive(Debug, crate::Entity, serde::Serialize, serde::Deserialize)]
- #[microrm_internal]
- pub struct KVStore {
- pub key: String,
- pub value: String,
- }
- #[test]
- fn delete_test() {
- let schema = crate::model::SchemaModel::new()
- .entity::<KVStore>();
- let db = crate::DB::new_in_memory(schema).unwrap();
- let qi = db.query_interface();
- qi.add(&KVStore {
- key: "a".to_string(),
- value: "a_value".to_string()
- });
- let insert_two = || {
- qi.add(&KVStore {
- key: "a".to_string(),
- value: "a_value".to_string()
- });
- qi.add(&KVStore {
- key: "a".to_string(),
- value: "another_value".to_string()
- });
- };
- assert!(qi.get_one_by(KVStoreColumns::Key, "a").is_some());
- // is_some() implies no errors were encountered
- assert!(qi.delete_by(KVStoreColumns::Key, "a").is_some());
- assert!(qi.get_one_by(KVStoreColumns::Key, "a").is_none());
- insert_two();
- // this should fail as there is more than one thing matching key='a'
- assert!(qi.get_one_by(KVStoreColumns::Key, "a").is_none());
- let all = qi.get_all_by(KVStoreColumns::Key, "a");
- assert!(all.is_some());
- assert_eq!(all.unwrap().len(), 2);
- assert!(qi.delete_by(KVStoreColumns::Key, "b").is_some());
- let all = qi.get_all_by(KVStoreColumns::Key, "a");
- assert!(all.is_some());
- assert_eq!(all.unwrap().len(), 2);
- assert!(qi.delete_by_multi(&[KVStoreColumns::Key, KVStoreColumns::Value], &crate::value_list![&"a", &"another_value"]).is_some());
- let one = qi.get_one_by(KVStoreColumns::Key, "a");
- assert!(one.is_some());
- assert_eq!(one.unwrap().value, "a_value");
- }
- }
|