use selene_core::{DbString, feature_register::FeatureId};
use crate::{
LimitValue, PipelineStatement, QueryPipeline, ReturnClause, SessionResetTarget, SetOp,
Statement, ValueExpr, WithClause,
ast::{
pattern::{
EdgeDirection, EdgePattern, GraphPattern, MatchClause, MatchMode, NodePattern,
PathMode, PathSelector, PatternElement, Quantifier,
},
statement::{ForStatement, LetBinding, OrderTerm, RowExpansionPositionKind},
},
};
use super::{FeatureUse, call, ddl, expr, mutation, record_feature};
pub(crate) fn statement(statement: &Statement, uses: &mut Vec<FeatureUse>) {
match statement {
Statement::Query(pipeline) => query_pipeline(pipeline, uses),
Statement::Composite { first, rest, .. } => {
query_pipeline(first, uses);
for (op, pipeline) in rest {
match op {
SetOp::Union | SetOp::UnionAll => {
record_feature(uses, FeatureId::GQ03, pipeline.span);
}
SetOp::Otherwise => record_feature(uses, FeatureId::GQ02, pipeline.span),
SetOp::Intersect => record_feature(uses, FeatureId::GQ06, pipeline.span),
SetOp::IntersectAll => record_feature(uses, FeatureId::GQ07, pipeline.span),
SetOp::Except => record_feature(uses, FeatureId::GQ04, pipeline.span),
SetOp::ExceptAll => record_feature(uses, FeatureId::GQ05, pipeline.span),
}
query_pipeline(pipeline, uses);
}
}
Statement::Chained { blocks, span } => {
record_feature(uses, FeatureId::GQ20, *span);
for block in blocks {
query_pipeline(block, uses);
}
}
Statement::Mutate(pipeline) => mutation::pipeline(pipeline, uses),
Statement::Ddl(statement) => ddl::statement(statement, uses),
Statement::Call(call) => call::procedure_call(call, uses),
Statement::Explain { inner, .. } => self::statement(inner, uses),
Statement::StartTransaction { .. }
| Statement::Commit { .. }
| Statement::Rollback { .. } => record_feature(uses, FeatureId::GT01, statement.span()),
Statement::SessionSetValue {
declared_type,
value,
span,
..
} => {
record_feature(uses, FeatureId::GS03, *span);
if let Some(ty) = declared_type {
expr::gql_type(ty, *span, uses);
}
expr::value(value, uses);
}
Statement::SessionSetTimeZone {
zone_source_kind,
span,
..
} => {
record_feature(uses, FeatureId::GS15, *span);
expr::character_string_literal(*zone_source_kind, *span, uses);
}
Statement::SessionSetGraph { .. } => {}
Statement::SessionReset { target, span } => match target {
SessionResetTarget::AllCharacteristics => {
record_feature(uses, FeatureId::GS04, *span);
}
SessionResetTarget::Parameters => record_feature(uses, FeatureId::GS08, *span),
SessionResetTarget::TimeZone => record_feature(uses, FeatureId::GS07, *span),
SessionResetTarget::Parameter(_) => {
record_feature(uses, FeatureId::GS08, *span);
record_feature(uses, FeatureId::GS16, *span);
}
},
Statement::SessionClose { .. } => {}
}
}
pub(crate) fn query_pipeline(pipeline: &QueryPipeline, uses: &mut Vec<FeatureUse>) {
let mut projection_names = None;
for (index, statement) in pipeline.statements.iter().enumerate() {
if index > 0 && matches!(statement, PipelineStatement::Match(_)) {
record_feature(uses, FeatureId::GQ20, statement.span());
}
pipeline_statement(statement, projection_names.as_deref(), uses);
match statement {
PipelineStatement::Return(clause) => {
projection_names = Some(projected_names(&clause.items));
}
PipelineStatement::With(clause) => {
projection_names = Some(projected_names(&clause.items));
}
PipelineStatement::Sorting(_)
| PipelineStatement::Limit(_)
| PipelineStatement::Offset(_) => {}
_ => projection_names = None,
}
}
}
pub(crate) fn pipeline_statement(
statement: &PipelineStatement,
projection_names: Option<&[DbString]>,
uses: &mut Vec<FeatureUse>,
) {
match statement {
PipelineStatement::Match(value) => match_clause(value, uses),
PipelineStatement::Filter(value) => {
record_feature(uses, FeatureId::GQ08, value.span());
expr::value(value, uses);
}
PipelineStatement::Let(values) => let_bindings(values, uses),
PipelineStatement::For(value) => for_statement(value, uses),
PipelineStatement::Sorting(values) => order_terms(values, projection_names, uses),
PipelineStatement::Limit(value) => {
record_feature(uses, FeatureId::GQ13, value.span());
limit_value(value, uses);
}
PipelineStatement::Offset(value) => {
record_feature(uses, FeatureId::GQ12, value.span());
limit_value(value, uses);
}
PipelineStatement::Return(value) => return_clause(value, uses),
PipelineStatement::With(value) => with_clause(value, uses),
PipelineStatement::Call(value) => call::procedure_call(value, uses),
PipelineStatement::CallSubquery(value) => {
record_feature(uses, FeatureId::GP01, value.span);
if value.variable_scope.is_some() {
record_feature(uses, FeatureId::GP03, value.span);
} else {
record_feature(uses, FeatureId::GP02, value.span);
}
query_pipeline(&value.body, uses);
}
}
}
fn projected_names(items: &[crate::ReturnItem]) -> Vec<DbString> {
items.iter().filter_map(projection_name).collect()
}
fn projection_name(item: &crate::ReturnItem) -> Option<DbString> {
item.alias.clone().or_else(|| match &item.expr {
ValueExpr::Variable { name, .. } => Some(name.clone()),
_ => None,
})
}
fn limit_value(value: &LimitValue, uses: &mut Vec<FeatureUse>) {
if let LimitValue::Parameter {
declared_type: Some(ty),
span,
..
} = value
{
record_feature(uses, FeatureId::IM_TYPED_PARAMS, *span);
expr::gql_type(ty, *span, uses);
}
}
pub(crate) fn return_clause(clause: &ReturnClause, uses: &mut Vec<FeatureUse>) {
if let Some(group_by) = &clause.group_by {
record_feature(uses, FeatureId::GQ15, clause.span);
for item in group_by {
expr::value(item, uses);
}
}
if let Some(having) = &clause.having {
expr::value(having, uses);
}
for item in &clause.items {
expr::value(&item.expr, uses);
}
}
pub(crate) fn with_clause(clause: &WithClause, uses: &mut Vec<FeatureUse>) {
if let Some(group_by) = &clause.group_by {
record_feature(uses, FeatureId::GQ15, clause.span);
for item in group_by {
expr::value(item, uses);
}
}
if let Some(having) = &clause.having {
expr::value(having, uses);
}
if let Some(where_clause) = &clause.where_clause {
expr::value(where_clause, uses);
}
for item in &clause.items {
expr::value(&item.expr, uses);
}
}
pub(crate) fn match_clause(clause: &MatchClause, uses: &mut Vec<FeatureUse>) {
if let Some(mode) = clause.match_mode {
match mode {
MatchMode::DifferentEdges => record_feature(uses, FeatureId::G002, clause.span),
MatchMode::RepeatableElements => record_feature(uses, FeatureId::G003, clause.span),
}
}
if clause.path_mode_explicit && clause.path_mode == PathMode::Walk {
record_feature(uses, FeatureId::G010, clause.span);
}
if clause.path_or_paths {
record_feature(uses, FeatureId::G014, clause.span);
}
match clause.path_mode {
PathMode::Walk => {}
PathMode::Trail => record_feature(uses, FeatureId::G011, clause.span),
PathMode::Simple => record_feature(uses, FeatureId::G012, clause.span),
PathMode::Acyclic => record_feature(uses, FeatureId::G013, clause.span),
}
if let Some(selector) = clause.selector {
match selector {
PathSelector::All => record_feature(uses, FeatureId::G015, clause.span),
PathSelector::Any { .. } => record_feature(uses, FeatureId::G016, clause.span),
PathSelector::AllShortest => record_feature(uses, FeatureId::G017, clause.span),
PathSelector::AnyShortest => record_feature(uses, FeatureId::G018, clause.span),
PathSelector::CountedShortest { .. } => {
record_feature(uses, FeatureId::G019, clause.span)
}
PathSelector::CountedShortestGroup { .. } => {
record_feature(uses, FeatureId::G020, clause.span)
}
}
}
for pattern in &clause.patterns {
graph_pattern(pattern, uses);
}
if let Some(where_clause) = &clause.where_clause {
expr::value(where_clause, uses);
}
}
pub(crate) fn graph_pattern(pattern: &GraphPattern, uses: &mut Vec<FeatureUse>) {
for element in &pattern.elements {
match element {
PatternElement::Node(node) => node_pattern(node, uses),
PatternElement::Edge(edge) => edge_pattern(edge, uses),
}
}
}
fn node_pattern(pattern: &NodePattern, uses: &mut Vec<FeatureUse>) {
for (_, value) in &pattern.properties {
expr::value(value, uses);
}
if let Some(inline_where) = &pattern.inline_where {
expr::value(inline_where, uses);
}
}
fn edge_pattern(pattern: &EdgePattern, uses: &mut Vec<FeatureUse>) {
if pattern.direction == EdgeDirection::Undirected {
record_feature(uses, FeatureId::GH02, pattern.span);
}
if let Some(quantifier) = pattern.quantifier {
record_feature(uses, FeatureId::G036, pattern.span);
match quantifier {
Quantifier::GraphPattern { max: Some(_), .. } => {
record_feature(uses, FeatureId::G060, pattern.span);
}
Quantifier::GraphPattern { max: None, .. } => {
record_feature(uses, FeatureId::G061, pattern.span);
}
Quantifier::Questioned => {
record_feature(uses, FeatureId::G037, pattern.span);
}
}
}
for (_, value) in &pattern.properties {
expr::value(value, uses);
}
if let Some(inline_where) = &pattern.inline_where {
expr::value(inline_where, uses);
}
}
fn let_bindings(bindings: &[LetBinding], uses: &mut Vec<FeatureUse>) {
if let Some(first) = bindings.first() {
record_feature(uses, FeatureId::GQ09, first.span);
}
for binding in bindings {
if let Some(ty) = &binding.declared_type {
expr::gql_type(ty, binding.span, uses);
}
expr::value(&binding.value, uses);
}
}
fn for_statement(statement: &ForStatement, uses: &mut Vec<FeatureUse>) {
record_feature(uses, FeatureId::GQ10, statement.span);
if let Some(position) = &statement.position {
let feature = match position.kind {
RowExpansionPositionKind::Ordinality => FeatureId::GQ11,
RowExpansionPositionKind::Offset => FeatureId::GQ24,
};
record_feature(uses, feature, statement.span);
}
expr::value(&statement.source, uses);
}
fn order_terms(
terms: &[OrderTerm],
projection_names: Option<&[DbString]>,
uses: &mut Vec<FeatureUse>,
) {
if let Some(first) = terms.first() {
record_feature(uses, FeatureId::GA07, first.span);
}
for term in terms {
sort_key_features(term, projection_names, uses);
expr::value(&term.expr, uses);
}
}
fn sort_key_features(
term: &OrderTerm,
projection_names: Option<&[DbString]>,
uses: &mut Vec<FeatureUse>,
) {
if !matches!(term.expr, ValueExpr::Variable { .. }) {
record_feature(uses, FeatureId::GQ14, term.expr.span());
}
if contains_unprojected_variable(&term.expr, projection_names) {
record_feature(uses, FeatureId::GQ16, term.expr.span());
}
if contains_aggregate_function(&term.expr) {
record_feature(uses, FeatureId::GF20, term.expr.span());
}
}
fn contains_unprojected_variable(value: &ValueExpr, projection_names: Option<&[DbString]>) -> bool {
let mut stack = vec![value];
while let Some(value) = stack.pop() {
if let ValueExpr::Variable { name, .. } = value
&& !projection_names.is_some_and(|names| names.contains(name))
{
return true;
}
value.for_each_child(&mut |child| stack.push(child));
}
false
}
fn contains_aggregate_function(value: &ValueExpr) -> bool {
let mut stack = vec![value];
while let Some(value) = stack.pop() {
if let ValueExpr::FunctionCall { name, .. } = value
&& is_aggregate_name(name.first())
{
return true;
}
value.for_each_child(&mut |child| stack.push(child));
}
false
}
fn is_aggregate_name(name: &DbString) -> bool {
[
"count",
"sum",
"avg",
"min",
"max",
"collect_list",
"stddev_pop",
"stddev_samp",
"percentile_cont",
"percentile_disc",
]
.iter()
.any(|candidate| name.as_str().eq_ignore_ascii_case(candidate))
}