mod binding;
mod expr;
mod mutation;
mod naming;
mod pattern;
#[cfg(test)]
mod test;
use reblessive::{Stack, Stk};
use surrealdb_types::ToSql;
use self::binding::Registry;
use self::expr::Scope;
use crate::expr::match_plan::{MatchColumn, MatchOrder, MatchOutput, MatchPlan, MatchStage};
use crate::expr::plan::{LogicalPlan, TopLevelExpr};
use crate::expr::{Expr, Idiom, Literal, Param};
use crate::gql::ast::{
GqlExpr, GqlGroupItem, GqlLiteral, GqlQuery, GqlStep, LinearQuery, MatchItem, OrderItem,
ReturnClause, ReturnItems, SetQuantifier,
};
use crate::syn::error::{SyntaxError, bail, syntax_error};
use crate::syn::token::Span;
pub(super) fn lower(query: GqlQuery) -> Result<LogicalPlan, SyntaxError> {
let LinearQuery {
steps,
ret,
span,
} = query.program;
let mut stack = Stack::new();
let plan = stack.enter(|stk| lower_program(stk, &steps, ret.as_ref(), span)).finish()?;
Ok(LogicalPlan {
expressions: vec![TopLevelExpr::Expr(Expr::Match(Box::new(plan)))],
})
}
async fn lower_program(
stk: &mut Stk,
steps: &[GqlStep],
ret: Option<&ReturnClause>,
span: Span,
) -> Result<MatchPlan, SyntaxError> {
reject_leading_optional(steps)?;
let mut analyzer = binding::Analyzer::new();
let mut stages: Vec<MatchStage> = Vec::new();
for step in steps {
match step {
GqlStep::Read(item) => {
for clause_bindings in analyzer.read(item)? {
let clause =
pattern::lower_clause(stk, &clause_bindings, analyzer.registry()).await?;
stages.push(MatchStage::Read(clause));
}
}
GqlStep::Mutate(stmt) => {
for mutation in
mutation::lower_statement(stk, analyzer.registry_mut(), stmt).await?
{
stages.push(MatchStage::Mutate(mutation));
}
}
}
}
let registry = analyzer.into_registry();
if stages.is_empty() {
bail!(
"A query without a MATCH clause is not supported yet",
@span => "start the query with a MATCH clause"
);
}
let output = match ret {
Some(ret) => Some(lower_output(stk, ret, ®istry).await?),
None => {
if !stages.iter().any(|s| matches!(s, MatchStage::Mutate(_))) {
bail!(
"A GQL query must end with a RETURN clause",
@span => "add a `RETURN …` clause"
);
}
None
}
};
Ok(MatchPlan {
bindings: registry.into_defs(),
stages,
output,
})
}
fn reject_leading_optional(steps: &[GqlStep]) -> Result<(), SyntaxError> {
if let Some(GqlStep::Read(MatchItem::Optional(block))) = steps.first() {
bail!(
"A query cannot start with OPTIONAL MATCH: OPTIONAL is a left-outer join and needs a \
preceding MATCH to join against",
@block.span => "begin with a plain `MATCH …` clause before any `OPTIONAL`"
);
}
Ok(())
}
struct Column {
name: String,
expr: Expr,
hidden: bool,
}
async fn lower_output(
stk: &mut Stk,
ret: &ReturnClause,
registry: &Registry,
) -> Result<MatchOutput, SyntaxError> {
let group_keys = lower_group_keys(stk, &ret.group_by, registry).await?;
let (mut columns, aggregating) = lower_return_items(stk, ret, registry, &group_keys).await?;
let distinct = matches!(ret.quantifier, Some(SetQuantifier::Distinct));
let mut order = Vec::with_capacity(ret.order_by.len());
for item in &ret.order_by {
order.push(
lower_order_item(stk, item, &mut columns, &group_keys, distinct, aggregating, registry)
.await?,
);
}
let skip = match &ret.skip {
Some(skip) => Some(lower_count(skip)?),
None => None,
};
let limit = match &ret.limit {
Some(limit) => Some(lower_count(limit)?),
None => None,
};
Ok(MatchOutput {
columns: columns
.into_iter()
.map(|c| MatchColumn {
name: c.name,
expr: c.expr,
hidden: c.hidden,
})
.collect(),
distinct,
group_by: aggregating.then_some(group_keys),
order,
skip,
limit,
})
}
async fn lower_group_keys(
stk: &mut Stk,
items: &[GqlGroupItem],
registry: &Registry,
) -> Result<Vec<Expr>, SyntaxError> {
let scope = Scope {
registry,
allow_aggregates: false,
};
let mut keys = Vec::with_capacity(items.len());
for item in items {
keys.push(expr::lower_value(stk, &item.expr, &scope).await?.into());
}
Ok(keys)
}
async fn lower_return_items(
stk: &mut Stk,
ret: &ReturnClause,
registry: &Registry,
group_keys: &[Expr],
) -> Result<(Vec<Column>, bool), SyntaxError> {
let scope = Scope {
registry,
allow_aggregates: true,
};
let mut columns: Vec<Column> = Vec::new();
match &ret.items {
ReturnItems::Star => {
if !group_keys.is_empty() {
bail!(
"RETURN * cannot be combined with GROUP BY",
@ret.span => "list the grouping keys and aggregates explicitly"
);
}
let mut names: Vec<&str> = registry
.bindings()
.iter()
.filter(|b| b.user_named)
.map(|b| b.name.as_str())
.collect();
names.sort_unstable();
if names.is_empty() {
bail!(
"RETURN * requires at least one named pattern variable",
@ret.span => "name a pattern element or list the return items explicitly"
);
}
for name in names {
columns.push(Column {
name: name.to_owned(),
expr: Expr::Idiom(Idiom::field(name)),
hidden: false,
});
}
Ok((columns, false))
}
ReturnItems::Items(items) => {
let aggregating = !group_keys.is_empty()
|| items.iter().any(|i| expr::gql_contains_aggregate(&i.expr));
for item in items {
let (name, name_span) = naming::column_name(item)?;
if columns.iter().any(|c| c.name == name) {
bail!(
"Duplicate column name `{name}`",
@name_span => "use `AS` to give the items distinct column names"
);
}
let is_aggregate = expr::gql_contains_aggregate(&item.expr);
let lowered: Expr = expr::lower_value(stk, &item.expr, &scope).await?.into();
if aggregating && !is_aggregate && !column_is_grouped(&lowered, group_keys) {
bail!(
"RETURN item `{name}` must be a GROUP BY key, an aggregate, or determined by \
the GROUP BY keys",
@item.expr.span() => "add it to GROUP BY or wrap it in an aggregate"
);
}
columns.push(Column {
name,
expr: lowered,
hidden: false,
});
}
Ok((columns, aggregating))
}
}
}
async fn lower_order_item(
stk: &mut Stk,
item: &OrderItem,
columns: &mut Vec<Column>,
group_keys: &[Expr],
distinct: bool,
aggregating: bool,
registry: &Registry,
) -> Result<MatchOrder, SyntaxError> {
if item.nulls_first.is_some() {
bail!("`NULLS FIRST`/`NULLS LAST` ordering is not supported yet", @item.span);
}
let ascending = item.ascending.unwrap_or(true);
let scope = Scope {
registry,
allow_aggregates: aggregating,
};
if distinct {
let column = order_output_column(stk, item, columns, &scope).await?;
return Ok(MatchOrder {
expr: Expr::Idiom(Idiom::field(column)),
ascending,
});
}
if aggregating {
let column = lower_aggregating_order(stk, item, columns, group_keys, &scope).await?;
return Ok(MatchOrder {
expr: Expr::Idiom(Idiom::field(column)),
ascending,
});
}
if let Some(name) = order_key_name(&item.expr)
&& let Some(column) = columns.iter().find(|c| c.name == name)
{
return Ok(MatchOrder {
expr: column.expr.clone(),
ascending,
});
}
let lowered: Expr = expr::lower_value(stk, &item.expr, &scope).await?.into();
Ok(MatchOrder {
expr: lowered,
ascending,
})
}
async fn lower_aggregating_order(
stk: &mut Stk,
item: &OrderItem,
columns: &mut Vec<Column>,
group_keys: &[Expr],
scope: &Scope<'_>,
) -> Result<String, SyntaxError> {
if let Some(name) = order_key_name(&item.expr)
&& let Some(column) = columns.iter().find(|c| !c.hidden && c.name == name)
{
return Ok(column.name.clone());
}
let is_aggregate = expr::gql_contains_aggregate(&item.expr);
let lowered: Expr = expr::lower_value(stk, &item.expr, scope).await?.into();
if let Some(column) = columns.iter().find(|c| c.expr == lowered) {
return Ok(column.name.clone());
}
if !is_aggregate && !column_is_grouped(&lowered, group_keys) {
bail!(
"ORDER BY key must be a returned column, a GROUP BY key, an aggregate, or determined by \
the GROUP BY keys",
@item.span => "order by a returned column, a grouping key, or an aggregate"
);
}
let name = format!("__order{}", columns.iter().filter(|c| c.hidden).count());
columns.push(Column {
name: name.clone(),
expr: lowered,
hidden: true,
});
Ok(name)
}
async fn order_output_column(
stk: &mut Stk,
item: &OrderItem,
columns: &[Column],
scope: &Scope<'_>,
) -> Result<String, SyntaxError> {
if let Some(name) = order_key_name(&item.expr)
&& let Some(column) = columns.iter().find(|c| c.name == name)
{
return Ok(column.name.clone());
}
let lowered: Expr = expr::lower_value(stk, &item.expr, scope).await?.into();
if let Some(column) = columns.iter().find(|c| c.expr == lowered) {
return Ok(column.name.clone());
}
bail!(
"With RETURN DISTINCT, ORDER BY may only reference returned columns",
@item.span => "return the sort expression under an alias and order by the alias"
);
}
fn column_is_grouped(expr: &Expr, keys: &[Expr]) -> bool {
let mut stack = vec![expr];
while let Some(e) = stack.pop() {
if keys.contains(e) {
continue;
}
match e {
Expr::Literal(Literal::Array(items)) => stack.extend(items.iter()),
Expr::Literal(Literal::Object(entries)) => {
stack.extend(entries.iter().map(|entry| &entry.value));
}
Expr::Literal(_) | Expr::Param(_) | Expr::Constant(_) => {}
Expr::Idiom(idiom) => {
let covered = keys.iter().any(
|key| matches!(key, Expr::Idiom(key) if !key.0.is_empty() && idiom.0.starts_with(&key.0)),
);
if !covered {
return false;
}
}
Expr::Binary {
left,
right,
..
} => {
stack.push(left);
stack.push(right);
}
Expr::Prefix {
expr,
..
}
| Expr::Postfix {
expr,
..
} => stack.push(expr),
_ => return false,
}
}
true
}
fn order_key_name(expr: &GqlExpr) -> Option<String> {
let mut names: Vec<&str> = Vec::new();
let mut base = expr;
while let GqlExpr::Property(inner, name, _) = base {
names.push(&name.name);
base = inner;
}
let GqlExpr::Variable(var) = base else {
return None;
};
names.push(&var.name);
names.reverse();
Some(names.join("."))
}
fn lower_count(expr: &GqlExpr) -> Result<Expr, SyntaxError> {
match expr {
GqlExpr::Literal(GqlLiteral::Integer(i), _) => Ok(Expr::Literal(Literal::Integer(*i))),
GqlExpr::Param {
name,
span,
} => {
naming::validate_param_name(name, *span)?;
Ok(Expr::Param(Param::from(name.clone())))
}
other => Err(syntax_error!(
"Expected an unsigned integer or a parameter",
@other.span()
)),
}
}
#[derive(Clone)]
pub struct PreparedGqlQuery(pub(crate) LogicalPlan);
impl std::fmt::Debug for PreparedGqlQuery {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("PreparedGqlQuery").field(&self.to_sql()).finish()
}
}
impl ToSql for PreparedGqlQuery {
fn fmt_sql(&self, f: &mut String, fmt: surrealdb_types::SqlFormat) {
match self.0.expressions.as_slice() {
[TopLevelExpr::Expr(Expr::Match(plan))] => plan.fmt_sql(f, fmt),
_ => self.0.fmt_sql(f, fmt),
}
}
}