First polishing pass

This commit is contained in:
Riley Apeldoorn 2024-04-21 09:49:02 +02:00
parent f8ebdce372
commit ad62fec21f
10 changed files with 501 additions and 477 deletions

View file

@ -3,6 +3,7 @@ use puppy::{
self, self,
alias::Username, alias::Username,
arrow::{FollowRequested, Follows}, arrow::{FollowRequested, Follows},
mixin::Profile,
Error, Error,
}, },
tl::Post, tl::Post,
@ -22,9 +23,9 @@ fn main() -> store::Result<()> {
} }
if true { if true {
println!("making riley follow linen"); println!("making riley follow linen");
if !db.contains::<Follows>((riley, linen))? { if !db.exists::<Follows>((riley, linen))? {
println!("follow relation does not exist yet"); println!("follow relation does not exist yet");
if !db.contains::<FollowRequested>((riley, linen))? { if !db.exists::<FollowRequested>((riley, linen))? {
println!("no pending follow request; creating"); println!("no pending follow request; creating");
puppy::fr::create(&db, riley, linen)?; puppy::fr::create(&db, riley, linen)?;
} else { } else {
@ -36,26 +37,31 @@ fn main() -> store::Result<()> {
} }
} }
println!("Posts on the instance:"); println!("Posts on the instance:");
for (key, Post { content, author }) in puppy::tl::fetch_all(&db)? { for Post {
let handle = db.reverse::<Username>(author)?; id,
content,
author,
} in puppy::tl::fetch_all(&db)?
{
let (_, Profile { account_name, .. }) = db.lookup(author)?;
let content = content.content.unwrap(); let content = content.content.unwrap();
println!("- {key} by @{handle} ({author}):\n{content}",) println!("- {id} by @{account_name} ({author}):\n{content}",)
} }
println!("Linen's followers:"); println!("Linen's followers:");
for id in puppy::fr::followers_of(&db, linen)? { for id in puppy::fr::followers_of(&db, linen)? {
let handle = db.reverse::<Username>(id)?; let (_, Profile { account_name, .. }) = db.lookup(id)?;
println!("- @{handle} ({id})"); println!("- @{account_name} ({id})");
} }
println!("Riley's following:"); println!("Riley's following:");
for id in puppy::fr::following_of(&db, riley)? { for id in puppy::fr::following_of(&db, riley)? {
let handle = db.reverse::<Username>(id)?; let (_, Profile { account_name, .. }) = db.lookup(id)?;
println!("- @{handle} ({id})"); println!("- @{account_name} ({id})");
} }
store::OK store::OK
} }
fn get_or_create_actor(db: &Store, username: &str) -> Result<Key, Error> { fn get_or_create_actor(db: &Store, username: &str) -> Result<Key, Error> {
let user = db.resolve::<Username>(username); let user = db.translate::<Username>(username);
match user { match user {
Ok(key) => { Ok(key) => {
println!("found '{username}' ({key})"); println!("found '{username}' ({key})");
@ -63,7 +69,7 @@ fn get_or_create_actor(db: &Store, username: &str) -> Result<Key, Error> {
} }
Err(Error::Missing) => { Err(Error::Missing) => {
println!("'{username}' doesn't exist yet, creating"); 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 { if let Ok(ref key) = r {
println!("created '{username}' with key {key}"); println!("created '{username}' with key {key}");
} }

View file

@ -3,19 +3,23 @@ use store::{
alias::Username, alias::Username,
arrow::AuthorOf, arrow::AuthorOf,
mixin::{Content, Profile}, mixin::{Content, Profile},
util::IterExt,
Keylike, Keylike,
}; };
mod tags { mod tags {
//! Type tags for vertices.
use store::Tag; use store::Tag;
pub const AUTHOR: Tag = Tag(0);
pub const ACTOR: Tag = Tag(0);
pub const POST: Tag = Tag(1); pub const POST: Tag = Tag(1);
} }
pub fn create_post(db: &Store, author: Key, content: impl ToString) -> store::Result<Key> { pub fn create_post(db: &Store, author: Key, content: impl ToString) -> store::Result<Key> {
let key = Key::gen(); let key = Key::gen();
db.transaction(|tx| { db.transaction(|tx| {
tx.define(key, tags::POST)?; tx.create_vertex(key, tags::POST)?;
tx.update::<Profile>(author, |_, mut profile| { tx.update::<Profile>(author, |_, mut profile| {
profile.post_count += 1; profile.post_count += 1;
Ok(profile) 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<Key> { pub fn create_actor(db: &Store, username: impl ToString) -> store::Result<Key> {
let key = Key::gen(); let key = Key::gen();
db.transaction(|tx| { db.transaction(|tx| {
tx.define(key, tags::AUTHOR)?; tx.create_vertex(key, tags::ACTOR)?;
tx.insert_alias(key, Username(username.to_string()))?; tx.insert_alias(key, Username(username.to_string()))?;
tx.insert(key, Profile { tx.insert(key, Profile {
post_count: 0, post_count: 0,
@ -51,10 +55,7 @@ pub fn list_posts_by_author(
) -> store::Result<Vec<(Key, Content)>> { ) -> store::Result<Vec<(Key, Content)>> {
db.transaction(|tx| { db.transaction(|tx| {
tx.list_outgoing::<AuthorOf>(author) tx.list_outgoing::<AuthorOf>(author)
.map(|r| { .bind_results(|(post_key, _)| tx.lookup::<Content>(post_key))
let (post_key, _) = r?;
tx.lookup::<Content>(post_key)
})
.collect() .collect()
}) })
} }
@ -62,24 +63,27 @@ pub fn list_posts_by_author(
pub mod tl { pub mod tl {
//! Timelines //! 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 struct Post {
pub id: Key,
pub author: Key, pub author: Key,
pub content: Content, pub content: Content,
} }
pub fn fetch_all(db: &Store) -> store::Result<Vec<(Key, Post)>> { pub fn fetch_all(db: &Store) -> Result<Vec<Post>> {
db.transaction(|tx| { db.transaction(|tx| {
let iter = tx.list::<Content>(); let iter = tx.list::<Content>();
iter.map(|r| { iter.bind_results(|(id, content)| {
let (post_id, content) = r?;
let author = tx let author = tx
.list_incoming::<AuthorOf>(post_id) .list_incoming::<AuthorOf>(id)
.map(|r| r.map(|(k, _)| k)) .keys()
.next() .next_or(Error::Missing)?;
.unwrap()?; Ok(Post {
Ok((post_id, Post { author, content })) id,
author,
content,
})
}) })
.collect() .collect()
}) })
@ -91,6 +95,7 @@ pub mod fr {
use store::{ use store::{
arrow::{FollowRequested, Follows}, arrow::{FollowRequested, Follows},
util::IterExt as _,
Key, Store, OK, Key, Store, OK,
}; };
@ -117,27 +122,15 @@ pub mod fr {
} }
pub fn list_pending(db: &Store, target: Key) -> store::Result<Vec<Key>> { pub fn list_pending(db: &Store, target: Key) -> store::Result<Vec<Key>> {
db.transaction(|tx| { db.transaction(|tx| tx.list_incoming::<FollowRequested>(target).keys().collect())
tx.list_incoming::<FollowRequested>(target)
.map(|r| r.map(|(k, _)| k))
.collect()
})
} }
pub fn following_of(db: &Store, actor: Key) -> store::Result<Vec<Key>> { pub fn following_of(db: &Store, actor: Key) -> store::Result<Vec<Key>> {
db.transaction(|tx| { db.transaction(|tx| tx.list_outgoing::<Follows>(actor).keys().collect())
tx.list_outgoing::<Follows>(actor)
.map(|r| r.map(|(k, _)| k))
.collect()
})
} }
pub fn followers_of(db: &Store, actor: Key) -> store::Result<Vec<Key>> { pub fn followers_of(db: &Store, actor: Key) -> store::Result<Vec<Key>> {
db.transaction(|tx| { db.transaction(|tx| tx.list_incoming::<Follows>(actor).keys().collect())
tx.list_incoming::<Follows>(actor)
.map(|r| r.map(|(k, _)| k))
.collect()
})
} }
#[cfg(test)] #[cfg(test)]
@ -147,11 +140,11 @@ pub mod fr {
Key, Store, OK, Key, Store, OK,
}; };
use crate::create_author; use crate::create_actor;
fn make_test_actors(db: &Store) -> store::Result<(Key, Key)> { fn make_test_actors(db: &Store) -> store::Result<(Key, Key)> {
let alice = create_author(&db, "alice")?; let alice = create_actor(&db, "alice")?;
let bob = create_author(&db, "bob")?; let bob = create_actor(&db, "bob")?;
eprintln!("alice={alice}, bob={bob}"); eprintln!("alice={alice}, bob={bob}");
Ok((alice, bob)) Ok((alice, bob))
} }
@ -162,11 +155,11 @@ pub mod fr {
let (alice, bob) = make_test_actors(&db)?; let (alice, bob) = make_test_actors(&db)?;
super::create(&db, alice, bob)?; super::create(&db, alice, bob)?;
assert!( assert!(
db.contains::<FollowRequested>((alice, bob))?, db.exists::<FollowRequested>((alice, bob))?,
"(alice -> bob) ∈ follow-requested" "(alice -> bob) ∈ follow-requested"
); );
assert!( assert!(
!db.contains::<Follows>((alice, bob))?, !db.exists::<Follows>((alice, bob))?,
"(alice -> bob) ∉ follows" "(alice -> bob) ∉ follows"
); );
let pending_for_bob = super::list_pending(&db, bob)?; let pending_for_bob = super::list_pending(&db, bob)?;
@ -183,11 +176,11 @@ pub mod fr {
super::accept(&db, alice, bob)?; super::accept(&db, alice, bob)?;
assert!( assert!(
db.contains::<Follows>((alice, bob))?, db.exists::<Follows>((alice, bob))?,
"(alice -> bob) ∈ follows" "(alice -> bob) ∈ follows"
); );
assert!( assert!(
!db.contains::<Follows>((bob, alice))?, !db.exists::<Follows>((bob, alice))?,
"(bob -> alice) ∉ follows" "(bob -> alice) ∉ follows"
); );

17
lib/store/src/alias.rs Normal file
View file

@ -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<String> {
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"));
}

40
lib/store/src/arrow.rs Normal file
View file

@ -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"));
}

View file

@ -63,7 +63,7 @@ pub trait Keylike: Sized {
/// exist. It should return [`Error::Missing`] if the key can't be found. /// exist. It should return [`Error::Missing`] if the key can't be found.
fn checked_translate(self, tx: &Transaction<'_>) -> Result<Key> { fn checked_translate(self, tx: &Transaction<'_>) -> Result<Key> {
let key = self.translate(tx)?; let key = self.translate(tx)?;
if !tx.exists(key)? { if !tx.is_registered(key)? {
Err(Error::Undefined { key }) Err(Error::Undefined { key })
} else { } else {
Ok(key) Ok(key)

View file

@ -1,3 +1,4 @@
#![feature(iterator_try_collect)]
//! The data store abstractions used by the ActivityPuppy project. //! The data store abstractions used by the ActivityPuppy project.
//! //!
//! Persistence in a puppy server is handled by this component, which implements a directed graph //! 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 //! 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 //! useful CRUD methods. Returning an `Ok` commits the transaction and returning `Err` rolls it
//! back. //! 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}; use std::{path::Path, sync::Arc};
@ -23,6 +27,12 @@ pub use key::{Key, Keylike, Tag};
pub use transaction::Transaction; pub use transaction::Transaction;
pub use {alias::Alias, arrow::Arrow, mixin::Mixin}; 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(()); pub const OK: Result<()> = Ok(());
/// Master list of all column family names in use. /// Master list of all column family names in use.
@ -52,7 +62,13 @@ pub struct Store {
inner: Arc<Backend>, inner: Arc<Backend>,
} }
/// The name of the puppy data store inside the state directory.
const STORE_NAME: &str = "main-store";
impl 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<Path>) -> Result<Store> { pub fn open(state_dir: impl AsRef<Path>) -> Result<Store> {
let mut db_opts = Options::default(); let mut db_opts = Options::default();
db_opts.create_if_missing(true); db_opts.create_if_missing(true);
@ -61,12 +77,11 @@ impl Store {
let inner = Arc::new(Backend::open_cf( let inner = Arc::new(Backend::open_cf(
&db_opts, &db_opts,
&tx_opts, &tx_opts,
state_dir.as_ref().join("main-store"), state_dir.as_ref().join(STORE_NAME),
SPACES, SPACES,
)?); )?);
Ok(Store { inner }) Ok(Store { inner })
} }
/// Construct a temporary store, for testing. This store gets erased after `f` is done. /// Construct a temporary store, for testing. This store gets erased after `f` is done.
pub fn with_tmp<T, E>(f: impl FnOnce(Store) -> Result<T, E>) -> Result<T, E> pub fn with_tmp<T, E>(f: impl FnOnce(Store) -> Result<T, E>) -> Result<T, E>
where where
@ -75,34 +90,33 @@ impl Store {
let tmp_dir = tempfile::tempdir().expect("couldn't create tempdir"); let tmp_dir = tempfile::tempdir().expect("couldn't create tempdir");
f(Store::open(tmp_dir)?) f(Store::open(tmp_dir)?)
} }
/// Delete the whole store. /// 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. /// **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<Path>) -> Result<()> { pub fn nuke(state_dir: impl AsRef<Path>) -> 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) .map_err(Error::from)
} }
/// Get the value of mixin `M` for `key`.
pub fn lookup<M>(&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]. /// Get the key associated with a given [alias][Alias].
pub fn resolve<A>(&self, s: impl ToString) -> Result<Key> pub fn translate<A>(&self, s: impl ToString) -> Result<Key>
where where
A: Alias, A: Alias,
{ {
self.transaction(|tx| tx.lookup_alias(A::from(s.to_string()))) self.transaction(|tx| tx.lookup_alias(A::from(s.to_string())))
} }
/// Reverse lookup an alias.
pub fn reverse<A>(&self, key: Key) -> Result<A>
where
A: Alias,
{
self.transaction(|tx| tx.lookup_alias_rev(key))
}
/// Quickly test whether a particular [arrow][Arrow] exists. /// Quickly test whether a particular [arrow][Arrow] exists.
pub fn contains<A>(&self, arrow: (Key, Key)) -> Result<bool> pub fn exists<A>(&self, arrow: (Key, Key)) -> Result<bool>
where where
A: Arrow, A: Arrow,
{ {
self.transaction(|tx| tx.arrow_exists::<A>(arrow)) self.transaction(|tx| tx.exists::<A>(arrow))
} }
} }
@ -116,102 +130,6 @@ impl AsRef<str> 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<String>,
pub about_string: Option<String>,
pub about_fields: Vec<(String, String)>,
}
impl Mixin for Profile {
const SPACE: Space = Space("profile");
}
#[derive(Encode, Decode)]
pub struct Content {
pub content: Option<String>,
pub summary: Option<String>,
}
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<String> {
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. /// Results from this component.
pub type Result<T, E = Error> = std::result::Result<T, E>; pub type Result<T, E = Error> = std::result::Result<T, E>;
@ -228,6 +146,7 @@ pub enum Error {
/// Returned if there is a conflict; for example, if the uniqueness property of an alias would /// Returned if there is a conflict; for example, if the uniqueness property of an alias would
/// be violated by inserting one. /// be violated by inserting one.
Conflict, Conflict,
/// Signals a failure related to the data store's backend.
Internal(rocksdb::Error), Internal(rocksdb::Error),
Encoding(bincode::error::EncodeError), Encoding(bincode::error::EncodeError),
Decoding(bincode::error::DecodeError), Decoding(bincode::error::DecodeError),

35
lib/store/src/mixin.rs Normal file
View file

@ -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<String>,
pub about_string: Option<String>,
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<String>,
pub summary: Option<String>,
}
impl Mixin for Content {
const SPACE: Space = Space("content");
}

View file

@ -4,8 +4,8 @@ use bincode::{Decode, Encode};
use rocksdb::{BoundColumnFamily, IteratorMode}; use rocksdb::{BoundColumnFamily, IteratorMode};
use crate::{ use crate::{
arrow::Direction, key::Tag, Alias, Arrow, Backend, Error, Key, Keylike, Mixin, Result, Store, arrow::Direction, key::Tag, util::IterExt as _, Alias, Arrow, Backend, Error, Key, Keylike,
OK, SPACES, Mixin, Result, Store, OK, SPACES,
}; };
impl Store { impl Store {
@ -33,7 +33,7 @@ impl Store {
result result
} }
/// Check whether a key exists in the registry, /// Check whether a key exists in the registry,
pub fn exists(&self, key: Key) -> Result<bool> { pub fn is_registered(&self, key: Key) -> Result<bool> {
let cf = self let cf = self
.inner .inner
.cf_handle("registry") .cf_handle("registry")
@ -41,7 +41,7 @@ impl Store {
self.inner self.inner
.get_pinned_cf(&cf, key) .get_pinned_cf(&cf, key)
.map(|opt| opt.is_some()) .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. /// Before you can manipulate a vertex, its needs to be registered.
impl Transaction<'_> { impl Transaction<'_> {
/// Register a new vertex. /// 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]) self.with("registry").set(key, [tag.0])
} }
/// Delete a vertex from the registry. /// 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? // TODO: also make this delete all related data?
self.with("registry").del(key) self.with("registry").del(key)
} }
/// Check whether a vertex is registered in the database. /// Check whether a vertex is registered in the database.
pub fn exists(&self, key: Key) -> Result<bool> { pub fn is_registered(&self, key: Key) -> Result<bool> {
self.with("registry").has(key) self.with("registry").has(key)
} }
} }
@ -93,7 +93,7 @@ impl Transaction<'_> {
} }
/// Associate a new mixin value with the key. /// 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::Conflict` if a mixin of this type is already associated with the vertex
/// - `Error::Undefined` if `key` is not in the registry. /// - `Error::Undefined` if `key` is not in the registry.
@ -112,9 +112,9 @@ impl Transaction<'_> {
ns.set(key, data) 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::Undefined` if the `key` is not registered
/// - `Error::Missing` if `key` does not exist in the keyspace associated with `M` /// - `Error::Missing` if `key` does not exist in the keyspace associated with `M`
@ -148,8 +148,7 @@ impl Transaction<'_> {
where where
M: Mixin, M: Mixin,
{ {
self.with(M::SPACE).list().map(|r| { self.with(M::SPACE).list().bind_results(|(k, v)| {
let (k, v) = r?;
let v = decode(v.as_ref())?; let v = decode(v.as_ref())?;
let k = Key::from_slice(k.as_ref()); let k = Key::from_slice(k.as_ref());
Ok((k, v)) Ok((k, v))
@ -170,14 +169,6 @@ impl Transaction<'_> {
let raw = self.with(l).get(alias.to_string())?; let raw = self.with(l).get(alias.to_string())?;
Ok(Key::from_slice(raw.as_ref())) Ok(Key::from_slice(raw.as_ref()))
} }
/// Given a key, figure out what the value of the alias is.
pub fn lookup_alias_rev<A>(&self, key: Key) -> Result<A>
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`]. /// Create a new alias of type `A` for the given [`Key`].
/// ///
/// If the alias already exists, this function returns `Conflict`. /// If the alias already exists, this function returns `Conflict`.
@ -218,24 +209,24 @@ impl Transaction<'_> {
{ {
let (l, _) = A::SPACE; let (l, _) = A::SPACE;
match self.with(l).get(tail.fuse(head)) { 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(Error::Missing) => Ok(None),
Err(err) => Err(err), Err(err) => Err(err),
} }
} }
/// Create a new arrow of type `A` and associate the label with it. /// Create a new arrow of type `A` and associate the label with it.
/// ///
/// **Errors** /// # Errors
/// ///
/// - `Error::Undefined` if either key is not registered /// - `Error::Undefined` if either key is not registered
pub fn insert_arrow<A>(&self, (tail, head): (Key, Key), label: A) -> Result<()> pub fn insert_arrow<A>(&self, (tail, head): (Key, Key), label: A) -> Result<()>
where where
A: Arrow, A: Arrow,
{ {
if !self.exists(tail)? { if !self.is_registered(tail)? {
return Err(Error::Undefined { key: tail }); return Err(Error::Undefined { key: tail });
} }
if !self.exists(head)? { if !self.is_registered(head)? {
return Err(Error::Undefined { key: head }); return Err(Error::Undefined { key: head });
} }
let (l, r) = A::SPACE; let (l, r) = A::SPACE;
@ -254,7 +245,7 @@ impl Transaction<'_> {
OK OK
} }
/// Check whether an arrow exists. /// Check whether an arrow exists.
pub fn arrow_exists<A>(&self, (tail, head): (Key, Key)) -> Result<bool> pub fn exists<A>(&self, (tail, head): (Key, Key)) -> Result<bool>
where where
A: Arrow, A: Arrow,
{ {
@ -279,8 +270,7 @@ impl Transaction<'_> {
where where
A: Arrow, A: Arrow,
{ {
self.with(A::SPACE.0).list().map(|r| { self.with(A::SPACE.0).list().bind_results(|(k, v)| {
let (k, v) = r?;
let (tail, head) = Key::split(k.as_ref()); let (tail, head) = Key::split(k.as_ref());
decode(v.as_ref()).map(|label| (tail, label, head)) decode(v.as_ref()).map(|label| (tail, label, head))
}) })
@ -304,13 +294,12 @@ impl Transaction<'_> {
let key = key.translate(&self).unwrap(); let key = key.translate(&self).unwrap();
#[cfg(test)] #[cfg(test)]
eprintln!("scanning {} using prefix {key}", space.0); eprintln!("scanning {} using prefix {key}", space.0);
self.with(space).scan(key).map(|r| { self.with(space).scan(key).bind_results(|(k, v)| {
let (k, v) = r?;
// Because we're prefix scanning on the first half of the key, we only want to // Because we're prefix scanning on the first half of the key, we only want to
// get the second here. // get the second here.
let (this, other) = Key::split(k.as_ref()); let (_this, other) = Key::split(k.as_ref());
#[cfg(test)] #[cfg(test)]
eprintln!(" found {this}:{other}"); eprintln!(" found {_this}:{other}");
decode(v.as_ref()).map(|label| (other, label)) decode(v.as_ref()).map(|label| (other, label))
}) })
} }
@ -383,7 +372,7 @@ impl<'db> Keyspace<'db> {
Ok((ref k, _)) => k.starts_with(&t), Ok((ref k, _)) => k.starts_with(&t),
_ => true, _ => true,
}) })
.map(|r| r.map_err(Error::Internal)) .map_err(Error::Internal)
} }
/// Show all items in the entire keyspace. /// Show all items in the entire keyspace.
fn list( fn list(
@ -393,7 +382,7 @@ impl<'db> Keyspace<'db> {
self.tx self.tx
.inner .inner
.full_iterator_cf(&self.cf, IteratorMode::Start) .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)] #[cfg(test)]
mod tests { 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<String> = 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::<Result<_>>()?;
eprintln!("test-arrow/l = {l:#?}");
let r: Vec<String> = 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::<Result<_>>()?;
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::<TestArrow>(target)
.map(|r| r.map(|(k, _)| k))
.collect::<Result<Vec<_>>>()?;
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::<TestArrow>(target)
.map(|r| r.map(|(k, _)| k))
.collect::<Result<Vec<_>>>()?;
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::<TestArrow>(origin)
.map(|r| r.map(|(k, _)| k))
.collect::<Result<Vec<_>>>()?;
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::<TestArrow>(origin)
.map(|r| r.map(|(k, _)| k))
.collect::<Result<Vec<_>>>()?;
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::<TestArrow>(origin)
.map(|r| r.map(|(k, _)| k))
.collect::<Result<Vec<_>>>()?;
for t in targets {
assert!(oo.contains(&t), "∀ t ∈ targets: t ∈ origin.outgoing");
let ti = tx
.list_incoming::<TestArrow>(t)
.map(|r| r.map(|(k, _)| k))
.collect::<Result<Vec<_>>>()?;
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::<TestArrow>(target)
.map(|r| r.map(|(k, _)| k))
.collect::<Result<Vec<_>>>()?;
for o in origins {
let oo = tx
.list_outgoing::<TestArrow>(o)
.map(|r| r.map(|(k, _)| k))
.collect::<Result<Vec<_>>>()?;
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<Key, Vec<Key>> = targets
.into_iter()
.map(|t| {
tx.list_incoming::<TestArrow>(t)
.map(|r| r.map(|(k, _)| k))
.collect::<Result<Vec<_>>>()
.map(|v| (t, v))
})
.collect::<Result<_>>()?;
// 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<Key, Vec<Key>> = targets
.into_iter()
.map(|t| {
tx.list_outgoing::<TestArrow>(t)
.map(|r| r.map(|(k, _)| k))
.collect::<Result<Vec<_>>>()
.map(|v| (t, v))
})
.collect::<Result<_>>()?;
// 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<Key, Vec<Key>> = origins
.into_iter()
.map(|o| {
tx.list_outgoing::<TestArrow>(o)
.map(|r| r.map(|(k, _)| k))
.collect::<Result<Vec<_>>>()
.map(|v| (o, v))
})
.collect::<Result<_>>()?;
// 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
})
})
}
}

View file

@ -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<String> = 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<String> = 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::<TestArrow>(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::<TestArrow>(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::<TestArrow>(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::<TestArrow>(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::<TestArrow>(origin).keys().try_collect()?;
for t in targets {
assert!(oo.contains(&t), "∀ t ∈ targets: t ∈ origin.outgoing");
let ti: Vec<_> = tx.list_incoming::<TestArrow>(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::<TestArrow>(target).keys().try_collect()?;
for o in origins {
let oo: Vec<_> = tx.list_outgoing::<TestArrow>(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<Key, Vec<Key>> = targets
.into_iter()
.map(|t| {
tx.list_incoming::<TestArrow>(t)
.keys()
.try_collect()
.map(|v: Vec<_>| (t, v))
})
.collect::<Result<_>>()?;
// 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<Key, Vec<Key>> = targets
.into_iter()
.map(|t| {
tx.list_outgoing::<TestArrow>(t)
.keys()
.try_collect()
.map(|v: Vec<_>| (t, v))
})
.collect::<Result<_>>()?;
// 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<Key, Vec<Key>> = origins
.into_iter()
.map(|o| {
tx.list_outgoing::<TestArrow>(o)
.keys()
.try_collect()
.map(|v: Vec<_>| (o, v))
})
.collect::<Result<_>>()?;
// 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
})
})
}

51
lib/store/src/util.rs Normal file
View file

@ -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<Item = Result<O, E>> + 'a
where
Self: Iterator<Item = Result<I, E>> + '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<Item = Result<I, O>> + 'a
where
Self: Iterator<Item = Result<I, E>> + '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<Item = Result<K, E>> + 'a
where
Self: Iterator<Item = Result<(K, V), E>> + 'a,
{
self.map_ok(|(k, _)| k)
}
/// Like and_then.
fn bind_results<'a, I, O, E>(
self,
mut f: impl FnMut(I) -> Result<O, E> + 'a,
) -> impl Iterator<Item = Result<O, E>> + 'a
where
Self: Iterator<Item = Result<I, E>> + '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<T, E>
where
Self: Iterator<Item = Result<T, E>> + 'a,
{
self.next().ok_or(e)?
}
}
impl<I> IterExt for I where I: Iterator {}