1use std::collections::{BTreeMap, BTreeSet};
24
25use uqa_core::{Edge, EdgeId, Value, Vertex, VertexId};
26
27use crate::agtype;
28use crate::cypher::ast::{
29 BinaryOp, CaseExpr, CypherClause, CypherExpr, CypherQuery, FunctionCall, InList, IsNotNull,
30 IsNull, ListComprehension, ListIndex, ListLiteral, ListSlice, Literal, MapLiteral, MatchClause,
31 NodePattern, OrderByItem, Parameter, PathElement, PathPattern, PropertyAccess, RelDirection,
32 RelPattern, ReturnItem, UnaryOp, Variable,
33};
34use crate::store::GraphStore;
35use crate::types::Direction;
36
37#[derive(Debug, Clone)]
40pub enum Binding {
41 Vertex(Vertex),
42 Edge(Edge),
43 Value(Value),
44 EdgeList(Vec<Edge>),
46}
47
48impl Binding {
49 fn property(&self, key: &str) -> Value {
50 match self {
51 Binding::Vertex(v) => v.properties.get(key).cloned().unwrap_or(Value::Null),
52 Binding::Edge(e) => e.properties.get(key).cloned().unwrap_or(Value::Null),
53 Binding::Value(v) => value_property(v, key).unwrap_or(Value::Null),
54 Binding::EdgeList(_) => Value::Null,
55 }
56 }
57
58 fn to_value(&self) -> Result<Value, CypherError> {
59 match self {
60 Binding::Vertex(v) => agtype::vertex_to_value(v).map_err(Into::into),
61 Binding::Edge(e) => agtype::edge_to_value(e).map_err(Into::into),
62 Binding::Value(v) => Ok(v.clone()),
63 Binding::EdgeList(edges) => Ok(Value::List(
64 edges
65 .iter()
66 .map(agtype::edge_to_value)
67 .collect::<Result<_, _>>()?,
68 )),
69 }
70 }
71}
72
73fn value_property(value: &Value, key: &str) -> Option<Value> {
77 if let Some(props) = agtype::entity_properties(value) {
78 return Some(props.get(key).cloned().unwrap_or(Value::Null));
79 }
80 match value {
81 Value::Map(map) => Some(map.get(key).cloned().unwrap_or(Value::Null)),
82 Value::Null => Some(Value::Null),
83 _ => None,
84 }
85}
86
87pub type BindingRow = BTreeMap<String, Binding>;
89
90pub type ResultRow = BTreeMap<String, Value>;
92
93#[derive(Debug, thiserror::Error, PartialEq)]
94pub enum CypherError {
95 #[error("undefined variable {0:?}")]
96 UndefinedVariable(String),
97 #[error("undefined parameter {0:?}")]
98 UndefinedParameter(String),
99 #[error("unsupported clause: {0}")]
100 Unsupported(String),
101 #[error("{0}")]
102 TypeError(String),
103 #[error("parse error: {0}")]
104 Parse(String),
105 #[error("relation \"{0}\" does not exist")]
106 MissingLabelRelation(String),
107 #[error("storage error: {0}")]
108 Storage(String),
109}
110
111impl From<crate::cypher::parser::ParseError> for CypherError {
112 fn from(err: crate::cypher::parser::ParseError) -> Self {
113 CypherError::Parse(err.to_string())
114 }
115}
116
117impl From<agtype::AgtypeConversionError> for CypherError {
118 fn from(err: agtype::AgtypeConversionError) -> Self {
119 CypherError::Storage(err.to_string())
120 }
121}
122
123impl From<crate::store::GraphStoreError> for CypherError {
124 fn from(err: crate::store::GraphStoreError) -> Self {
125 CypherError::Storage(err.to_string())
126 }
127}
128
129fn boolean_cast_error(value: &Value) -> CypherError {
130 CypherError::TypeError(format!(
131 "cannot cast agtype {} to type boolean",
132 agtype::agtype_type_name(value)
133 ))
134}
135
136fn strict_bool(value: &Value) -> Result<Option<bool>, CypherError> {
139 match value {
140 Value::Bool(b) => Ok(Some(*b)),
141 Value::Null => Ok(None),
142 other => Err(boolean_cast_error(other)),
143 }
144}
145
146const MAX_EXACT_F64_INTEGER: i64 = 9_007_199_254_740_992;
147
148fn usize_to_i64(value: usize, context: &str) -> Result<i64, CypherError> {
149 i64::try_from(value).map_err(|_| {
150 CypherError::TypeError(format!(
151 "{context} {value} exceeds the agtype integer range"
152 ))
153 })
154}
155
156fn nonnegative_i64_to_usize(value: i64, context: &str) -> Result<usize, CypherError> {
157 if value < 0 {
158 return Err(CypherError::TypeError(format!(
159 "{context} must not be negative, got {value}"
160 )));
161 }
162 usize::try_from(value).map_err(|_| {
163 CypherError::TypeError(format!(
164 "{context} {value} exceeds the platform index range"
165 ))
166 })
167}
168
169fn nonnegative_i64_to_u64(value: i64, context: &str) -> Result<u64, CypherError> {
170 u64::try_from(value)
171 .map_err(|_| CypherError::TypeError(format!("{context} must not be negative, got {value}")))
172}
173
174fn exact_i64_to_f64(value: i64, context: &str) -> Result<f64, CypherError> {
175 if (-MAX_EXACT_F64_INTEGER..=MAX_EXACT_F64_INTEGER).contains(&value) {
176 Ok(value as f64)
177 } else {
178 Err(CypherError::TypeError(format!(
179 "{context} {value} cannot be represented exactly as a float"
180 )))
181 }
182}
183
184fn trunc_f64_to_i64(value: f64, context: &str) -> Result<i64, CypherError> {
185 if !value.is_finite() {
186 return Err(CypherError::TypeError(format!(
187 "{context} must be finite, got {value}"
188 )));
189 }
190 let truncated = value.trunc();
191 if !(-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&truncated) {
192 return Err(CypherError::TypeError(format!(
193 "{context} {value} is outside the agtype integer range"
194 )));
195 }
196 Ok(truncated as i64)
197}
198
199pub struct CypherExecutor<'a, G: GraphStore> {
201 pub store: &'a G,
202 pub graph: &'a str,
203 pub params: BTreeMap<String, Value>,
204}
205
206#[derive(Debug, Clone)]
211struct MatchState {
212 row: BindingRow,
213 position: Option<Vertex>,
214 trail: Vec<Value>,
215}
216
217impl<'a, G: GraphStore> CypherExecutor<'a, G> {
218 pub fn new(store: &'a G, graph: &'a str) -> Self {
219 Self {
220 store,
221 graph,
222 params: BTreeMap::new(),
223 }
224 }
225
226 pub fn with_params(mut self, params: BTreeMap<String, Value>) -> Self {
227 self.params = params;
228 self
229 }
230
231 pub fn execute(
232 &self,
233 query: &CypherQuery,
234 ) -> Result<(Vec<String>, Vec<ResultRow>), CypherError> {
235 let mut bindings: Vec<BindingRow> = vec![BTreeMap::new()];
236 let mut columns: Vec<String> = Vec::new();
237 let mut rows: Vec<ResultRow> = Vec::new();
238 for clause in &query.clauses {
239 match clause {
240 CypherClause::Match(m) => {
241 bindings = self.exec_match(m, &bindings)?;
242 }
243 CypherClause::With(w) => {
244 let (cols, projected) = self.exec_return_like(
245 &w.items,
246 w.distinct,
247 w.order_by.as_deref(),
248 w.skip.as_ref(),
249 w.limit.as_ref(),
250 &bindings,
251 )?;
252 let mut next = Vec::with_capacity(projected.len());
253 for row in projected {
254 if let Some(filter) = &w.r#where {
255 if !self.where_passes(filter, &row)? {
256 continue;
257 }
258 }
259 next.push(Self::row_to_bindings(&cols, &row));
260 }
261 bindings = next;
262 }
263 CypherClause::Return(r) => {
264 let (cols, ret_rows) = self.exec_return_like(
265 &r.items,
266 r.distinct,
267 r.order_by.as_deref(),
268 r.skip.as_ref(),
269 r.limit.as_ref(),
270 &bindings,
271 )?;
272 columns = cols;
273 rows = ret_rows;
274 }
275 CypherClause::Create(_)
276 | CypherClause::Merge(_)
277 | CypherClause::Set(_)
278 | CypherClause::Delete(_)
279 | CypherClause::Unwind(_) => {
280 return Err(CypherError::Unsupported(format!("{clause:?}")));
281 }
282 }
283 }
284 Ok((columns, rows))
285 }
286
287 pub(crate) fn row_to_bindings(cols: &[String], row: &ResultRow) -> BindingRow {
288 let mut out = BindingRow::new();
289 for col in cols {
290 if let Some(v) = row.get(col) {
291 out.insert(col.clone(), Binding::Value(v.clone()));
292 }
293 }
294 out
295 }
296
297 }
301
302mod expression;
303mod functions;
304mod helpers;
305mod matching;
306mod projection;
307
308use helpers::{
309 aggregate_avg, aggregate_extreme, aggregate_sum, agtype_add, agtype_div, agtype_mod,
310 agtype_pow, domain_float_fn, float_fn, is_aggregate, is_aggregate_name, null_or_bool,
311 numeric_op, pattern_variables, regex_match, return_label, sort_keyed, str_predicate, string_fn,
312 unsupported_argument, validated_path_elements,
313};