use crate::ast::{BatchStmt, CalStatement};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum StatementClass {
Read,
Evolve,
Control,
Destructive,
}
pub fn classify(stmt: &CalStatement) -> StatementClass {
match stmt {
CalStatement::Recall(_)
| CalStatement::SetOp(_)
| CalStatement::Exists(_)
| CalStatement::Assemble(_)
| CalStatement::History(_)
| CalStatement::Explain(_)
| CalStatement::Describe(_)
| CalStatement::Coalesce(_) => StatementClass::Read,
CalStatement::RunQuery(_) => StatementClass::Read,
CalStatement::EntityAt(_)
| CalStatement::RunTrace(_)
| CalStatement::RunsTouching(_)
| CalStatement::DerivedFrom(_)
| CalStatement::ShowForks(_)
| CalStatement::Related(_)
| CalStatement::Novelty(_) => StatementClass::Read,
CalStatement::ReportSubject(_) => StatementClass::Read,
CalStatement::Merge(_) => StatementClass::Evolve,
CalStatement::Add(_)
| CalStatement::AddWorkflow(_)
| CalStatement::Supersede(_)
| CalStatement::SupersedeWorkflow(_)
| CalStatement::Accumulate(_)
| CalStatement::Revert(_)
| CalStatement::Remember(_) => StatementClass::Evolve,
CalStatement::DefineTemplate(_)
| CalStatement::DropTemplate(_)
| CalStatement::DefineQuery(_)
| CalStatement::DropQuery(_) => StatementClass::Control,
CalStatement::Grant(_) | CalStatement::Revoke(_) => StatementClass::Control,
CalStatement::ShowGrants(_) => StatementClass::Read,
CalStatement::Approve(_)
| CalStatement::Reject(_)
| CalStatement::ApplyRec(_)
| CalStatement::RollbackRec(_)
| CalStatement::RunLoop(_) => StatementClass::Control,
CalStatement::Forget(_) | CalStatement::Purge(_) => StatementClass::Destructive,
CalStatement::Batch(b) => classify_batch(b),
}
}
fn classify_batch(b: &BatchStmt) -> StatementClass {
let positional = b.statements.iter().map(|e| classify(&e.statement));
let labeled = b
.labeled
.iter()
.flatten()
.map(|(_, e)| classify(&e.statement));
positional
.chain(labeled)
.max()
.unwrap_or(StatementClass::Read)
}
pub fn query_is_read_only(input: &str) -> bool {
match crate::parser::parse(input) {
Ok(q) => classify(&q.statement) == StatementClass::Read,
Err(_) => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn class_of(q: &str) -> StatementClass {
classify(&crate::parser::parse(q).expect(q).statement)
}
#[test]
fn reads_classify_as_read() {
assert_eq!(class_of(r#"RECALL facts WHERE subject = "john""#), StatementClass::Read);
assert_eq!(
class_of("HISTORY OF sha256:684c6c9bda818630a870119d0726e4d242ed537af061658ef6f3acb158a2c67d"),
StatementClass::Read
);
assert_eq!(class_of("DESCRIBE CAPABILITIES"), StatementClass::Read);
}
#[test]
fn evolve_and_control_and_destroy_classify_correctly() {
assert_eq!(
class_of(r#"ADD fact SET subject = "j" SET relation = "likes" SET object = "rust" REASON "t""#),
StatementClass::Evolve
);
assert_eq!(
class_of("FORGET sha256:684c6c9bda818630a870119d0726e4d242ed537af061658ef6f3acb158a2c67d"),
StatementClass::Destructive
);
assert_eq!(
class_of(r#"DEFINE TEMPLATE brief AS "{{content}}""#),
StatementClass::Control
);
assert_eq!(class_of(r#"DROP TEMPLATE "brief""#), StatementClass::Control);
}
#[test]
fn batch_takes_its_most_privileged_entry() {
assert_eq!(
class_of(r#"BATCH { RECALL facts WHERE subject = "a" ; RECALL facts WHERE subject = "b" }"#),
StatementClass::Read
);
assert_eq!(
class_of(
r#"BATCH { RECALL facts WHERE subject = "a" ; ADD fact SET subject = "j" SET relation = "r" SET object = "o" REASON "t" }"#
),
StatementClass::Evolve
);
}
#[test]
fn query_is_read_only_fails_closed() {
assert!(query_is_read_only(r#"RECALL facts WHERE subject = "john""#));
assert!(!query_is_read_only(
r#"ADD fact SET subject = "j" SET relation = "r" SET object = "o" REASON "t""#
));
assert!(!query_is_read_only("DELETE FROM facts"));
assert!(!query_is_read_only(""));
}
#[test]
fn class_ordering_puts_destructive_on_top() {
assert!(StatementClass::Read < StatementClass::Evolve);
assert!(StatementClass::Evolve < StatementClass::Control);
assert!(StatementClass::Control < StatementClass::Destructive);
}
}