diff --git a/.gitignore b/.gitignore index ea8c4bf..1fef432 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/.state diff --git a/Cargo.lock b/Cargo.lock index 8ff9903..83b4da7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -777,9 +777,8 @@ dependencies = [ [[package]] name = "librocksdb-sys" -version = "0.16.0+8.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce3d60bc059831dc1c83903fb45c103f75db65c5a7bf22272764d9cc683e348c" +version = "0.17.0+9.0.0" +source = "git+https://github.com/rust-rocksdb/rust-rocksdb.git#961abc8e45b30b43cad3659305d5703eb349fc31" dependencies = [ "bindgen", "bzip2-sys", @@ -1196,8 +1195,7 @@ dependencies = [ [[package]] name = "rocksdb" version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bd13e55d6d7b8cd0ea569161127567cd587676c99f4472f779a0279aa60a7a7" +source = "git+https://github.com/rust-rocksdb/rust-rocksdb.git#961abc8e45b30b43cad3659305d5703eb349fc31" dependencies = [ "libc", "librocksdb-sys", diff --git a/bin/pupctl/src/main.rs b/bin/pupctl/src/main.rs index 2284bc4..c60d855 100644 --- a/bin/pupctl/src/main.rs +++ b/bin/pupctl/src/main.rs @@ -1,3 +1,13 @@ +use puppy::{store::alias::Username, Store}; + fn main() { - println!("pupctl") + let db = Store::open(".state").unwrap(); + // let riley = puppy::create_author(&db, "riley").unwrap(); + let riley = db + .transaction(|tx| tx.lookup_alias(Username("riley".to_string()))) + .unwrap(); + puppy::create_post(&db, riley, "hello!").unwrap(); + for (key, post) in puppy::list_posts_by_author(&db, riley).unwrap() { + println!("post {key}: {:?} by user {riley}", post.content) + } } diff --git a/flake.nix b/flake.nix index 6425682..906e016 100644 --- a/flake.nix +++ b/flake.nix @@ -23,7 +23,6 @@ shellHook = '' export LIBCLANG_PATH="${pkgs.llvmPackages_16.libclang.lib}/lib"; - export ROCKSDB_LIB_DIR="${pkgs.rocksdb}/lib"; ''; }; }; diff --git a/lib/puppy/src/lib.rs b/lib/puppy/src/lib.rs index 8b13789..3f0e198 100644 --- a/lib/puppy/src/lib.rs +++ b/lib/puppy/src/lib.rs @@ -1 +1,52 @@ +pub use store::{self, Key, Store}; +use store::{ + alias::Username, + arrow::{AuthorOf, Direction}, + value::{Content, Profile}, + Keylike, +}; +pub fn create_post(db: &Store, author: Key, content: impl ToString) -> store::Result { + let key = Key::gen(); + db.transaction(|tx| { + tx.update::(author, |_, mut profile| { + profile.post_count += 1; + Ok(profile) + })?; + tx.insert(key, Content { + content: Some(content.to_string()), + summary: None, + })?; + tx.insert_arrow((author, key), AuthorOf)?; + Ok(key) + }) +} + +pub fn create_author(db: &Store, username: impl ToString) -> store::Result { + let key = Key::gen(); + db.transaction(|tx| { + tx.insert_alias(key, Username(username.to_string()))?; + tx.insert(key, Profile { + post_count: 0, + account_name: username.to_string(), + display_name: None, + about_string: None, + about_fields: Vec::new(), + })?; + Ok(key) + }) +} + +pub fn list_posts_by_author( + db: &Store, + author: impl Keylike, +) -> store::Result> { + db.transaction(|tx| { + tx.list_arrows_with::(Direction::Outgoing, author) + .map(|r| { + let (post_key, _) = r?; + tx.lookup::(post_key) + }) + .collect() + }) +} diff --git a/lib/store/Cargo.toml b/lib/store/Cargo.toml index 6741542..ef19a20 100644 --- a/lib/store/Cargo.toml +++ b/lib/store/Cargo.toml @@ -7,6 +7,6 @@ path = "src/lib.rs" [dependencies] ulid = "*" -rocksdb = "*" +rocksdb = { git = "https://github.com/rust-rocksdb/rust-rocksdb.git" } derive_more = "*" bincode = "2.0.0-rc.3" diff --git a/lib/store/src/key.rs b/lib/store/src/key.rs index 5c35a91..9523504 100644 --- a/lib/store/src/key.rs +++ b/lib/store/src/key.rs @@ -1,10 +1,21 @@ +use std::fmt::Display; + use crate::{Alias, Result, Transaction}; /// A unique identifier for vertices in the database. #[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd)] pub struct Key([u8; 16]); +impl Display for Key { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + ulid::Ulid::from_bytes(self.0).fmt(f) + } +} + impl Key { + pub fn gen() -> Key { + Key(ulid::Ulid::new().to_bytes()) + } pub(crate) fn from_slice(buf: &[u8]) -> Key { let mut key = [0; 16]; key.copy_from_slice(&buf); diff --git a/lib/store/src/lib.rs b/lib/store/src/lib.rs index 9ba4d1c..a8e11af 100644 --- a/lib/store/src/lib.rs +++ b/lib/store/src/lib.rs @@ -12,7 +12,7 @@ use std::sync::Arc; use derive_more::From; -use rocksdb::MultiThreaded; +use rocksdb::{MultiThreaded, Options, TransactionDBOptions}; type Backend = rocksdb::TransactionDB; @@ -23,6 +23,21 @@ pub use key::{Key, Keylike}; pub use transaction::Transaction; pub use {alias::Alias, arrow::Arrow, value::Value}; +pub const OK: Result<()> = Ok(()); + +/// Master list of all column family names in use. +const SPACES: &[&'static str] = &[ + "registry", + "username/l", + "username/r", + "follows/l", + "follows/r", + "profile", + "content", + "created-by/l", + "created-by/r", +]; + /// The handle to the data store. /// /// This type can be cloned freely. @@ -31,6 +46,23 @@ pub struct Store { inner: Arc, } +impl Store { + pub fn open(state_dir: &str) -> Result { + let mut db_opts = Options::default(); + db_opts.create_if_missing(true); + db_opts.create_missing_column_families(true); + let tx_opts = TransactionDBOptions::default(); + // NOTE: it crashes here because there hasn't been a release yet that includes https://github.com/rust-rocksdb/rust-rocksdb/pull/868 + let inner = Arc::new(Backend::open_cf( + &db_opts, + &tx_opts, + format!("{state_dir}/main-store"), + SPACES, + )?); + Ok(Store { inner }) + } +} + /// An isolated keyspace. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct Space(&'static str); @@ -54,11 +86,27 @@ pub mod value { } #[derive(Encode, Decode)] - pub struct Profile {} + pub struct Profile { + pub post_count: usize, + pub account_name: String, + pub display_name: Option, + pub about_string: Option, + pub about_fields: Vec<(String, String)>, + } impl Value for Profile { const SPACE: Space = Space("profile"); } + + #[derive(Encode, Decode)] + pub struct Content { + pub content: Option, + pub summary: Option, + } + + impl Value for Content { + const SPACE: Space = Space("content"); + } } pub mod arrow { @@ -78,6 +126,13 @@ pub mod arrow { Incoming, Outgoing, } + + #[derive(Encode, Decode)] + pub struct AuthorOf; + + impl Arrow for AuthorOf { + const SPACE: (Space, Space) = (Space("created-by/l"), Space("created-by/r")); + } } pub mod alias { diff --git a/lib/store/src/transaction.rs b/lib/store/src/transaction.rs index aca9a64..b073133 100644 --- a/lib/store/src/transaction.rs +++ b/lib/store/src/transaction.rs @@ -3,18 +3,9 @@ use std::{collections::HashMap, sync::Arc}; use bincode::{Decode, Encode}; use rocksdb::BoundColumnFamily; -use crate::{arrow::Direction, Alias, Arrow, Backend, Error, Key, Keylike, Result, Store, Value}; - -const OK: Result<()> = Ok(()); -/// Master list of all column family names in use. -const SPACES: &[&'static str] = &[ - "registry", - "username/l", - "username/r", - "follows/l", - "follows/r", - "profile", -]; +use crate::{ + arrow::Direction, Alias, Arrow, Backend, Error, Key, Keylike, Result, Store, Value, OK, SPACES, +}; impl Store { /// Initiate a transaction. @@ -120,17 +111,21 @@ impl Transaction<'_> { Ok(Key::from_slice(raw.as_ref())) } /// Create a new alias of type `A` for the given [`Key`]. + /// + /// If the alias already exists, this function returns `Conflict`. pub fn insert_alias(&self, key: Key, alias: A) -> Result<()> where A: Alias, { let (l, r) = A::SPACE; let alias = alias.to_string(); + if self.with(l).has(&alias)? { + return Err(Error::Conflict); + } self.with(l).set(&alias, key)?; self.with(r).set(key, &alias)?; OK } - /// Delete the alias of type `A` that points to `key`. pub fn remove_alias(&self, key: Key) -> Result<()> where