use crate::api::{RedDBError, RedDBResult};
use crate::storage::query::ast::QueryExpr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CandidateDisposition {
ReadOnly,
Mutating,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidatedCandidate {
pub rql: String,
pub disposition: CandidateDisposition,
pub statement_type: &'static str,
}
impl ValidatedCandidate {
pub fn is_read_only(&self) -> bool {
matches!(self.disposition, CandidateDisposition::ReadOnly)
}
}
pub fn validate_candidate(rql: &str) -> RedDBResult<ValidatedCandidate> {
let trimmed = rql.trim();
if trimmed.is_empty() {
return Err(RedDBError::Query(
"ASK planner produced an empty RQL candidate".to_string(),
));
}
let parsed = crate::storage::query::parser::parse(trimmed).map_err(|err| {
RedDBError::Query(format!(
"ASK planner produced an invalid RQL candidate: {err}"
))
})?;
let (disposition, statement_type) = classify(&parsed.query);
Ok(ValidatedCandidate {
rql: trimmed.to_string(),
disposition,
statement_type,
})
}
fn classify(query: &QueryExpr) -> (CandidateDisposition, &'static str) {
use CandidateDisposition::{Mutating, ReadOnly};
match query {
QueryExpr::Table(_) => (ReadOnly, "select"),
QueryExpr::Graph(_) => (ReadOnly, "graph"),
QueryExpr::Join(_) => (ReadOnly, "join"),
QueryExpr::Path(_) => (ReadOnly, "path"),
QueryExpr::Vector(_) => (ReadOnly, "vector"),
QueryExpr::Hybrid(_) => (ReadOnly, "hybrid"),
QueryExpr::GraphCommand(_) => (ReadOnly, "graph_command"),
QueryExpr::SearchCommand(_) => (ReadOnly, "search"),
QueryExpr::RankOf(_) => (ReadOnly, "rank_of"),
QueryExpr::ApproxRankOf(_) => (ReadOnly, "approx_rank_of"),
QueryExpr::RankRange(_) => (ReadOnly, "rank_range"),
QueryExpr::Insert(_) => (Mutating, "insert"),
QueryExpr::Update(_) => (Mutating, "update"),
QueryExpr::Delete(_) => (Mutating, "delete"),
QueryExpr::Truncate(_) => (Mutating, "truncate"),
QueryExpr::CreateTable(_) => (Mutating, "create_table"),
QueryExpr::CreateCollection(_) => (Mutating, "create_collection"),
QueryExpr::CreateQueue(_) => (Mutating, "create_queue"),
QueryExpr::CreateIndex(_) => (Mutating, "create_index"),
QueryExpr::AlterTable(_) => (Mutating, "alter_table"),
QueryExpr::EventsBackfill(_) => (Mutating, "events_backfill"),
_ => (Mutating, "mutating"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validate_candidate_classifies_read_only_select() {
let candidate =
validate_candidate("SELECT * FROM travelers WHERE passport = 'FDD-1'").unwrap();
assert!(candidate.is_read_only());
assert_eq!(candidate.statement_type, "select");
}
#[test]
fn validate_candidate_classifies_mutating_delete() {
let candidate =
validate_candidate("DELETE FROM travelers WHERE passport = 'FDD-1'").unwrap();
assert_eq!(candidate.disposition, CandidateDisposition::Mutating);
assert_eq!(candidate.statement_type, "delete");
}
#[test]
fn validate_candidate_rejects_invalid() {
let err = validate_candidate("this is not valid rql").unwrap_err();
assert!(
err.to_string().contains("invalid RQL candidate"),
"got: {err}"
);
}
#[test]
fn validate_candidate_rejects_empty() {
let err = validate_candidate(" ").unwrap_err();
assert!(err.to_string().contains("empty RQL candidate"));
}
}