use serde::{Deserialize, Serialize};
use crate::{
DbError, ElementId, IncidenceRecord, ProjectionId, PropertyKeyId, PropertySubject,
PropertyValue, RelationId,
catalog::{ProjectionDefinition, PropertyFamily},
overlay::StateView,
projection::GraphProjection,
traversal::{self, Direction, Walk},
value::parse_value_token,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PreparedQuery {
text: String,
plan: QueryPlan,
}
impl PreparedQuery {
pub(crate) fn prepare(query: &str, state: &impl StateView) -> Result<Self, DbError> {
let trimmed = query.trim();
if trimmed.is_empty() {
return Err(DbError::EmptyQuery);
}
let logical = parse_oxql(trimmed)?;
let plan = bind_and_lower(logical, state)?;
Ok(Self {
text: trimmed.to_owned(),
plan,
})
}
#[must_use]
pub fn text(&self) -> &str {
&self.text
}
#[must_use]
pub fn explain(&self) -> String {
self.plan.explain()
}
pub(crate) fn execute(&self, state: &impl StateView) -> Result<QueryResult, DbError> {
self.plan.execute(state)
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct QueryResult {
rows: Vec<QueryRow>,
}
impl QueryResult {
#[must_use]
pub(crate) const fn new(rows: Vec<QueryRow>) -> Self {
Self { rows }
}
#[must_use]
pub fn rows(&self) -> &[QueryRow] {
&self.rows
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct QueryRow {
pub values: Vec<QueryValue>,
}
impl QueryRow {
#[must_use]
pub(crate) fn single(value: QueryValue) -> Self {
Self {
values: vec![value],
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum QueryValue {
Element(ElementId),
Relation(RelationId),
Incidence(IncidenceRecord),
Subject(PropertySubject),
Property(PropertyValue),
Text(String),
Projection(ProjectionId),
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum QueryPlan {
ElementScan,
RelationScan,
IncidenceScan,
ElementLabelScan {
label: String,
label_id: crate::LabelId,
},
ElementPropertyEqual {
key: String,
key_id: PropertyKeyId,
value: PropertyValue,
},
ElementFilter {
predicate: BoundPredicate,
},
RelationTypeScan {
relation_type: String,
relation_type_id: crate::RelationTypeId,
},
GraphWalk {
projection: ProjectionId,
element: ElementId,
options: Walk,
},
CatalogScan,
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum BoundPredicate {
Compare {
key: String,
key_id: PropertyKeyId,
op: CompareOp,
value: PropertyValue,
},
And(Box<Self>, Box<Self>),
Or(Box<Self>, Box<Self>),
}
impl BoundPredicate {
fn explain(&self) -> String {
match self {
Self::Compare { key, op, value, .. } => format!("{key} {} {value}", op.spelling()),
Self::And(left, right) => format!("({} AND {})", left.explain(), right.explain()),
Self::Or(left, right) => format!("({} OR {})", left.explain(), right.explain()),
}
}
fn evaluate(&self, state: &impl StateView, element: ElementId) -> bool {
match self {
Self::Compare {
key_id, op, value, ..
} => state
.property(PropertySubject::Element(element), *key_id)
.is_some_and(|actual| op.matches(actual.as_ref().cmp(value))),
Self::And(left, right) => {
left.evaluate(state, element) && right.evaluate(state, element)
}
Self::Or(left, right) => {
left.evaluate(state, element) || right.evaluate(state, element)
}
}
}
}
impl QueryPlan {
fn explain(&self) -> String {
match self {
Self::ElementScan => "oxql scan elements".to_owned(),
Self::RelationScan => "oxql scan relations".to_owned(),
Self::IncidenceScan => "oxql scan incidences".to_owned(),
Self::ElementLabelScan { label, .. } => {
format!("oxql label index lookup elements label={label}")
}
Self::ElementPropertyEqual { key, value, .. } => {
format!("oxql property equality lookup elements {key}={value}")
}
Self::ElementFilter { predicate } => {
format!("oxql element filter {}", predicate.explain())
}
Self::RelationTypeScan { relation_type, .. } => {
format!("oxql relation-type index lookup type={relation_type}")
}
Self::GraphWalk {
projection,
element,
options,
} => format!(
"oxql graph projection {projection} walk from {element} depth {} direction {:?} limit {}",
options.max_depth, options.direction, options.limit,
),
Self::CatalogScan => "catalog metadata scan".to_owned(),
}
}
fn execute(&self, state: &impl StateView) -> Result<QueryResult, DbError> {
match self {
Self::ElementScan => Ok(scan_elements(state)),
Self::RelationScan => Ok(scan_relations(state)),
Self::IncidenceScan => Ok(scan_incidences(state)),
Self::ElementLabelScan { label_id, .. } => {
Ok(scan_elements_with_label(state, *label_id))
}
Self::ElementPropertyEqual { key_id, value, .. } => {
scan_elements_with_property(state, *key_id, value)
}
Self::ElementFilter { predicate } => Ok(filter_elements(state, predicate)),
Self::RelationTypeScan {
relation_type_id, ..
} => Ok(scan_relations_with_type(state, *relation_type_id)),
Self::GraphWalk {
projection,
element,
options,
} => execute_graph_walk(state, *projection, *element, *options),
Self::CatalogScan => Ok(scan_catalog(state)),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum LogicalOp {
ElementScan,
RelationScan,
IncidenceScan,
CatalogScan,
ElementLabelScan {
label: String,
},
RelationTypeScan {
relation_type: String,
},
ElementPropertyEqual {
key: String,
value: String,
},
GraphWalk {
projection: String,
element: String,
options: Walk,
},
ElementWhere {
predicate: LogicalPredicate,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CompareOp {
Eq,
Lt,
Le,
Gt,
Ge,
}
impl CompareOp {
const fn matches(self, ordering: core::cmp::Ordering) -> bool {
use core::cmp::Ordering::{Equal, Greater, Less};
match self {
Self::Eq => matches!(ordering, Equal),
Self::Lt => matches!(ordering, Less),
Self::Le => matches!(ordering, Less | Equal),
Self::Gt => matches!(ordering, Greater),
Self::Ge => matches!(ordering, Greater | Equal),
}
}
const fn spelling(self) -> &'static str {
match self {
Self::Eq => "=",
Self::Lt => "<",
Self::Le => "<=",
Self::Gt => ">",
Self::Ge => ">=",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum LogicalPredicate {
Compare {
key: String,
op: CompareOp,
value: String,
},
And(Box<Self>, Box<Self>),
Or(Box<Self>, Box<Self>),
}
fn parse_oxql(query: &str) -> Result<LogicalOp, DbError> {
let tokens = query.split_whitespace().collect::<Vec<_>>();
let upper = tokens
.iter()
.map(|token| token.to_ascii_uppercase())
.collect::<Vec<_>>();
if upper.len() >= 3
&& upper[0].as_str() == "MATCH"
&& upper[1].as_str() == "ELEMENTS"
&& upper[2].as_str() == "WHERE"
{
return parse_element_where(&tokens[3..], &upper[3..]);
}
match upper.as_slice() {
[command] if command == "CATALOG" => Ok(LogicalOp::CatalogScan),
[verb, family] if verb == "MATCH" && family == "ELEMENTS" => Ok(LogicalOp::ElementScan),
[verb, family] if verb == "MATCH" && family == "RELATIONS" => Ok(LogicalOp::RelationScan),
[verb, family] if verb == "MATCH" && family == "INCIDENCES" => Ok(LogicalOp::IncidenceScan),
[verb, family, has, object, _name]
if verb == "MATCH" && family == "ELEMENTS" && has == "HAS" && object == "LABEL" =>
{
Ok(LogicalOp::ElementLabelScan {
label: tokens[4].to_owned(),
})
}
[verb, family, r#type, _name]
if verb == "MATCH" && family == "RELATIONS" && r#type == "TYPE" =>
{
Ok(LogicalOp::RelationTypeScan {
relation_type: tokens[3].to_owned(),
})
}
[graph, _projection, neighbors, _element]
if graph == "GRAPH" && neighbors == "NEIGHBORS" =>
{
Ok(LogicalOp::GraphWalk {
projection: tokens[1].to_owned(),
element: tokens[3].to_owned(),
options: Walk::default(),
})
}
[
graph,
_projection,
walk,
from,
_element,
depth,
_max_depth,
..,
] if graph == "GRAPH" && walk == "WALK" && from == "FROM" && depth == "DEPTH" => {
parse_graph_walk(&tokens, &upper)
}
_tokens => Err(DbError::unsupported("unsupported OxQL profile query")),
}
}
fn parse_graph_walk(tokens: &[&str], upper: &[String]) -> Result<LogicalOp, DbError> {
let max_depth = tokens[6]
.parse::<usize>()
.map_err(|_error| DbError::unsupported("walk depth must be an integer"))?;
let mut options = Walk {
max_depth,
..Walk::default()
};
let mut saw_direction = false;
let mut saw_limit = false;
let mut index = 7;
while index < tokens.len() {
let Some(value) = tokens.get(index + 1) else {
return Err(DbError::unsupported("walk option requires a value"));
};
match upper[index].as_str() {
"DIRECTION" if !saw_direction => {
options.direction = parse_walk_direction(&upper[index + 1])?;
saw_direction = true;
}
"DIRECTION" => return Err(DbError::unsupported("walk direction specified twice")),
"LIMIT" if !saw_limit => {
options.limit = value
.parse::<usize>()
.map_err(|_error| DbError::unsupported("walk limit must be an integer"))?;
saw_limit = true;
}
"LIMIT" => return Err(DbError::unsupported("walk limit specified twice")),
_option => return Err(DbError::unsupported("unsupported walk option")),
}
index += 2;
}
Ok(LogicalOp::GraphWalk {
projection: tokens[1].to_owned(),
element: tokens[4].to_owned(),
options,
})
}
fn parse_walk_direction(direction: &str) -> Result<Direction, DbError> {
match direction {
"OUTGOING" => Ok(Direction::Outgoing),
"INCOMING" => Ok(Direction::Incoming),
"BOTH" => Ok(Direction::Both),
_direction => Err(DbError::unsupported(
"walk direction must be outgoing, incoming, or both",
)),
}
}
fn parse_element_where(tokens: &[&str], upper: &[String]) -> Result<LogicalOp, DbError> {
let mut cursor = 0;
let predicate = parse_predicate_or(tokens, upper, &mut cursor)?;
if cursor != tokens.len() {
return Err(DbError::unsupported(
"trailing tokens after WHERE predicate",
));
}
match predicate {
LogicalPredicate::Compare {
key,
op: CompareOp::Eq,
value,
} => Ok(LogicalOp::ElementPropertyEqual { key, value }),
predicate => Ok(LogicalOp::ElementWhere { predicate }),
}
}
fn parse_predicate_or(
tokens: &[&str],
upper: &[String],
cursor: &mut usize,
) -> Result<LogicalPredicate, DbError> {
let mut left = parse_predicate_and(tokens, upper, cursor)?;
while upper.get(*cursor).map(String::as_str) == Some("OR") {
*cursor += 1;
let right = parse_predicate_and(tokens, upper, cursor)?;
left = LogicalPredicate::Or(Box::new(left), Box::new(right));
}
Ok(left)
}
fn parse_predicate_and(
tokens: &[&str],
upper: &[String],
cursor: &mut usize,
) -> Result<LogicalPredicate, DbError> {
let mut left = parse_predicate_factor(tokens, upper, cursor)?;
while upper.get(*cursor).map(String::as_str) == Some("AND") {
*cursor += 1;
let right = parse_predicate_factor(tokens, upper, cursor)?;
left = LogicalPredicate::And(Box::new(left), Box::new(right));
}
Ok(left)
}
fn parse_predicate_factor(
tokens: &[&str],
upper: &[String],
cursor: &mut usize,
) -> Result<LogicalPredicate, DbError> {
if tokens.get(*cursor) == Some(&"(") {
*cursor += 1;
let inner = parse_predicate_or(tokens, upper, cursor)?;
if tokens.get(*cursor) != Some(&")") {
return Err(DbError::unsupported(
"unbalanced parentheses in WHERE predicate",
));
}
*cursor += 1;
return Ok(inner);
}
parse_comparison(tokens, cursor)
}
fn parse_comparison(tokens: &[&str], cursor: &mut usize) -> Result<LogicalPredicate, DbError> {
let key = tokens
.get(*cursor)
.ok_or_else(|| DbError::unsupported("expected property key in WHERE predicate"))?;
let operator = tokens
.get(*cursor + 1)
.ok_or_else(|| DbError::unsupported("expected comparison operator in WHERE predicate"))?;
let value = tokens
.get(*cursor + 2)
.ok_or_else(|| DbError::unsupported("expected value in WHERE predicate"))?;
let op = parse_compare_op(operator)?;
*cursor += 3;
Ok(LogicalPredicate::Compare {
key: (*key).to_owned(),
op,
value: (*value).to_owned(),
})
}
fn parse_compare_op(token: &str) -> Result<CompareOp, DbError> {
match token {
"=" => Ok(CompareOp::Eq),
"<" => Ok(CompareOp::Lt),
"<=" => Ok(CompareOp::Le),
">" => Ok(CompareOp::Gt),
">=" => Ok(CompareOp::Ge),
_operator => Err(DbError::unsupported(
"comparison operator must be =, <, <=, >, or >=",
)),
}
}
fn bind_predicate(
predicate: LogicalPredicate,
state: &impl StateView,
) -> Result<BoundPredicate, DbError> {
match predicate {
LogicalPredicate::Compare { key, op, value } => {
let key_id = state
.catalog()
.property_key_id(&key)
.ok_or_else(|| DbError::unsupported(format!("unknown property key {key}")))?;
let value = parse_value_token(&value).map_err(DbError::unsupported)?;
state.validate_lookup_value_for_family(key_id, PropertyFamily::Element, &value)?;
Ok(BoundPredicate::Compare {
key,
key_id,
op,
value,
})
}
LogicalPredicate::And(left, right) => Ok(BoundPredicate::And(
Box::new(bind_predicate(*left, state)?),
Box::new(bind_predicate(*right, state)?),
)),
LogicalPredicate::Or(left, right) => Ok(BoundPredicate::Or(
Box::new(bind_predicate(*left, state)?),
Box::new(bind_predicate(*right, state)?),
)),
}
}
fn bind_and_lower(op: LogicalOp, state: &impl StateView) -> Result<QueryPlan, DbError> {
match op {
LogicalOp::ElementScan => Ok(QueryPlan::ElementScan),
LogicalOp::RelationScan => Ok(QueryPlan::RelationScan),
LogicalOp::IncidenceScan => Ok(QueryPlan::IncidenceScan),
LogicalOp::CatalogScan => Ok(QueryPlan::CatalogScan),
LogicalOp::ElementLabelScan { label } => {
let label_id = state
.catalog()
.label_id(&label)
.ok_or_else(|| DbError::unsupported(format!("unknown label {label}")))?;
Ok(QueryPlan::ElementLabelScan { label, label_id })
}
LogicalOp::RelationTypeScan { relation_type } => {
let relation_type_id = state
.catalog()
.relation_type_id(&relation_type)
.ok_or_else(|| {
DbError::unsupported(format!("unknown relation type {relation_type}"))
})?;
Ok(QueryPlan::RelationTypeScan {
relation_type,
relation_type_id,
})
}
LogicalOp::ElementPropertyEqual { key, value } => {
let key_id = state
.catalog()
.property_key_id(&key)
.ok_or_else(|| DbError::unsupported(format!("unknown property key {key}")))?;
let value = parse_value_token(&value).map_err(DbError::unsupported)?;
state.validate_lookup_value_for_family(key_id, PropertyFamily::Element, &value)?;
Ok(QueryPlan::ElementPropertyEqual { key, key_id, value })
}
LogicalOp::GraphWalk {
projection,
element,
options,
} => lower_graph_walk(&projection, &element, options, state),
LogicalOp::ElementWhere { predicate } => Ok(QueryPlan::ElementFilter {
predicate: bind_predicate(predicate, state)?,
}),
}
}
fn lower_graph_walk(
projection: &str,
element: &str,
options: Walk,
state: &impl StateView,
) -> Result<QueryPlan, DbError> {
let projection = state
.catalog()
.projection_id(projection)
.ok_or_else(|| DbError::unsupported(format!("unknown projection {projection}")))?;
let entry = state
.catalog()
.projection(projection)
.ok_or(DbError::UnknownProjection { id: projection })?;
if matches!(&entry.definition, ProjectionDefinition::Hypergraph(_)) {
return Err(DbError::invalid_projection("projection is not a graph"));
}
let raw = element
.parse::<u64>()
.map_err(|_error| DbError::unsupported("element id must be an integer"))?;
Ok(QueryPlan::GraphWalk {
projection,
element: ElementId::new(raw),
options,
})
}
fn scan_elements(state: &impl StateView) -> QueryResult {
QueryResult::new(
state
.elements()
.map(|record| QueryRow::single(QueryValue::Element(record.id)))
.collect(),
)
}
fn scan_relations(state: &impl StateView) -> QueryResult {
QueryResult::new(
state
.relations()
.map(|record| QueryRow::single(QueryValue::Relation(record.id)))
.collect(),
)
}
fn scan_incidences(state: &impl StateView) -> QueryResult {
QueryResult::new(
state
.incidences()
.map(|record| QueryRow::single(QueryValue::Incidence(*record)))
.collect(),
)
}
fn scan_elements_with_label(state: &impl StateView, label_id: crate::LabelId) -> QueryResult {
QueryResult::new(
state
.elements_with_label(label_id)
.into_iter()
.map(|id| QueryRow::single(QueryValue::Element(id)))
.collect(),
)
}
fn scan_elements_with_property(
state: &impl StateView,
key: PropertyKeyId,
value: &PropertyValue,
) -> Result<QueryResult, DbError> {
Ok(QueryResult::new(
state
.typed_property_equal_for_family(key, PropertyFamily::Element, value)?
.into_iter()
.filter_map(|subject| match subject {
PropertySubject::Element(id) => Some(QueryRow::single(QueryValue::Element(id))),
PropertySubject::Relation(_) | PropertySubject::Incidence(_) => None,
})
.collect(),
))
}
fn filter_elements(state: &impl StateView, predicate: &BoundPredicate) -> QueryResult {
QueryResult::new(
state
.elements()
.filter(|record| predicate.evaluate(state, record.id))
.map(|record| QueryRow::single(QueryValue::Element(record.id)))
.collect(),
)
}
fn scan_relations_with_type(
state: &impl StateView,
relation_type: crate::RelationTypeId,
) -> QueryResult {
QueryResult::new(
state
.relations_with_type(relation_type)
.into_iter()
.map(|id| QueryRow::single(QueryValue::Relation(id)))
.collect(),
)
}
fn scan_catalog(state: &impl StateView) -> QueryResult {
let rows = state
.catalog()
.projections()
.map(|entry| QueryRow {
values: vec![
QueryValue::Projection(entry.id),
QueryValue::Text(entry.definition.name().to_owned()),
],
})
.collect();
QueryResult::new(rows)
}
fn execute_graph_walk(
state: &impl StateView,
projection: ProjectionId,
element: ElementId,
options: Walk,
) -> Result<QueryResult, DbError> {
let graph = graph_projection(state, projection)?;
let subgraph = traversal::walk_graph_projection(&graph, &[element], options)?;
let rows = subgraph
.nodes
.iter()
.map(|node| QueryRow::single(QueryValue::Element(node.element)))
.collect();
Ok(QueryResult::new(rows))
}
fn graph_projection(
state: &impl StateView,
projection: ProjectionId,
) -> Result<GraphProjection, DbError> {
let entry = state
.catalog()
.projection(projection)
.ok_or(DbError::UnknownProjection { id: projection })?;
match &entry.definition {
ProjectionDefinition::Graph(definition) => {
GraphProjection::from_state(state, definition.clone())
}
ProjectionDefinition::Hypergraph(_definition) => {
Err(DbError::invalid_projection("projection is not a graph"))
}
}
}