From fb990ce233f71588112296823480f0f8cb3ef9f1 Mon Sep 17 00:00:00 2001 From: Joey Hines Date: Sun, 15 Jan 2023 18:55:55 -0700 Subject: [PATCH] Initial commit + Pulled out database logic from Geoffrey-RS + Added import/export feature from json + Updated tests + Clippy + fmt --- .gitignore | 3 + Cargo.lock | 340 +++++++++++++++++++++++++++++++++++++++ Cargo.toml | 19 +++ src/database.rs | 373 +++++++++++++++++++++++++++++++++++++++++++ src/error.rs | 50 ++++++ src/lib.rs | 66 ++++++++ src/metadata.rs | 23 +++ src/migration/mod.rs | 32 ++++ src/model.rs | 29 ++++ src/query/mod.rs | 30 ++++ 10 files changed, 965 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/database.rs create mode 100644 src/error.rs create mode 100644 src/lib.rs create mode 100644 src/metadata.rs create mode 100644 src/migration/mod.rs create mode 100644 src/model.rs create mode 100644 src/query/mod.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3661c57 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +/test_database +/.idea diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..b195356 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,340 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itoa" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" + +[[package]] +name = "j_db" +version = "0.1.0" +dependencies = [ + "byteorder", + "json", + "lazy_static", + "log", + "regex", + "serde", + "serde_json", + "sled", +] + +[[package]] +name = "json" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "proc-macro2" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "ryu" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sled" +version = "0.34.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" +dependencies = [ + "crc32fast", + "crossbeam-epoch", + "crossbeam-utils", + "fs2", + "fxhash", + "libc", + "log", + "parking_lot", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6285701 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "j_db" +version = "0.1.0" +authors = ["Joey Hines "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +sled = "0.34.7" +serde = { version = "1.0", features = ["derive"]} +serde_json = "1.0" +byteorder = "1.4.2" +log = "0.4.17" +json = "0.12.4" +regex = "1.5.6" + +[dev-dependencies] +lazy_static = "1.4.0" diff --git a/src/database.rs b/src/database.rs new file mode 100644 index 0000000..a2ca11a --- /dev/null +++ b/src/database.rs @@ -0,0 +1,373 @@ +use crate::error::{JDbError, Result}; +use crate::metadata::DBMetadata; +use crate::model::JdbModel; +use crate::query::QueryBuilder; +use json::JsonValue; +use sled::IVec; +use std::convert::TryInto; +use std::path::Path; + +pub const DB_METADATA_ID: u64 = 1; + +fn option_bytes_to_model(bytes: Option, id: u64) -> Result { + if let Some(bytes) = bytes { + Ok(T::try_from_bytes(&bytes)?) + } else { + log::debug!("{} of id {} was not found in the database", T::tree(), id); + Err(JDbError::NotFound) + } +} + +pub struct Database { + pub(crate) db: sled::Db, +} + +impl Database { + pub fn new(db_path: &Path) -> Result { + let db = sled::open(db_path)?; + let db = Self { db }; + + let version = match db.version() { + Ok(version) => version, + Err(_) => { + let db_metadata = DBMetadata { + id: Some(DB_METADATA_ID), + version: 0, + }; + db.insert(db_metadata)?.version + } + }; + + log::info!("jDb Version V{}", version); + + Ok(db) + } + + fn get_tree(&self) -> Result + where + T: JdbModel, + { + Ok(self.db.open_tree::(T::tree())?) + } + + pub fn insert(&self, mut model: T) -> Result + where + T: JdbModel, + { + let id = match model.id() { + Some(id) => id, + None => { + let id = self.db.generate_id()?; + model.set_id(id); + id + } + }; + + let match_count = self + .filter(|o_id, o: &T| o_id != id && !o.check_unique(&model))? + .count(); + + if match_count > 0 { + log::debug!("{} is not unique: {:?}", T::tree(), model); + return Err(JDbError::NotUnique); + } + + let tree = self.get_tree::()?; + let id_bytes = id.to_be_bytes(); + + tree.insert(id_bytes, model.to_bytes()?)?; + + Ok(model) + } + + pub fn get(&self, id: u64) -> Result + where + T: JdbModel, + { + let tree = self.get_tree::()?; + option_bytes_to_model(tree.get(id.to_be_bytes())?, id) + } + + pub fn clear_tree(&self) -> Result<()> + where + T: JdbModel, + { + self.db.drop_tree(T::tree())?; + + Ok(()) + } + + pub fn filter<'a, T>( + &self, + f: impl Fn(u64, &T) -> bool + 'a, + ) -> Result + 'a> + where + T: JdbModel, + { + let tree = self.db.open_tree(T::tree())?; + + Ok(tree.iter().filter_map(move |e| { + if let Ok((id, data)) = e { + let id = u64::from_be_bytes(id.to_vec().try_into().unwrap()); + let data = match T::try_from_bytes(&data) { + Ok(data) => data, + Err(err) => { + log::debug!( + "Invalid data: {}", + String::from_utf8(data.to_vec()).unwrap_or_default() + ); + panic!("Unable to parse {} model from bytes: {}", T::tree(), err); + } + }; + + if f(id, &data) { + Some(data) + } else { + None + } + } else { + None + } + })) + } + + pub fn run_query(&self, query_builder: QueryBuilder) -> Result> + where + T: JdbModel, + { + let result: Vec = self + .filter(|id, loc: &T| { + for query in &query_builder.queries { + let res = query(id, loc); + + if !res { + return false; + } + } + + true + })? + .collect(); + + if result.is_empty() { + Err(JDbError::NotFound) + } else { + Ok(result) + } + } + + pub fn remove(&self, id: u64) -> Result + where + T: JdbModel, + { + let tree = self.db.open_tree(T::tree())?; + option_bytes_to_model(tree.remove(id.to_be_bytes())?, id) + } + + pub fn tree_iter(&self) -> Result + where + T: JdbModel, + { + Ok(self.db.open_tree(T::tree()).map(|tree| tree.iter())?) + } + + pub fn version(&self) -> Result { + Ok(self.get::(DB_METADATA_ID)?.version) + } + + pub(crate) fn set_version(&mut self, version: u64) -> Result<()> { + let mut md = self.get::(DB_METADATA_ID)?; + + md.version = version; + + self.insert(md)?; + + Ok(()) + } + + pub fn dump_db(&self) -> Result { + let mut json = JsonValue::new_object(); + + let mut global_array = JsonValue::new_array(); + for model in self.db.iter() { + let (_, model) = model?; + let model_str = String::from_utf8(model.to_vec()).unwrap(); + let model_json = json::parse(&model_str)?; + global_array.push(json::from(model_json))?; + } + + json.insert("global", global_array)?; + + for tree in self.db.tree_names() { + let tree = self.db.open_tree(tree)?; + let mut tree_array = JsonValue::new_array(); + + for model in tree.iter() { + let (_, model) = model?; + let model_str = String::from_utf8(model.to_vec()).unwrap(); + let model_json = json::parse(&model_str)?; + tree_array.push(json::from(model_json))?; + } + + let tree_name = String::from_utf8(tree.name().to_vec()).unwrap(); + json.insert(&tree_name, tree_array)?; + } + + Ok(json) + } + + pub fn import_db(&self, json: JsonValue) -> Result<()> { + for model in json["global"].members() { + let id_bytes = model["id"].as_u64().unwrap().to_be_bytes(); + self.db.insert(id_bytes, model.to_string().as_bytes())?; + } + + for (tree, models) in json.entries() { + for model in models.members() { + let id_bytes = model["id"].as_u64().unwrap().to_be_bytes(); + + if tree == "global" { + self.db.insert(id_bytes, model.to_string().as_bytes())?; + } else { + let tree = self.db.open_tree(tree)?; + tree.insert(id_bytes, model.to_string().as_bytes())?; + } + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::model::JdbModel; + use crate::test::{cleanup, User, DB, LOCK}; + use std::time::Instant; + + #[test] + fn test_insert() { + let _lock = LOCK.lock().unwrap(); + cleanup(); + let user = User::new("Test", 1); + + let user2 = DB.insert::(user.clone()).unwrap(); + + assert!(user2.id().is_some()); + assert_eq!(user.name, user2.name); + + cleanup(); + } + + #[test] + fn test_unique_insert() { + let _lock = LOCK.lock().unwrap(); + cleanup(); + let user1 = User::new("Test", 1); + let user2 = User::new("Test", 1); + + DB.insert::(user1.clone()).unwrap(); + assert_eq!(DB.insert::(user2.clone()).is_err(), true); + + cleanup(); + } + + #[test] + fn test_get() { + let _lock = LOCK.lock().unwrap(); + cleanup(); + let user1 = User::new("Test", 1); + + let user1_db = DB.insert::(user1.clone()).unwrap(); + + let user1_get = DB.get::(user1_db.id().unwrap()).unwrap(); + + assert_eq!(user1_get.name, user1.name); + cleanup(); + } + + #[test] + fn test_filter() { + let _lock = LOCK.lock().unwrap(); + cleanup(); + let user = User::new("Test", 1); + let user = DB.insert::(user.clone()).unwrap(); + + let count = DB + .filter(|id: u64, u: &User| { + assert_eq!(id, user.id().unwrap()); + u.id().unwrap() == user.id().unwrap() + }) + .unwrap() + .count(); + + assert_eq!(count, 1); + cleanup(); + } + + #[test] + fn test_remove() { + let _lock = LOCK.lock().unwrap(); + cleanup(); + let user = User::new("CoolZero123", 1); + + let user_insert = DB.insert::(user.clone()).unwrap(); + + let user_remove = DB.remove::(user_insert.id().unwrap()).unwrap(); + + assert!(DB.get::(user_insert.id().unwrap()).is_err()); + assert_eq!(user_remove.id().unwrap(), user_insert.id().unwrap()); + cleanup(); + } + + #[test] + fn test_speed() { + let _lock = LOCK.lock().unwrap(); + cleanup(); + let insert_count = 1000; + let timer = Instant::now(); + for i in 0..insert_count { + let user = User::new(&format!("User{}", i), 0); + + DB.insert::(user).unwrap(); + } + + DB.db.flush().unwrap(); + let sec_elapsed = timer.elapsed().as_secs_f32(); + println!( + "Completed in {}s. {} inserts per second", + sec_elapsed, + insert_count as f32 / sec_elapsed + ); + + cleanup() + } + + #[test] + fn test_dump_and_load() { + let _lock = LOCK.lock().unwrap(); + cleanup(); + + let mut users = vec![]; + for i in 0..10 { + let user = User::new(&format!("User{}", i), 0); + + let u = DB.insert::(user).unwrap(); + + users.push(u); + } + + let out = DB.dump_db().unwrap(); + + println!("{}", out); + + DB.import_db(out).unwrap(); + + for user in users { + let import_user = DB.get::(user.id().unwrap()).unwrap(); + assert_eq!(user.name, import_user.name); + } + + cleanup(); + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..ebc8f8c --- /dev/null +++ b/src/error.rs @@ -0,0 +1,50 @@ +pub type Result = std::result::Result; + +#[derive(Debug)] +pub enum JDbError { + SledError(sled::Error), + SerdeJsonError(serde_json::Error), + NotUnique, + NotFound, + RegexError(regex::Error), + JsonError(json::JsonError), +} + +impl std::error::Error for JDbError {} + +impl std::fmt::Display for JDbError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + JDbError::SledError(e) => write!(f, "Sled Error: {}", e), + JDbError::SerdeJsonError(e) => write!(f, "Serde JSON Error: {}", e), + JDbError::NotUnique => write!(f, "Entry is not unique."), + JDbError::NotFound => write!(f, "Entry was not found."), + JDbError::RegexError(e) => write!(f, "Regex Error: {}", e), + JDbError::JsonError(e) => write!(f, "JSON Error: {}", e), + } + } +} + +impl From for JDbError { + fn from(e: sled::Error) -> Self { + JDbError::SledError(e) + } +} + +impl From for JDbError { + fn from(e: serde_json::Error) -> Self { + JDbError::SerdeJsonError(e) + } +} + +impl From for JDbError { + fn from(e: regex::Error) -> Self { + Self::RegexError(e) + } +} + +impl From for JDbError { + fn from(e: json::Error) -> Self { + Self::JsonError(e) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a18b7e7 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,66 @@ +#![allow(dead_code)] + +extern crate core; + +pub mod database; +pub mod error; +pub mod metadata; +pub mod migration; +pub mod model; +pub mod query; + +#[cfg(test)] +mod test { + use crate::database::Database; + use crate::model::JdbModel; + use lazy_static::lazy_static; + use serde::{Deserialize, Serialize}; + use std::path::Path; + use std::sync::Mutex; + + #[derive(Serialize, Deserialize, Debug, Clone)] + pub struct User { + id: Option, + pub name: String, + pub age: u16, + } + + impl User { + pub fn new(name: &str, age: u16) -> Self { + Self { + id: None, + name: name.to_string(), + age, + } + } + } + + impl JdbModel for User { + fn id(&self) -> Option { + self.id + } + + fn set_id(&mut self, id: u64) { + self.id = Some(id) + } + + fn tree() -> String { + "User".to_string() + } + + fn check_unique(&self, other: &Self) -> bool { + self.name != other.name + } + } + + lazy_static! { + pub static ref DB: Database = Database::new(Path::new("test_database")).unwrap(); + pub static ref LOCK: Mutex<()> = Mutex::default(); + } + + pub fn cleanup() { + DB.clear_tree::().unwrap(); + DB.db.clear().unwrap(); + DB.db.flush().unwrap(); + } +} diff --git a/src/metadata.rs b/src/metadata.rs new file mode 100644 index 0000000..e47c34a --- /dev/null +++ b/src/metadata.rs @@ -0,0 +1,23 @@ +use crate::database::DB_METADATA_ID; +use crate::model::JdbModel; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, Hash, Eq, PartialEq)] +pub struct DBMetadata { + pub id: Option, + pub version: u64, +} + +impl JdbModel for DBMetadata { + fn id(&self) -> Option { + Some(DB_METADATA_ID) + } + + fn set_id(&mut self, _id: u64) { + self.id = Some(DB_METADATA_ID) + } + + fn tree() -> String { + "DBMetadata".to_string() + } +} diff --git a/src/migration/mod.rs b/src/migration/mod.rs new file mode 100644 index 0000000..a408947 --- /dev/null +++ b/src/migration/mod.rs @@ -0,0 +1,32 @@ +use crate::database::Database; +use crate::error::Result; +use crate::metadata::DBMetadata; + +pub trait Migration { + fn up(&self, db: &Database) -> Result<()>; + fn down(&self, db: &Database) -> Result<()>; + fn version(&self) -> u64; +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Direction { + Up, + Down, +} + +pub fn do_migration(db: &Database, migration: T, direction: Direction) -> Result<()> { + if direction == Direction::Up { + migration.up(db)?; + } else { + migration.down(db)?; + } + + let metadata = DBMetadata { + id: None, + version: migration.version(), + }; + + db.insert(metadata)?; + + Ok(()) +} diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..bfdba85 --- /dev/null +++ b/src/model.rs @@ -0,0 +1,29 @@ +use serde::de::DeserializeOwned; +use serde::Serialize; +use std::fmt::Debug; + +pub trait JdbModel: Serialize + DeserializeOwned + Debug { + fn id(&self) -> Option; + fn set_id(&mut self, id: u64); + fn tree() -> String; + + fn check_unique(&self, _: &Self) -> bool { + true + } + + fn to_json_string(&self) -> Result { + serde_json::to_string(self) + } + + fn try_from_str(s: &str) -> Result { + serde_json::from_str(s) + } + + fn to_bytes(&self) -> Result, serde_json::Error> { + serde_json::to_vec(self) + } + + fn try_from_bytes(b: &[u8]) -> Result { + serde_json::from_slice(b) + } +} diff --git a/src/query/mod.rs b/src/query/mod.rs new file mode 100644 index 0000000..8cd2ab6 --- /dev/null +++ b/src/query/mod.rs @@ -0,0 +1,30 @@ +use crate::model::JdbModel; + +pub type GeoffreyDBQuery = Box bool>; + +pub struct QueryBuilder { + pub queries: Vec>, +} + +impl Default for QueryBuilder { + fn default() -> Self { + Self::new() + } +} + +impl QueryBuilder { + pub fn new() -> Self { + QueryBuilder { + queries: Vec::new(), + } + } + + pub fn add_query_clause(mut self, clause: GeoffreyDBQuery) -> Self { + self.queries.push(clause); + self + } + + pub fn with_id(self, id: u64) -> Self { + self.add_query_clause(Box::new(move |entry_id, _| entry_id == id)) + } +}