First polishing pass
This commit is contained in:
parent
f8ebdce372
commit
ad62fec21f
10 changed files with 501 additions and 477 deletions
|
@ -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}");
|
||||
}
|
||||
|
|
|
@ -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
17
lib/store/src/alias.rs
Normal 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
40
lib/store/src/arrow.rs
Normal 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"));
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
35
lib/store/src/mixin.rs
Normal 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");
|
||||
}
|
|
@ -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;
|
||||
|
|
256
lib/store/src/transaction/tests.rs
Normal file
256
lib/store/src/transaction/tests.rs
Normal 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
51
lib/store/src/util.rs
Normal 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 {}
|
Loading…
Reference in a new issue