molframe_query/execution/
plan.rs1use crate::ast::{Column, Expr};
4use crate::glob::Glob;
5use molframe_core::contract::AnalysisPolicy;
6use molframe_core::diagnostic::{Code, Diagnostic};
7use molframe_core::structure::Structure;
8use molframe_core::symbol::SymbolId;
9use std::collections::BTreeSet;
10
11#[derive(Clone, Debug, PartialEq)]
13pub struct LogicalPlan(pub(crate) Expr);
14
15#[derive(Clone, Debug)]
20pub struct PhysicalQuery {
21 pub(crate) expr: PhysicalExpr,
22 pub(crate) warnings: Vec<Diagnostic>,
23}
24
25#[derive(Clone, Debug)]
26pub(crate) enum PhysicalExpr {
27 All,
28 None,
29 And(Box<Self>, Box<Self>),
30 Or(Box<Self>, Box<Self>),
31 Not(Box<Self>),
32 ResolvedMembership {
33 column: Column,
34 symbols: BTreeSet<SymbolId>,
35 },
36 Logical(Expr),
37}
38
39pub(crate) fn bind_expr(
44 expr: &Expr,
45 structure: &Structure,
46 policy: &AnalysisPolicy,
47 mut warnings: Vec<Diagnostic>,
48) -> PhysicalQuery {
49 let expr = lower(expr, structure, policy, &mut warnings);
50
51 warnings.sort_by_key(Diagnostic::code);
52
53 PhysicalQuery { expr, warnings }
54}
55
56fn lower(
61 expr: &Expr,
62 structure: &Structure,
63 policy: &AnalysisPolicy,
64 warnings: &mut Vec<Diagnostic>,
65) -> PhysicalExpr {
66 match expr {
67 Expr::All => PhysicalExpr::All,
68 Expr::None => PhysicalExpr::None,
69 Expr::And(left, right) => conjunction(
70 lower(left, structure, policy, warnings),
71 lower(right, structure, policy, warnings),
72 ),
73 Expr::Or(left, right) => disjunction(
74 lower(left, structure, policy, warnings),
75 lower(right, structure, policy, warnings),
76 ),
77 Expr::Not(target) => match lower(target, structure, policy, warnings) {
78 PhysicalExpr::All => PhysicalExpr::None,
79 PhysicalExpr::None => PhysicalExpr::All,
80 target => PhysicalExpr::Not(Box::new(target)),
81 },
82 Expr::Membership { column, values }
83 if symbol_column(*column, policy)
84 && !(matches!(column, Column::AlternateLocation)
85 && values
86 .iter()
87 .any(|value| value.eq_ignore_ascii_case("none"))) =>
88 {
89 let column = crate::predicate::resolved_column(*column, policy);
90 let symbols = resolve_symbols(structure, values);
91
92 if symbols.is_empty() {
93 warnings.push(Diagnostic::new(Code::W4003));
94 PhysicalExpr::None
95 } else {
96 PhysicalExpr::ResolvedMembership { column, symbols }
97 }
98 }
99 other => PhysicalExpr::Logical(other.clone()),
100 }
101}
102
103fn conjunction(left: PhysicalExpr, right: PhysicalExpr) -> PhysicalExpr {
108 match (left, right) {
109 (PhysicalExpr::None, _) | (_, PhysicalExpr::None) => PhysicalExpr::None,
110 (PhysicalExpr::All, right) => right,
111 (left, PhysicalExpr::All) => left,
112 (left, right) if cost(&right) < cost(&left) => {
113 PhysicalExpr::And(Box::new(right), Box::new(left))
114 }
115 (left, right) => PhysicalExpr::And(Box::new(left), Box::new(right)),
116 }
117}
118
119fn disjunction(left: PhysicalExpr, right: PhysicalExpr) -> PhysicalExpr {
121 match (left, right) {
122 (PhysicalExpr::All, _) | (_, PhysicalExpr::All) => PhysicalExpr::All,
123 (PhysicalExpr::None, right) => right,
124 (left, PhysicalExpr::None) => left,
125 (left, right) => PhysicalExpr::Or(Box::new(left), Box::new(right)),
126 }
127}
128
129#[inline]
133fn cost(expr: &PhysicalExpr) -> u8 {
134 match expr {
135 PhysicalExpr::None | PhysicalExpr::All => 0,
136 PhysicalExpr::ResolvedMembership { .. } => 1,
137 PhysicalExpr::And(_, _) | PhysicalExpr::Or(_, _) | PhysicalExpr::Not(_) => 2,
138 PhysicalExpr::Logical(Expr::Geometric(_)) => 5,
139 PhysicalExpr::Logical(_) => 3,
140 }
141}
142
143fn symbol_column(column: Column, policy: &AnalysisPolicy) -> bool {
147 if policy.identifiers == molframe_core::contract::Namespace::Explicit
148 && matches!(
149 column,
150 Column::Chain | Column::ResidueName | Column::AtomName
151 )
152 {
153 return false;
154 }
155
156 matches!(
157 crate::predicate::resolved_column(column, policy),
158 Column::LabelChain
159 | Column::AuthChain
160 | Column::LabelResidueName
161 | Column::AuthResidueName
162 | Column::LabelAtomName
163 | Column::AuthAtomName
164 | Column::AlternateLocation
165 | Column::Entity
166 | Column::Element
167 | Column::SegmentId
168 | Column::InsertionCode
169 )
170}
171
172fn resolve_symbols(structure: &Structure, values: &[Box<str>]) -> BTreeSet<SymbolId> {
178 let dictionary = &structure.data().dictionary;
179 let mut symbols = BTreeSet::new();
180 let mut patterns = Vec::new();
181
182 for value in values {
183 if is_plain_literal(value) {
184 if let Some(symbol) = dictionary.get(value) {
185 symbols.insert(symbol);
186 }
187 } else {
188 patterns.push(Glob::new(value));
189 }
190 }
191
192 if patterns.is_empty() {
193 return symbols;
194 }
195
196 for (symbol, text) in dictionary.iter() {
197 if patterns.iter().any(|pattern| pattern.matches(text)) {
198 symbols.insert(symbol);
199 }
200 }
201
202 symbols
203}
204
205#[inline]
210fn is_plain_literal(pattern: &str) -> bool {
211 !pattern
212 .bytes()
213 .any(|byte| matches!(byte, b'*' | b'?' | b'[' | b'\\'))
214}
215
216#[cfg(test)]
217#[path = "plan_tests.rs"]
218mod tests;