From ad62fec21f91ece56428de1d3f339615aa4fb6c8 Mon Sep 17 00:00:00 2001 From: Riley Apeldoorn Date: Sun, 21 Apr 2024 09:49:02 +0200 Subject: [PATCH] First polishing pass --- bin/pupctl/src/main.rs | 28 ++- lib/puppy/src/lib.rs | 71 +++--- lib/store/src/alias.rs | 17 ++ lib/store/src/arrow.rs | 40 ++++ lib/store/src/key.rs | 2 +- lib/store/src/lib.rs | 139 +++--------- lib/store/src/mixin.rs | 35 +++ lib/store/src/transaction.rs | 339 ++--------------------------- lib/store/src/transaction/tests.rs | 256 ++++++++++++++++++++++ lib/store/src/util.rs | 51 +++++ 10 files changed, 501 insertions(+), 477 deletions(-) create mode 100644 lib/store/src/alias.rs create mode 100644 lib/store/src/arrow.rs create mode 100644 lib/store/src/mixin.rs create mode 100644 lib/store/src/transaction/tests.rs create mode 100644 lib/store/src/util.rs diff --git a/bin/pupctl/src/main.rs b/bin/pupctl/src/main.rs index 8d56254..13fafe9 100644 --- a/bin/pupctl/src/main.rs +++ b/bin/pupctl/src/main.rs @@ -3,6 +3,7 @@ use puppy::{ self, alias::Username, arrow::{FollowRequested, Follows}, + mixin::Profile, Error, }, tl::Post, @@ -22,9 +23,9 @@ fn main() -> store::Result<()> { } if true { println!("making riley follow linen"); - if !db.contains::((riley, linen))? { + if !db.exists::((riley, linen))? { println!("follow relation does not exist yet"); - if !db.contains::((riley, linen))? { + if !db.exists::((riley, linen))? { println!("no pending follow request; creating"); puppy::fr::create(&db, riley, linen)?; } else { @@ -36,26 +37,31 @@ fn main() -> store::Result<()> { } } println!("Posts on the instance:"); - for (key, Post { content, author }) in puppy::tl::fetch_all(&db)? { - let handle = db.reverse::(author)?; + for Post { + id, + content, + author, + } in puppy::tl::fetch_all(&db)? + { + let (_, Profile { account_name, .. }) = db.lookup(author)?; let content = content.content.unwrap(); - println!("- {key} by @{handle} ({author}):\n{content}",) + println!("- {id} by @{account_name} ({author}):\n{content}",) } println!("Linen's followers:"); for id in puppy::fr::followers_of(&db, linen)? { - let handle = db.reverse::(id)?; - println!("- @{handle} ({id})"); + let (_, Profile { account_name, .. }) = db.lookup(id)?; + println!("- @{account_name} ({id})"); } println!("Riley's following:"); for id in puppy::fr::following_of(&db, riley)? { - let handle = db.reverse::(id)?; - println!("- @{handle} ({id})"); + let (_, Profile { account_name, .. }) = db.lookup(id)?; + println!("- @{account_name} ({id})"); } store::OK } fn get_or_create_actor(db: &Store, username: &str) -> Result { - let user = db.resolve::(username); + let user = db.translate::(username); match user { Ok(key) => { println!("found '{username}' ({key})"); @@ -63,7 +69,7 @@ fn get_or_create_actor(db: &Store, username: &str) -> Result { } Err(Error::Missing) => { println!("'{username}' doesn't exist yet, creating"); - let r = puppy::create_author(&db, username); + let r = puppy::create_actor(&db, username); if let Ok(ref key) = r { println!("created '{username}' with key {key}"); } diff --git a/lib/puppy/src/lib.rs b/lib/puppy/src/lib.rs index 6acd704..0846f37 100644 --- a/lib/puppy/src/lib.rs +++ b/lib/puppy/src/lib.rs @@ -3,19 +3,23 @@ use store::{ alias::Username, arrow::AuthorOf, mixin::{Content, Profile}, + util::IterExt, Keylike, }; mod tags { + //! Type tags for vertices. + use store::Tag; - pub const AUTHOR: Tag = Tag(0); + + pub const ACTOR: Tag = Tag(0); pub const POST: Tag = Tag(1); } pub fn create_post(db: &Store, author: Key, content: impl ToString) -> store::Result { let key = Key::gen(); db.transaction(|tx| { - tx.define(key, tags::POST)?; + tx.create_vertex(key, tags::POST)?; tx.update::(author, |_, mut profile| { profile.post_count += 1; Ok(profile) @@ -29,10 +33,10 @@ pub fn create_post(db: &Store, author: Key, content: impl ToString) -> store::Re }) } -pub fn create_author(db: &Store, username: impl ToString) -> store::Result { +pub fn create_actor(db: &Store, username: impl ToString) -> store::Result { let key = Key::gen(); db.transaction(|tx| { - tx.define(key, tags::AUTHOR)?; + tx.create_vertex(key, tags::ACTOR)?; tx.insert_alias(key, Username(username.to_string()))?; tx.insert(key, Profile { post_count: 0, @@ -51,10 +55,7 @@ pub fn list_posts_by_author( ) -> store::Result> { db.transaction(|tx| { tx.list_outgoing::(author) - .map(|r| { - let (post_key, _) = r?; - tx.lookup::(post_key) - }) + .bind_results(|(post_key, _)| tx.lookup::(post_key)) .collect() }) } @@ -62,24 +63,27 @@ pub fn list_posts_by_author( pub mod tl { //! Timelines - use store::{arrow::AuthorOf, mixin::Content, Key, Store}; + use store::{arrow::AuthorOf, mixin::Content, util::IterExt as _, Error, Key, Result, Store}; pub struct Post { + pub id: Key, pub author: Key, pub content: Content, } - pub fn fetch_all(db: &Store) -> store::Result> { + pub fn fetch_all(db: &Store) -> Result> { db.transaction(|tx| { let iter = tx.list::(); - iter.map(|r| { - let (post_id, content) = r?; + iter.bind_results(|(id, content)| { let author = tx - .list_incoming::(post_id) - .map(|r| r.map(|(k, _)| k)) - .next() - .unwrap()?; - Ok((post_id, Post { author, content })) + .list_incoming::(id) + .keys() + .next_or(Error::Missing)?; + Ok(Post { + id, + author, + content, + }) }) .collect() }) @@ -91,6 +95,7 @@ pub mod fr { use store::{ arrow::{FollowRequested, Follows}, + util::IterExt as _, Key, Store, OK, }; @@ -117,27 +122,15 @@ pub mod fr { } pub fn list_pending(db: &Store, target: Key) -> store::Result> { - db.transaction(|tx| { - tx.list_incoming::(target) - .map(|r| r.map(|(k, _)| k)) - .collect() - }) + db.transaction(|tx| tx.list_incoming::(target).keys().collect()) } pub fn following_of(db: &Store, actor: Key) -> store::Result> { - db.transaction(|tx| { - tx.list_outgoing::(actor) - .map(|r| r.map(|(k, _)| k)) - .collect() - }) + db.transaction(|tx| tx.list_outgoing::(actor).keys().collect()) } pub fn followers_of(db: &Store, actor: Key) -> store::Result> { - db.transaction(|tx| { - tx.list_incoming::(actor) - .map(|r| r.map(|(k, _)| k)) - .collect() - }) + db.transaction(|tx| tx.list_incoming::(actor).keys().collect()) } #[cfg(test)] @@ -147,11 +140,11 @@ pub mod fr { Key, Store, OK, }; - use crate::create_author; + use crate::create_actor; fn make_test_actors(db: &Store) -> store::Result<(Key, Key)> { - let alice = create_author(&db, "alice")?; - let bob = create_author(&db, "bob")?; + let alice = create_actor(&db, "alice")?; + let bob = create_actor(&db, "bob")?; eprintln!("alice={alice}, bob={bob}"); Ok((alice, bob)) } @@ -162,11 +155,11 @@ pub mod fr { let (alice, bob) = make_test_actors(&db)?; super::create(&db, alice, bob)?; assert!( - db.contains::((alice, bob))?, + db.exists::((alice, bob))?, "(alice -> bob) ∈ follow-requested" ); assert!( - !db.contains::((alice, bob))?, + !db.exists::((alice, bob))?, "(alice -> bob) ∉ follows" ); let pending_for_bob = super::list_pending(&db, bob)?; @@ -183,11 +176,11 @@ pub mod fr { super::accept(&db, alice, bob)?; assert!( - db.contains::((alice, bob))?, + db.exists::((alice, bob))?, "(alice -> bob) ∈ follows" ); assert!( - !db.contains::((bob, alice))?, + !db.exists::((bob, alice))?, "(bob -> alice) ∉ follows" ); diff --git a/lib/store/src/alias.rs b/lib/store/src/alias.rs new file mode 100644 index 0000000..44d9b98 --- /dev/null +++ b/lib/store/src/alias.rs @@ -0,0 +1,17 @@ +//! Alternative keys. + +use derive_more::{Display, From}; + +use crate::Space; + +/// An alternative unique key for a vertex. +pub trait Alias: ToString + From { + const SPACE: (Space, Space); +} + +#[derive(Display, From)] +pub struct Username(pub String); + +impl Alias for Username { + const SPACE: (Space, Space) = (Space("username/l"), Space("username/r")); +} diff --git a/lib/store/src/arrow.rs b/lib/store/src/arrow.rs new file mode 100644 index 0000000..1cd602e --- /dev/null +++ b/lib/store/src/arrow.rs @@ -0,0 +1,40 @@ +//! Relations between nodes. + +use bincode::{Decode, Encode}; + +use crate::Space; + +/// A directed edge between two vertices. +pub trait Arrow: Encode + Decode { + const SPACE: (Space, Space); +} + +/// Which way an arrow is pointing when viewed from a particular vertex. +pub enum Direction { + Incoming, + Outgoing, +} + +/// The node this arrow points away from is the "author" of the node the arrow points to. +#[derive(Encode, Decode)] +pub struct AuthorOf; + +impl Arrow for AuthorOf { + const SPACE: (Space, Space) = (Space("created-by/l"), Space("created-by/r")); +} + +/// The origin of this arrow has follow requested the target. +#[derive(Encode, Decode)] +pub struct FollowRequested; + +impl Arrow for FollowRequested { + const SPACE: (Space, Space) = (Space("pending-fr/l"), Space("pending-fr/r")); +} + +/// The origin "follows" the target. +#[derive(Encode, Decode)] +pub struct Follows; + +impl Arrow for Follows { + const SPACE: (Space, Space) = (Space("follows/l"), Space("follows/r")); +} diff --git a/lib/store/src/key.rs b/lib/store/src/key.rs index 2cbd049..df0d23e 100644 --- a/lib/store/src/key.rs +++ b/lib/store/src/key.rs @@ -63,7 +63,7 @@ pub trait Keylike: Sized { /// exist. It should return [`Error::Missing`] if the key can't be found. fn checked_translate(self, tx: &Transaction<'_>) -> Result { let key = self.translate(tx)?; - if !tx.exists(key)? { + if !tx.is_registered(key)? { Err(Error::Undefined { key }) } else { Ok(key) diff --git a/lib/store/src/lib.rs b/lib/store/src/lib.rs index 0b9022a..f6f3a32 100644 --- a/lib/store/src/lib.rs +++ b/lib/store/src/lib.rs @@ -1,3 +1,4 @@ +#![feature(iterator_try_collect)] //! The data store abstractions used by the ActivityPuppy project. //! //! Persistence in a puppy server is handled by this component, which implements a directed graph @@ -8,6 +9,9 @@ //! a [`Transaction`], returns a result with some value. The `Transaction` object contains some //! useful CRUD methods. Returning an `Ok` commits the transaction and returning `Err` rolls it //! back. +//! +//! This component is specialized to puppy's storage needs, and probably won't be much use unless +//! you're writing something that interfaces with puppy. use std::{path::Path, sync::Arc}; @@ -23,6 +27,12 @@ pub use key::{Key, Keylike, Tag}; pub use transaction::Transaction; pub use {alias::Alias, arrow::Arrow, mixin::Mixin}; +pub mod alias; +pub mod arrow; +pub mod mixin; +pub mod util; + +/// A shorthand for committing a [`Transaction`] (because I think `Ok(())` is ugly). pub const OK: Result<()> = Ok(()); /// Master list of all column family names in use. @@ -52,7 +62,13 @@ pub struct Store { inner: Arc, } +/// The name of the puppy data store inside the state directory. +const STORE_NAME: &str = "main-store"; + impl Store { + /// Open a data store in the given `state_dir`. + /// + /// If the data store does not exist yet, it will be created. pub fn open(state_dir: impl AsRef) -> Result { let mut db_opts = Options::default(); db_opts.create_if_missing(true); @@ -61,12 +77,11 @@ impl Store { let inner = Arc::new(Backend::open_cf( &db_opts, &tx_opts, - state_dir.as_ref().join("main-store"), + state_dir.as_ref().join(STORE_NAME), SPACES, )?); Ok(Store { inner }) } - /// Construct a temporary store, for testing. This store gets erased after `f` is done. pub fn with_tmp(f: impl FnOnce(Store) -> Result) -> Result where @@ -75,34 +90,33 @@ impl Store { let tmp_dir = tempfile::tempdir().expect("couldn't create tempdir"); f(Store::open(tmp_dir)?) } - /// Delete the whole store. /// /// **This deletes all data in the store**. Do not run this unless you want to delete all the state of the instance. pub fn nuke(state_dir: impl AsRef) -> Result<()> { - Backend::destroy(&Options::default(), state_dir.as_ref().join("main-store")) + Backend::destroy(&Options::default(), state_dir.as_ref().join(STORE_NAME)) .map_err(Error::from) } + /// Get the value of mixin `M` for `key`. + pub fn lookup(&self, key: impl Keylike) -> Result<(Key, M)> + where + M: Mixin, + { + self.transaction(|tx| tx.lookup(key)) + } /// Get the key associated with a given [alias][Alias]. - pub fn resolve(&self, s: impl ToString) -> Result + pub fn translate(&self, s: impl ToString) -> Result where A: Alias, { self.transaction(|tx| tx.lookup_alias(A::from(s.to_string()))) } - /// Reverse lookup an alias. - pub fn reverse(&self, key: Key) -> Result - where - A: Alias, - { - self.transaction(|tx| tx.lookup_alias_rev(key)) - } /// Quickly test whether a particular [arrow][Arrow] exists. - pub fn contains(&self, arrow: (Key, Key)) -> Result + pub fn exists(&self, arrow: (Key, Key)) -> Result where A: Arrow, { - self.transaction(|tx| tx.arrow_exists::(arrow)) + self.transaction(|tx| tx.exists::(arrow)) } } @@ -116,102 +130,6 @@ impl AsRef for Space { } } -pub mod mixin { - //! Modules of information. - - use bincode::{Decode, Encode}; - - use crate::Space; - - /// A simple piece of data associated with a vertex. - pub trait Mixin: Encode + Decode { - const SPACE: Space; - } - - #[derive(Encode, Decode)] - pub struct Profile { - pub post_count: usize, - pub account_name: String, - pub display_name: Option, - pub about_string: Option, - pub about_fields: Vec<(String, String)>, - } - - impl Mixin for Profile { - const SPACE: Space = Space("profile"); - } - - #[derive(Encode, Decode)] - pub struct Content { - pub content: Option, - pub summary: Option, - } - - impl Mixin for Content { - const SPACE: Space = Space("content"); - } -} - -pub mod arrow { - //! Relations between nodes. - - use bincode::{Decode, Encode}; - - use crate::Space; - - /// A directed edge between two vertices. - pub trait Arrow: Encode + Decode { - const SPACE: (Space, Space); - } - - /// Which way an arrow is pointing when viewed from a particular vertex. - pub enum Direction { - Incoming, - Outgoing, - } - - #[derive(Encode, Decode)] - pub struct AuthorOf; - - impl Arrow for AuthorOf { - const SPACE: (Space, Space) = (Space("created-by/l"), Space("created-by/r")); - } - - #[derive(Encode, Decode)] - pub struct FollowRequested; - - impl Arrow for FollowRequested { - const SPACE: (Space, Space) = (Space("pending-fr/l"), Space("pending-fr/r")); - } - - #[derive(Encode, Decode)] - pub struct Follows; - - impl Arrow for Follows { - const SPACE: (Space, Space) = (Space("follows/l"), Space("follows/r")); - } -} - -pub mod alias { - //! Alternative keys. - - use derive_more::{Display, From}; - - use crate::Space; - - /// An alternative unique key for a vertex. - pub trait Alias: ToString + From { - const SPACE: (Space, Space); - } - - #[derive(Display, From)] - pub struct Username(pub String); - - impl Alias for Username { - const SPACE: (Space, Space) = (Space("username/l"), Space("username/r")); - } -} - /// Results from this component. pub type Result = std::result::Result; @@ -228,6 +146,7 @@ pub enum Error { /// Returned if there is a conflict; for example, if the uniqueness property of an alias would /// be violated by inserting one. Conflict, + /// Signals a failure related to the data store's backend. Internal(rocksdb::Error), Encoding(bincode::error::EncodeError), Decoding(bincode::error::DecodeError), diff --git a/lib/store/src/mixin.rs b/lib/store/src/mixin.rs new file mode 100644 index 0000000..237808f --- /dev/null +++ b/lib/store/src/mixin.rs @@ -0,0 +1,35 @@ +//! Modules of information. + +use bincode::{Decode, Encode}; + +use crate::Space; + +/// A simple piece of data associated with a vertex. +pub trait Mixin: Encode + Decode { + const SPACE: Space; +} + +/// Information needed to render a social media profile. +#[derive(Encode, Decode)] +pub struct Profile { + pub post_count: usize, + pub account_name: String, + pub display_name: Option, + pub about_string: Option, + pub about_fields: Vec<(String, String)>, +} + +impl Mixin for Profile { + const SPACE: Space = Space("profile"); +} + +/// Contents of a post. +#[derive(Encode, Decode)] +pub struct Content { + pub content: Option, + pub summary: Option, +} + +impl Mixin for Content { + const SPACE: Space = Space("content"); +} diff --git a/lib/store/src/transaction.rs b/lib/store/src/transaction.rs index 03c2544..94e3623 100644 --- a/lib/store/src/transaction.rs +++ b/lib/store/src/transaction.rs @@ -4,8 +4,8 @@ use bincode::{Decode, Encode}; use rocksdb::{BoundColumnFamily, IteratorMode}; use crate::{ - arrow::Direction, key::Tag, Alias, Arrow, Backend, Error, Key, Keylike, Mixin, Result, Store, - OK, SPACES, + arrow::Direction, key::Tag, util::IterExt as _, Alias, Arrow, Backend, Error, Key, Keylike, + Mixin, Result, Store, OK, SPACES, }; impl Store { @@ -33,7 +33,7 @@ impl Store { result } /// Check whether a key exists in the registry, - pub fn exists(&self, key: Key) -> Result { + pub fn is_registered(&self, key: Key) -> Result { let cf = self .inner .cf_handle("registry") @@ -41,7 +41,7 @@ impl Store { self.inner .get_pinned_cf(&cf, key) .map(|opt| opt.is_some()) - .map_err(Error::from) + .map_err(Error::Internal) } } @@ -59,16 +59,16 @@ pub struct Transaction<'db> { /// Before you can manipulate a vertex, its needs to be registered. impl Transaction<'_> { /// Register a new vertex. - pub fn define(&self, key: Key, tag: Tag) -> Result<()> { + pub fn create_vertex(&self, key: Key, tag: Tag) -> Result<()> { self.with("registry").set(key, [tag.0]) } /// Delete a vertex from the registry. - pub fn delete(&self, key: Key) -> Result<()> { + pub fn delete_vertex(&self, key: Key) -> Result<()> { // TODO: also make this delete all related data? self.with("registry").del(key) } /// Check whether a vertex is registered in the database. - pub fn exists(&self, key: Key) -> Result { + pub fn is_registered(&self, key: Key) -> Result { self.with("registry").has(key) } } @@ -93,7 +93,7 @@ impl Transaction<'_> { } /// Associate a new mixin value with the key. /// - /// **Errors** + /// # Errors /// /// - `Error::Conflict` if a mixin of this type is already associated with the vertex /// - `Error::Undefined` if `key` is not in the registry. @@ -112,9 +112,9 @@ impl Transaction<'_> { ns.set(key, data) } } - /// Apply an update function to the value identified by the key. + /// Apply an update function to the mixin identified by the key. /// - /// **Errors** + /// # Errors /// /// - `Error::Undefined` if the `key` is not registered /// - `Error::Missing` if `key` does not exist in the keyspace associated with `M` @@ -148,8 +148,7 @@ impl Transaction<'_> { where M: Mixin, { - self.with(M::SPACE).list().map(|r| { - let (k, v) = r?; + self.with(M::SPACE).list().bind_results(|(k, v)| { let v = decode(v.as_ref())?; let k = Key::from_slice(k.as_ref()); Ok((k, v)) @@ -170,14 +169,6 @@ impl Transaction<'_> { let raw = self.with(l).get(alias.to_string())?; Ok(Key::from_slice(raw.as_ref())) } - /// Given a key, figure out what the value of the alias is. - pub fn lookup_alias_rev(&self, key: Key) -> Result - where - A: Alias, - { - let raw = self.with(A::SPACE.1).get(key)?; - Ok(A::from(String::from_utf8_lossy(raw.as_ref()).into_owned())) - } /// Create a new alias of type `A` for the given [`Key`]. /// /// If the alias already exists, this function returns `Conflict`. @@ -218,24 +209,24 @@ impl Transaction<'_> { { let (l, _) = A::SPACE; match self.with(l).get(tail.fuse(head)) { - Ok(raw) => decode(raw.as_ref()).map(Some), // BUG: This is broken for unit structs apparently. + Ok(raw) => decode(raw.as_ref()).map(Some), Err(Error::Missing) => Ok(None), Err(err) => Err(err), } } /// Create a new arrow of type `A` and associate the label with it. /// - /// **Errors** + /// # Errors /// /// - `Error::Undefined` if either key is not registered pub fn insert_arrow(&self, (tail, head): (Key, Key), label: A) -> Result<()> where A: Arrow, { - if !self.exists(tail)? { + if !self.is_registered(tail)? { return Err(Error::Undefined { key: tail }); } - if !self.exists(head)? { + if !self.is_registered(head)? { return Err(Error::Undefined { key: head }); } let (l, r) = A::SPACE; @@ -254,7 +245,7 @@ impl Transaction<'_> { OK } /// Check whether an arrow exists. - pub fn arrow_exists(&self, (tail, head): (Key, Key)) -> Result + pub fn exists(&self, (tail, head): (Key, Key)) -> Result where A: Arrow, { @@ -279,8 +270,7 @@ impl Transaction<'_> { where A: Arrow, { - self.with(A::SPACE.0).list().map(|r| { - let (k, v) = r?; + self.with(A::SPACE.0).list().bind_results(|(k, v)| { let (tail, head) = Key::split(k.as_ref()); decode(v.as_ref()).map(|label| (tail, label, head)) }) @@ -304,13 +294,12 @@ impl Transaction<'_> { let key = key.translate(&self).unwrap(); #[cfg(test)] eprintln!("scanning {} using prefix {key}", space.0); - self.with(space).scan(key).map(|r| { - let (k, v) = r?; + self.with(space).scan(key).bind_results(|(k, v)| { // Because we're prefix scanning on the first half of the key, we only want to // get the second here. - let (this, other) = Key::split(k.as_ref()); + let (_this, other) = Key::split(k.as_ref()); #[cfg(test)] - eprintln!(" found {this}:{other}"); + eprintln!(" found {_this}:{other}"); decode(v.as_ref()).map(|label| (other, label)) }) } @@ -383,7 +372,7 @@ impl<'db> Keyspace<'db> { Ok((ref k, _)) => k.starts_with(&t), _ => true, }) - .map(|r| r.map_err(Error::Internal)) + .map_err(Error::Internal) } /// Show all items in the entire keyspace. fn list( @@ -393,7 +382,7 @@ impl<'db> Keyspace<'db> { self.tx .inner .full_iterator_cf(&self.cf, IteratorMode::Start) - .map(|r| r.map_err(Error::Internal)) + .map_err(Error::Internal) } } @@ -411,286 +400,4 @@ where } #[cfg(test)] -mod tests { - use super::*; - use crate::Space; - - #[derive(Encode, Decode)] - struct TestArrow; - - impl Arrow for TestArrow { - const SPACE: (Space, Space) = (Space("test-arrow/l"), Space("test-arrow/r")); - } - - const TEST_TAG: Tag = Tag(69); - - macro_rules! keygen { - { $($name:ident)* } => { - $( - let $name = Key::gen(); - eprintln!(concat!(stringify!($name), "={}"), $name); - )* - } - } - - fn with_test_arrow(f: impl Fn(Key, Key, &Transaction<'_>, usize) -> Result<()>) -> Result<()> { - Store::with_tmp(|db| { - // Run these tests 128 times because misuse of prefix iterator may cause weird, - // obscure bugs :3 - // - // Also, because we don't wipe the store between test runs, we have more chances - // to discover weird bugs that we wouldn't catch if there was only a single run. - Ok(for n in 0..128 { - eprintln!("--- run {n} ---"); - db.transaction(|tx| { - keygen!(target origin); - - tx.define(target, TEST_TAG)?; - tx.define(origin, TEST_TAG)?; - - tx.insert_arrow((origin, target), TestArrow)?; - - let l: Vec = tx - .with("test-arrow/l") - .list() - .map(|r| r.map(|(k, _)| Key::split(k.as_ref()))) - .map(|r| r.map(|(a, b)| format!("({a}, {b})"))) - .collect::>()?; - - eprintln!("test-arrow/l = {l:#?}"); - - let r: Vec = tx - .with("test-arrow/r") - .list() - .map(|r| r.map(|(k, _)| Key::split(k.as_ref()))) - .map(|r| r.map(|(a, b)| format!("({a}, {b})"))) - .collect::>()?; - - eprintln!("test-arrow/r = {r:#?}"); - - f(origin, target, &tx, n) - })?; - eprintln!("--- end run {n} ---"); - }) - }) - } - - #[test] - fn target_incoming() -> Result<()> { - with_test_arrow(|origin, target, tx, _| { - let ti = tx - .list_incoming::(target) - .map(|r| r.map(|(k, _)| k)) - .collect::>>()?; - - eprintln!("target.incoming = {ti:#?}"); - - assert!(ti.contains(&origin), "origin ∈ target.incoming"); - assert!(!ti.contains(&target), "target ∉ target.incoming"); - - OK - }) - } - - #[test] - fn target_outgoing() -> Result<()> { - with_test_arrow(|origin, target, tx, _| { - let to = tx - .list_outgoing::(target) - .map(|r| r.map(|(k, _)| k)) - .collect::>>()?; - - eprintln!("target.outgoing = {to:#?}"); - - assert!(!to.contains(&target), "target ∉ target.outgoing"); - assert!(!to.contains(&origin), "origin ∉ target.outgoing"); - - OK - }) - } - - #[test] - fn origin_incoming() -> Result<()> { - with_test_arrow(|origin, target, tx, _| { - let oi = tx - .list_incoming::(origin) - .map(|r| r.map(|(k, _)| k)) - .collect::>>()?; - - eprintln!("origin.incoming = {oi:#?}"); - - assert!(!oi.contains(&origin), "origin ∉ origin.incoming"); - assert!(!oi.contains(&target), "target ∉ origin.incoming"); - - OK - }) - } - - #[test] - fn origin_outgoing() -> Result<()> { - with_test_arrow(|origin, target, tx, _| { - let oo = tx - .list_outgoing::(origin) - .map(|r| r.map(|(k, _)| k)) - .collect::>>()?; - - eprintln!("origin.outgoing = {oo:#?}"); - - assert!(oo.contains(&target), "target ∈ origin.outgoing"); - assert!(!oo.contains(&origin), "origin ∉ origin.outgoing"); - - OK - }) - } - - #[test] - fn fanout() -> Result<()> { - let targets: [Key; 128] = std::array::from_fn(|_| Key::gen()); - let origin = Key::gen(); - Store::with_tmp(|db| { - db.transaction(|tx| { - tx.define(origin, TEST_TAG)?; - for t in targets { - tx.define(t, TEST_TAG)?; - tx.insert_arrow((origin, t), TestArrow)?; - } - - // TODO: we keep doing this, we should make a function or something for it - let oo = tx - .list_outgoing::(origin) - .map(|r| r.map(|(k, _)| k)) - .collect::>>()?; - - for t in targets { - assert!(oo.contains(&t), "∀ t ∈ targets: t ∈ origin.outgoing"); - let ti = tx - .list_incoming::(t) - .map(|r| r.map(|(k, _)| k)) - .collect::>>()?; - assert!( - ti == vec! {origin}, - "∀ t ∈ targets: t.incoming = {{origin}}" - ); - } - - OK - }) - }) - } - - #[test] - fn fanin() -> Result<()> { - let origins: [Key; 128] = std::array::from_fn(|_| Key::gen()); - let target = Key::gen(); - Store::with_tmp(|db| { - db.transaction(|tx| { - tx.define(target, TEST_TAG)?; - for o in origins { - tx.define(o, TEST_TAG)?; - tx.insert_arrow((o, target), TestArrow)?; - } - - let ti = tx - .list_incoming::(target) - .map(|r| r.map(|(k, _)| k)) - .collect::>>()?; - - for o in origins { - let oo = tx - .list_outgoing::(o) - .map(|r| r.map(|(k, _)| k)) - .collect::>>()?; - assert!(ti.contains(&o), "∀ o ∈ origins: o ∈ target.incoming"); - assert!( - oo == vec! {target}, - "∀ o ∈ origins: o.outgoing = {{target}}" - ); - } - - OK - }) - }) - } - - #[test] - fn distinct_many_to_many() -> Result<()> { - let origins: [Key; 32] = std::array::from_fn(|_| Key::gen()); - let targets: [Key; 32] = std::array::from_fn(|_| Key::gen()); - Store::with_tmp(|db| { - db.transaction(|tx| { - for t in targets { - tx.define(t, TEST_TAG)?; - } - for o in origins { - tx.define(o, TEST_TAG)?; - for t in targets { - tx.insert_arrow((o, t), TestArrow)?; - } - } - - let ti: HashMap> = targets - .into_iter() - .map(|t| { - tx.list_incoming::(t) - .map(|r| r.map(|(k, _)| k)) - .collect::>>() - .map(|v| (t, v)) - }) - .collect::>()?; - - // For each origin point, there must be a target that has it as "incoming". - assert!( - origins - .into_iter() - .all(|o| { targets.into_iter().any(|t| { ti[&t].contains(&o) }) }), - "∀ o ∈ origins: ∃ t ∈ targets: o ∈ t.incoming" - ); - - // Each target has each origin as incoming. - assert!( - origins - .into_iter() - .all(|o| { targets.into_iter().all(|t| { ti[&t].contains(&o) }) }), - "∀ o ∈ origins: ∀ t ∈ targets: o ∈ t.incoming" - ); - - let to: HashMap> = targets - .into_iter() - .map(|t| { - tx.list_outgoing::(t) - .map(|r| r.map(|(k, _)| k)) - .collect::>>() - .map(|v| (t, v)) - }) - .collect::>()?; - - // Our arrows point only from origins to targets, and there's a bug if there - // exists a target such that its outgoing set is non-empty. - assert!( - !targets.into_iter().any(|t| !to[&t].is_empty()), - "∄ t ∈ targets: t.outgoing ≠ ∅" - ); - - let oo: HashMap> = origins - .into_iter() - .map(|o| { - tx.list_outgoing::(o) - .map(|r| r.map(|(k, _)| k)) - .collect::>>() - .map(|v| (o, v)) - }) - .collect::>()?; - - // Each origin has each target as outgoing. - assert!( - origins - .into_iter() - .all(|o| targets.into_iter().all(|t| oo[&o].contains(&t))), - "∀ o ∈ origins: ∀ t ∈ targets: t ∈ o.outgoing" - ); - - OK - }) - }) - } -} +mod tests; diff --git a/lib/store/src/transaction/tests.rs b/lib/store/src/transaction/tests.rs new file mode 100644 index 0000000..ee0670a --- /dev/null +++ b/lib/store/src/transaction/tests.rs @@ -0,0 +1,256 @@ +use super::*; +use crate::Space; + +#[derive(Encode, Decode)] +struct TestArrow; + +impl Arrow for TestArrow { + const SPACE: (Space, Space) = (Space("test-arrow/l"), Space("test-arrow/r")); +} + +const TEST_TAG: Tag = Tag(69); + +macro_rules! keygen { + { $($name:ident)* } => { + $( + let $name = Key::gen(); + eprintln!(concat!(stringify!($name), "={}"), $name); + )* + } +} + +fn with_test_arrow(f: impl Fn(Key, Key, &Transaction<'_>, usize) -> Result<()>) -> Result<()> { + Store::with_tmp(|db| { + // Run these tests 128 times because misuse of prefix iterator may cause weird, + // obscure bugs :3 + // + // Also, because we don't wipe the store between test runs, we have more chances + // to discover weird bugs that we wouldn't catch if there was only a single run. + Ok(for n in 0..128 { + eprintln!("--- run {n} ---"); + db.transaction(|tx| { + keygen!(target origin); + + tx.create_vertex(target, TEST_TAG)?; + tx.create_vertex(origin, TEST_TAG)?; + + tx.insert_arrow((origin, target), TestArrow)?; + + let l: Vec = tx + .with("test-arrow/l") + .list() + .map_ok(|(k, _)| Key::split(k.as_ref())) + .map_ok(|(a, b)| format!("({a}, {b})")) + .try_collect()?; + + eprintln!("test-arrow/l = {l:#?}"); + + let r: Vec = tx + .with("test-arrow/r") + .list() + .map_ok(|(k, _)| Key::split(k.as_ref())) + .map_ok(|(a, b)| format!("({a}, {b})")) + .try_collect()?; + + eprintln!("test-arrow/r = {r:#?}"); + + f(origin, target, &tx, n) + })?; + eprintln!("--- end run {n} ---"); + }) + }) +} + +#[test] +fn target_incoming() -> Result<()> { + with_test_arrow(|origin, target, tx, _| { + let ti: Vec<_> = tx.list_incoming::(target).keys().try_collect()?; + + eprintln!("target.incoming = {ti:#?}"); + + assert!(ti.contains(&origin), "origin ∈ target.incoming"); + assert!(!ti.contains(&target), "target ∉ target.incoming"); + + OK + }) +} + +#[test] +fn target_outgoing() -> Result<()> { + with_test_arrow(|origin, target, tx, _| { + let to: Vec<_> = tx.list_outgoing::(target).keys().try_collect()?; + + eprintln!("target.outgoing = {to:#?}"); + + assert!(!to.contains(&target), "target ∉ target.outgoing"); + assert!(!to.contains(&origin), "origin ∉ target.outgoing"); + + OK + }) +} + +#[test] +fn origin_incoming() -> Result<()> { + with_test_arrow(|origin, target, tx, _| { + let oi: Vec<_> = tx.list_incoming::(origin).keys().try_collect()?; + + eprintln!("origin.incoming = {oi:#?}"); + + assert!(!oi.contains(&origin), "origin ∉ origin.incoming"); + assert!(!oi.contains(&target), "target ∉ origin.incoming"); + + OK + }) +} + +#[test] +fn origin_outgoing() -> Result<()> { + with_test_arrow(|origin, target, tx, _| { + let oo: Vec<_> = tx.list_outgoing::(origin).keys().try_collect()?; + + eprintln!("origin.outgoing = {oo:#?}"); + + assert!(oo.contains(&target), "target ∈ origin.outgoing"); + assert!(!oo.contains(&origin), "origin ∉ origin.outgoing"); + + OK + }) +} + +#[test] +fn fanout() -> Result<()> { + let targets: [Key; 128] = std::array::from_fn(|_| Key::gen()); + let origin = Key::gen(); + Store::with_tmp(|db| { + db.transaction(|tx| { + tx.create_vertex(origin, TEST_TAG)?; + for t in targets { + tx.create_vertex(t, TEST_TAG)?; + tx.insert_arrow((origin, t), TestArrow)?; + } + + let oo: Vec<_> = tx.list_outgoing::(origin).keys().try_collect()?; + + for t in targets { + assert!(oo.contains(&t), "∀ t ∈ targets: t ∈ origin.outgoing"); + let ti: Vec<_> = tx.list_incoming::(t).keys().try_collect()?; + assert!( + ti == vec! {origin}, + "∀ t ∈ targets: t.incoming = {{origin}}" + ); + } + + OK + }) + }) +} + +#[test] +fn fanin() -> Result<()> { + let origins: [Key; 128] = std::array::from_fn(|_| Key::gen()); + let target = Key::gen(); + Store::with_tmp(|db| { + db.transaction(|tx| { + tx.create_vertex(target, TEST_TAG)?; + for o in origins { + tx.create_vertex(o, TEST_TAG)?; + tx.insert_arrow((o, target), TestArrow)?; + } + + let ti: Vec<_> = tx.list_incoming::(target).keys().try_collect()?; + + for o in origins { + let oo: Vec<_> = tx.list_outgoing::(o).keys().try_collect()?; + assert!(ti.contains(&o), "∀ o ∈ origins: o ∈ target.incoming"); + assert!( + oo == vec! {target}, + "∀ o ∈ origins: o.outgoing = {{target}}" + ); + } + + OK + }) + }) +} + +#[test] +fn distinct_many_to_many() -> Result<()> { + let origins: [Key; 32] = std::array::from_fn(|_| Key::gen()); + let targets: [Key; 32] = std::array::from_fn(|_| Key::gen()); + Store::with_tmp(|db| { + db.transaction(|tx| { + for t in targets { + tx.create_vertex(t, TEST_TAG)?; + } + for o in origins { + tx.create_vertex(o, TEST_TAG)?; + for t in targets { + tx.insert_arrow((o, t), TestArrow)?; + } + } + + let ti: HashMap> = targets + .into_iter() + .map(|t| { + tx.list_incoming::(t) + .keys() + .try_collect() + .map(|v: Vec<_>| (t, v)) + }) + .collect::>()?; + + // For each origin point, there must be a target that has it as "incoming". + assert!( + origins + .into_iter() + .all(|o| { targets.into_iter().any(|t| { ti[&t].contains(&o) }) }), + "∀ o ∈ origins: ∃ t ∈ targets: o ∈ t.incoming" + ); + + // Each target has each origin as incoming. + assert!( + origins + .into_iter() + .all(|o| { targets.into_iter().all(|t| { ti[&t].contains(&o) }) }), + "∀ o ∈ origins: ∀ t ∈ targets: o ∈ t.incoming" + ); + + let to: HashMap> = targets + .into_iter() + .map(|t| { + tx.list_outgoing::(t) + .keys() + .try_collect() + .map(|v: Vec<_>| (t, v)) + }) + .collect::>()?; + + // Our arrows point only from origins to targets, and there's a bug if there + // exists a target such that its outgoing set is non-empty. + assert!( + !targets.into_iter().any(|t| !to[&t].is_empty()), + "∄ t ∈ targets: t.outgoing ≠ ∅" + ); + + let oo: HashMap> = origins + .into_iter() + .map(|o| { + tx.list_outgoing::(o) + .keys() + .try_collect() + .map(|v: Vec<_>| (o, v)) + }) + .collect::>()?; + + // Each origin has each target as outgoing. + assert!( + origins + .into_iter() + .all(|o| targets.into_iter().all(|t| oo[&o].contains(&t))), + "∀ o ∈ origins: ∀ t ∈ targets: t ∈ o.outgoing" + ); + + OK + }) + }) +} diff --git a/lib/store/src/util.rs b/lib/store/src/util.rs new file mode 100644 index 0000000..e082a37 --- /dev/null +++ b/lib/store/src/util.rs @@ -0,0 +1,51 @@ +//! Utilities for making the code clearer. + +/// Helpers for working with iterators of results of key-value pairs. +pub trait IterExt: Iterator + Sized { + /// Map over the values inside the iterator's results. + fn map_ok<'a, I, O, E>( + self, + mut f: impl FnMut(I) -> O + 'a, + ) -> impl Iterator> + 'a + where + Self: Iterator> + 'a, + { + self.map(move |r| r.map(|x| f(x))) + } + /// Map over the errors contained in the iterator. + fn map_err<'a, I, O, E>( + self, + mut f: impl FnMut(E) -> O + 'a, + ) -> impl Iterator> + 'a + where + Self: Iterator> + 'a, + { + self.map(move |r| r.map_err(|x| f(x))) + } + /// Discard the second element of the tuple. + fn keys<'a, K, V, E>(self) -> impl Iterator> + 'a + where + Self: Iterator> + 'a, + { + self.map_ok(|(k, _)| k) + } + /// Like and_then. + fn bind_results<'a, I, O, E>( + self, + mut f: impl FnMut(I) -> Result + 'a, + ) -> impl Iterator> + 'a + where + Self: Iterator> + 'a, + { + self.map(move |r| r.and_then(|x| f(x))) + } + /// Get the next element, or return the given error. + fn next_or<'a, T, E>(&mut self, e: E) -> Result + where + Self: Iterator> + 'a, + { + self.next().ok_or(e)? + } +} + +impl IterExt for I where I: Iterator {}