use serde::{Deserialize, Serialize};
use sqry_core::graph::unified::bind::scope::arena::ScopeKind;
use sqry_core::graph::unified::edge::kind::EdgeKind;
use sqry_core::graph::unified::node::kind::NodeKind;
use sqry_core::schema::Visibility;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct QueryPlan {
pub root: PlanNode,
}
impl QueryPlan {
#[must_use]
pub fn new(root: PlanNode) -> Self {
Self { root }
}
#[inline]
#[must_use]
pub fn root(&self) -> &PlanNode {
&self.root
}
#[must_use]
pub fn operator_count(&self) -> usize {
self.root.operator_count()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PlanNode {
NodeScan {
kind: Option<NodeKind>,
visibility: Option<Visibility>,
name_pattern: Option<StringPattern>,
},
EdgeTraversal {
direction: Direction,
edge_kind: Option<EdgeKind>,
max_depth: u32,
},
Filter {
predicate: Predicate,
},
SetOp {
op: SetOperation,
left: Box<PlanNode>,
right: Box<PlanNode>,
},
Chain {
steps: Vec<PlanNode>,
},
}
impl PlanNode {
#[must_use]
pub fn operator_count(&self) -> usize {
match self {
PlanNode::NodeScan { .. }
| PlanNode::EdgeTraversal { .. }
| PlanNode::Filter { .. } => 1,
PlanNode::SetOp { left, right, .. } => {
1 + left.operator_count() + right.operator_count()
}
PlanNode::Chain { steps } => {
1 + steps.iter().map(PlanNode::operator_count).sum::<usize>()
}
}
}
#[must_use]
pub fn is_context_free(&self) -> bool {
matches!(self, PlanNode::NodeScan { .. } | PlanNode::SetOp { .. })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Direction {
Forward,
Reverse,
Both,
}
impl Direction {
#[must_use]
pub const fn invert(self) -> Self {
match self {
Direction::Forward => Direction::Reverse,
Direction::Reverse => Direction::Forward,
Direction::Both => Direction::Both,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SetOperation {
Union,
Intersect,
Difference,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Predicate {
HasCaller,
HasCallee,
IsUnused,
Callers(PredicateValue),
Callees(PredicateValue),
Imports(PredicateValue),
Exports(PredicateValue),
References(PredicateValue),
Implements(PredicateValue),
InFile(PathPattern),
InScope(ScopeKind),
MatchesName(StringPattern),
Returns(String),
And(Vec<Predicate>),
Or(Vec<Predicate>),
Not(Box<Predicate>),
}
impl Predicate {
#[must_use]
pub fn has_subquery(&self) -> bool {
match self {
Predicate::HasCaller
| Predicate::HasCallee
| Predicate::IsUnused
| Predicate::InFile(_)
| Predicate::InScope(_)
| Predicate::MatchesName(_)
| Predicate::Returns(_) => false,
Predicate::Callers(v)
| Predicate::Callees(v)
| Predicate::Imports(v)
| Predicate::Exports(v)
| Predicate::References(v)
| Predicate::Implements(v) => v.is_subquery(),
Predicate::And(list) | Predicate::Or(list) => list.iter().any(Predicate::has_subquery),
Predicate::Not(inner) => inner.has_subquery(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PredicateValue {
Pattern(StringPattern),
Regex(RegexPattern),
Subquery(Box<PlanNode>),
}
impl PredicateValue {
#[must_use]
pub const fn is_subquery(&self) -> bool {
matches!(self, PredicateValue::Subquery(_))
}
#[must_use]
pub fn as_subquery(&self) -> Option<&PlanNode> {
match self {
PredicateValue::Subquery(plan) => Some(plan),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct StringPattern {
pub raw: String,
pub mode: MatchMode,
#[serde(default)]
pub case_insensitive: bool,
}
impl StringPattern {
#[must_use]
pub fn exact(raw: impl Into<String>) -> Self {
Self {
raw: raw.into(),
mode: MatchMode::Exact,
case_insensitive: false,
}
}
#[must_use]
pub fn glob(raw: impl Into<String>) -> Self {
Self {
raw: raw.into(),
mode: MatchMode::Glob,
case_insensitive: false,
}
}
#[must_use]
pub fn prefix(raw: impl Into<String>) -> Self {
Self {
raw: raw.into(),
mode: MatchMode::Prefix,
case_insensitive: false,
}
}
#[must_use]
pub fn suffix(raw: impl Into<String>) -> Self {
Self {
raw: raw.into(),
mode: MatchMode::Suffix,
case_insensitive: false,
}
}
#[must_use]
pub fn contains(raw: impl Into<String>) -> Self {
Self {
raw: raw.into(),
mode: MatchMode::Contains,
case_insensitive: false,
}
}
#[must_use]
pub fn case_insensitive(mut self) -> Self {
self.case_insensitive = true;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MatchMode {
Exact,
Glob,
Prefix,
Suffix,
Contains,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct PathPattern {
pub glob: String,
}
impl PathPattern {
#[must_use]
pub fn new(glob: impl Into<String>) -> Self {
Self { glob: glob.into() }
}
#[inline]
#[must_use]
pub fn as_str(&self) -> &str {
&self.glob
}
}
impl<S: Into<String>> From<S> for PathPattern {
fn from(s: S) -> Self {
Self::new(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct RegexPattern {
pub pattern: String,
#[serde(default)]
pub flags: RegexFlags,
}
impl RegexPattern {
#[must_use]
pub fn new(pattern: impl Into<String>) -> Self {
Self {
pattern: pattern.into(),
flags: RegexFlags::default(),
}
}
#[must_use]
pub fn with_flags(pattern: impl Into<String>, flags: RegexFlags) -> Self {
Self {
pattern: pattern.into(),
flags,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct RegexFlags {
#[serde(default)]
pub case_insensitive: bool,
#[serde(default)]
pub multiline: bool,
#[serde(default)]
pub dot_all: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn query_plan_wraps_root() {
let root = PlanNode::NodeScan {
kind: Some(NodeKind::Function),
visibility: None,
name_pattern: None,
};
let plan = QueryPlan::new(root.clone());
assert_eq!(plan.root(), &root);
}
#[test]
fn operator_count_single_node() {
let scan = PlanNode::NodeScan {
kind: None,
visibility: None,
name_pattern: None,
};
assert_eq!(scan.operator_count(), 1);
}
#[test]
fn operator_count_nested_setop_and_chain() {
let scan = PlanNode::NodeScan {
kind: None,
visibility: None,
name_pattern: None,
};
let traverse = PlanNode::EdgeTraversal {
direction: Direction::Forward,
edge_kind: None,
max_depth: 1,
};
let set = PlanNode::SetOp {
op: SetOperation::Union,
left: Box::new(scan.clone()),
right: Box::new(scan.clone()),
};
let chain = PlanNode::Chain {
steps: vec![set.clone(), traverse],
};
assert_eq!(chain.operator_count(), 5);
assert_eq!(set.operator_count(), 3);
}
#[test]
fn context_free_variants() {
let scan = PlanNode::NodeScan {
kind: None,
visibility: None,
name_pattern: None,
};
let setop = PlanNode::SetOp {
op: SetOperation::Intersect,
left: Box::new(scan.clone()),
right: Box::new(scan.clone()),
};
let traverse = PlanNode::EdgeTraversal {
direction: Direction::Reverse,
edge_kind: None,
max_depth: 1,
};
let filter = PlanNode::Filter {
predicate: Predicate::HasCaller,
};
assert!(scan.is_context_free());
assert!(setop.is_context_free());
assert!(!traverse.is_context_free());
assert!(!filter.is_context_free());
}
#[test]
fn direction_invert_is_involution() {
assert_eq!(Direction::Forward.invert(), Direction::Reverse);
assert_eq!(Direction::Reverse.invert(), Direction::Forward);
assert_eq!(Direction::Both.invert(), Direction::Both);
for d in [Direction::Forward, Direction::Reverse, Direction::Both] {
assert_eq!(d.invert().invert(), d);
}
}
#[test]
fn predicate_has_subquery_detects_nested_calls() {
let leaf = Predicate::HasCaller;
assert!(!leaf.has_subquery());
let attr = Predicate::InFile(PathPattern::new("src/api/**"));
assert!(!attr.has_subquery());
let sub = Predicate::Callers(PredicateValue::Subquery(Box::new(PlanNode::NodeScan {
kind: Some(NodeKind::Method),
visibility: None,
name_pattern: None,
})));
assert!(sub.has_subquery());
let pattern = Predicate::Callers(PredicateValue::Pattern(StringPattern::exact("foo")));
assert!(!pattern.has_subquery());
let nested_in_and = Predicate::And(vec![leaf.clone(), sub.clone()]);
assert!(nested_in_and.has_subquery());
let nested_in_not = Predicate::Not(Box::new(sub));
assert!(nested_in_not.has_subquery());
let and_no_sub = Predicate::And(vec![leaf.clone(), attr.clone()]);
assert!(!and_no_sub.has_subquery());
}
#[test]
fn predicate_value_is_subquery() {
let plan = PlanNode::NodeScan {
kind: None,
visibility: None,
name_pattern: None,
};
let sub = PredicateValue::Subquery(Box::new(plan.clone()));
let pat = PredicateValue::Pattern(StringPattern::exact("foo"));
let re = PredicateValue::Regex(RegexPattern::new("^foo$"));
assert!(sub.is_subquery());
assert!(!pat.is_subquery());
assert!(!re.is_subquery());
assert_eq!(sub.as_subquery(), Some(&plan));
assert_eq!(pat.as_subquery(), None);
assert_eq!(re.as_subquery(), None);
}
#[test]
fn string_pattern_builders_preserve_raw() {
let raw = "parse_*";
assert_eq!(StringPattern::exact(raw).raw, raw);
assert_eq!(StringPattern::glob(raw).mode, MatchMode::Glob);
assert_eq!(StringPattern::prefix(raw).mode, MatchMode::Prefix);
assert_eq!(StringPattern::suffix(raw).mode, MatchMode::Suffix);
assert_eq!(StringPattern::contains(raw).mode, MatchMode::Contains);
}
#[test]
fn string_pattern_case_insensitive_toggle() {
let p = StringPattern::exact("Foo").case_insensitive();
assert!(p.case_insensitive);
}
#[test]
fn path_pattern_from_str_and_as_str() {
let p: PathPattern = "src/**/*.rs".into();
assert_eq!(p.as_str(), "src/**/*.rs");
let p2 = PathPattern::new(String::from("docs/**"));
assert_eq!(p2.as_str(), "docs/**");
}
#[test]
fn regex_pattern_default_flags_are_false() {
let r = RegexPattern::new("^foo$");
assert_eq!(r.pattern, "^foo$");
assert!(!r.flags.case_insensitive);
assert!(!r.flags.multiline);
assert!(!r.flags.dot_all);
}
#[test]
fn regex_pattern_with_flags() {
let flags = RegexFlags {
case_insensitive: true,
multiline: true,
dot_all: false,
};
let r = RegexPattern::with_flags("foo", flags);
assert!(r.flags.case_insensitive);
assert!(r.flags.multiline);
assert!(!r.flags.dot_all);
}
}