Skip to main content

oxgraph_db/
query.rs

1//! Native `OxQL` query execution.
2
3use serde::{Deserialize, Serialize};
4
5use crate::{
6    DbError, ElementId, IncidenceRecord, ProjectionId, PropertyKeyId, PropertySubject,
7    PropertyValue, RelationId,
8    catalog::{ProjectionDefinition, PropertyFamily},
9    overlay::StateView,
10    projection::GraphProjection,
11    traversal::{self, Direction, Walk},
12    value::parse_value_token,
13};
14
15/// Prepared query plan.
16///
17/// # Performance
18///
19/// Cloning is `O(query length)`.
20#[derive(Clone, Debug, Eq, PartialEq)]
21pub struct PreparedQuery {
22    /// Original query text.
23    text: String,
24    /// Physical plan.
25    plan: QueryPlan,
26}
27
28impl PreparedQuery {
29    /// Parses and prepares one query string against current catalog metadata.
30    ///
31    /// # Errors
32    ///
33    /// Returns [`DbError::EmptyQuery`] or [`DbError::UnsupportedQuery`] when the
34    /// query is outside the supported profile.
35    ///
36    /// # Performance
37    ///
38    /// This function is `O(query length + catalog lookup cost)`.
39    pub(crate) fn prepare(query: &str, state: &impl StateView) -> Result<Self, DbError> {
40        let trimmed = query.trim();
41        if trimmed.is_empty() {
42            return Err(DbError::EmptyQuery);
43        }
44        let logical = parse_oxql(trimmed)?;
45        let plan = bind_and_lower(logical, state)?;
46        Ok(Self {
47            text: trimmed.to_owned(),
48            plan,
49        })
50    }
51
52    /// Returns the source query text.
53    ///
54    /// # Performance
55    ///
56    /// This method is `O(1)`.
57    #[must_use]
58    pub fn text(&self) -> &str {
59        &self.text
60    }
61
62    /// Returns a stable physical explanation.
63    ///
64    /// # Performance
65    ///
66    /// This method is `O(plan size)`.
67    #[must_use]
68    pub fn explain(&self) -> String {
69        self.plan.explain()
70    }
71
72    /// Executes this prepared plan.
73    ///
74    /// # Errors
75    ///
76    /// Returns [`DbError`] when a referenced projection cannot be materialized.
77    ///
78    /// # Performance
79    ///
80    /// This method is `O(scanned rows × predicate size + plan output +
81    /// projection build cost when used)`.
82    pub(crate) fn execute(&self, state: &impl StateView) -> Result<QueryResult, DbError> {
83        self.plan.execute(state)
84    }
85}
86
87/// Query result materialized for JSON, CLI, and embedded consumers.
88///
89/// # Performance
90///
91/// Iterating rows is `O(row count)`.
92#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
93pub struct QueryResult {
94    /// Materialized rows.
95    rows: Vec<QueryRow>,
96}
97
98impl QueryResult {
99    /// Creates a result from rows.
100    ///
101    /// # Performance
102    ///
103    /// This function is `O(1)` excluding row ownership transfer.
104    #[must_use]
105    pub(crate) const fn new(rows: Vec<QueryRow>) -> Self {
106        Self { rows }
107    }
108
109    /// Returns result rows.
110    ///
111    /// # Performance
112    ///
113    /// This method is `O(1)`.
114    #[must_use]
115    pub fn rows(&self) -> &[QueryRow] {
116        &self.rows
117    }
118}
119
120/// One query result row.
121///
122/// # Performance
123///
124/// Cloning is `O(value count + string/property bytes)`.
125#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
126pub struct QueryRow {
127    /// Values carried by the row.
128    pub values: Vec<QueryValue>,
129}
130
131impl QueryRow {
132    /// Creates a single-value row.
133    ///
134    /// # Performance
135    ///
136    /// This function is `O(1)` excluding value ownership transfer.
137    #[must_use]
138    pub(crate) fn single(value: QueryValue) -> Self {
139        Self {
140            values: vec![value],
141        }
142    }
143}
144
145/// Query value variants used by `OxQL`.
146///
147/// # Performance
148///
149/// Cloning is `O(value bytes)`.
150#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
151pub enum QueryValue {
152    /// Canonical element id.
153    Element(ElementId),
154    /// Canonical relation id.
155    Relation(RelationId),
156    /// Canonical incidence record.
157    Incidence(IncidenceRecord),
158    /// Property subject.
159    Subject(PropertySubject),
160    /// Typed property value.
161    Property(PropertyValue),
162    /// Human-readable text value.
163    Text(String),
164    /// Projection id.
165    Projection(ProjectionId),
166}
167
168/// Private physical query plan.
169#[derive(Clone, Debug, Eq, PartialEq)]
170enum QueryPlan {
171    /// Scan all elements.
172    ElementScan,
173    /// Scan all relations.
174    RelationScan,
175    /// Scan all incidences.
176    IncidenceScan,
177    /// Scan elements with a label.
178    ElementLabelScan {
179        /// Label name for explanations.
180        label: String,
181        /// Label ID.
182        label_id: crate::LabelId,
183    },
184    /// Scan elements by property equality.
185    ElementPropertyEqual {
186        /// Property key name for explanations.
187        key: String,
188        /// Property key ID.
189        key_id: PropertyKeyId,
190        /// Required property value.
191        value: PropertyValue,
192    },
193    /// Filter elements by a bound compound property predicate.
194    ElementFilter {
195        /// Bound predicate tree.
196        predicate: BoundPredicate,
197    },
198    /// Scan relations by relation type.
199    RelationTypeScan {
200        /// Relation type name for explanations.
201        relation_type: String,
202        /// Relation type ID.
203        relation_type_id: crate::RelationTypeId,
204    },
205    /// Walk a graph projection.
206    GraphWalk {
207        /// Projection ID.
208        projection: ProjectionId,
209        /// Canonical starting element.
210        element: ElementId,
211        /// Traversal options.
212        options: Walk,
213    },
214    /// Catalog metadata scan.
215    CatalogScan,
216}
217
218/// Bound element predicate produced by lowering a [`LogicalPredicate`].
219#[derive(Clone, Debug, Eq, PartialEq)]
220enum BoundPredicate {
221    /// `key <op> value` against a resolved key and typed value.
222    Compare {
223        /// Property key name for explanations.
224        key: String,
225        /// Resolved property key id.
226        key_id: PropertyKeyId,
227        /// Comparison operator.
228        op: CompareOp,
229        /// Typed comparison value.
230        value: PropertyValue,
231    },
232    /// Conjunction of two predicates.
233    And(Box<Self>, Box<Self>),
234    /// Disjunction of two predicates.
235    Or(Box<Self>, Box<Self>),
236}
237
238impl BoundPredicate {
239    /// Formats this predicate for plan explanations.
240    fn explain(&self) -> String {
241        match self {
242            Self::Compare { key, op, value, .. } => format!("{key} {} {value}", op.spelling()),
243            Self::And(left, right) => format!("({} AND {})", left.explain(), right.explain()),
244            Self::Or(left, right) => format!("({} OR {})", left.explain(), right.explain()),
245        }
246    }
247
248    /// Returns whether `element` satisfies this predicate in `state`.
249    ///
250    /// A missing property never satisfies a comparison.
251    fn evaluate(&self, state: &impl StateView, element: ElementId) -> bool {
252        match self {
253            Self::Compare {
254                key_id, op, value, ..
255            } => state
256                .property(PropertySubject::Element(element), *key_id)
257                .is_some_and(|actual| op.matches(actual.as_ref().cmp(value))),
258            Self::And(left, right) => {
259                left.evaluate(state, element) && right.evaluate(state, element)
260            }
261            Self::Or(left, right) => {
262                left.evaluate(state, element) || right.evaluate(state, element)
263            }
264        }
265    }
266}
267
268impl QueryPlan {
269    /// Explains this physical plan.
270    fn explain(&self) -> String {
271        match self {
272            Self::ElementScan => "oxql scan elements".to_owned(),
273            Self::RelationScan => "oxql scan relations".to_owned(),
274            Self::IncidenceScan => "oxql scan incidences".to_owned(),
275            Self::ElementLabelScan { label, .. } => {
276                format!("oxql label index lookup elements label={label}")
277            }
278            Self::ElementPropertyEqual { key, value, .. } => {
279                format!("oxql property equality lookup elements {key}={value}")
280            }
281            Self::ElementFilter { predicate } => {
282                format!("oxql element filter {}", predicate.explain())
283            }
284            Self::RelationTypeScan { relation_type, .. } => {
285                format!("oxql relation-type index lookup type={relation_type}")
286            }
287            Self::GraphWalk {
288                projection,
289                element,
290                options,
291            } => format!(
292                "oxql graph projection {projection} walk from {element} depth {} direction {:?} limit {}",
293                options.max_depth, options.direction, options.limit,
294            ),
295            Self::CatalogScan => "catalog metadata scan".to_owned(),
296        }
297    }
298
299    /// Executes this physical plan.
300    fn execute(&self, state: &impl StateView) -> Result<QueryResult, DbError> {
301        match self {
302            Self::ElementScan => Ok(scan_elements(state)),
303            Self::RelationScan => Ok(scan_relations(state)),
304            Self::IncidenceScan => Ok(scan_incidences(state)),
305            Self::ElementLabelScan { label_id, .. } => {
306                Ok(scan_elements_with_label(state, *label_id))
307            }
308            Self::ElementPropertyEqual { key_id, value, .. } => {
309                scan_elements_with_property(state, *key_id, value)
310            }
311            Self::ElementFilter { predicate } => Ok(filter_elements(state, predicate)),
312            Self::RelationTypeScan {
313                relation_type_id, ..
314            } => Ok(scan_relations_with_type(state, *relation_type_id)),
315            Self::GraphWalk {
316                projection,
317                element,
318                options,
319            } => execute_graph_walk(state, *projection, *element, *options),
320            Self::CatalogScan => Ok(scan_catalog(state)),
321        }
322    }
323}
324
325/// Resolved logical operation emitted by parsing, before catalog binding.
326///
327/// The parsers ([`parse_oxql`], [`parse_cypher`]) are purely token/string
328/// driven and never touch the catalog; they produce one of these ops carrying
329/// raw names and literals. [`bind_and_lower`] then resolves names to catalog
330/// IDs, validates value families/types, and produces the physical
331/// [`QueryPlan`].
332#[derive(Clone, Debug, Eq, PartialEq)]
333enum LogicalOp {
334    /// Scan all elements.
335    ElementScan,
336    /// Scan all relations.
337    RelationScan,
338    /// Scan all incidences.
339    IncidenceScan,
340    /// Scan catalog metadata.
341    CatalogScan,
342    /// Scan elements carrying a named label.
343    ElementLabelScan {
344        /// Unresolved label name.
345        label: String,
346    },
347    /// Scan relations of a named relation type.
348    RelationTypeScan {
349        /// Unresolved relation type name.
350        relation_type: String,
351    },
352    /// Scan elements whose named property equals a literal token.
353    ElementPropertyEqual {
354        /// Unresolved property key name.
355        key: String,
356        /// Unparsed property value token.
357        value: String,
358    },
359    /// Walk a named graph projection from a raw element-ID token.
360    GraphWalk {
361        /// Unresolved projection name.
362        projection: String,
363        /// Unparsed element-ID token.
364        element: String,
365        /// Parsed traversal options.
366        options: Walk,
367    },
368    /// Filter elements by a compound property predicate.
369    ElementWhere {
370        /// Unbound predicate tree carrying raw key names and value tokens.
371        predicate: LogicalPredicate,
372    },
373}
374
375/// Comparison operator in an element property predicate.
376#[derive(Clone, Copy, Debug, Eq, PartialEq)]
377enum CompareOp {
378    /// Equality (`=`).
379    Eq,
380    /// Strictly less than (`<`).
381    Lt,
382    /// Less than or equal (`<=`).
383    Le,
384    /// Strictly greater than (`>`).
385    Gt,
386    /// Greater than or equal (`>=`).
387    Ge,
388}
389
390impl CompareOp {
391    /// Returns whether `ordering` of the stored value against the literal
392    /// satisfies this operator.
393    const fn matches(self, ordering: core::cmp::Ordering) -> bool {
394        use core::cmp::Ordering::{Equal, Greater, Less};
395        match self {
396            Self::Eq => matches!(ordering, Equal),
397            Self::Lt => matches!(ordering, Less),
398            Self::Le => matches!(ordering, Less | Equal),
399            Self::Gt => matches!(ordering, Greater),
400            Self::Ge => matches!(ordering, Greater | Equal),
401        }
402    }
403
404    /// Returns the source spelling of this operator.
405    const fn spelling(self) -> &'static str {
406        match self {
407            Self::Eq => "=",
408            Self::Lt => "<",
409            Self::Le => "<=",
410            Self::Gt => ">",
411            Self::Ge => ">=",
412        }
413    }
414}
415
416/// Raw element predicate parsed from a `WHERE` clause, before catalog binding.
417///
418/// Precedence is `OR` (loosest) over `AND` over comparisons; parentheses
419/// override it. Leaves carry the raw key name and value token; binding resolves
420/// them to catalog ids and typed values.
421#[derive(Clone, Debug, Eq, PartialEq)]
422enum LogicalPredicate {
423    /// `key <op> value` comparison against a raw value token.
424    Compare {
425        /// Unresolved property key name.
426        key: String,
427        /// Comparison operator.
428        op: CompareOp,
429        /// Unparsed property value token.
430        value: String,
431    },
432    /// Conjunction of two predicates.
433    And(Box<Self>, Box<Self>),
434    /// Disjunction of two predicates.
435    Or(Box<Self>, Box<Self>),
436}
437
438/// Parses native `OxQL` into a logical operation without catalog access.
439fn parse_oxql(query: &str) -> Result<LogicalOp, DbError> {
440    let tokens = query.split_whitespace().collect::<Vec<_>>();
441    let upper = tokens
442        .iter()
443        .map(|token| token.to_ascii_uppercase())
444        .collect::<Vec<_>>();
445    if upper.len() >= 3
446        && upper[0].as_str() == "MATCH"
447        && upper[1].as_str() == "ELEMENTS"
448        && upper[2].as_str() == "WHERE"
449    {
450        return parse_element_where(&tokens[3..], &upper[3..]);
451    }
452    match upper.as_slice() {
453        [command] if command == "CATALOG" => Ok(LogicalOp::CatalogScan),
454        [verb, family] if verb == "MATCH" && family == "ELEMENTS" => Ok(LogicalOp::ElementScan),
455        [verb, family] if verb == "MATCH" && family == "RELATIONS" => Ok(LogicalOp::RelationScan),
456        [verb, family] if verb == "MATCH" && family == "INCIDENCES" => Ok(LogicalOp::IncidenceScan),
457        [verb, family, has, object, _name]
458            if verb == "MATCH" && family == "ELEMENTS" && has == "HAS" && object == "LABEL" =>
459        {
460            Ok(LogicalOp::ElementLabelScan {
461                label: tokens[4].to_owned(),
462            })
463        }
464        [verb, family, r#type, _name]
465            if verb == "MATCH" && family == "RELATIONS" && r#type == "TYPE" =>
466        {
467            Ok(LogicalOp::RelationTypeScan {
468                relation_type: tokens[3].to_owned(),
469            })
470        }
471        [graph, _projection, neighbors, _element]
472            if graph == "GRAPH" && neighbors == "NEIGHBORS" =>
473        {
474            Ok(LogicalOp::GraphWalk {
475                projection: tokens[1].to_owned(),
476                element: tokens[3].to_owned(),
477                options: Walk::default(),
478            })
479        }
480        [
481            graph,
482            _projection,
483            walk,
484            from,
485            _element,
486            depth,
487            _max_depth,
488            ..,
489        ] if graph == "GRAPH" && walk == "WALK" && from == "FROM" && depth == "DEPTH" => {
490            parse_graph_walk(&tokens, &upper)
491        }
492        _tokens => Err(DbError::unsupported("unsupported OxQL profile query")),
493    }
494}
495
496/// Parses the optional `DIRECTION`/`LIMIT` clauses of an `OxQL` graph walk.
497fn parse_graph_walk(tokens: &[&str], upper: &[String]) -> Result<LogicalOp, DbError> {
498    let max_depth = tokens[6]
499        .parse::<usize>()
500        .map_err(|_error| DbError::unsupported("walk depth must be an integer"))?;
501    let mut options = Walk {
502        max_depth,
503        ..Walk::default()
504    };
505    let mut saw_direction = false;
506    let mut saw_limit = false;
507    let mut index = 7;
508    while index < tokens.len() {
509        let Some(value) = tokens.get(index + 1) else {
510            return Err(DbError::unsupported("walk option requires a value"));
511        };
512        match upper[index].as_str() {
513            "DIRECTION" if !saw_direction => {
514                options.direction = parse_walk_direction(&upper[index + 1])?;
515                saw_direction = true;
516            }
517            "DIRECTION" => return Err(DbError::unsupported("walk direction specified twice")),
518            "LIMIT" if !saw_limit => {
519                options.limit = value
520                    .parse::<usize>()
521                    .map_err(|_error| DbError::unsupported("walk limit must be an integer"))?;
522                saw_limit = true;
523            }
524            "LIMIT" => return Err(DbError::unsupported("walk limit specified twice")),
525            _option => return Err(DbError::unsupported("unsupported walk option")),
526        }
527        index += 2;
528    }
529    Ok(LogicalOp::GraphWalk {
530        projection: tokens[1].to_owned(),
531        element: tokens[4].to_owned(),
532        options,
533    })
534}
535
536/// Parses one `OxQL` graph walk direction.
537fn parse_walk_direction(direction: &str) -> Result<Direction, DbError> {
538    match direction {
539        "OUTGOING" => Ok(Direction::Outgoing),
540        "INCOMING" => Ok(Direction::Incoming),
541        "BOTH" => Ok(Direction::Both),
542        _direction => Err(DbError::unsupported(
543            "walk direction must be outgoing, incoming, or both",
544        )),
545    }
546}
547
548/// Parses an `OxQL` element `WHERE` predicate into a logical operation.
549///
550/// Collapses a lone `key = value` to the indexed
551/// [`LogicalOp::ElementPropertyEqual`] fast path; any compound or ordered
552/// predicate becomes [`LogicalOp::ElementWhere`].
553fn parse_element_where(tokens: &[&str], upper: &[String]) -> Result<LogicalOp, DbError> {
554    let mut cursor = 0;
555    let predicate = parse_predicate_or(tokens, upper, &mut cursor)?;
556    if cursor != tokens.len() {
557        return Err(DbError::unsupported(
558            "trailing tokens after WHERE predicate",
559        ));
560    }
561    match predicate {
562        LogicalPredicate::Compare {
563            key,
564            op: CompareOp::Eq,
565            value,
566        } => Ok(LogicalOp::ElementPropertyEqual { key, value }),
567        predicate => Ok(LogicalOp::ElementWhere { predicate }),
568    }
569}
570
571/// Parses a disjunction: `and ( OR and )*`.
572fn parse_predicate_or(
573    tokens: &[&str],
574    upper: &[String],
575    cursor: &mut usize,
576) -> Result<LogicalPredicate, DbError> {
577    let mut left = parse_predicate_and(tokens, upper, cursor)?;
578    while upper.get(*cursor).map(String::as_str) == Some("OR") {
579        *cursor += 1;
580        let right = parse_predicate_and(tokens, upper, cursor)?;
581        left = LogicalPredicate::Or(Box::new(left), Box::new(right));
582    }
583    Ok(left)
584}
585
586/// Parses a conjunction: `factor ( AND factor )*`.
587fn parse_predicate_and(
588    tokens: &[&str],
589    upper: &[String],
590    cursor: &mut usize,
591) -> Result<LogicalPredicate, DbError> {
592    let mut left = parse_predicate_factor(tokens, upper, cursor)?;
593    while upper.get(*cursor).map(String::as_str) == Some("AND") {
594        *cursor += 1;
595        let right = parse_predicate_factor(tokens, upper, cursor)?;
596        left = LogicalPredicate::And(Box::new(left), Box::new(right));
597    }
598    Ok(left)
599}
600
601/// Parses a parenthesized group or a single comparison.
602fn parse_predicate_factor(
603    tokens: &[&str],
604    upper: &[String],
605    cursor: &mut usize,
606) -> Result<LogicalPredicate, DbError> {
607    if tokens.get(*cursor) == Some(&"(") {
608        *cursor += 1;
609        let inner = parse_predicate_or(tokens, upper, cursor)?;
610        if tokens.get(*cursor) != Some(&")") {
611            return Err(DbError::unsupported(
612                "unbalanced parentheses in WHERE predicate",
613            ));
614        }
615        *cursor += 1;
616        return Ok(inner);
617    }
618    parse_comparison(tokens, cursor)
619}
620
621/// Parses one `key <op> value` comparison.
622fn parse_comparison(tokens: &[&str], cursor: &mut usize) -> Result<LogicalPredicate, DbError> {
623    let key = tokens
624        .get(*cursor)
625        .ok_or_else(|| DbError::unsupported("expected property key in WHERE predicate"))?;
626    let operator = tokens
627        .get(*cursor + 1)
628        .ok_or_else(|| DbError::unsupported("expected comparison operator in WHERE predicate"))?;
629    let value = tokens
630        .get(*cursor + 2)
631        .ok_or_else(|| DbError::unsupported("expected value in WHERE predicate"))?;
632    let op = parse_compare_op(operator)?;
633    *cursor += 3;
634    Ok(LogicalPredicate::Compare {
635        key: (*key).to_owned(),
636        op,
637        value: (*value).to_owned(),
638    })
639}
640
641/// Parses one comparison operator token.
642fn parse_compare_op(token: &str) -> Result<CompareOp, DbError> {
643    match token {
644        "=" => Ok(CompareOp::Eq),
645        "<" => Ok(CompareOp::Lt),
646        "<=" => Ok(CompareOp::Le),
647        ">" => Ok(CompareOp::Gt),
648        ">=" => Ok(CompareOp::Ge),
649        _operator => Err(DbError::unsupported(
650            "comparison operator must be =, <, <=, >, or >=",
651        )),
652    }
653}
654
655/// Binds a raw element predicate against the catalog.
656fn bind_predicate(
657    predicate: LogicalPredicate,
658    state: &impl StateView,
659) -> Result<BoundPredicate, DbError> {
660    match predicate {
661        LogicalPredicate::Compare { key, op, value } => {
662            let key_id = state
663                .catalog()
664                .property_key_id(&key)
665                .ok_or_else(|| DbError::unsupported(format!("unknown property key {key}")))?;
666            let value = parse_value_token(&value).map_err(DbError::unsupported)?;
667            state.validate_lookup_value_for_family(key_id, PropertyFamily::Element, &value)?;
668            Ok(BoundPredicate::Compare {
669                key,
670                key_id,
671                op,
672                value,
673            })
674        }
675        LogicalPredicate::And(left, right) => Ok(BoundPredicate::And(
676            Box::new(bind_predicate(*left, state)?),
677            Box::new(bind_predicate(*right, state)?),
678        )),
679        LogicalPredicate::Or(left, right) => Ok(BoundPredicate::Or(
680            Box::new(bind_predicate(*left, state)?),
681            Box::new(bind_predicate(*right, state)?),
682        )),
683    }
684}
685
686/// Binds a logical operation against the catalog and lowers it to a plan.
687///
688/// This is the single seam where names resolve to catalog IDs and where
689/// property literals are parsed and family/type-validated. Parsing above never
690/// touches the catalog; execution below never resolves names.
691fn bind_and_lower(op: LogicalOp, state: &impl StateView) -> Result<QueryPlan, DbError> {
692    match op {
693        LogicalOp::ElementScan => Ok(QueryPlan::ElementScan),
694        LogicalOp::RelationScan => Ok(QueryPlan::RelationScan),
695        LogicalOp::IncidenceScan => Ok(QueryPlan::IncidenceScan),
696        LogicalOp::CatalogScan => Ok(QueryPlan::CatalogScan),
697        LogicalOp::ElementLabelScan { label } => {
698            let label_id = state
699                .catalog()
700                .label_id(&label)
701                .ok_or_else(|| DbError::unsupported(format!("unknown label {label}")))?;
702            Ok(QueryPlan::ElementLabelScan { label, label_id })
703        }
704        LogicalOp::RelationTypeScan { relation_type } => {
705            let relation_type_id = state
706                .catalog()
707                .relation_type_id(&relation_type)
708                .ok_or_else(|| {
709                    DbError::unsupported(format!("unknown relation type {relation_type}"))
710                })?;
711            Ok(QueryPlan::RelationTypeScan {
712                relation_type,
713                relation_type_id,
714            })
715        }
716        LogicalOp::ElementPropertyEqual { key, value } => {
717            let key_id = state
718                .catalog()
719                .property_key_id(&key)
720                .ok_or_else(|| DbError::unsupported(format!("unknown property key {key}")))?;
721            let value = parse_value_token(&value).map_err(DbError::unsupported)?;
722            state.validate_lookup_value_for_family(key_id, PropertyFamily::Element, &value)?;
723            Ok(QueryPlan::ElementPropertyEqual { key, key_id, value })
724        }
725        LogicalOp::GraphWalk {
726            projection,
727            element,
728            options,
729        } => lower_graph_walk(&projection, &element, options, state),
730        LogicalOp::ElementWhere { predicate } => Ok(QueryPlan::ElementFilter {
731            predicate: bind_predicate(predicate, state)?,
732        }),
733    }
734}
735
736/// Resolves and validates an `OxQL` graph traversal into a physical walk.
737fn lower_graph_walk(
738    projection: &str,
739    element: &str,
740    options: Walk,
741    state: &impl StateView,
742) -> Result<QueryPlan, DbError> {
743    let projection = state
744        .catalog()
745        .projection_id(projection)
746        .ok_or_else(|| DbError::unsupported(format!("unknown projection {projection}")))?;
747    let entry = state
748        .catalog()
749        .projection(projection)
750        .ok_or(DbError::UnknownProjection { id: projection })?;
751    if matches!(&entry.definition, ProjectionDefinition::Hypergraph(_)) {
752        return Err(DbError::invalid_projection("projection is not a graph"));
753    }
754    let raw = element
755        .parse::<u64>()
756        .map_err(|_error| DbError::unsupported("element id must be an integer"))?;
757    Ok(QueryPlan::GraphWalk {
758        projection,
759        element: ElementId::new(raw),
760        options,
761    })
762}
763
764/// Scans all elements.
765fn scan_elements(state: &impl StateView) -> QueryResult {
766    QueryResult::new(
767        state
768            .elements()
769            .map(|record| QueryRow::single(QueryValue::Element(record.id)))
770            .collect(),
771    )
772}
773
774/// Scans all relations.
775fn scan_relations(state: &impl StateView) -> QueryResult {
776    QueryResult::new(
777        state
778            .relations()
779            .map(|record| QueryRow::single(QueryValue::Relation(record.id)))
780            .collect(),
781    )
782}
783
784/// Scans all incidences.
785fn scan_incidences(state: &impl StateView) -> QueryResult {
786    QueryResult::new(
787        state
788            .incidences()
789            .map(|record| QueryRow::single(QueryValue::Incidence(*record)))
790            .collect(),
791    )
792}
793
794/// Scans elements with one label.
795fn scan_elements_with_label(state: &impl StateView, label_id: crate::LabelId) -> QueryResult {
796    QueryResult::new(
797        state
798            .elements_with_label(label_id)
799            .into_iter()
800            .map(|id| QueryRow::single(QueryValue::Element(id)))
801            .collect(),
802    )
803}
804
805/// Scans elements with a property value.
806fn scan_elements_with_property(
807    state: &impl StateView,
808    key: PropertyKeyId,
809    value: &PropertyValue,
810) -> Result<QueryResult, DbError> {
811    Ok(QueryResult::new(
812        state
813            .typed_property_equal_for_family(key, PropertyFamily::Element, value)?
814            .into_iter()
815            .filter_map(|subject| match subject {
816                PropertySubject::Element(id) => Some(QueryRow::single(QueryValue::Element(id))),
817                PropertySubject::Relation(_) | PropertySubject::Incidence(_) => None,
818            })
819            .collect(),
820    ))
821}
822
823/// Filters elements by a bound compound predicate.
824///
825/// # Performance
826///
827/// `O(elements × predicate nodes)`: one scan, evaluating the predicate per
828/// element. A lone equality is lowered to the indexed property fast path
829/// instead and never reaches this scan.
830fn filter_elements(state: &impl StateView, predicate: &BoundPredicate) -> QueryResult {
831    QueryResult::new(
832        state
833            .elements()
834            .filter(|record| predicate.evaluate(state, record.id))
835            .map(|record| QueryRow::single(QueryValue::Element(record.id)))
836            .collect(),
837    )
838}
839
840/// Scans relations with one relation type.
841fn scan_relations_with_type(
842    state: &impl StateView,
843    relation_type: crate::RelationTypeId,
844) -> QueryResult {
845    QueryResult::new(
846        state
847            .relations_with_type(relation_type)
848            .into_iter()
849            .map(|id| QueryRow::single(QueryValue::Relation(id)))
850            .collect(),
851    )
852}
853
854/// Scans catalog entries.
855fn scan_catalog(state: &impl StateView) -> QueryResult {
856    let rows = state
857        .catalog()
858        .projections()
859        .map(|entry| QueryRow {
860            values: vec![
861                QueryValue::Projection(entry.id),
862                QueryValue::Text(entry.definition.name().to_owned()),
863            ],
864        })
865        .collect();
866    QueryResult::new(rows)
867}
868
869/// Executes a graph walk.
870fn execute_graph_walk(
871    state: &impl StateView,
872    projection: ProjectionId,
873    element: ElementId,
874    options: Walk,
875) -> Result<QueryResult, DbError> {
876    let graph = graph_projection(state, projection)?;
877    let subgraph = traversal::walk_graph_projection(&graph, &[element], options)?;
878    let rows = subgraph
879        .nodes
880        .iter()
881        .map(|node| QueryRow::single(QueryValue::Element(node.element)))
882        .collect();
883    Ok(QueryResult::new(rows))
884}
885
886/// Materializes a graph projection by ID.
887fn graph_projection(
888    state: &impl StateView,
889    projection: ProjectionId,
890) -> Result<GraphProjection, DbError> {
891    let entry = state
892        .catalog()
893        .projection(projection)
894        .ok_or(DbError::UnknownProjection { id: projection })?;
895    match &entry.definition {
896        ProjectionDefinition::Graph(definition) => {
897            GraphProjection::from_state(state, definition.clone())
898        }
899        ProjectionDefinition::Hypergraph(_definition) => {
900            Err(DbError::invalid_projection("projection is not a graph"))
901        }
902    }
903}