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, UnwindClause, 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 #[error("serialization failure: {0}")]
110 SerializationFailure(String),
111}
112
113impl From<crate::cypher::parser::ParseError> for CypherError {
114 fn from(err: crate::cypher::parser::ParseError) -> Self {
115 CypherError::Parse(err.to_string())
116 }
117}
118
119impl From<agtype::AgtypeConversionError> for CypherError {
120 fn from(err: agtype::AgtypeConversionError) -> Self {
121 CypherError::Storage(err.to_string())
122 }
123}
124
125impl From<crate::store::GraphStoreError> for CypherError {
126 fn from(err: crate::store::GraphStoreError) -> Self {
127 match err {
128 crate::store::GraphStoreError::SerializationFailure(message) => {
129 Self::SerializationFailure(message)
130 }
131 other => Self::Storage(other.to_string()),
132 }
133 }
134}
135
136fn boolean_cast_error(value: &Value) -> CypherError {
137 CypherError::TypeError(format!(
138 "cannot cast agtype {} to type boolean",
139 agtype::agtype_type_name(value)
140 ))
141}
142
143fn strict_bool(value: &Value) -> Result<Option<bool>, CypherError> {
146 match value {
147 Value::Bool(b) => Ok(Some(*b)),
148 Value::Null => Ok(None),
149 other => Err(boolean_cast_error(other)),
150 }
151}
152
153const MAX_EXACT_F64_INTEGER: i64 = 9_007_199_254_740_992;
154
155fn usize_to_i64(value: usize, context: &str) -> Result<i64, CypherError> {
156 i64::try_from(value).map_err(|_| {
157 CypherError::TypeError(format!(
158 "{context} {value} exceeds the agtype integer range"
159 ))
160 })
161}
162
163fn nonnegative_i64_to_usize(value: i64, context: &str) -> Result<usize, CypherError> {
164 if value < 0 {
165 return Err(CypherError::TypeError(format!(
166 "{context} must not be negative, got {value}"
167 )));
168 }
169 usize::try_from(value).map_err(|_| {
170 CypherError::TypeError(format!(
171 "{context} {value} exceeds the platform index range"
172 ))
173 })
174}
175
176fn nonnegative_i64_to_u64(value: i64, context: &str) -> Result<u64, CypherError> {
177 u64::try_from(value)
178 .map_err(|_| CypherError::TypeError(format!("{context} must not be negative, got {value}")))
179}
180
181fn exact_i64_to_f64(value: i64, context: &str) -> Result<f64, CypherError> {
182 if (-MAX_EXACT_F64_INTEGER..=MAX_EXACT_F64_INTEGER).contains(&value) {
183 Ok(value as f64)
184 } else {
185 Err(CypherError::TypeError(format!(
186 "{context} {value} cannot be represented exactly as a float"
187 )))
188 }
189}
190
191fn trunc_f64_to_i64(value: f64, context: &str) -> Result<i64, CypherError> {
192 if !value.is_finite() {
193 return Err(CypherError::TypeError(format!(
194 "{context} must be finite, got {value}"
195 )));
196 }
197 let truncated = value.trunc();
198 if !(-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&truncated) {
199 return Err(CypherError::TypeError(format!(
200 "{context} {value} is outside the agtype integer range"
201 )));
202 }
203 Ok(truncated as i64)
204}
205
206pub struct CypherExecutor<'a, G: GraphStore> {
208 pub store: &'a G,
209 pub graph: &'a str,
210 pub params: BTreeMap<String, Value>,
211}
212
213#[derive(Debug, Clone)]
218struct MatchState {
219 row: BindingRow,
220 position: Option<Vertex>,
221 trail: Vec<Value>,
222}
223
224impl<'a, G: GraphStore> CypherExecutor<'a, G> {
225 pub fn new(store: &'a G, graph: &'a str) -> Self {
226 Self {
227 store,
228 graph,
229 params: BTreeMap::new(),
230 }
231 }
232
233 pub fn with_params(mut self, params: BTreeMap<String, Value>) -> Self {
234 self.params = params;
235 self
236 }
237
238 pub fn execute(
239 &self,
240 query: &CypherQuery,
241 ) -> Result<(Vec<String>, Vec<ResultRow>), CypherError> {
242 let mut bindings: Vec<BindingRow> = vec![BTreeMap::new()];
243 let mut columns: Vec<String> = Vec::new();
244 let mut rows: Vec<ResultRow> = Vec::new();
245 for clause in &query.clauses {
246 match clause {
247 CypherClause::Match(m) => {
248 bindings = self.exec_match(m, &bindings)?;
249 }
250 CypherClause::With(w) => {
251 let (cols, projected) = self.exec_return_like(
252 &w.items,
253 w.distinct,
254 w.order_by.as_deref(),
255 w.skip.as_ref(),
256 w.limit.as_ref(),
257 &bindings,
258 )?;
259 let mut next = Vec::with_capacity(projected.len());
260 for row in projected {
261 if let Some(filter) = &w.r#where {
262 if !self.where_passes(filter, &row)? {
263 continue;
264 }
265 }
266 next.push(Self::row_to_bindings(&cols, &row));
267 }
268 bindings = next;
269 }
270 CypherClause::Return(r) => {
271 let (cols, ret_rows) = self.exec_return_like(
272 &r.items,
273 r.distinct,
274 r.order_by.as_deref(),
275 r.skip.as_ref(),
276 r.limit.as_ref(),
277 &bindings,
278 )?;
279 columns = cols;
280 rows = ret_rows;
281 }
282 CypherClause::Unwind(clause) => {
283 bindings = self.exec_unwind(clause, bindings)?;
284 }
285 CypherClause::Create(_)
286 | CypherClause::Merge(_)
287 | CypherClause::Set(_)
288 | CypherClause::Delete(_) => {
289 return Err(CypherError::Unsupported(format!("{clause:?}")));
290 }
291 }
292 }
293 Ok((columns, rows))
294 }
295
296 pub(crate) fn row_to_bindings(cols: &[String], row: &ResultRow) -> BindingRow {
297 let mut out = BindingRow::new();
298 for col in cols {
299 if let Some(v) = row.get(col) {
300 out.insert(col.clone(), Binding::Value(v.clone()));
301 }
302 }
303 out
304 }
305
306 pub(crate) fn exec_unwind(
307 &self,
308 clause: &UnwindClause,
309 bindings: Vec<BindingRow>,
310 ) -> Result<Vec<BindingRow>, CypherError> {
311 let mut next = Vec::new();
312 for row in bindings {
313 let items = match self.eval(&clause.expr, &row)? {
314 Value::List(items) => items,
315 Value::Null => continue,
316 other => vec![other],
317 };
318 for item in items {
319 let mut new_row = row.clone();
320 new_row.insert(clause.variable.clone(), Binding::Value(item));
321 next.push(new_row);
322 }
323 }
324 Ok(next)
325 }
326
327 }
331
332mod expression;
333mod functions;
334mod helpers;
335mod matching;
336mod projection;
337
338use helpers::{
339 aggregate_avg, aggregate_extreme, aggregate_sum, agtype_add, agtype_div, agtype_mod,
340 agtype_pow, domain_float_fn, float_fn, is_aggregate, is_aggregate_name, null_or_bool,
341 numeric_op, pattern_variables, regex_match, return_label, sort_keyed, str_predicate, string_fn,
342 unsupported_argument, validated_path_elements,
343};