From e6deae71fa5b33a9a77345c0ef9e0da263502c4b Mon Sep 17 00:00:00 2001 From: Ziyang Hu Date: Tue, 25 Oct 2022 13:06:20 +0800 Subject: [PATCH] access level API --- Cargo.lock | 36 +++++++++++----- docs/source/sysops.rst | 21 ++++++--- docs/tutorial/tutorial.ipynb | 16 +++++-- src/cozoscript.pest | 14 +++--- src/parse/query.rs | 4 +- src/parse/sys.rs | 15 +++++++ src/query/compile.rs | 13 +++++- src/query/relation.rs | 34 +++++++-------- src/query/stored.rs | 46 +++++++++++++++++--- src/runtime/db.rs | 14 ++++-- src/runtime/relation.rs | 82 ++++++++++++++++++++++++++++++++---- 11 files changed, 233 insertions(+), 62 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1e7b0f78..70e79f01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -280,9 +280,9 @@ checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" [[package]] name = "clap" -version = "3.2.21" +version = "3.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed5341b2301a26ab80be5cbdced622e80ed808483c52e45e3310a877d3b37d7" +checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" dependencies = [ "atty", "bitflags", @@ -292,7 +292,7 @@ dependencies = [ "once_cell", "strsim", "termcolor", - "textwrap", + "textwrap 0.16.0", ] [[package]] @@ -788,9 +788,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.135" +version = "0.2.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c" +checksum = "55edcf6c0bb319052dea84732cf99db461780fd5e8d3eb46ab6ff312ab31f197" [[package]] name = "link-cplusplus" @@ -859,7 +859,7 @@ dependencies = [ "supports-hyperlinks", "supports-unicode", "terminal_size", - "textwrap", + "textwrap 0.15.2", "thiserror", "unicode-width", ] @@ -1394,7 +1394,7 @@ dependencies = [ "serde_json", "sha1", "threadpool", - "time 0.3.15", + "time 0.3.16", "tiny_http", "url", ] @@ -1646,15 +1646,21 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.15.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d" dependencies = [ "smawk", "unicode-linebreak", "unicode-width", ] +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + [[package]] name = "thiserror" version = "1.0.37" @@ -1728,14 +1734,22 @@ dependencies = [ [[package]] name = "time" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d634a985c4d4238ec39cacaed2e7ae552fbd3c476b552c1deac3021b7d7eaf0c" +checksum = "0fab5c8b9980850e06d92ddbe3ab839c062c801f3927c0fb8abd6fc8e918fbca" dependencies = [ "libc", "num_threads", + "serde", + "time-core", ] +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + [[package]] name = "tiny_http" version = "0.12.0" diff --git a/docs/source/sysops.rst b/docs/source/sysops.rst index 2cfd88bd..2aa5ab51 100644 --- a/docs/source/sysops.rst +++ b/docs/source/sysops.rst @@ -28,26 +28,37 @@ Ops on stored relations List all stored relations currently in the database -.. function:: ::relation columns +.. function:: ::columns List all columns for the stored relation ````. -.. function:: ::relation remove (, )* +.. function:: ::remove (, )* Remove stored relations. Several can be specified, joined by commas. -.. function:: ::relation rename -> (, -> )* +.. function:: ::rename -> (, -> )* Rename stored relation ```` into ````. Several may be specified, joined by commas. -.. function:: ::relation show_triggers +.. function:: ::show_triggers Display triggers associated with the stored relation ````. -.. function:: ::relation set_triggers +.. function:: ::set_triggers Set triggers for the stored relation ````. This is explained in more detail in the transactions chapter. +.. function:: ::access_level + + Sets the access level of ```` to the given level. The levels are: + + * ``normal`` allows everything, + * ``protected`` disallows ``::remove`` and ``:replace``, + * ``read_only`` additionally disallows any mutations and setting triggers, + * ``hidden`` additionally disallows any data access (metadata access via ``::relations``, etc., are still allowed). + + It is recommended to give the appropriate access levels to tables to prevent data loss from programming mistakes. + ------------------------------------ Monitor and kill ------------------------------------ diff --git a/docs/tutorial/tutorial.ipynb b/docs/tutorial/tutorial.ipynb index 56369141..c1117bfa 100644 --- a/docs/tutorial/tutorial.ipynb +++ b/docs/tutorial/tutorial.ipynb @@ -1797,7 +1797,7 @@ } ], "source": [ - "::relation columns stored " + "::columns stored " ] }, { @@ -2295,7 +2295,7 @@ } ], "source": [ - "::relation remove stored" + "::remove stored" ] }, { @@ -2728,7 +2728,7 @@ } ], "source": [ - "::relation columns fd" + "::columns fd" ] }, { @@ -2740,6 +2740,14 @@ "We won't overload you with all the complexities in this tutorial." ] }, + { + "cell_type": "markdown", + "id": "726ae4cf-d643-4c27-abe2-56c04050e02e", + "metadata": {}, + "source": [ + "The stored relations API are designed to be easy to use. However, sometimes being too easy can be bad, for example, it is possible to wipe an important stored relation full of data by a `:replace` erroneously introduced in a rarely used piece of code somewhere. For valuable stored relations, therefore, we recommend that you set the appropriate _access level_, as explained [here](https://cozodb.github.io/current/manual/stored.html#stored-relations)." + ] + }, { "cell_type": "markdown", "id": "30504cd6-7fb6-4ae3-ba2c-79eefa288095", @@ -2799,7 +2807,7 @@ } ], "source": [ - "::relation remove fd" + "::remove fd" ] }, { diff --git a/src/cozoscript.pest b/src/cozoscript.pest index 097a5425..6179cc37 100644 --- a/src/cozoscript.pest +++ b/src/cozoscript.pest @@ -3,18 +3,20 @@ query_script = {SOI ~ (option | rule | const_rule | algo_rule)+ ~ EOI} query_script_inner = {"{" ~ (option | rule | const_rule | algo_rule)+ ~ "}"} multi_script = {SOI ~ query_script_inner+ ~ EOI} sys_script = {SOI ~ "::" ~ (compact_op | list_relations_op | list_relation_op | remove_relations_op | trigger_relation_op | - trigger_relation_show_op | rename_relations_op | running_op | kill_op | explain_op) ~ EOI} + trigger_relation_show_op | rename_relations_op | running_op | kill_op | explain_op | access_level_op) ~ EOI} compact_op = {"compact"} running_op = {"running"} kill_op = {"kill" ~ int} explain_op = {"explain" ~ query_script_inner} list_relations_op = {"relations"} -list_relation_op = {"relation" ~ "columns" ~ compound_ident} -remove_relations_op = {"relation" ~ "remove" ~ (compound_ident ~ ",")* ~ compound_ident } -rename_relations_op = {"relation" ~ "rename" ~ (rename_pair ~ ",")* ~ rename_pair } -trigger_relation_show_op = {"relation" ~ "show_triggers" ~ compound_ident } -trigger_relation_op = {"relation" ~ "set_triggers" ~ compound_ident ~ trigger_clause* } +list_relation_op = {"columns" ~ compound_ident} +remove_relations_op = {"remove" ~ (compound_ident ~ ",")* ~ compound_ident } +rename_relations_op = {"rename" ~ (rename_pair ~ ",")* ~ rename_pair } +access_level_op = {"access_level" ~ access_level ~ compound_ident} +access_level = {("normal" | "protected" | "read_only" | "hidden")} +trigger_relation_show_op = {"show_triggers" ~ compound_ident } +trigger_relation_op = {"set_triggers" ~ compound_ident ~ trigger_clause* } trigger_clause = { "on" ~ (trigger_put | trigger_rm | trigger_replace) ~ query_script_inner } trigger_put = {"put"} trigger_rm = {"rm"} diff --git a/src/parse/query.rs b/src/parse/query.rs index 8c9e1fea..bd5c5d6f 100644 --- a/src/parse/query.rs +++ b/src/parse/query.rs @@ -774,7 +774,9 @@ fn make_empty_const_rule(prog: &mut InputProgram, bindings: &[Symbol]) { entry_symbol.clone(), InputInlineRulesOrAlgo::Algo { algo: AlgoApply { - algo: AlgoHandle { name: entry_symbol }, + algo: AlgoHandle { + name: Symbol::new("Constant", Default::default()), + }, rule_args: vec![], options, head: bindings.to_vec(), diff --git a/src/parse/sys.rs b/src/parse/sys.rs index 3bf29a7f..a1341821 100644 --- a/src/parse/sys.rs +++ b/src/parse/sys.rs @@ -9,6 +9,7 @@ use crate::data::symb::Symbol; use crate::data::value::DataValue; use crate::parse::query::parse_query; use crate::parse::{ExtractSpan, Pairs, Rule, SourceSpan}; +use crate::runtime::relation::AccessLevel; pub(crate) enum SysOp { Compact, @@ -21,6 +22,7 @@ pub(crate) enum SysOp { RenameRelation(Vec<(Symbol, Symbol)>), ShowTrigger(Symbol), SetTriggers(Symbol, Vec, Vec, Vec), + SetAccessLevel(Symbol, AccessLevel), } #[derive(Debug, Diagnostic, Error)] @@ -76,6 +78,19 @@ pub(crate) fn parse_sys( .collect_vec(); SysOp::RenameRelation(rename_pairs) } + Rule::access_level_op => { + let mut ps = inner.into_inner(); + let access_level = match ps.next().unwrap().as_str() { + "normal" => AccessLevel::Normal, + "protected" => AccessLevel::Protected, + "read_only" => AccessLevel::ReadOnly, + "hidden" => AccessLevel::Hidden, + _ => unreachable!() + }; + let rel_p = ps.next().unwrap(); + let rel = Symbol::new(rel_p.as_str(), rel_p.extract_span()); + SysOp::SetAccessLevel(rel, access_level) + } Rule::trigger_relation_show_op => { let rels_p = inner.into_inner().next().unwrap(); let rel = Symbol::new(rels_p.as_str(), rels_p.extract_span()); diff --git a/src/query/compile.rs b/src/query/compile.rs index 0381193f..83034092 100644 --- a/src/query/compile.rs +++ b/src/query/compile.rs @@ -1,19 +1,21 @@ use std::collections::{BTreeMap, BTreeSet}; use itertools::Itertools; -use miette::{ensure, Context, Diagnostic, Result}; +use miette::{bail, ensure, Context, Diagnostic, Result}; use thiserror::Error; use crate::data::aggr::Aggregation; use crate::data::expr::Expr; use crate::data::program::{ - MagicAlgoApply, MagicAtom, MagicInlineRule, MagicRulesOrAlgo, MagicSymbol, StratifiedMagicProgram, + MagicAlgoApply, MagicAtom, MagicInlineRule, MagicRulesOrAlgo, MagicSymbol, + StratifiedMagicProgram, }; use crate::data::symb::Symbol; use crate::data::value::DataValue; use crate::parse::SourceSpan; use crate::query::relation::RelAlgebra; use crate::runtime::in_mem::InMemRelation; +use crate::runtime::relation::{AccessLevel, InsufficientAccessLevel}; use crate::runtime::transact::SessionTx; pub(crate) type CompiledProgram = BTreeMap; @@ -195,6 +197,13 @@ impl SessionTx { } MagicAtom::Relation(rel_app) => { let store = self.get_relation(&rel_app.name, false)?; + if store.access_level < AccessLevel::ReadOnly { + bail!(InsufficientAccessLevel( + store.name.to_string(), + "reading rows".to_string(), + store.access_level + )); + } ensure!( store.arity() == rel_app.args.len(), ArityMismatch( diff --git a/src/query/relation.rs b/src/query/relation.rs index bf0fec95..4e1782fe 100644 --- a/src/query/relation.rs +++ b/src/query/relation.rs @@ -21,7 +21,7 @@ use crate::utils::swap_option_result; pub(crate) enum RelAlgebra { Fixed(InlineFixedRA), InMem(InMemRelationRA), - Relation(RelationRA), + Stored(StoredRA), Join(Box), NegJoin(Box), Reorder(ReorderRA), @@ -34,7 +34,7 @@ impl RelAlgebra { match self { RelAlgebra::Fixed(i) => i.span, RelAlgebra::InMem(i) => i.span, - RelAlgebra::Relation(i) => i.span, + RelAlgebra::Stored(i) => i.span, RelAlgebra::Join(i) => i.span, RelAlgebra::NegJoin(i) => i.span, RelAlgebra::Reorder(i) => i.relation.span(), @@ -253,7 +253,7 @@ impl Debug for RelAlgebra { .field(&r.storage.rule_name) .field(&r.filters) .finish(), - RelAlgebra::Relation(r) => f + RelAlgebra::Stored(r) => f .debug_tuple("Derived") .field(&bindings) .field(&r.storage.name) @@ -307,7 +307,7 @@ impl RelAlgebra { RelAlgebra::InMem(d) => { d.fill_binding_indices()?; } - RelAlgebra::Relation(v) => { + RelAlgebra::Stored(v) => { v.fill_binding_indices()?; } RelAlgebra::Reorder(r) => { @@ -357,7 +357,7 @@ impl RelAlgebra { storage: RelationHandle, span: SourceSpan, ) -> Self { - Self::Relation(RelationRA { + Self::Stored(StoredRA { bindings, storage, filters: vec![], @@ -412,14 +412,14 @@ impl RelAlgebra { span, }) } - RelAlgebra::Relation(RelationRA { + RelAlgebra::Stored(StoredRA { bindings, storage, mut filters, span, }) => { filters.push(filter); - RelAlgebra::Relation(RelationRA { + RelAlgebra::Stored(StoredRA { bindings, storage, filters, @@ -720,14 +720,14 @@ fn get_eliminate_indices(bindings: &[Symbol], eliminate: &BTreeSet) -> B } #[derive(Debug)] -pub(crate) struct RelationRA { +pub(crate) struct StoredRA { pub(crate) bindings: Vec, pub(crate) storage: RelationHandle, pub(crate) filters: Vec, pub(crate) span: SourceSpan, } -impl RelationRA { +impl StoredRA { fn fill_binding_indices(&mut self) -> Result<()> { let bindings: BTreeMap<_, _> = self .bindings @@ -1253,7 +1253,7 @@ impl RelAlgebra { match self { RelAlgebra::Fixed(r) => r.do_eliminate_temp_vars(used), RelAlgebra::InMem(_r) => Ok(()), - RelAlgebra::Relation(_v) => Ok(()), + RelAlgebra::Stored(_v) => Ok(()), RelAlgebra::Join(r) => r.do_eliminate_temp_vars(used), RelAlgebra::Reorder(r) => r.relation.eliminate_temp_vars(used), RelAlgebra::Filter(r) => r.do_eliminate_temp_vars(used), @@ -1266,7 +1266,7 @@ impl RelAlgebra { match self { RelAlgebra::Fixed(r) => Some(&r.to_eliminate), RelAlgebra::InMem(_) => None, - RelAlgebra::Relation(_) => None, + RelAlgebra::Stored(_) => None, RelAlgebra::Join(r) => Some(&r.to_eliminate), RelAlgebra::Reorder(_) => None, RelAlgebra::Filter(r) => Some(&r.to_eliminate), @@ -1290,7 +1290,7 @@ impl RelAlgebra { match self { RelAlgebra::Fixed(f) => f.bindings.clone(), RelAlgebra::InMem(d) => d.bindings.clone(), - RelAlgebra::Relation(v) => v.bindings.clone(), + RelAlgebra::Stored(v) => v.bindings.clone(), RelAlgebra::Join(j) => j.bindings(), RelAlgebra::Reorder(r) => r.bindings(), RelAlgebra::Filter(r) => r.parent.bindings_after_eliminate(), @@ -1311,7 +1311,7 @@ impl RelAlgebra { match self { RelAlgebra::Fixed(f) => Ok(Box::new(f.data.iter().map(|t| Ok(Tuple(t.clone()))))), RelAlgebra::InMem(r) => r.iter(epoch, use_delta), - RelAlgebra::Relation(v) => v.iter(tx), + RelAlgebra::Stored(v) => v.iter(tx), RelAlgebra::Join(j) => j.iter(tx, epoch, use_delta), RelAlgebra::Reorder(r) => r.iter(tx, epoch, use_delta), RelAlgebra::Filter(r) => r.iter(tx, epoch, use_delta), @@ -1360,7 +1360,7 @@ impl NegJoin { "mem_neg_mat_join" } } - RelAlgebra::Relation(_) => { + RelAlgebra::Stored(_) => { let join_indices = self .joiner .join_indices( @@ -1403,7 +1403,7 @@ impl NegJoin { eliminate_indices, ) } - RelAlgebra::Relation(v) => { + RelAlgebra::Stored(v) => { let join_indices = self .joiner .join_indices( @@ -1481,7 +1481,7 @@ impl InnerJoin { "mem_mat_join" } } - RelAlgebra::Relation(_) => { + RelAlgebra::Stored(_) => { let join_indices = self .joiner .join_indices( @@ -1549,7 +1549,7 @@ impl InnerJoin { self.materialized_join(tx, eliminate_indices, epoch, use_delta) } } - RelAlgebra::Relation(r) => { + RelAlgebra::Stored(r) => { let join_indices = self .joiner .join_indices( diff --git a/src/query/stored.rs b/src/query/stored.rs index 22cd6e76..332e3e8c 100644 --- a/src/query/stored.rs +++ b/src/query/stored.rs @@ -8,13 +8,13 @@ use thiserror::Error; use crate::algo::constant::Constant; use crate::algo::AlgoHandle; use crate::data::expr::Expr; -use crate::data::program::{AlgoApply, InputProgram, InputInlineRulesOrAlgo, RelationOp}; +use crate::data::program::{AlgoApply, InputInlineRulesOrAlgo, InputProgram, RelationOp}; use crate::data::relation::{ColumnDef, NullableColType}; use crate::data::symb::Symbol; use crate::data::tuple::{EncodedTuple, Tuple}; use crate::data::value::DataValue; use crate::parse::parse_script; -use crate::runtime::relation::InputRelationHandle; +use crate::runtime::relation::{AccessLevel, InputRelationHandle, InsufficientAccessLevel}; use crate::runtime::transact::SessionTx; use crate::Db; @@ -36,6 +36,13 @@ impl SessionTx { let mut replaced_old_triggers = None; if op == RelationOp::Replace { if let Ok(old_handle) = self.get_relation(&meta.name, true) { + if old_handle.access_level < AccessLevel::Normal { + bail!(InsufficientAccessLevel( + old_handle.name.to_string(), + "relation replacement".to_string(), + old_handle.access_level + )); + } if old_handle.has_triggers() { replaced_old_triggers = Some((old_handle.put_triggers, old_handle.rm_triggers)) } @@ -76,6 +83,13 @@ impl SessionTx { match op { RelationOp::Rm => { + if relation_store.access_level < AccessLevel::Protected { + bail!(InsufficientAccessLevel( + relation_store.name.to_string(), + "row removal".to_string(), + relation_store.access_level + )); + } let key_extractors = make_extractors( &relation_store.metadata.keys, &metadata.keys, @@ -147,6 +161,14 @@ impl SessionTx { } } RelationOp::Ensure => { + if relation_store.access_level < AccessLevel::ReadOnly { + bail!(InsufficientAccessLevel( + relation_store.name.to_string(), + "row check".to_string(), + relation_store.access_level + )); + } + let mut key_extractors = make_extractors( &relation_store.metadata.keys, &metadata.keys, @@ -198,6 +220,14 @@ impl SessionTx { } } RelationOp::EnsureNot => { + if relation_store.access_level < AccessLevel::ReadOnly { + bail!(InsufficientAccessLevel( + relation_store.name.to_string(), + "row check".to_string(), + relation_store.access_level + )); + } + let key_extractors = make_extractors( &relation_store.metadata.keys, &metadata.keys, @@ -225,6 +255,14 @@ impl SessionTx { } } RelationOp::Create | RelationOp::Replace | RelationOp::Put => { + if relation_store.access_level < AccessLevel::Protected { + bail!(InsufficientAccessLevel( + relation_store.name.to_string(), + "row insertion".to_string(), + relation_store.access_level + )); + } + let mut key_extractors = make_extractors( &relation_store.metadata.keys, &metadata.keys, @@ -398,9 +436,7 @@ fn make_const_rule( rule_symbol.clone(), InputInlineRulesOrAlgo::Algo { algo: AlgoApply { - algo: AlgoHandle { - name: rule_symbol, - }, + algo: AlgoHandle { name: rule_symbol }, rule_args: vec![], options, head: bindings, diff --git a/src/runtime/db.rs b/src/runtime/db.rs index 1749773a..b1786b10 100644 --- a/src/runtime/db.rs +++ b/src/runtime/db.rs @@ -25,7 +25,7 @@ use crate::parse::sys::SysOp; use crate::parse::{parse_script, CozoScript, SourceSpan}; use crate::query::compile::{CompiledProgram, CompiledRule, CompiledRuleSet}; use crate::query::relation::{ - FilteredRA, InMemRelationRA, InnerJoin, NegJoin, RelAlgebra, RelationRA, ReorderRA, + FilteredRA, InMemRelationRA, InnerJoin, NegJoin, RelAlgebra, StoredRA, ReorderRA, UnificationRA, }; use crate::runtime::relation::{RelationHandle, RelationId}; @@ -306,7 +306,7 @@ impl Db { json!(null), json!(filters.iter().map(|f| f.to_string()).collect_vec()), ), - RelAlgebra::Relation(RelationRA { + RelAlgebra::Stored(StoredRA { storage, filters, .. }) => ( "load_stored", @@ -479,6 +479,12 @@ impl Db { tx.commit_tx()?; Ok(json!({"headers": ["status"], "rows": [["OK"]]})) } + SysOp::SetAccessLevel(name, level) => { + let mut tx = self.transact_write()?; + tx.set_access_level(name, level)?; + tx.commit_tx()?; + Ok(json!({"headers": ["status"], "rows": [["OK"]]})) + } } } pub(crate) fn run_query( @@ -720,9 +726,11 @@ impl Db { let n_dependents = meta.metadata.non_keys.len(); let arity = n_keys + n_dependents; let name = meta.name; + let access_level = meta.access_level.to_string(); collected.push(json!([ name, arity, + access_level, n_keys, n_dependents, meta.put_triggers.len(), @@ -732,7 +740,7 @@ impl Db { it.next(); } Ok(json!({"rows": collected, "headers": - ["name", "arity", "n_keys", "n_non_keys", "n_put_triggers", "n_rm_triggers", "n_replace_triggers"]})) + ["name", "arity", "access_level", "n_keys", "n_non_keys", "n_put_triggers", "n_rm_triggers", "n_replace_triggers"]})) } } diff --git a/src/runtime/relation.rs b/src/runtime/relation.rs index c01575c9..cb8d9ede 100644 --- a/src/runtime/relation.rs +++ b/src/runtime/relation.rs @@ -1,6 +1,6 @@ use std::cmp::max; use std::cmp::Ordering::Greater; -use std::fmt::{Debug, Formatter}; +use std::fmt::{Debug, Display, Formatter}; use std::sync::atomic::Ordering; use log::error; @@ -64,15 +64,38 @@ pub(crate) struct RelationHandle { pub(crate) put_triggers: Vec, pub(crate) rm_triggers: Vec, pub(crate) replace_triggers: Vec, - pub(crate) state: RelationState, + pub(crate) access_level: AccessLevel, } -#[derive(Clone, Eq, PartialEq, serde_derive::Serialize, serde_derive::Deserialize)] -pub(crate) enum RelationState { - Normal, - Protected, +#[derive( + Copy, + Clone, + Debug, + Eq, + PartialEq, + serde_derive::Serialize, + serde_derive::Deserialize, + Default, + Ord, + PartialOrd, +)] +pub(crate) enum AccessLevel { + Hidden, ReadOnly, - Hidden + Protected, + #[default] + Normal, +} + +impl Display for AccessLevel { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + AccessLevel::Normal => f.write_str("normal"), + AccessLevel::Protected => f.write_str("protected"), + AccessLevel::ReadOnly => f.write_str("read_only"), + AccessLevel::Hidden => f.write_str("hidden"), + } + } } #[derive(Debug, Error, Diagnostic)] @@ -300,6 +323,13 @@ impl SessionTx { replaces: Vec, ) -> Result<()> { let mut original = self.get_relation(&name, true)?; + if original.access_level < AccessLevel::Protected { + bail!(InsufficientAccessLevel( + original.name.to_string(), + "set triggers".to_string(), + original.access_level + )) + } original.put_triggers = puts; original.rm_triggers = rms; original.replace_triggers = replaces; @@ -335,7 +365,7 @@ impl SessionTx { put_triggers: vec![], rm_triggers: vec![], replace_triggers: vec![], - state: RelationState::Normal + access_level: AccessLevel::Normal, }; self.tx.put(&encoded, &meta.id.raw_encode())?; @@ -370,6 +400,13 @@ impl SessionTx { } pub(crate) fn destroy_relation(&mut self, name: &str) -> Result<(Vec, Vec)> { let store = self.get_relation(name, true)?; + if store.access_level < AccessLevel::Normal { + bail!(InsufficientAccessLevel( + store.name.to_string(), + "relation removal".to_string(), + store.access_level + )) + } let key = DataValue::Str(SmartString::from(name as &str)); let encoded = Tuple(vec![key]).encode_as_key(RelationId::SYSTEM); self.tx.del(&encoded)?; @@ -377,6 +414,20 @@ impl SessionTx { let upper_bound = Tuple::default().encode_as_key(store.id.next()); Ok((lower_bound, upper_bound)) } + pub(crate) fn set_access_level(&mut self, rel: Symbol, level: AccessLevel) -> Result<()> { + let mut meta = self.get_relation(&rel, true)?; + meta.access_level = level; + + let name_key = + Tuple(vec![DataValue::Str(meta.name.clone())]).encode_as_key(RelationId::SYSTEM); + + let mut meta_val = vec![]; + meta.serialize(&mut Serializer::new(&mut meta_val).with_struct_map()) + .unwrap(); + self.tx.put(&name_key, &meta_val)?; + + Ok(()) + } pub(crate) fn rename_relation(&mut self, old: Symbol, new: Symbol) -> Result<()> { let new_key = DataValue::Str(new.name.clone()); let new_encoded = Tuple(vec![new_key]).encode_as_key(RelationId::SYSTEM); @@ -389,6 +440,13 @@ impl SessionTx { let old_encoded = Tuple(vec![old_key]).encode_as_key(RelationId::SYSTEM); let mut rel = self.get_relation(&old, true)?; + if rel.access_level < AccessLevel::Normal { + bail!(InsufficientAccessLevel( + rel.name.to_string(), + "renaming relation".to_string(), + rel.access_level + )); + } rel.name = new.name; let mut meta_val = vec![]; @@ -399,3 +457,11 @@ impl SessionTx { Ok(()) } } + +#[derive(Debug, Error, Diagnostic)] +#[error("Insufficient access level {2} for {1} on stored relation '{0}'")] +pub(crate) struct InsufficientAccessLevel( + pub(crate) String, + pub(crate) String, + pub(crate) AccessLevel, +);