/* This file is part of jellything (https://codeberg.org/metamuffin/jellything) which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2025 metamuffin */ use anyhow::Context; use bincode::{Decode, Encode}; use jellycommon::{ user::{NodeUserData, User}, Node, }; use log::info; use redb::{Database, TableDefinition}; use serde::{Deserialize, Serialize}; use std::{ borrow::Borrow, fs::create_dir_all, ops::Deref, path::Path, sync::{Arc, RwLock}, }; use tantivy::{ directory::MmapDirectory, schema::{Field, Schema, FAST, INDEXED, STORED, STRING, TEXT}, DateOptions, Index, IndexReader, IndexWriter, ReloadPolicy, }; pub use redb; pub use tantivy; pub const T_USER: TableDefinition<&str, Ser> = TableDefinition::new("user"); pub const T_USER_NODE: TableDefinition<(&str, &str), Ser> = TableDefinition::new("user_node"); pub const T_INVITE: TableDefinition<&str, Ser<()>> = TableDefinition::new("invite"); pub const T_NODE: TableDefinition<&str, Ser> = TableDefinition::new("node"); #[derive(Clone)] pub struct DataAcid { pub inner: Arc, pub node_index: Arc, } impl DataAcid { pub fn open(path: &Path) -> Result { create_dir_all(path).context("creating database directory")?; info!("opening kv store..."); let db = redb::Database::create(path.join("data")).context("opening kv store")?; info!("opening node index..."); let ft_node = NodeIndex::new(path).context("in node index")?; let r = Self { inner: db.into(), node_index: ft_node.into(), }; { // this creates all tables such that read operations on them do not fail. let txn = r.begin_write()?; drop(txn.open_table(T_INVITE)?); drop(txn.open_table(T_USER)?); drop(txn.open_table(T_USER_NODE)?); drop(txn.open_table(T_NODE)?); txn.commit()?; } info!("ready"); Ok(r) } } impl Deref for DataAcid { type Target = Database; fn deref(&self) -> &Self::Target { &self.inner } } pub struct NodeIndex { pub schema: Schema, pub reader: IndexReader, pub writer: RwLock, pub index: Index, pub id: Field, pub title: Field, pub releasedate: Field, pub description: Field, pub parent: Field, pub f_index: Field, } impl NodeIndex { fn new(path: &Path) -> anyhow::Result { let mut schema = Schema::builder(); let id = schema.add_text_field("id", TEXT | STORED | FAST); let title = schema.add_text_field("title", TEXT); let description = schema.add_text_field("description", TEXT); let parent = schema.add_text_field("parent", STRING | FAST); let f_index = schema.add_u64_field("index", FAST); let releasedate = schema.add_date_field( "releasedate", DateOptions::from(INDEXED) .set_fast() .set_precision(tantivy::DateTimePrecision::Seconds), ); let schema = schema.build(); create_dir_all(path.join("node_index"))?; let directory = MmapDirectory::open(path.join("node_index")).context("opening index directory")?; let index = Index::open_or_create(directory, schema.clone()).context("creating index")?; let reader = index .reader_builder() .reload_policy(ReloadPolicy::OnCommitWithDelay) .try_into() .context("creating reader")?; let writer = index.writer(30_000_000).context("creating writer")?; Ok(Self { index, writer: writer.into(), reader, schema, parent, f_index, releasedate, id, description, title, }) } } pub trait TableExt { fn get(self, db: &DataAcid, key: KeyRef) -> anyhow::Result>; fn insert(self, db: &DataAcid, key: KeyRef, value: Value) -> anyhow::Result<()>; fn remove(self, db: &DataAcid, key: KeyRef) -> anyhow::Result>; } impl<'a, 'b, 'c, Key, Value, KeyRef> TableExt for redb::TableDefinition<'a, Key, Ser> where Key: Borrow<::SelfType<'b>> + redb::Key, Value: Encode + Decode + std::fmt::Debug + Serialize + for<'x> Deserialize<'x>, KeyRef: Borrow<::SelfType<'c>>, { fn get(self, db: &DataAcid, key: KeyRef) -> anyhow::Result> { let txn = db.inner.begin_read()?; let table = txn.open_table(self)?; let user = table.get(key)?.map(|v| v.value().0); drop(table); Ok(user) } fn insert(self, db: &DataAcid, key: KeyRef, value: Value) -> anyhow::Result<()> { let txn = db.inner.begin_write()?; let mut table = txn.open_table(self)?; table.insert(key, Ser(value))?; drop(table); txn.commit()?; Ok(()) } fn remove(self, db: &DataAcid, key: KeyRef) -> anyhow::Result> { let txn = db.inner.begin_write()?; let mut table = txn.open_table(self)?; let prev = table.remove(key)?.map(|v| v.value().0); drop(table); txn.commit()?; Ok(prev) } } // pub trait TableIterExt< // 'a, // Key: redb::redb::Key + 'static, // Value: redb::redb::Value + 'static, // F: FnOnce(&redb::Range<'a, Key, Value>) -> anyhow::Result, // T: 'static, // > // { // fn iter(self, db: &'a DataAcid, f: F) -> anyhow::Result; // } // impl<'a, Key, Value, F, T> TableIterExt<'a, Key, Value, F, T> // for TableDefinition<'static, Key, Value> // where // Key: redb::redb::Key, // Value: redb::redb::Value, // F: FnOnce(&redb::Range<'a, Key, Value>) -> anyhow::Result, // T: 'static, // { // fn iter(self, db: &DataAcid, f: F) -> anyhow::Result { // let txn = db.begin_read()?; // let table = txn.open_table(self)?; // let iter = table.iter()?; // let ret = f(&iter)?; // drop(iter); // drop(table); // drop(txn); // Ok(ret) // } // } #[derive(Debug)] #[cfg(not(feature = "db_json"))] pub struct Ser(pub T); #[cfg(not(feature = "db_json"))] impl redb::Value for Ser { type SelfType<'a> = Ser where Self: 'a; type AsBytes<'a> = Vec where Self: 'a; fn fixed_width() -> Option { None } fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> where Self: 'a, { Ser(bincode::decode_from_slice(data, bincode::config::legacy()) .unwrap() .0) } fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> where Self: 'a, Self: 'b, { bincode::encode_to_vec(&value.0, bincode::config::legacy()).unwrap() } fn type_name() -> redb::TypeName { redb::TypeName::new("bincode") } } #[derive(Debug)] #[cfg(feature = "db_json")] pub struct Ser(pub T); #[cfg(feature = "db_json")] impl Deserialize<'a> + std::fmt::Debug> redb::Value for Ser { type SelfType<'a> = Ser where Self: 'a; type AsBytes<'a> = Vec where Self: 'a; fn fixed_width() -> Option { None } fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> where Self: 'a, { Ser(serde_json::from_slice(data).unwrap()) } fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> where Self: 'a, Self: 'b, { serde_json::to_vec(&value.0).unwrap() } fn type_name() -> redb::TypeName { redb::TypeName::new("json") } }