Skip to main content

uqa_graph/cypher/
labels.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Default-label requirements and diagnostics for Cypher queries.
8
9/// Check required AGE default label relations using the caller's selected graph handle.
10pub fn validate_default_label_relations(
11    store: &crate::GraphStoreHandle,
12    graph: &str,
13    query: &super::CypherQuery,
14) -> Result<(), super::CypherError> {
15    use super::CypherError;
16    let labels = store
17        .graph_labels(graph)
18        .map_err(|error| CypherError::Storage(error.to_string()))?;
19    let (requires_vertex, requires_edge) = label_requirements(query);
20    for (required, kind) in [
21        (requires_vertex, crate::LabelKind::Vertex),
22        (requires_edge, crate::LabelKind::Edge),
23    ] {
24        if required
25            && !labels
26                .iter()
27                .any(|label| label.id == kind.default_label_id())
28        {
29            return Err(CypherError::MissingLabelRelation(format!(
30                "{graph}.{}",
31                kind.default_label_name()
32            )));
33        }
34    }
35    Ok(())
36}
37
38fn mark_path_requirements(path: &super::PathPattern, required: &mut (bool, bool)) {
39    use super::PathElement;
40
41    for element in &path.elements {
42        match element {
43            PathElement::Node(node) => {
44                required.0 = true;
45                if let Some(properties) = &node.properties {
46                    for expression in properties.values() {
47                        mark_expression_requirements(expression, required);
48                    }
49                }
50            }
51            PathElement::Rel(relation) => {
52                required.1 = true;
53                if let Some(properties) = &relation.properties {
54                    for expression in properties.values() {
55                        mark_expression_requirements(expression, required);
56                    }
57                }
58            }
59        }
60    }
61}
62
63fn mark_expression_requirements(expression: &super::CypherExpr, required: &mut (bool, bool)) {
64    use super::CypherExpr;
65
66    match expression {
67        CypherExpr::FunctionCall(call) => {
68            for argument in &call.args {
69                mark_expression_requirements(argument, required);
70            }
71        }
72        CypherExpr::BinaryOp(binary) => {
73            mark_expression_requirements(&binary.left, required);
74            mark_expression_requirements(&binary.right, required);
75        }
76        CypherExpr::UnaryOp(unary) => {
77            mark_expression_requirements(&unary.operand, required);
78        }
79        CypherExpr::ListIndex(index) => {
80            mark_expression_requirements(&index.expr, required);
81            mark_expression_requirements(&index.index, required);
82        }
83        CypherExpr::ListSlice(slice) => {
84            mark_expression_requirements(&slice.expr, required);
85            if let Some(start) = &slice.start {
86                mark_expression_requirements(start, required);
87            }
88            if let Some(end) = &slice.end {
89                mark_expression_requirements(end, required);
90            }
91        }
92        CypherExpr::ListComprehension(comprehension) => {
93            mark_expression_requirements(&comprehension.list_expr, required);
94            if let Some(filter) = &comprehension.filter {
95                mark_expression_requirements(filter, required);
96            }
97            if let Some(map) = &comprehension.map_expr {
98                mark_expression_requirements(map, required);
99            }
100        }
101        CypherExpr::InList(list) => {
102            mark_expression_requirements(&list.expr, required);
103            mark_expression_requirements(&list.list_expr, required);
104        }
105        CypherExpr::IsNull(null) => mark_expression_requirements(&null.expr, required),
106        CypherExpr::IsNotNull(not_null) => {
107            mark_expression_requirements(&not_null.expr, required);
108        }
109        CypherExpr::CaseExpr(case) => {
110            if let Some(operand) = &case.operand {
111                mark_expression_requirements(operand, required);
112            }
113            for (condition, result) in &case.whens {
114                mark_expression_requirements(condition, required);
115                mark_expression_requirements(result, required);
116            }
117            if let Some(else_expression) = &case.else_expr {
118                mark_expression_requirements(else_expression, required);
119            }
120        }
121        CypherExpr::ListLiteral(list) => {
122            for element in &list.elements {
123                mark_expression_requirements(element, required);
124            }
125        }
126        CypherExpr::MapLiteral(map) => {
127            for (_, value) in &map.pairs {
128                mark_expression_requirements(value, required);
129            }
130        }
131        CypherExpr::ExistsPattern(path) => mark_path_requirements(path, required),
132        CypherExpr::PropertyAccess(_)
133        | CypherExpr::Parameter(_)
134        | CypherExpr::Literal(_)
135        | CypherExpr::Variable(_) => {}
136    }
137}
138
139fn mark_return_requirements(
140    items: &[super::ReturnItem],
141    order_by: Option<&[super::OrderByItem]>,
142    skip: Option<&super::CypherExpr>,
143    limit: Option<&super::CypherExpr>,
144    required: &mut (bool, bool),
145) {
146    for item in items {
147        mark_expression_requirements(&item.expr, required);
148    }
149    for item in order_by.into_iter().flatten() {
150        mark_expression_requirements(&item.expr, required);
151    }
152    if let Some(skip) = skip {
153        mark_expression_requirements(skip, required);
154    }
155    if let Some(limit) = limit {
156        mark_expression_requirements(limit, required);
157    }
158}
159
160fn label_requirements(query: &super::CypherQuery) -> (bool, bool) {
161    use super::CypherClause;
162
163    let mut required = (false, false);
164    for clause in &query.clauses {
165        match clause {
166            CypherClause::Match(clause) => {
167                for path in &clause.patterns {
168                    mark_path_requirements(path, &mut required);
169                }
170                if let Some(filter) = &clause.r#where {
171                    mark_expression_requirements(filter, &mut required);
172                }
173            }
174            CypherClause::Create(clause) => {
175                for path in &clause.patterns {
176                    mark_path_requirements(path, &mut required);
177                }
178            }
179            CypherClause::Merge(clause) => {
180                mark_path_requirements(&clause.pattern, &mut required);
181                for item in clause
182                    .on_create_set
183                    .iter()
184                    .chain(&clause.on_match_set)
185                    .flatten()
186                {
187                    mark_expression_requirements(&item.target, &mut required);
188                    mark_expression_requirements(&item.value, &mut required);
189                }
190            }
191            CypherClause::Set(clause) => {
192                for item in &clause.items {
193                    mark_expression_requirements(&item.target, &mut required);
194                    mark_expression_requirements(&item.value, &mut required);
195                }
196            }
197            CypherClause::Delete(clause) => {
198                for expression in &clause.expressions {
199                    mark_expression_requirements(expression, &mut required);
200                }
201            }
202            CypherClause::Return(clause) => mark_return_requirements(
203                &clause.items,
204                clause.order_by.as_deref(),
205                clause.skip.as_ref(),
206                clause.limit.as_ref(),
207                &mut required,
208            ),
209            CypherClause::With(clause) => {
210                mark_return_requirements(
211                    &clause.items,
212                    clause.order_by.as_deref(),
213                    clause.skip.as_ref(),
214                    clause.limit.as_ref(),
215                    &mut required,
216                );
217                if let Some(filter) = &clause.r#where {
218                    mark_expression_requirements(filter, &mut required);
219                }
220            }
221            CypherClause::Unwind(clause) => {
222                mark_expression_requirements(&clause.expr, &mut required);
223            }
224        }
225    }
226    required
227}
228
229#[cfg(test)]
230mod tests;