From 65ca3f3450d0067668111f6e13cc3089768c9efe Mon Sep 17 00:00:00 2001 From: metamuffin Date: Thu, 5 Feb 2026 20:31:55 +0100 Subject: remove read/write distinction for kv transactions; traitify database --- database/src/counters.rs | 61 ----------- database/src/filter/binning.rs | 52 --------- database/src/filter/mod.rs | 107 ------------------ database/src/kv/binning.rs | 148 +++++++++++++++++++++++++ database/src/kv/counters.rs | 59 ++++++++++ database/src/kv/mod.rs | 217 +++++++++++++++++++++++++++++++++++++ database/src/kv/prefix_iterator.rs | 22 ++++ database/src/kv/sort/mod.rs | 29 +++++ database/src/kv/sort/none.rs | 50 +++++++++ database/src/kv/sort/value.rs | 86 +++++++++++++++ database/src/lib.rs | 71 ++++++++++-- database/src/prefix_iterator.rs | 22 ---- database/src/query.rs | 51 --------- database/src/sort/mod.rs | 37 ------- database/src/sort/none.rs | 53 --------- database/src/sort/value.rs | 89 --------------- database/src/table.rs | 184 ------------------------------- 17 files changed, 674 insertions(+), 664 deletions(-) delete mode 100644 database/src/counters.rs delete mode 100644 database/src/filter/binning.rs delete mode 100644 database/src/filter/mod.rs create mode 100644 database/src/kv/binning.rs create mode 100644 database/src/kv/counters.rs create mode 100644 database/src/kv/mod.rs create mode 100644 database/src/kv/prefix_iterator.rs create mode 100644 database/src/kv/sort/mod.rs create mode 100644 database/src/kv/sort/none.rs create mode 100644 database/src/kv/sort/value.rs delete mode 100644 database/src/prefix_iterator.rs delete mode 100644 database/src/query.rs delete mode 100644 database/src/sort/mod.rs delete mode 100644 database/src/sort/none.rs delete mode 100644 database/src/sort/value.rs delete mode 100644 database/src/table.rs (limited to 'database') diff --git a/database/src/counters.rs b/database/src/counters.rs deleted file mode 100644 index 17e2de8..0000000 --- a/database/src/counters.rs +++ /dev/null @@ -1,61 +0,0 @@ -/* - 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) 2026 metamuffin -*/ -use crate::{ - filter::binning::Binning, - query::Query, - table::{RowNum, TableNum}, -}; -use anyhow::Result; -use jellykv::{ReadTransaction, WriteTransaction}; -use jellyobject::Object; -use std::collections::HashMap; - -pub(crate) struct Counters(pub HashMap); -impl Counters { - pub fn update( - &self, - txn: &mut dyn WriteTransaction, - ob: Object<'_>, - remove: bool, - ) -> Result<()> { - for (b, &table) in &self.0 { - let mut k = vec![vec![]]; - b.apply(ob, &mut k); - if k.is_empty() { - continue; - } - let mut counter = read_counter(txn, table)?; - if remove { - counter += k.len() as u64; - } else { - counter -= k.len() as u64; - } - write_counter(txn, table, counter)?; - } - Ok(()) - } - pub fn count(&self, txn: &dyn ReadTransaction, query: &Query) -> Result> { - let mut total = 0; - for binning in query.filter.get_bins() { - let Some(b) = self.0.get(&binning) else { - return Ok(None); - }; - total += read_counter(txn, *b)?; - } - Ok(Some(total)) - } -} - -pub fn write_counter(txn: &mut dyn WriteTransaction, t: TableNum, value: u64) -> Result<()> { - txn.set(&t.to_be_bytes(), &value.to_be_bytes()) -} -pub fn read_counter(txn: &dyn ReadTransaction, t: TableNum) -> Result { - Ok(txn - .get(&t.to_be_bytes())? - .map(|k| k.as_slice().try_into().map(RowNum::from_be_bytes).ok()) - .flatten() - .unwrap_or(0)) -} diff --git a/database/src/filter/binning.rs b/database/src/filter/binning.rs deleted file mode 100644 index 3265e1a..0000000 --- a/database/src/filter/binning.rs +++ /dev/null @@ -1,52 +0,0 @@ -/* - 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) 2026 metamuffin -*/ - -use jellyobject::{Object, Path}; - -/// Sorted list of components to bin objects by filterable values. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Binning(Vec); - -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] -pub enum BinningComponent { - Has(Path), - Match(Path), -} - -impl Binning { - pub fn new(mut comps: Vec) -> Self { - comps.sort(); - Self(comps) - } - pub fn apply(&self, ob: Object<'_>, keys: &mut Vec>) { - for f in &self.0 { - f.apply(ob, keys); - } - } -} -impl BinningComponent { - pub fn apply(&self, ob: Object<'_>, keys: &mut Vec>) { - match self { - BinningComponent::Has(path) => { - let has = path.get_matching_value(ob).is_some(); - for co in keys { - co.push(has as u8) - } - } - BinningComponent::Match(path) => { - let mut new_out = Vec::new(); - for value in path.get_matching_values(ob) { - for mut co in keys.clone() { - co.extend((co.len() as u32).to_be_bytes()); - co.extend(value); - new_out.push(co); - } - } - *keys = new_out; - } - } - } -} diff --git a/database/src/filter/mod.rs b/database/src/filter/mod.rs deleted file mode 100644 index c40e4c0..0000000 --- a/database/src/filter/mod.rs +++ /dev/null @@ -1,107 +0,0 @@ -/* - 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) 2026 metamuffin -*/ -pub mod binning; - -use crate::{ - filter::binning::{Binning, BinningComponent}, - query::Filter, -}; - -impl Filter { - pub fn get_bins(&self) -> Vec { - fn recurse(f: &Filter) -> Vec> { - match f { - Filter::True => vec![vec![]], - Filter::All(filters) => { - let mut o = vec![vec![]]; - for filter in filters { - let mut new_o = Vec::new(); - for par in recurse(filter) { - for mut prev in o.clone() { - prev.extend(par.clone()); - new_o.push(prev); - } - } - o = new_o; - } - o - } - Filter::Any(filters) => filters.iter().flat_map(|f| recurse(f)).collect(), - Filter::Match(path, _) => vec![vec![BinningComponent::Match(path.to_owned())]], - Filter::Has(path) => vec![vec![BinningComponent::Has(path.to_owned())]], - } - } - recurse(self).into_iter().map(Binning::new).collect() - } -} - -#[cfg(test)] -mod test { - use crate::{ - filter::binning::{Binning, BinningComponent}, - query::Filter, - }; - use jellyobject::{Path, Tag}; - - #[test] - fn all() { - let f = Filter::All(vec![ - Filter::Has(Path(vec![Tag(0)])), - Filter::Has(Path(vec![Tag(1)])), - ]); - let bins = vec![Binning::new(vec![ - BinningComponent::Has(Path(vec![Tag(0)])), - BinningComponent::Has(Path(vec![Tag(1)])), - ])]; - assert_eq!(f.get_bins(), bins) - } - - #[test] - fn any() { - let f = Filter::Any(vec![ - Filter::Has(Path(vec![Tag(0)])), - Filter::Has(Path(vec![Tag(1)])), - ]); - let bins = vec![ - Binning::new(vec![BinningComponent::Has(Path(vec![Tag(0)]))]), - Binning::new(vec![BinningComponent::Has(Path(vec![Tag(1)]))]), - ]; - assert_eq!(f.get_bins(), bins) - } - - #[test] - fn nested() { - let f = Filter::All(vec![ - Filter::Any(vec![ - Filter::Has(Path(vec![Tag(0)])), - Filter::Has(Path(vec![Tag(1)])), - ]), - Filter::Any(vec![ - Filter::Has(Path(vec![Tag(2)])), - Filter::Has(Path(vec![Tag(3)])), - ]), - ]); - let bins = vec![ - Binning::new(vec![ - BinningComponent::Has(Path(vec![Tag(0)])), - BinningComponent::Has(Path(vec![Tag(2)])), - ]), - Binning::new(vec![ - BinningComponent::Has(Path(vec![Tag(1)])), - BinningComponent::Has(Path(vec![Tag(2)])), - ]), - Binning::new(vec![ - BinningComponent::Has(Path(vec![Tag(0)])), - BinningComponent::Has(Path(vec![Tag(3)])), - ]), - Binning::new(vec![ - BinningComponent::Has(Path(vec![Tag(1)])), - BinningComponent::Has(Path(vec![Tag(3)])), - ]), - ]; - assert_eq!(f.get_bins(), bins) - } -} diff --git a/database/src/kv/binning.rs b/database/src/kv/binning.rs new file mode 100644 index 0000000..7bec294 --- /dev/null +++ b/database/src/kv/binning.rs @@ -0,0 +1,148 @@ +/* + 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) 2026 metamuffin +*/ + +use jellyobject::{Object, Path}; + +use crate::Filter; + +/// Sorted list of components to bin objects by filterable values. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Binning(Vec); + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +pub enum BinningComponent { + Has(Path), + Match(Path), +} + +impl Binning { + pub fn new(mut comps: Vec) -> Self { + comps.sort(); + Self(comps) + } + pub fn apply(&self, ob: Object<'_>, keys: &mut Vec>) { + for f in &self.0 { + f.apply(ob, keys); + } + } +} +impl BinningComponent { + pub fn apply(&self, ob: Object<'_>, keys: &mut Vec>) { + match self { + BinningComponent::Has(path) => { + let has = path.get_matching_value(ob).is_some(); + for co in keys { + co.push(has as u8) + } + } + BinningComponent::Match(path) => { + let mut new_out = Vec::new(); + for value in path.get_matching_values(ob) { + for mut co in keys.clone() { + co.extend((co.len() as u32).to_be_bytes()); + co.extend(value); + new_out.push(co); + } + } + *keys = new_out; + } + } + } +} + +impl Filter { + pub fn get_bins(&self) -> Vec { + fn recurse(f: &Filter) -> Vec> { + match f { + Filter::True => vec![vec![]], + Filter::All(filters) => { + let mut o = vec![vec![]]; + for filter in filters { + let mut new_o = Vec::new(); + for par in recurse(filter) { + for mut prev in o.clone() { + prev.extend(par.clone()); + new_o.push(prev); + } + } + o = new_o; + } + o + } + Filter::Any(filters) => filters.iter().flat_map(|f| recurse(f)).collect(), + Filter::Match(path, _) => vec![vec![BinningComponent::Match(path.to_owned())]], + Filter::Has(path) => vec![vec![BinningComponent::Has(path.to_owned())]], + } + } + recurse(self).into_iter().map(Binning::new).collect() + } +} + +#[cfg(test)] +mod test { + use jellyobject::{Path, Tag}; + + use crate::{Filter, kv::binning::{Binning, BinningComponent}}; + + #[test] + fn all() { + let f = Filter::All(vec![ + Filter::Has(Path(vec![Tag(0)])), + Filter::Has(Path(vec![Tag(1)])), + ]); + let bins = vec![Binning::new(vec![ + BinningComponent::Has(Path(vec![Tag(0)])), + BinningComponent::Has(Path(vec![Tag(1)])), + ])]; + assert_eq!(f.get_bins(), bins) + } + + #[test] + fn any() { + let f = Filter::Any(vec![ + Filter::Has(Path(vec![Tag(0)])), + Filter::Has(Path(vec![Tag(1)])), + ]); + let bins = vec![ + Binning::new(vec![BinningComponent::Has(Path(vec![Tag(0)]))]), + Binning::new(vec![BinningComponent::Has(Path(vec![Tag(1)]))]), + ]; + assert_eq!(f.get_bins(), bins) + } + + #[test] + fn nested() { + let f = Filter::All(vec![ + Filter::Any(vec![ + Filter::Has(Path(vec![Tag(0)])), + Filter::Has(Path(vec![Tag(1)])), + ]), + Filter::Any(vec![ + Filter::Has(Path(vec![Tag(2)])), + Filter::Has(Path(vec![Tag(3)])), + ]), + ]); + let bins = vec![ + Binning::new(vec![ + BinningComponent::Has(Path(vec![Tag(0)])), + BinningComponent::Has(Path(vec![Tag(2)])), + ]), + Binning::new(vec![ + BinningComponent::Has(Path(vec![Tag(1)])), + BinningComponent::Has(Path(vec![Tag(2)])), + ]), + Binning::new(vec![ + BinningComponent::Has(Path(vec![Tag(0)])), + BinningComponent::Has(Path(vec![Tag(3)])), + ]), + Binning::new(vec![ + BinningComponent::Has(Path(vec![Tag(1)])), + BinningComponent::Has(Path(vec![Tag(3)])), + ]), + ]; + assert_eq!(f.get_bins(), bins) + } +} diff --git a/database/src/kv/counters.rs b/database/src/kv/counters.rs new file mode 100644 index 0000000..fae7b42 --- /dev/null +++ b/database/src/kv/counters.rs @@ -0,0 +1,59 @@ +/* + 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) 2026 metamuffin +*/ +use crate::{ + Query, RowNum, + kv::{self, TableNum, binning::Binning}, +}; +use anyhow::Result; +use jellyobject::Object; +use std::collections::HashMap; + +pub(crate) struct Counters(pub HashMap); +impl Counters { + pub fn update( + &self, + txn: &mut dyn jellykv::Transaction, + ob: Object<'_>, + remove: bool, + ) -> Result<()> { + for (b, &table) in &self.0 { + let mut k = vec![vec![]]; + b.apply(ob, &mut k); + if k.is_empty() { + continue; + } + let mut counter = read_counter(txn, table)?; + if remove { + counter += k.len() as u64; + } else { + counter -= k.len() as u64; + } + write_counter(txn, table, counter)?; + } + Ok(()) + } + pub fn count(&self, txn: &dyn jellykv::Transaction, query: &Query) -> Result> { + let mut total = 0; + for binning in query.filter.get_bins() { + let Some(b) = self.0.get(&binning) else { + return Ok(None); + }; + total += read_counter(txn, *b)?; + } + Ok(Some(total)) + } +} + +pub fn write_counter(txn: &mut dyn jellykv::Transaction, t: TableNum, value: u64) -> Result<()> { + txn.set(&t.to_be_bytes(), &value.to_be_bytes()) +} +pub fn read_counter(txn: &dyn jellykv::Transaction, t: TableNum) -> Result { + Ok(txn + .get(&t.to_be_bytes())? + .map(|k| k.as_slice().try_into().map(RowNum::from_be_bytes).ok()) + .flatten() + .unwrap_or(0)) +} diff --git a/database/src/kv/mod.rs b/database/src/kv/mod.rs new file mode 100644 index 0000000..b4bd000 --- /dev/null +++ b/database/src/kv/mod.rs @@ -0,0 +1,217 @@ +/* + 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) 2026 metamuffin +*/ + +pub mod binning; +pub mod counters; +pub mod prefix_iterator; +pub mod sort; + +use crate::{ + Database, Query, RowIter, RowNum, Transaction, + kv::{binning::Binning, sort::SortKey}, +}; +use anyhow::Result; +use jellyobject::ObjectBuffer; + +pub type IndexKey = (Binning, SortKey); +pub type TableNum = u32; + +impl Database for T { + fn transaction(&self, f: &mut dyn FnMut(&mut dyn Transaction) -> Result<()>) -> Result<()> { + todo!() + } +} + +impl Transaction for T { + fn insert(&self, entry: ObjectBuffer) -> Result { + todo!() + } + + fn get(&self, row: RowNum) -> Result> { + todo!() + } + + fn remove(&self, row: RowNum) -> Result<()> { + todo!() + } + + fn update(&self, row: RowNum, entry: ObjectBuffer) -> Result<()> { + todo!() + } + + fn query(&self, query: Query) -> Result { + todo!() + } + + fn query_single(&self, query: Query) -> Result> { + todo!() + } + + fn count(&self, query: Query) -> Result { + todo!() + } +} + +// pub struct Table { +// id: TableNum, +// pub(crate) counters: HashMap, +// pub(crate) indices: HashMap, +// } +// impl Table { +// pub fn new(id: u32) -> Self { +// Self { +// id, +// counters: HashMap::new(), +// indices: HashMap::new(), +// } +// } +// fn key(&self, row: RowNum) -> Vec { +// let mut key = Vec::new(); +// key.extend(self.id.to_be_bytes()); +// key.extend(row.to_be_bytes()); +// key +// } +// pub fn iter(&self, txn: &dyn ReadTransaction) -> Result>> { +// Ok(PrefixIterator { +// inner: txn.iter(&self.id.to_be_bytes(), false)?, +// prefix: self.id.to_be_bytes().to_vec().into(), +// } +// .map(|r| r.map(|r| RowNum::from_be_bytes(r.try_into().unwrap())))) +// } +// pub fn insert(&self, txn: &mut dyn WriteTransaction, entry: ObjectBuffer) -> Result { +// let mut id_counter = read_counter(txn, self.id)?; +// let row = id_counter; +// id_counter += 1; +// write_counter(txn, self.id, id_counter)?; + +// txn.set(&self.key(row), bytemuck::cast_slice(entry.0.as_slice()))?; + +// let ob = entry.as_object(); +// // for idx in self.indices.values() { +// // idx.add(txn, row, ob)?; +// // } + +// Ok(row) +// } +// pub fn get(&self, txn: &dyn ReadTransaction, row: RowNum) -> Result> { +// Ok(txn.get(&self.key(row))?.map(ObjectBuffer::from)) +// } +// pub fn remove(&self, db: &mut dyn WriteTransaction, row: RowNum) -> Result { +// let Some(entry) = self.get(db, row)? else { +// return Ok(false); +// }; +// let ob = entry.as_object(); +// // for index in self.indices.values() { +// // index.remove(db, row, ob)?; +// // } +// db.del(&self.key(row))?; +// Ok(true) +// } +// pub fn update( +// &self, +// txn: &mut dyn WriteTransaction, +// row: RowNum, +// entry: ObjectBuffer, +// ) -> Result<()> { +// let before = self +// .get(txn, row)? +// .ok_or(anyhow!("row to update missing"))?; +// let before = before.as_object(); +// let after = entry.as_object(); + +// txn.set(&self.key(row), bytemuck::cast_slice(entry.0.as_slice()))?; + +// // for index in self.indices.values() { +// // if !index.compare(before, after) { +// // index.remove(txn, row, before)?; +// // index.add(txn, row, after)?; +// // } +// // } + +// Ok(()) +// } +// pub fn query(&self, txn: &dyn ReadTransaction, query: Query) -> Result { +// // query +// // .filter +// // .get_bins() +// // .into_iter() +// // .flat_map(|b| { +// // let ikey = (b, query.sort.key()); +// // self.indices.get(&ikey) +// // }) +// // .map(|i| i.query(txn, &query.sort)) +// todo!() +// } +// pub fn query_single(&self, txn: &dyn ReadTransaction, query: Query) -> Result> { +// self.query(txn, query)?.next().transpose() +// } +// pub fn count(&self, txn: &dyn ReadTransaction, query: Query) -> Result { +// let mut total = 0; +// for b in query.filter.get_bins() { +// if let Some(&c) = self.counters.get(&b) { +// total += read_counter(txn, c)?; +// } +// } +// Ok(total) +// } +// } + +// #[cfg(test)] +// mod test { +// use { +// table::Table, +// test_shared::{NAME, new_bob}, +// }; +// use anyhow::Result; +// use jellykv::Database; + +// #[test] +// pub fn insert_get() -> Result<()> { +// let db = jellykv::memory::new(); +// let table = Table::new(5); + +// let mut bob_row = 0; +// db.write_transaction(&mut |txn| { +// bob_row = table.insert(txn, new_bob())?; +// Ok(()) +// })?; + +// let mut bob = None; +// db.read_transaction(&mut |txn| { +// bob = table.get(txn, bob_row)?; +// Ok(()) +// })?; + +// assert_eq!(bob.unwrap().as_object().get(NAME).unwrap(), "Bob"); +// Ok(()) +// } + +// #[test] +// pub fn update() -> Result<()> { +// let db = jellykv::memory::new(); +// let table = Table::new(5); + +// let mut bob_row = 0; +// let mut bob = None; + +// db.write_transaction(&mut |txn| { +// bob_row = table.insert(txn, new_bob())?; +// Ok(()) +// })?; +// db.write_transaction(&mut |txn| { +// let better_bob = new_bob().as_object().insert(NAME, "Better Bob"); +// table.update(txn, bob_row, better_bob)?; +// Ok(()) +// })?; +// db.read_transaction(&mut |txn| { +// bob = table.get(txn, bob_row)?; +// Ok(()) +// })?; + +// assert_eq!(bob.unwrap().as_object().get(NAME).unwrap(), "Better Bob"); +// Ok(()) +// } +// } diff --git a/database/src/kv/prefix_iterator.rs b/database/src/kv/prefix_iterator.rs new file mode 100644 index 0000000..9a73558 --- /dev/null +++ b/database/src/kv/prefix_iterator.rs @@ -0,0 +1,22 @@ +/* + 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) 2026 metamuffin +*/ + +use anyhow::Result; +use std::borrow::Cow; + +pub struct PrefixIterator<'a> { + pub inner: Box>> + 'a>, + pub prefix: Cow<'a, [u8]>, +} +impl Iterator for PrefixIterator<'_> { + type Item = Result>; + fn next(&mut self) -> Option { + self.inner.next().filter(|k| match k { + Ok(v) => v.starts_with(&self.prefix), + Err(_) => true, + }) + } +} diff --git a/database/src/kv/sort/mod.rs b/database/src/kv/sort/mod.rs new file mode 100644 index 0000000..403ba73 --- /dev/null +++ b/database/src/kv/sort/mod.rs @@ -0,0 +1,29 @@ +/* + 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) 2026 metamuffin +*/ + +use jellyobject::Path; + +use crate::{MultiBehaviour, Sort}; + +pub mod none; +pub mod value; + +#[derive(Hash, PartialEq, Eq)] +pub enum SortKey { + None, + Value(Path, MultiBehaviour), + Text(Path), +} + +impl Sort { + pub fn key(&self) -> SortKey { + match self { + Sort::None => SortKey::None, + Sort::Value(vs) => SortKey::Value(vs.path.clone(), vs.multi), + Sort::TextSearch(p, _) => SortKey::Text(p.to_owned()), + } + } +} diff --git a/database/src/kv/sort/none.rs b/database/src/kv/sort/none.rs new file mode 100644 index 0000000..b4d5db2 --- /dev/null +++ b/database/src/kv/sort/none.rs @@ -0,0 +1,50 @@ +/* + 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) 2026 metamuffin +*/ + +use crate::{ + RowNum, + kv::{TableNum, binning::Binning}, +}; +use jellyobject::Object; + +pub struct UnsortedIndex { + id: TableNum, + binning: Binning, +} + +impl UnsortedIndex { + pub fn new(id: TableNum, binning: Binning) -> Self { + Self { id, binning } + } + fn keys(&self, id: RowNum, ob: Object) -> Vec> { + let mut keys = vec![self.id.to_be_bytes().to_vec()]; + self.binning.apply(ob, &mut keys); + for k in &mut keys { + k.extend(id.to_ne_bytes()); + } + keys + } +} +// impl Index for UnsortedIndex { +// fn add(&self, db: &mut dyn WriteTransaction, id: RowNum, ob: Object) -> Result<()> { +// for key in self.keys(id, ob) { +// db.set(&key, &[])?; +// } +// Ok(()) +// } +// fn remove(&self, db: &mut dyn WriteTransaction, id: RowNum, ob: Object) -> Result<()> { +// for key in self.keys(id, ob) { +// db.del(&key)?; +// } +// Ok(()) +// } +// fn compare(&self, before: Object, after: Object) -> bool { +// self.keys(0, before) == self.keys(0, after) +// } +// fn query(&self, txn: &mut dyn ReadTransaction, _sort: &Sort) -> Result { +// todo!() +// } +// } diff --git a/database/src/kv/sort/value.rs b/database/src/kv/sort/value.rs new file mode 100644 index 0000000..0d4ceb7 --- /dev/null +++ b/database/src/kv/sort/value.rs @@ -0,0 +1,86 @@ +/* + 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) 2026 metamuffin +*/ + +use jellyobject::Object; + +use crate::{ + MultiBehaviour, RowNum, ValueSort, + kv::{TableNum, binning::Binning}, +}; + +pub struct ValueIndex { + id: TableNum, + binning: Binning, + sort: ValueSort, +} + +impl ValueIndex { + pub fn new(id: TableNum, binning: Binning, sort: ValueSort) -> Self { + Self { id, binning, sort } + } + fn keys(&self, id: RowNum, ob: Object) -> Vec> { + let mut keys = vec![self.id.to_be_bytes().to_vec()]; + self.binning.apply(ob, &mut keys); + self.sort.apply(ob, &mut keys); + for k in &mut keys { + k.extend(id.to_ne_bytes()); + } + keys + } +} +// impl Index for ValueIndex { +// fn add(&self, db: &mut dyn WriteTransaction, id: RowNum, ob: Object) -> Result<()> { +// for key in self.keys(id, ob) { +// db.set(&key, &[])?; +// } +// Ok(()) +// } +// fn remove(&self, db: &mut dyn WriteTransaction, id: RowNum, ob: Object) -> Result<()> { +// for key in self.keys(id, ob) { +// db.del(&key)?; +// } +// Ok(()) +// } +// fn compare(&self, before: Object, after: Object) -> bool { +// self.keys(0, before) == self.keys(0, after) +// } +// fn query(&self, txn: &mut dyn ReadTransaction, sort: &Sort) -> Result { +// todo!() +// } +// } +impl ValueSort { + fn apply(&self, ob: Object, keys: &mut Vec>) { + match self.multi { + MultiBehaviour::First => { + if let Some(val) = self.path.get_matching_value(ob) { + for k in keys.iter_mut() { + k.extend(val); + } + } else { + keys.clear(); + } + } + MultiBehaviour::ForEach => { + let mut keys_out = Vec::new(); + for val in self.path.get_matching_values(ob) { + for mut k in keys.clone() { + k.extend(val); + keys_out.push(k); + } + } + *keys = keys_out + } + MultiBehaviour::Max => todo!(), + MultiBehaviour::Min => todo!(), + MultiBehaviour::Count => { + let count = self.path.get_matching_values(ob).len() as u32; + for k in keys.iter_mut() { + k.extend(count.to_be_bytes()); + } + } + } + } +} diff --git a/database/src/lib.rs b/database/src/lib.rs index 00e6905..f6f3fa9 100644 --- a/database/src/lib.rs +++ b/database/src/lib.rs @@ -3,15 +3,70 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin */ -pub mod filter; -pub mod prefix_iterator; -pub mod query; -pub mod sort; -pub mod table; +pub mod kv; #[cfg(test)] pub mod test_shared; -pub mod counters; -pub type Pad32 = u32; +use anyhow::Result; +use jellyobject::{ObjectBuffer, Path}; -pub use jellykv as kv; +pub type RowNum = u64; +pub type RowIter = Box>>; + +pub trait Database { + fn transaction(&self, f: &mut dyn FnMut(&mut dyn Transaction) -> Result<()>) -> Result<()>; +} + +pub trait Transaction { + fn insert(&self, entry: ObjectBuffer) -> Result; + fn get(&self, row: RowNum) -> Result>; + fn remove(&self, row: RowNum) -> Result<()>; + fn update(&self, row: RowNum, entry: ObjectBuffer) -> Result<()>; + fn query(&self, query: Query) -> Result; + fn query_single(&self, query: Query) -> Result>; + fn count(&self, query: Query) -> Result; +} + +#[derive(Default)] +pub struct Query { + pub filter: Filter, + pub sort: Sort, +} + +#[derive(Default)] +pub enum Sort { + #[default] + None, + Value(ValueSort), + TextSearch(Path, String), +} + +pub struct ValueSort { + pub order: SortOrder, + pub path: Path, + pub multi: MultiBehaviour, + pub offset: Option>, +} + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum MultiBehaviour { + First, + ForEach, + Max, + Min, + Count, +} +pub enum SortOrder { + Ascending, + Descending, +} + +#[derive(Debug, Clone, Default)] +pub enum Filter { + #[default] + True, + All(Vec), + Any(Vec), + Match(Path, Vec), + Has(Path), +} diff --git a/database/src/prefix_iterator.rs b/database/src/prefix_iterator.rs deleted file mode 100644 index 9a73558..0000000 --- a/database/src/prefix_iterator.rs +++ /dev/null @@ -1,22 +0,0 @@ -/* - 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) 2026 metamuffin -*/ - -use anyhow::Result; -use std::borrow::Cow; - -pub struct PrefixIterator<'a> { - pub inner: Box>> + 'a>, - pub prefix: Cow<'a, [u8]>, -} -impl Iterator for PrefixIterator<'_> { - type Item = Result>; - fn next(&mut self) -> Option { - self.inner.next().filter(|k| match k { - Ok(v) => v.starts_with(&self.prefix), - Err(_) => true, - }) - } -} diff --git a/database/src/query.rs b/database/src/query.rs deleted file mode 100644 index 002a872..0000000 --- a/database/src/query.rs +++ /dev/null @@ -1,51 +0,0 @@ -/* - 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) 2026 metamuffin -*/ - -use jellyobject::Path; - -#[derive(Default)] -pub struct Query { - pub filter: Filter, - pub sort: Sort, -} - -#[derive(Default)] -pub enum Sort { - #[default] - None, - Value(ValueSort), - TextSearch(Path, String), -} - -pub struct ValueSort { - pub order: SortOrder, - pub path: Path, - pub multi: MultiBehaviour, - pub offset: Option>, -} - -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] -pub enum MultiBehaviour { - First, - ForEach, - Max, - Min, - Count, -} -pub enum SortOrder { - Ascending, - Descending, -} - -#[derive(Debug, Clone, Default)] -pub enum Filter { - #[default] - True, - All(Vec), - Any(Vec), - Match(Path, Vec), - Has(Path), -} diff --git a/database/src/sort/mod.rs b/database/src/sort/mod.rs deleted file mode 100644 index 58d8eff..0000000 --- a/database/src/sort/mod.rs +++ /dev/null @@ -1,37 +0,0 @@ -/* - 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) 2026 metamuffin -*/ - -use crate::{ - query::{MultiBehaviour, Sort}, - table::IndexKey, -}; -use anyhow::Result; -use jellykv::WriteTransaction; -use jellyobject::{Object, Path}; - -pub mod none; -pub mod value; - -#[derive(Hash, PartialEq, Eq)] -pub enum SortKey { - None, - Value(Path, MultiBehaviour), - Text(Path), -} - -impl Sort { - pub fn key(&self) -> SortKey { - match self { - Sort::None => SortKey::None, - Sort::Value(vs) => SortKey::Value(vs.path.clone(), vs.multi), - Sort::TextSearch(p, _) => SortKey::Text(p.to_owned()), - } - } -} - -pub fn index_add(txn: &mut dyn WriteTransaction, ik: &IndexKey, ob: &Object) -> Result<()> { - -} diff --git a/database/src/sort/none.rs b/database/src/sort/none.rs deleted file mode 100644 index efaa7f8..0000000 --- a/database/src/sort/none.rs +++ /dev/null @@ -1,53 +0,0 @@ -/* - 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) 2026 metamuffin -*/ - -use crate::{ - filter::binning::Binning, - query::Sort, - table::{RowIter, RowNum, TableNum}, -}; -use anyhow::Result; -use jellykv::{ReadTransaction, WriteTransaction}; -use jellyobject::Object; - -pub struct UnsortedIndex { - id: TableNum, - binning: Binning, -} - -impl UnsortedIndex { - pub fn new(id: TableNum, binning: Binning) -> Self { - Self { id, binning } - } - fn keys(&self, id: RowNum, ob: Object) -> Vec> { - let mut keys = vec![self.id.to_be_bytes().to_vec()]; - self.binning.apply(ob, &mut keys); - for k in &mut keys { - k.extend(id.to_ne_bytes()); - } - keys - } -} -impl Index for UnsortedIndex { - fn add(&self, db: &mut dyn WriteTransaction, id: RowNum, ob: Object) -> Result<()> { - for key in self.keys(id, ob) { - db.set(&key, &[])?; - } - Ok(()) - } - fn remove(&self, db: &mut dyn WriteTransaction, id: RowNum, ob: Object) -> Result<()> { - for key in self.keys(id, ob) { - db.del(&key)?; - } - Ok(()) - } - fn compare(&self, before: Object, after: Object) -> bool { - self.keys(0, before) == self.keys(0, after) - } - fn query(&self, txn: &mut dyn ReadTransaction, _sort: &Sort) -> Result { - todo!() - } -} diff --git a/database/src/sort/value.rs b/database/src/sort/value.rs deleted file mode 100644 index 6e42fd7..0000000 --- a/database/src/sort/value.rs +++ /dev/null @@ -1,89 +0,0 @@ -/* - 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) 2026 metamuffin -*/ - -use crate::{ - filter::binning::Binning, - query::{MultiBehaviour, Sort, ValueSort}, - sort::Index, - table::{RowIter, RowNum, TableNum}, -}; -use anyhow::Result; -use jellykv::{ReadTransaction, WriteTransaction}; -use jellyobject::Object; - -pub struct ValueIndex { - id: TableNum, - binning: Binning, - sort: ValueSort, -} - -impl ValueIndex { - pub fn new(id: TableNum, binning: Binning, sort: ValueSort) -> Self { - Self { id, binning, sort } - } - fn keys(&self, id: RowNum, ob: Object) -> Vec> { - let mut keys = vec![self.id.to_be_bytes().to_vec()]; - self.binning.apply(ob, &mut keys); - self.sort.apply(ob, &mut keys); - for k in &mut keys { - k.extend(id.to_ne_bytes()); - } - keys - } -} -impl Index for ValueIndex { - fn add(&self, db: &mut dyn WriteTransaction, id: RowNum, ob: Object) -> Result<()> { - for key in self.keys(id, ob) { - db.set(&key, &[])?; - } - Ok(()) - } - fn remove(&self, db: &mut dyn WriteTransaction, id: RowNum, ob: Object) -> Result<()> { - for key in self.keys(id, ob) { - db.del(&key)?; - } - Ok(()) - } - fn compare(&self, before: Object, after: Object) -> bool { - self.keys(0, before) == self.keys(0, after) - } - fn query(&self, txn: &mut dyn ReadTransaction, sort: &Sort) -> Result { - todo!() - } -} -impl ValueSort { - fn apply(&self, ob: Object, keys: &mut Vec>) { - match self.multi { - MultiBehaviour::First => { - if let Some(val) = self.path.get_matching_value(ob) { - for k in keys.iter_mut() { - k.extend(val); - } - } else { - keys.clear(); - } - } - MultiBehaviour::ForEach => { - let mut keys_out = Vec::new(); - for val in self.path.get_matching_values(ob) { - for mut k in keys.clone() { - k.extend(val); - keys_out.push(k); - } - } - *keys = keys_out - } - MultiBehaviour::Max => todo!(), - MultiBehaviour::Min => todo!(), - MultiBehaviour::Count => { - let count = self.path.get_matching_values(ob).len() as u32; - for k in keys.iter_mut() { - k.extend(count.to_be_bytes()); - } - } - } - } -} diff --git a/database/src/table.rs b/database/src/table.rs deleted file mode 100644 index ee8d66b..0000000 --- a/database/src/table.rs +++ /dev/null @@ -1,184 +0,0 @@ -/* - 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) 2026 metamuffin -*/ - -use crate::{ - counters::{read_counter, write_counter}, - filter::binning::Binning, - prefix_iterator::PrefixIterator, - query::Query, - sort::{Index, SortKey}, -}; -use anyhow::{Result, anyhow}; -use jellykv::{ReadTransaction, WriteTransaction}; -use jellyobject::ObjectBuffer; -use std::collections::HashMap; - -pub type TableNum = u32; -pub type RowNum = u64; - -pub type RowIter = Box>>; -pub type IndexKey = (Binning, SortKey); - -pub struct Table { - id: TableNum, - pub(crate) counters: HashMap, - pub(crate) indices: HashMap, -} -impl Table { - pub fn new(id: u32) -> Self { - Self { - id, - counters: HashMap::new(), - indices: HashMap::new(), - } - } - fn key(&self, row: RowNum) -> Vec { - let mut key = Vec::new(); - key.extend(self.id.to_be_bytes()); - key.extend(row.to_be_bytes()); - key - } - pub fn iter(&self, txn: &dyn ReadTransaction) -> Result>> { - Ok(PrefixIterator { - inner: txn.iter(&self.id.to_be_bytes(), false)?, - prefix: self.id.to_be_bytes().to_vec().into(), - } - .map(|r| r.map(|r| RowNum::from_be_bytes(r.try_into().unwrap())))) - } - pub fn insert(&self, txn: &mut dyn WriteTransaction, entry: ObjectBuffer) -> Result { - let mut id_counter = read_counter(txn, self.id)?; - let row = id_counter; - id_counter += 1; - write_counter(txn, self.id, id_counter)?; - - txn.set(&self.key(row), bytemuck::cast_slice(entry.0.as_slice()))?; - - let ob = entry.as_object(); - for idx in self.indices.values() { - idx.add(txn, row, ob)?; - } - - Ok(row) - } - pub fn get(&self, txn: &dyn ReadTransaction, row: RowNum) -> Result> { - Ok(txn.get(&self.key(row))?.map(ObjectBuffer::from)) - } - pub fn remove(&self, db: &mut dyn WriteTransaction, row: RowNum) -> Result { - let Some(entry) = self.get(db, row)? else { - return Ok(false); - }; - let ob = entry.as_object(); - for index in self.indices.values() { - index.remove(db, row, ob)?; - } - db.del(&self.key(row))?; - Ok(true) - } - pub fn update( - &self, - txn: &mut dyn WriteTransaction, - row: RowNum, - entry: ObjectBuffer, - ) -> Result<()> { - let before = self - .get(txn, row)? - .ok_or(anyhow!("row to update missing"))?; - let before = before.as_object(); - let after = entry.as_object(); - - txn.set(&self.key(row), bytemuck::cast_slice(entry.0.as_slice()))?; - - for index in self.indices.values() { - if !index.compare(before, after) { - index.remove(txn, row, before)?; - index.add(txn, row, after)?; - } - } - - Ok(()) - } - pub fn query(&self, txn: &dyn ReadTransaction, query: Query) -> Result { - // query - // .filter - // .get_bins() - // .into_iter() - // .flat_map(|b| { - // let ikey = (b, query.sort.key()); - // self.indices.get(&ikey) - // }) - // .map(|i| i.query(txn, &query.sort)) - todo!() - } - pub fn query_single(&self, txn: &dyn ReadTransaction, query: Query) -> Result> { - self.query(txn, query)?.next().transpose() - } - pub fn count(&self, txn: &dyn ReadTransaction, query: Query) -> Result { - let mut total = 0; - for b in query.filter.get_bins() { - if let Some(&c) = self.counters.get(&b) { - total += read_counter(txn, c)?; - } - } - Ok(total) - } -} - -#[cfg(test)] -mod test { - use crate::{ - table::Table, - test_shared::{NAME, new_bob}, - }; - use anyhow::Result; - use jellykv::Database; - - #[test] - pub fn insert_get() -> Result<()> { - let db = jellykv::memory::new(); - let table = Table::new(5); - - let mut bob_row = 0; - db.write_transaction(&mut |txn| { - bob_row = table.insert(txn, new_bob())?; - Ok(()) - })?; - - let mut bob = None; - db.read_transaction(&mut |txn| { - bob = table.get(txn, bob_row)?; - Ok(()) - })?; - - assert_eq!(bob.unwrap().as_object().get(NAME).unwrap(), "Bob"); - Ok(()) - } - - #[test] - pub fn update() -> Result<()> { - let db = jellykv::memory::new(); - let table = Table::new(5); - - let mut bob_row = 0; - let mut bob = None; - - db.write_transaction(&mut |txn| { - bob_row = table.insert(txn, new_bob())?; - Ok(()) - })?; - db.write_transaction(&mut |txn| { - let better_bob = new_bob().as_object().insert(NAME, "Better Bob"); - table.update(txn, bob_row, better_bob)?; - Ok(()) - })?; - db.read_transaction(&mut |txn| { - bob = table.get(txn, bob_row)?; - Ok(()) - })?; - - assert_eq!(bob.unwrap().as_object().get(NAME).unwrap(), "Better Bob"); - Ok(()) - } -} -- cgit v1.3