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,
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::<Follows>((riley, linen))? {
if !db.exists::<Follows>((riley, linen))? {
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");
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::<Username>(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::<Username>(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::<Username>(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<Key, Error> {
let user = db.resolve::<Username>(username);
let user = db.translate::<Username>(username);
match user {
Ok(key) => {
println!("found '{username}' ({key})");
@ -63,7 +69,7 @@ fn get_or_create_actor(db: &Store, username: &str) -> Result<Key, Error> {
}
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}");
}

View file

@ -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<Key> {
let key = Key::gen();
db.transaction(|tx| {
tx.define(key, tags::POST)?;
tx.create_vertex(key, tags::POST)?;
tx.update::<Profile>(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<Key> {
pub fn create_actor(db: &Store, username: impl ToString) -> store::Result<Key> {
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<Vec<(Key, Content)>> {
db.transaction(|tx| {
tx.list_outgoing::<AuthorOf>(author)
.map(|r| {
let (post_key, _) = r?;
tx.lookup::<Content>(post_key)
})
.bind_results(|(post_key, _)| tx.lookup::<Content>(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<Vec<(Key, Post)>> {
pub fn fetch_all(db: &Store) -> Result<Vec<Post>> {
db.transaction(|tx| {
let iter = tx.list::<Content>();
iter.map(|r| {
let (post_id, content) = r?;
iter.bind_results(|(id, content)| {
let author = tx
.list_incoming::<AuthorOf>(post_id)
.map(|r| r.map(|(k, _)| k))
.next()
.unwrap()?;
Ok((post_id, Post { author, content }))
.list_incoming::<AuthorOf>(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<Vec<Key>> {
db.transaction(|tx| {
tx.list_incoming::<FollowRequested>(target)
.map(|r| r.map(|(k, _)| k))
.collect()
})
db.transaction(|tx| tx.list_incoming::<FollowRequested>(target).keys().collect())
}
pub fn following_of(db: &Store, actor: Key) -> store::Result<Vec<Key>> {
db.transaction(|tx| {
tx.list_outgoing::<Follows>(actor)
.map(|r| r.map(|(k, _)| k))
.collect()
})
db.transaction(|tx| tx.list_outgoing::<Follows>(actor).keys().collect())
}
pub fn followers_of(db: &Store, actor: Key) -> store::Result<Vec<Key>> {
db.transaction(|tx| {
tx.list_incoming::<Follows>(actor)
.map(|r| r.map(|(k, _)| k))
.collect()
})
db.transaction(|tx| tx.list_incoming::<Follows>(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::<FollowRequested>((alice, bob))?,
db.exists::<FollowRequested>((alice, bob))?,
"(alice -> bob) ∈ follow-requested"
);
assert!(
!db.contains::<Follows>((alice, bob))?,
!db.exists::<Follows>((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::<Follows>((alice, bob))?,
db.exists::<Follows>((alice, bob))?,
"(alice -> bob) ∈ follows"
);
assert!(
!db.contains::<Follows>((bob, alice))?,
!db.exists::<Follows>((bob, alice))?,
"(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.
fn checked_translate(self, tx: &Transaction<'_>) -> Result<Key> {
let key = self.translate(tx)?;
if !tx.exists(key)? {
if !tx.is_registered(key)? {
Err(Error::Undefined { key })
} else {
Ok(key)

View file

@ -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<Backend>,
}
/// 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<Path>) -> Result<Store> {
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<T, E>(f: impl FnOnce(Store) -> Result<T, E>) -> Result<T, E>
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<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)
}
/// 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].
pub fn resolve<A>(&self, s: impl ToString) -> Result<Key>
pub fn translate<A>(&self, s: impl ToString) -> Result<Key>
where
A: Alias,
{
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.
pub fn contains<A>(&self, arrow: (Key, Key)) -> Result<bool>
pub fn exists<A>(&self, arrow: (Key, Key)) -> Result<bool>
where
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.
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
/// 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),

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 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<bool> {
pub fn is_registered(&self, key: Key) -> Result<bool> {
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<bool> {
pub fn is_registered(&self, key: Key) -> Result<bool> {
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<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`].
///
/// 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<A>(&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<A>(&self, (tail, head): (Key, Key)) -> Result<bool>
pub fn exists<A>(&self, (tail, head): (Key, Key)) -> Result<bool>
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<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
})
})
}
}
mod tests;

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 {}