Compare commits

...

2 commits

Author SHA1 Message Date
3f652d13a8
tui test and fix permissions 2024-02-16 18:14:26 -08:00
820abbc089
maybe kv instead of tables? 2024-02-15 22:31:33 -08:00
16 changed files with 673 additions and 413 deletions

235
Cargo.lock generated
View file

@ -359,6 +359,21 @@ dependencies = [
"thiserror",
]
[[package]]
name = "cassowary"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "castaway"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc"
dependencies = [
"rustversion",
]
[[package]]
name = "cc"
version = "1.0.83"
@ -526,6 +541,19 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf43edc576402991846b093a7ca18a3477e0ef9c588cde84964b5d3e43016642"
[[package]]
name = "compact_str"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f"
dependencies = [
"castaway",
"cfg-if",
"itoa",
"ryu",
"static_assertions",
]
[[package]]
name = "concurrent-queue"
version = "2.4.0"
@ -625,6 +653,31 @@ version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
[[package]]
name = "crossterm"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
dependencies = [
"bitflags 2.4.2",
"crossterm_winapi",
"libc",
"mio",
"parking_lot 0.12.1",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
dependencies = [
"winapi",
]
[[package]]
name = "crunchy"
version = "0.2.2"
@ -708,6 +761,7 @@ dependencies = [
"dag-resolve",
"rusqlite",
"serde",
"serde_json",
"thiserror",
]
@ -777,6 +831,12 @@ dependencies = [
"zeroize",
]
[[package]]
name = "either"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
[[package]]
name = "equivalent"
version = "1.0.1"
@ -1071,7 +1131,7 @@ checksum = "5e87caa7459145f5e5f167bf34db4532901404c679e62339fb712a0e3ccf722a"
dependencies = [
"cosmic-text",
"etagere",
"lru",
"lru 0.11.1",
"wgpu",
]
@ -1407,6 +1467,12 @@ dependencies = [
"hashbrown 0.14.3",
]
[[package]]
name = "indoc"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8"
[[package]]
name = "instant"
version = "0.1.12"
@ -1430,6 +1496,15 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "itertools"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.10"
@ -1571,6 +1646,15 @@ dependencies = [
"hashbrown 0.14.3",
]
[[package]]
name = "lru"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db2c024b41519440580066ba82aab04092b333e09066a5eb86c7c4890df31f22"
dependencies = [
"hashbrown 0.14.3",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
@ -1984,6 +2068,12 @@ dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "paste"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
[[package]]
name = "percent-encoding"
version = "2.3.1"
@ -2193,6 +2283,26 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "795915a3930a5d6bafd9053d37602fea3e61be2e5d4d788983a8ba9654c1c6f2"
[[package]]
name = "ratatui"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcb12f8fbf6c62614b0d56eb352af54f6a22410c3b079eb53ee93c7b97dd31d8"
dependencies = [
"bitflags 2.4.2",
"cassowary",
"compact_str",
"crossterm",
"indoc",
"itertools",
"lru 0.12.2",
"paste",
"stability",
"strum",
"unicode-segmentation",
"unicode-width",
]
[[package]]
name = "raw-window-handle"
version = "0.5.2"
@ -2318,6 +2428,12 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rustversion"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
[[package]]
name = "rustybuzz"
version = "0.8.0"
@ -2414,6 +2530,36 @@ dependencies = [
"digest",
]
[[package]]
name = "signal-hook"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-mio"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
dependencies = [
"libc",
"mio",
"signal-hook",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
dependencies = [
"libc",
]
[[package]]
name = "signature"
version = "2.2.0"
@ -2562,6 +2708,16 @@ dependencies = [
"der",
]
[[package]]
name = "stability"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebd1b177894da2a2d9120208c3386066af06a488255caabc5de8ddca22dbc3ce"
dependencies = [
"quote",
"syn 1.0.109",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
@ -2586,6 +2742,28 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
[[package]]
name = "strum"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18"
dependencies = [
"heck",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.48",
]
[[package]]
name = "subtle"
version = "2.5.0"
@ -2780,6 +2958,28 @@ version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4"
[[package]]
name = "tui"
version = "0.1.0"
dependencies = [
"anyhow",
"crossterm",
"dag-resolve",
"dag-resolve-impls",
"ratatui",
"tui-term",
]
[[package]]
name = "tui-term"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a3a054e8a92d4b51f9c28e04712e18ef3721ea25552577e944ee9870cb7445b"
dependencies = [
"ratatui",
"vt100",
]
[[package]]
name = "twox-hash"
version = "1.6.3"
@ -2881,6 +3081,39 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "vt100"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84cd863bf0db7e392ba3bd04994be3473491b31e66340672af5d11943c6274de"
dependencies = [
"itoa",
"log",
"unicode-width",
"vte",
]
[[package]]
name = "vte"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197"
dependencies = [
"arrayvec",
"utf8parse",
"vte_generate_state_changes",
]
[[package]]
name = "vte_generate_state_changes"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"

View file

@ -48,7 +48,7 @@ struct State<R: Resolver> {
impl<R: Resolver> State<R> {
fn create_event(&mut self, content: CoreContent<R::EventType>) -> Result<()> {
match self.room.create_event(content, &self.secret) {
match self.room.create_event(&mut *self.store, content, &self.secret) {
Ok(event) => {
self.db.execute(
"INSERT INTO _events (id, json) VALUES (?, ?)",
@ -62,7 +62,7 @@ impl<R: Resolver> State<R> {
}
fn append_event(&mut self, event: Event<R::EventType>) -> Result<()> {
match self.room.append_event(event) {
match self.room.append_event(&mut *self.store, event) {
Ok(event) => {
self.db.execute(
"INSERT INTO _events (id, json) VALUES (?, ?)",
@ -113,8 +113,7 @@ impl<R: Resolver> State<R> {
)?;
}
let db = Rc::new(db);
let store = Database::from_conn(db.clone())
.init(room.get_resolver().get_state_config())?;
let store = Database::from_conn(db.clone()).init()?;
Ok(State {
db,
room,
@ -145,17 +144,20 @@ fn open(path: impl AsRef<Path>) -> Result<Opened> {
CoreContent::Create(c) => match c.resolver.as_str() {
"kv" => {
let mut room = Room::from_root(KVResolver, serde_json::from_str(&event_json)?)?;
let mut events = vec![];
while let Some(row) = rows.next()? {
let s: String = row.get(0)?;
let ev = serde_json::from_str(&s)?;
room.append_event(ev)?;
events.push(ev);
}
drop(event);
drop(rows);
drop(q);
let db = Rc::new(db);
let store = Database::from_conn(db.clone())
.init(room.get_resolver().get_state_config())?;
let mut store = Database::from_conn(db.clone()).init()?;
for event in events {
room.append_event(&mut *store, event)?;
}
Ok(Opened::Kv(State {
db,
room,
@ -165,17 +167,20 @@ fn open(path: impl AsRef<Path>) -> Result<Opened> {
},
"forum-v0" => {
let mut room = Room::from_root(ForumResolver, serde_json::from_str(&event_json)?)?;
let mut events = vec![];
while let Some(row) = rows.next()? {
let s: String = row.get(0)?;
let ev = serde_json::from_str(&s)?;
room.append_event(ev)?;
events.push(ev);
}
drop(event);
drop(rows);
drop(q);
let db = Rc::new(db);
let store = Database::from_conn(db.clone())
.init(room.get_resolver().get_state_config())?;
let mut store = Database::from_conn(db.clone()).init()?;
for event in events {
room.append_event(&mut *store, event)?;
}
Ok(Opened::Forum(State {
db,
room,
@ -234,26 +239,28 @@ fn send_event<R: Resolver + Debug>(state: &mut State<R>, data: &str) -> Result<(
fn print_info<R: Resolver + Debug>(state: State<R>) -> Result<()> {
let event_count: u64 = state.db.query_row("SELECT count(*) FROM _events", [], |r| r.get(0))?;
let row_count: u64 = state.db.query_row("SELECT count(*) FROM data", [], |r| r.get(0))?;
println!("room type: {}", state.room.get_resolver().name());
println!("room events: {} total", event_count);
println!("room state schema:");
for (table_name, table_schema) in state.room.get_resolver().get_state_config().tables {
print!(" {} (", table_name);
for (idx, (column_name, ctype)) in table_schema.columns.iter().enumerate() {
if idx != 0 {
print!(", ");
}
print!("{} {:?}", column_name, ctype);
}
print!(") indexed on (");
for (idx, index) in table_schema.indexes.iter().enumerate() {
if idx != 0 {
print!(", ");
}
print!("{} {:?}", index.column, index.index_type);
}
println!(")");
}
println!("room events: {}", event_count);
println!("room db records: {}", row_count);
// println!("room state schema:");
// for (table_name, table_schema) in state.room.get_resolver().get_state_config().tables {
// print!(" {} (", table_name);
// for (idx, (column_name, ctype)) in table_schema.columns.iter().enumerate() {
// if idx != 0 {
// print!(", ");
// }
// print!("{} {:?}", column_name, ctype);
// }
// print!(") indexed on (");
// for (idx, index) in table_schema.indexes.iter().enumerate() {
// if idx != 0 {
// print!(", ");
// }
// print!("{} {:?}", index.column, index.index_type);
// }
// println!(")");
// }
Ok(())
}

View file

@ -9,4 +9,5 @@ edition = "2021"
dag-resolve = { version = "0.1.0", path = "../proto" }
rusqlite = { version = "0.30.0", features = ["bundled"] }
serde = { version = "1.0.196", features = ["derive"] }
serde_json = "1.0.113"
thiserror = "1.0.57"

View file

@ -7,10 +7,7 @@ use serde::{Deserialize, Serialize};
use dag_resolve::{
actor::ActorId,
event::{CoreContent, Event, EventId},
proto::table::{
ColumnType, IndexType, State, StateConfig, Table as _, TableConfig, TableRow,
Text,
},
proto::{data::Text, table::Database},
resolver::{Command, Resolver, Verification},
};
@ -115,7 +112,7 @@ impl ForumMemberAcl {
ForumMemberAcl::Operator => "operator",
}
}
fn from_str(s: &str) -> Self {
match s {
"mute" => ForumMemberAcl::Mute,
@ -150,42 +147,52 @@ impl ForumRoomAcl {
impl Resolver for ForumResolver {
type EventType = ForumEventContent;
fn resolve<S: State>(&self, _state: &S, event: &Event<Self::EventType>) -> Vec<Command> {
fn resolve<S: Database>(&self, _state: &S, event: &Event<Self::EventType>) -> Vec<Command> {
let row = match event.content() {
CoreContent::Create(_) => Command::insert(
"members",
TableRow::new()
.with("actor", event.author().to_owned())
.with("acl", ForumMemberAcl::Operator.to_str()),
),
CoreContent::Custom(ForumEventContent::Post { .. }) => Command::insert(
"replies",
TableRow::new()
.with("event", event.id().to_owned())
.with("time", event.timestamp()),
),
CoreContent::Create(_) => {
let author = event.author().to_string().as_bytes().to_vec();
let key: Vec<_> = b"member\xff".into_iter().copied().chain(author).collect();
let value = ForumMemberAcl::Operator.to_str().as_bytes();
Command::put(key, value)
}
CoreContent::Custom(ForumEventContent::Post { .. }) => {
let key: Vec<_> = b"posts\xff"
.into_iter()
.copied()
.chain(event.id().to_string().as_bytes().to_vec())
.collect();
Command::put(key, b"")
}
CoreContent::Custom(ForumEventContent::Reply {
post, reference, ..
}) => Command::insert(
"replies",
TableRow::new()
.with("event", event.id().to_owned())
.with("post", post.to_owned())
.with("reference", reference.to_owned()),
),
CoreContent::Custom(ForumEventContent::Config { name, topic, acl }) => Command::insert(
"config",
TableRow::new()
.with("name", name.to_owned())
.with("topic", topic.to_owned())
.with("acl", acl.to_str()),
),
CoreContent::Custom(ForumEventContent::Member { id, acl }) => Command::insert(
"members",
TableRow::new()
.with("actor", id.to_owned())
.with("acl", acl.to_str()),
),
}) => {
let key: Vec<_> = b"replies\xff"
.into_iter()
.copied()
.chain(event.id().to_string().as_bytes().to_vec())
.collect();
let value: Vec<_> = vec![]
.into_iter()
.copied()
.chain(post.to_string().as_bytes().to_vec())
.chain([0xff])
.chain(reference.to_string().as_bytes().to_vec())
.collect();
Command::put(key, value)
}
CoreContent::Custom(ForumEventContent::Config { name, topic, acl }) => {
return vec![
Command::put(b"name", serde_json::to_vec(name).unwrap()),
Command::put(b"topic", serde_json::to_vec(topic).unwrap()),
Command::put(b"acl", acl.to_str().as_bytes()),
]
}
CoreContent::Custom(ForumEventContent::Member { id, acl }) => {
let author = id.to_string().as_bytes().to_vec();
let key: Vec<_> = b"member\xff".into_iter().copied().chain(author).collect();
let value = acl.to_str().as_bytes();
Command::put(key, value)
}
};
vec![row]
}
@ -198,52 +205,21 @@ impl Resolver for ForumResolver {
"forum-v0"
}
fn get_state_config(&self) -> StateConfig {
let config = TableConfig::new()
.with_column("name", ColumnType::Text)
.with_column("topic", ColumnType::Text)
.with_index("name", IndexType::LookupUnique)
.with_index("topic", IndexType::LookupUnique);
let members = TableConfig::new()
.with_column("actor", ColumnType::Actor)
.with_column("acl", ColumnType::String)
.with_index("actor", IndexType::LookupUnique);
let posts = TableConfig::new()
.with_column("event", ColumnType::Event)
.with_column("time", ColumnType::Integer)
.with_index("time", IndexType::Ordered);
let replies = TableConfig::new()
.with_column("event", ColumnType::Event)
.with_column("post", ColumnType::Event)
.with_column("parent", ColumnType::Event)
.with_index("post", IndexType::Lookup);
StateConfig::new()
.with_table("config", config)
.with_table("members", members)
.with_table("posts", posts)
.with_table("replies", replies)
.verify()
}
fn verify<S: State>(&self, state: &S, event: &Event<Self::EventType>) -> Verification {
fn verify<D: Database>(&self, state: &D, event: &Event<Self::EventType>) -> Verification {
let key: Vec<_> = b"members-"
.into_iter()
.copied()
.chain(event.author().to_string().as_bytes().to_vec())
.collect();
let member_entry = state
.table("members")
.unwrap()
.lookup_optional("actor", event.author().to_owned())
.unwrap()
.and_then(|r| r.values.get("acl").cloned())
.and_then(|d| d.as_string().map(ToOwned::to_owned))
.get(&key)
.and_then(|b| String::from_utf8(b).ok())
.map(|s| ForumMemberAcl::from_str(&s))
.unwrap_or_default();
let room_entry = state
.table("config")
.unwrap()
.lookup_one("key", "acl")
.unwrap()
.values
.get("acl")
.and_then(|d| d.as_string())
.map(|s| ForumRoomAcl::from_str(s))
.get(b"acl")
.and_then(|d| String::from_utf8(d).ok())
.map(|s| ForumRoomAcl::from_str(&s))
.unwrap_or_default();
match event.content() {

View file

@ -3,9 +3,7 @@
use serde::{Deserialize, Serialize};
use dag_resolve::{
event::{CoreContent, Event},
proto::table::{ColumnType, IndexType, State, StateConfig, Table, TableConfig, TableRow},
resolver::{Command, Resolver, Verification},
event::{CoreContent, Event}, proto::table::Database, resolver::{Command, Resolver, Verification}
};
use std::cmp::Ordering;
@ -29,19 +27,14 @@ impl KVResolver {
impl Resolver for KVResolver {
type EventType = KVEventContent;
fn resolve<S: State>(&self, _state: &S, event: &Event<KVEventContent>) -> Vec<Command> {
fn resolve<D: Database>(&self, _state: &D, event: &Event<KVEventContent>) -> Vec<Command> {
match &event.content() {
CoreContent::Create(_) => {
let row = TableRow::new()
.with("key", "owner".to_owned())
.with("value", event.author().to_owned());
vec![Command::insert("meta", row)]
vec![Command::Put { key: b"owner".into(), value: event.author().to_string().as_bytes().to_vec() }]
},
CoreContent::Custom(KVEventContent::Set(k, v)) => {
let row = TableRow::new()
.with("key", k.to_owned())
.with("value", v.to_owned());
vec![Command::insert("kv", row)]
let key = b"kv\xff".into_iter().chain(k.as_bytes()).cloned().collect();
vec![Command::Put { key, value: v.as_bytes().to_owned() }]
}
}
}
@ -54,23 +47,11 @@ impl Resolver for KVResolver {
"kv"
}
fn get_state_config(&self) -> StateConfig {
let meta = TableConfig::new()
.with_column("key", ColumnType::String)
.with_column("value", ColumnType::String)
.with_index("key", IndexType::LookupUnique);
let table = TableConfig::new()
.with_column("key", ColumnType::String)
.with_column("value", ColumnType::String)
.with_index("key", IndexType::LookupUnique);
StateConfig::new().with_table("meta", meta).with_table("kv", table).verify()
}
fn verify<S: State>(&self, state: &S, event: &Event<Self::EventType>) -> Verification {
let entry = state.table("meta").unwrap().lookup_optional("owner", event.author().to_owned()).unwrap();
match entry {
Some(_) => Verification::Valid,
None => Verification::Unauthorized,
fn verify<D: Database>(&self, state: &D, event: &Event<Self::EventType>) -> Verification {
if state.get(b"owner").unwrap() == event.author().to_string().as_bytes() {
Verification::Valid
} else {
Verification::Invalid
}
}
}

View file

@ -1,4 +1,4 @@
//! Contains premade stores to store state/data
pub mod memory;
// pub mod memory;
pub mod sqlite;

View file

@ -1,22 +1,13 @@
//! Store state in a sqlite database
use std::{collections::HashMap, path::Path, rc::Rc};
use std::{path::Path, rc::Rc};
use dag_resolve::proto::table::{self, State};
use rusqlite::params_from_iter;
use dag_resolve::proto::table::{self, Database as _};
use thiserror::Error;
#[derive(Debug)]
pub struct Database {
connection: Rc<rusqlite::Connection>,
config: Option<table::StateConfig>,
}
#[derive(Debug)]
pub struct Table {
connection: Rc<rusqlite::Connection>,
name: String,
config: table::TableConfig,
}
#[derive(Debug, Error)]
@ -27,249 +18,70 @@ impl Database {
let db = rusqlite::Connection::open(path).unwrap();
Ok(Database {
connection: Rc::new(db),
config: None,
})
}
pub fn from_conn(conn: Rc<rusqlite::Connection>) -> Self {
Database {
connection: conn,
config: None,
}
}
pub fn init(mut self, config: table::StateConfig) -> Result<Box<Self>, Error> {
self.config = Some(config);
self.reset()?;
pub fn init(self) -> Result<Box<Self>, Error> {
self.reset();
Ok(Box::new(self))
}
}
impl table::State for Database {
type Table = Table;
type Err = Error;
pub struct Query;
fn table(&self, name: &str) -> Result<Table, Self::Err> {
let Some(table) = self.config.as_ref().unwrap().tables.get(name) else {
panic!("table does not exist");
};
Ok(Table {
connection: self.connection.clone(),
name: name.to_owned(),
config: table.to_owned(),
})
}
fn reset(&mut self) -> Result<(), Self::Err> {
let mut sql = String::new();
for (table_name, table_column) in &self.config.as_ref().unwrap().tables {
// FIXME: don't drop and resolve from scratch every time
sql.push_str("DROP TABLE IF EXISTS '");
sql.push_str(&table_name.replace('\'', "''"));
sql.push_str("';\n");
sql.push_str("CREATE TABLE '");
sql.push_str(&table_name.replace('\'', "''"));
sql.push_str("' (");
for (idx, (column_name, column_config)) in table_column.columns.iter().enumerate() {
if idx != 0 {
sql.push_str(", ");
}
sql.push_str(&column_name.replace('\'', "''"));
sql.push_str(match column_config {
table::ColumnType::String => " TEXT",
table::ColumnType::Text => " TEXT",
table::ColumnType::Integer => " INT",
table::ColumnType::Float => " REAL",
table::ColumnType::Event => " TEXT",
table::ColumnType::Actor => " TEXT",
});
}
sql.push_str(");\n");
}
self.connection.execute_batch(&sql).unwrap();
Ok(())
}
}
impl table::Table for Table {
type Err = Error;
fn all(&self) -> Result<Vec<table::TableRow>, Self::Err> {
let mut sql = String::from("SELECT * FROM '");
sql.push_str(&self.name.replace('\'', "''"));
sql.push('\'');
let mut sql_query = self.connection.prepare(&sql).unwrap();
let mut sql_rows = sql_query.query([]).unwrap();
let mut rows = vec![];
while let Some(sql_row) = sql_rows.next().unwrap() {
let mut map = HashMap::new();
for (column_name, column_type) in &self.config.columns {
let val = match column_type {
table::ColumnType::String => {
table::ColumnValue::String(sql_row.get(column_name.as_str()).unwrap())
}
table::ColumnType::Text => todo!(),
table::ColumnType::Integer => {
table::ColumnValue::Integer(sql_row.get(column_name.as_str()).unwrap())
}
table::ColumnType::Float => {
table::ColumnValue::Float(sql_row.get(column_name.as_str()).unwrap())
}
table::ColumnType::Actor => todo!(),
table::ColumnType::Event => todo!(),
};
map.insert(column_name.to_string(), val);
}
rows.push(table::TableRow { values: map });
}
Ok(rows)
}
fn lookup(
&self,
column: &str,
value: impl Into<table::ColumnValue>,
) -> Result<Vec<table::TableRow>, Self::Err> {
let Some(column_config) = self.config.columns.get(column) else {
panic!("does not exist");
};
let has_index = self.config.indexes.iter().any(|idx| {
idx.column == column
&& matches!(
idx.index_type,
table::IndexType::Lookup | table::IndexType::LookupUnique
)
});
if !has_index {
panic!("no lookup index");
}
let mut sql = String::from("SELECT * FROM ");
sql.push_str(&self.name.replace('\'', "''"));
sql.push_str(" WHERE ");
sql.push_str(&column.replace('\'', "''"));
sql.push_str(" = ");
match (column_config, value.into()) {
(table::ColumnType::String, table::ColumnValue::String(s)) => {
sql.push('\'');
sql.push_str(&s.replace('\'', "''"));
sql.push('\'');
}
(table::ColumnType::Integer, table::ColumnValue::Integer(i)) => {
sql.push_str(&i.to_string());
}
(_, _) => todo!(),
};
let mut sql_query = self.connection.prepare(&sql).unwrap();
let mut sql_rows = sql_query.query([]).unwrap();
let mut rows = vec![];
while let Some(sql_row) = sql_rows.next().unwrap() {
let mut map = HashMap::new();
for (column_name, column_type) in &self.config.columns {
let val = match column_type {
table::ColumnType::String => {
table::ColumnValue::String(sql_row.get(column_name.as_str()).unwrap())
}
table::ColumnType::Text => todo!(),
table::ColumnType::Integer => {
table::ColumnValue::Integer(sql_row.get(column_name.as_str()).unwrap())
}
table::ColumnType::Float => {
table::ColumnValue::Float(sql_row.get(column_name.as_str()).unwrap())
}
table::ColumnType::Event => todo!(),
table::ColumnType::Actor => todo!(),
};
map.insert(column_name.to_string(), val);
}
rows.push(table::TableRow { values: map });
}
Ok(rows)
}
fn lookup_one(
&self,
column: &str,
value: impl Into<table::ColumnValue>,
) -> Result<table::TableRow, Self::Err> {
Ok(self
.lookup(column, value)
.unwrap()
.into_iter()
.next()
.unwrap())
}
fn lookup_optional(
&self,
column: &str,
value: impl Into<table::ColumnValue>,
) -> Result<Option<table::TableRow>, Self::Err> {
Ok(self.lookup(column, value).unwrap().into_iter().next())
}
fn range(
&self,
_column: &str,
_paginate: table::Paginate,
_limit: u64,
) -> Result<Vec<table::TableRow>, Self::Err> {
impl table::Query for Query {
fn get_single(self) -> Option<Vec<u8>> {
todo!()
}
fn search_text(
&self,
_column: &str,
_search: &str,
_limit: u64,
_after: u64,
) -> Result<Vec<table::TableRow>, Self::Err> {
fn get_all(self) -> Vec<Vec<u8>> {
todo!()
}
fn insert(&mut self, mut row: table::TableRow) -> Result<(), Self::Err> {
let mut sql = String::from("INSERT INTO ");
sql.push_str(&self.name.replace('\'', "''"));
sql.push_str(" (");
for (idx, name) in self.config.columns.keys().enumerate() {
if idx != 0 {
sql.push_str(", ");
}
sql.push_str(&name.replace('\'', "''"));
}
sql.push_str(") VALUES (");
use rusqlite::types::Value;
let mut params: Vec<Value> = vec![];
for (idx, (name, column_type)) in self.config.columns.iter().enumerate() {
if idx != 0 {
sql.push_str(", ");
}
sql.push('?');
match (column_type, row.values.remove(name).unwrap()) {
(table::ColumnType::Text, table::ColumnValue::Text(t)) => {
params.push(Value::Text(t.0))
}
(table::ColumnType::String, table::ColumnValue::String(s)) => {
params.push(Value::Text(s))
}
(table::ColumnType::Integer, table::ColumnValue::Integer(i)) => {
params.push(Value::Integer(i.try_into().unwrap()))
}
(table::ColumnType::Float, table::ColumnValue::Float(r)) => {
params.push(Value::Real(r))
}
(table::ColumnType::Event, table::ColumnValue::Event(event_id)) => {
params.push(Value::Text(event_id.to_string()))
}
(table::ColumnType::Actor, table::ColumnValue::Actor(actor_id)) => {
params.push(Value::Text(actor_id.to_string()))
}
(column_type, column_value) => todo!("column_type={column_type:?} with value {column_value:?}"),
};
}
sql.push(')');
self.connection
.execute(&sql, params_from_iter(params))
.unwrap();
Ok(())
fn get_iter(self) -> impl Iterator<Item = Vec<u8>> {
vec![].into_iter()
}
fn count(self) -> u64 {
todo!()
}
}
impl table::Database for Database {
type Query = Query;
fn reset(&self) {
self.connection.execute_batch("
DROP TABLE IF EXISTS data;
CREATE TABLE data (key BLOB, value BLOB);
").unwrap();
}
fn query(&self, _selector: table::Selector) -> Self::Query {
todo!()
}
fn query_reverse(&self, _selector: table::Selector) -> Self::Query {
todo!()
}
fn get(&self, key: &[u8]) -> Option<Vec<u8>> {
let mut s = self.connection.prepare("SELECT value FROM data WHERE key = ?").unwrap();
s.query_row([key.to_vec()], |f| f.get(0)).ok()
}
fn put(&self, key: &[u8], value: &[u8]) {
let mut s = self.connection.prepare("INSERT INTO data (key, value) VALUES (?, ?)").unwrap();
s.execute((key, value)).unwrap();
}
fn delete(&self, _key: &[u8]) {
todo!()
}
}

View file

@ -0,0 +1,13 @@
//! common data types
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Text(Vec<TextPart>);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TextPart {
pub lang: String,
pub mime_type: String,
pub content: String,
}

View file

@ -2,4 +2,5 @@ pub mod actor;
pub mod event;
pub mod resolver;
pub mod room;
pub mod data;
pub mod table;

View file

@ -1,6 +1,7 @@
use serde::{Deserialize, Serialize};
use super::table::{State, StateConfig, TableRow};
// use super::table::{State, StateConfig, TableRow};
use super::table::Database;
use crate::event::{Event, EventId};
use std::{cmp::Ordering, collections::HashSet, fmt::Debug};
@ -9,20 +10,17 @@ pub trait Resolver {
type EventType: Clone + Debug + Serialize + for<'a> Deserialize<'a>;
/// Given a set of ordered events, resolve the final state
fn resolve<S: State>(&self, state: &S, event: &Event<Self::EventType>) -> Vec<Command>;
fn resolve<D: Database>(&self, state: &D, event: &Event<Self::EventType>) -> Vec<Command>;
/// Given two events, decide which one comes first
/// if Ordering::Equal is returned, the timestamp then event id is used
fn tiebreak(&self, a: &Event<Self::EventType>, b: &Event<Self::EventType>) -> Ordering;
/// Verify if an event can be sent or not
fn verify<S: State>(&self, state: &S, event: &Event<Self::EventType>) -> Verification;
fn verify<D: Database>(&self, state: &D, event: &Event<Self::EventType>) -> Verification;
/// TEMP: Get the name/id of this resolver
fn name(&self) -> &str;
/// Get the schema for state
fn get_state_config(&self) -> StateConfig;
}
#[derive(Debug)]
@ -41,17 +39,13 @@ pub enum Verification {
/// Effects a resolver can produce
pub enum Command {
/// Insert a new row into the database
Insert {
table: String,
row: TableRow,
Put {
key: Vec<u8>,
value: Vec<u8>,
},
// // TODO: how does delete/update work?
// /// Delete an existing row
// Delete { query: StateQuery },
// /// Update an existing row
// Update {},
/// Delete an existing row
Delete { key: Vec<u8> },
// /// Notify someone outside of the room that they can join
// Invite {},
@ -62,8 +56,8 @@ pub enum Command {
}
impl Command {
pub fn insert(table: impl Into<String>, row: TableRow) -> Self {
Self::Insert { table: table.into(), row }
pub fn put(key: impl Into<Vec<u8>>, value: impl Into<Vec<u8>>) -> Self {
Command::Put { key: key.into(), value: value.into() }
}
}

View file

@ -3,12 +3,12 @@ use crate::{
event::EventId,
event::{CoreContent, CreateContent, Event, HashType, SignatureType},
proto,
resolver::{sort, Resolver, Command},
resolver::{sort, Command, Resolver, Verification},
Error, Result,
};
use std::fmt::Debug;
use super::table::Table;
use super::table::Database;
#[derive(Debug)]
pub struct Room<R: Resolver> {
@ -65,27 +65,27 @@ impl<R: Resolver> Room<R> {
&self.resolver
}
pub fn resolve_state<S>(&self, state: &mut S)
pub fn resolve_state<D>(&self, state: &mut D)
where
S: proto::table::State,
D: proto::table::Database,
{
let resolver = self.get_resolver();
let sorted = sort(|a, b| resolver.tiebreak(a, b), &self.events);
state.reset().unwrap();
state.reset();
for event in sorted {
let effects = resolver.resolve(state, event);
for effect in effects {
match effect {
Command::Insert { table, row } => {
state.table(&table).unwrap().insert(row).unwrap();
},
Command::Put { key, value } => state.put(&key, &value),
Command::Delete { key } => state.delete(&key),
}
}
}
}
pub fn create_event(
pub fn create_event<D: Database>(
&mut self,
state: &mut D,
event_content: CoreContent<R::EventType>,
secret: &ActorSecret,
) -> Result<&Event<R::EventType>> {
@ -93,11 +93,16 @@ impl<R: Resolver> Room<R> {
.with_references(std::mem::take(&mut self.heads))
.then_hash()?
.and_sign()?;
self.append_event(event)
self.append_event(state, event)
}
pub fn append_event(&mut self, event: Event<R::EventType>) -> Result<&Event<R::EventType>> {
pub fn append_event<D: Database>(&mut self, state: &mut D, event: Event<R::EventType>) -> Result<&Event<R::EventType>> {
event.verify_room(self).expect("event failed verification");
match self.get_resolver().verify(state, &event) {
Verification::Valid => {},
Verification::Unauthorized => panic!("unauthorized"),
Verification::Invalid => panic!("invalid data"),
}
if self.events.iter().any(|p| p == &event) {
return Err(Error::AlreadyExists);
}

View file

@ -0,0 +1,26 @@
pub trait Database {
type Query: Query;
fn query(&self, selector: Selector) -> Self::Query;
fn query_reverse(&self, selector: Selector) -> Self::Query;
fn reset(&self);
fn get(&self, key: &[u8]) -> Option<Vec<u8>>;
fn put(&self, key: &[u8], value: &[u8]);
fn delete(&self, key: &[u8]);
}
pub struct Selector(Vec<u8>);
pub trait Query {
fn get_single(self) -> Option<Vec<u8>>;
fn get_all(self) -> Vec<Vec<u8>>;
fn get_iter(self) -> impl Iterator<Item = Vec<u8>>;
fn count(self) -> u64;
}
impl Into<Selector> for Vec<u8> {
fn into(self) -> Selector {
Selector(self)
}
}

View file

@ -0,0 +1,13 @@
//! Each room has a small, well defined, domain specific relational
//! database associated with it. The database's contents will then be defined
//! by the events it receives. This module contains database traits and
//! how the database's schema is defined.
//!
//! This is a work in progress; the goal is trying to find a good
//! effort/payoff ratio in the database schema definition.
// mod old;
mod kv;
// pub use old::*;
pub use kv::*;

View file

@ -1,11 +1,3 @@
//! Each room has a small, well defined, domain specific relational
//! database associated with it. The database's contents will then be defined
//! by the events it receives. This module contains database traits and
//! how the database's schema is defined.
//!
//! This is a work in progress; the goal is trying to find a good
//! effort/payoff ratio in the database schema definition.
// TODO: properly type things
// maybe replace TableRow with impl Serialize?
// and make schema a trait instead of builder?
@ -13,7 +5,7 @@
use std::{collections::HashMap, fmt::Debug};
use serde::{Deserialize, Serialize};
use crate::{actor::ActorId, event::EventId};
use crate::{actor::ActorId, event::EventId, resolver::Command};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
// TODO: actually implement Text
@ -88,7 +80,6 @@ pub struct IndexConfig {
#[derive(Debug, Default, Clone)]
pub struct TableRow {
pub id: u64,
pub values: HashMap<String, ColumnValue>,
}
@ -313,3 +304,111 @@ impl TableRow {
self
}
}
// ==========================
// alternative way of querying the db
/// A way to query the database
pub struct Query;
// trait Queryable {}
pub enum QuerySelector {
/// Select one specific value
Lookup(ColumnValue),
/// Select starting from a specific (inclusive)
From(ColumnValue),
/// Select ending at a specific value (inclusive)
Until(ColumnValue),
}
pub enum QuerySorter {
/// Lowest to highest.
Ascending,
/// Highest to lowest.
Descending,
}
// do i do sql?
// how much of this becomes a trait, how much is a query struct?
impl Query {
pub fn new() -> Self {
Self
}
/// sql FROM
pub fn from(self, _table: impl Into<String>) -> Self {
todo!()
}
/// sql WHERE
pub fn matching(self, _column: impl Into<String>, _matcher: QuerySelector) -> Self {
todo!()
}
/// sql ORDER BY
pub fn sorted(self, _column: impl Into<String>, _sorter: QuerySorter) -> Self {
todo!()
}
/// sql JOIN ON _column = other.column
pub fn join_to(self, _column: impl Into<String>) -> Self {
todo!()
}
/// sql JOIN ON _other._column = column
pub fn join_from(self, _other: Query, _column: impl Into<String>) -> Self {
todo!()
}
/// Select a single item
pub fn first(self) -> Option<TableRow> {
todo!()
}
/// Select all items
// do i really want this? it allows unbounded access
pub fn all(self) -> Vec<TableRow> {
todo!()
}
/// Iterate over items
pub fn iter(self) -> impl Iterator<Item = TableRow> {
todo!();
#[allow(unused)]
vec![].into_iter()
}
}
// these would only be available in the resolver
impl Query {
/// Create a set of commands that will update the selected rows
pub fn update<F: FnMut(TableRow) -> ()>(self, _f: F) -> Vec<Command> {
todo!()
}
/// Create a set of commands that will replace the selected rows with new rows
pub fn put(self, _rows: impl IntoIterator<Item = TableRow>) -> Vec<Command> {
todo!()
}
/// Create a command that will delete the selected rows
pub fn delete(self) -> Command {
todo!()
}
}
// impl TableRow {
// /// creates a command that deletes the row
// pub fn delete(self) -> Command {
// todo!()
// }
// /// creates a command that updates the row with new data
// pub fn update(&self, _row: TableRow) -> Command {
// todo!()
// }
// }

14
crates/tui/Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "tui"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.79"
crossterm = "0.27.0"
dag-resolve = { version = "0.1.0", path = "../proto" }
dag-resolve-impls = { version = "0.1.0", path = "../impls" }
ratatui = "0.26.1"
tui-term = "0.1.8"

85
crates/tui/src/main.rs Normal file
View file

@ -0,0 +1,85 @@
use std::{io::stdout, time::Duration};
use anyhow::Result;
use crossterm::{
event::{self, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{
backend::CrosstermBackend,
widgets::{Block, Paragraph},
Frame, Terminal,
};
fn main() -> Result<()> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
let mut state = State {
view: View::Base
};
loop {
terminal.draw(|f| ui(&state, f))?;
if let Some(message) = handle_events(&state)? {
match message {
Message::Quit => break,
Message::View(v) => state.view = v,
}
}
}
stdout().execute(LeaveAlternateScreen)?;
disable_raw_mode()?;
Ok(())
}
#[derive(Debug)]
enum Message {
Quit,
View(View),
}
#[derive(Debug)]
struct State {
view: View,
}
#[derive(Debug)]
enum View {
Base,
Help,
}
fn handle_events(state: &State) -> Result<Option<Message>> {
if event::poll(Duration::from_millis(50))? {
match event::read()? {
event::Event::Key(key) if key.kind == KeyEventKind::Press => match (&state.view, key.code) {
(View::Base, KeyCode::Char('q') | KeyCode::Esc) => return Ok(Some(Message::Quit)),
(View::Base, KeyCode::Char('h' | '?')) => {
return Ok(Some(Message::View(View::Help)))
}
(View::Help, KeyCode::Char('q')) => return Ok(Some(Message::View(View::Base))),
(_, _) => Ok(None)
},
_ => Ok(None)
}
} else {
Ok(None)
}
}
fn ui(state: &State, f: &mut Frame) {
match state.view {
View::Base => {
let p = Paragraph::new("hello world!").block(Block::bordered().title("hello"));
f.render_widget(p, f.size())
}
View::Help => {
let p = Paragraph::new("help info").block(Block::bordered().title("help"));
f.render_widget(p, f.size())
}
}
}