use super::ast::*;
use crate::types::{GqlType, TypeValidator};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone)]
pub struct ValidationError {
#[allow(dead_code)] pub message: String,
#[allow(dead_code)] pub location: Option<Location>,
#[allow(dead_code)] pub error_type: ValidationErrorType,
}
#[derive(Debug, Clone)]
pub enum ValidationErrorType {
Structural,
Semantic,
Type,
Syntax,
}
#[derive(Debug, Clone)]
struct ValidationContext {
declared_variables: HashSet<String>,
variable_types: HashMap<String, GqlType>,
function_signatures: HashMap<String, FunctionSignature>,
_current_scope: Vec<String>,
has_graph_context: bool,
property_types: HashMap<String, GqlType>,
}
#[derive(Debug, Clone)]
struct FunctionSignature {
argument_types: Vec<GqlType>,
return_type: GqlType,
variadic: bool,
}
impl ValidationContext {
fn new() -> Self {
let mut ctx = Self {
declared_variables: HashSet::new(),
variable_types: HashMap::new(),
function_signatures: HashMap::new(),
_current_scope: Vec::new(),
has_graph_context: false,
property_types: HashMap::new(),
};
ctx.register_builtin_functions();
ctx
}
fn register_builtin_functions(&mut self) {
use crate::ast::ast::TypeSpec as GqlType;
self.function_signatures.insert(
"DATETIME".to_string(),
FunctionSignature {
argument_types: vec![GqlType::String { max_length: None }],
return_type: GqlType::ZonedDateTime { precision: None },
variadic: false,
},
);
self.function_signatures.insert(
"NOW".to_string(),
FunctionSignature {
argument_types: vec![],
return_type: GqlType::ZonedDateTime { precision: None },
variadic: false,
},
);
self.function_signatures.insert(
"DURATION".to_string(),
FunctionSignature {
argument_types: vec![GqlType::String { max_length: None }],
return_type: GqlType::Duration { precision: None },
variadic: false,
},
);
self.function_signatures.insert(
"DURATION_NUMERIC".to_string(),
FunctionSignature {
argument_types: vec![GqlType::Double, GqlType::String { max_length: None }],
return_type: GqlType::Duration { precision: None },
variadic: false,
},
);
self.function_signatures.insert(
"TIME_WINDOW".to_string(),
FunctionSignature {
argument_types: vec![
GqlType::ZonedDateTime { precision: None },
GqlType::ZonedDateTime { precision: None },
],
return_type: GqlType::Duration { precision: None }, variadic: false,
},
);
self.function_signatures.insert(
"count".to_string(),
FunctionSignature {
argument_types: vec![], return_type: GqlType::BigInt,
variadic: true,
},
);
self.function_signatures.insert(
"COUNT".to_string(),
FunctionSignature {
argument_types: vec![], return_type: GqlType::BigInt,
variadic: true,
},
);
self.function_signatures.insert(
"sum".to_string(),
FunctionSignature {
argument_types: vec![GqlType::Double], return_type: GqlType::Double,
variadic: false,
},
);
self.function_signatures.insert(
"SUM".to_string(),
FunctionSignature {
argument_types: vec![GqlType::Double], return_type: GqlType::Double,
variadic: false,
},
);
self.function_signatures.insert(
"avg".to_string(),
FunctionSignature {
argument_types: vec![GqlType::Double], return_type: GqlType::Double,
variadic: false,
},
);
self.function_signatures.insert(
"AVG".to_string(),
FunctionSignature {
argument_types: vec![GqlType::Double], return_type: GqlType::Double,
variadic: false,
},
);
self.function_signatures.insert(
"min".to_string(),
FunctionSignature {
argument_types: vec![GqlType::Double],
return_type: GqlType::Double,
variadic: false,
},
);
self.function_signatures.insert(
"MIN".to_string(),
FunctionSignature {
argument_types: vec![GqlType::Double],
return_type: GqlType::Double,
variadic: false,
},
);
self.function_signatures.insert(
"max".to_string(),
FunctionSignature {
argument_types: vec![GqlType::Double],
return_type: GqlType::Double,
variadic: false,
},
);
self.function_signatures.insert(
"MAX".to_string(),
FunctionSignature {
argument_types: vec![GqlType::Double],
return_type: GqlType::Double,
variadic: false,
},
);
self.function_signatures.insert(
"collect".to_string(),
FunctionSignature {
argument_types: vec![], return_type: GqlType::List {
element_type: Box::new(GqlType::String { max_length: None }), max_length: None,
},
variadic: true,
},
);
self.function_signatures.insert(
"COLLECT".to_string(),
FunctionSignature {
argument_types: vec![], return_type: GqlType::List {
element_type: Box::new(GqlType::String { max_length: None }), max_length: None,
},
variadic: true,
},
);
self.function_signatures.insert(
"UPPER".to_string(),
FunctionSignature {
argument_types: vec![GqlType::String { max_length: None }],
return_type: GqlType::String { max_length: None },
variadic: false,
},
);
self.function_signatures.insert(
"LOWER".to_string(),
FunctionSignature {
argument_types: vec![GqlType::String { max_length: None }],
return_type: GqlType::String { max_length: None },
variadic: false,
},
);
self.function_signatures.insert(
"ROUND".to_string(),
FunctionSignature {
argument_types: vec![GqlType::Double],
return_type: GqlType::Double,
variadic: false,
},
);
self.function_signatures.insert(
"TRIM".to_string(),
FunctionSignature {
argument_types: vec![GqlType::String { max_length: None }],
return_type: GqlType::String { max_length: None },
variadic: false,
},
);
self.function_signatures.insert(
"SUBSTRING".to_string(),
FunctionSignature {
argument_types: vec![GqlType::String { max_length: None }, GqlType::BigInt],
return_type: GqlType::String { max_length: None },
variadic: false,
},
);
self.function_signatures.insert(
"REPLACE".to_string(),
FunctionSignature {
argument_types: vec![
GqlType::String { max_length: None },
GqlType::String { max_length: None },
GqlType::String { max_length: None },
],
return_type: GqlType::String { max_length: None },
variadic: false,
},
);
self.function_signatures.insert(
"REVERSE".to_string(),
FunctionSignature {
argument_types: vec![GqlType::String { max_length: None }],
return_type: GqlType::String { max_length: None },
variadic: false,
},
);
self.function_signatures.insert(
"UPPER".to_string(),
FunctionSignature {
argument_types: vec![GqlType::String { max_length: None }],
return_type: GqlType::String { max_length: None },
variadic: false,
},
);
self.function_signatures.insert(
"LOWER".to_string(),
FunctionSignature {
argument_types: vec![GqlType::String { max_length: None }],
return_type: GqlType::String { max_length: None },
variadic: false,
},
);
self.function_signatures.insert(
"ROUND".to_string(),
FunctionSignature {
argument_types: vec![GqlType::Double],
return_type: GqlType::Double,
variadic: false,
},
);
self.function_signatures.insert(
"TRIM".to_string(),
FunctionSignature {
argument_types: vec![GqlType::String { max_length: None }],
return_type: GqlType::String { max_length: None },
variadic: false,
},
);
self.function_signatures.insert(
"SUBSTRING".to_string(),
FunctionSignature {
argument_types: vec![GqlType::String { max_length: None }, GqlType::BigInt],
return_type: GqlType::String { max_length: None },
variadic: false,
},
);
self.function_signatures.insert(
"REPLACE".to_string(),
FunctionSignature {
argument_types: vec![
GqlType::String { max_length: None },
GqlType::String { max_length: None },
GqlType::String { max_length: None },
],
return_type: GqlType::String { max_length: None },
variadic: false,
},
);
self.function_signatures.insert(
"REVERSE".to_string(),
FunctionSignature {
argument_types: vec![GqlType::String { max_length: None }],
return_type: GqlType::String { max_length: None },
variadic: false,
},
);
self.function_signatures.insert(
"LABELS".to_string(),
FunctionSignature {
argument_types: vec![GqlType::Reference { target_type: None }], return_type: GqlType::List {
element_type: Box::new(GqlType::String { max_length: None }),
max_length: None,
}, variadic: false,
},
);
self.function_signatures.insert(
"TYPE".to_string(),
FunctionSignature {
argument_types: vec![GqlType::String { max_length: None }], return_type: GqlType::String { max_length: None }, variadic: false,
},
);
self.function_signatures.insert(
"ID".to_string(),
FunctionSignature {
argument_types: vec![GqlType::Reference { target_type: None }], return_type: GqlType::String { max_length: None }, variadic: false,
},
);
self.function_signatures.insert(
"PROPERTIES".to_string(),
FunctionSignature {
argument_types: vec![GqlType::Reference { target_type: None }], return_type: GqlType::Record, variadic: false,
},
);
self.function_signatures.insert(
"INFERRED_LABELS".to_string(),
FunctionSignature {
argument_types: vec![GqlType::Reference { target_type: None }], return_type: GqlType::List {
element_type: Box::new(GqlType::String { max_length: None }),
max_length: None,
}, variadic: false,
},
);
self.function_signatures.insert(
"SIZE".to_string(),
FunctionSignature {
argument_types: vec![GqlType::String { max_length: None }], return_type: GqlType::Double, variadic: false,
},
);
self.function_signatures.insert(
"TEXT_SEARCH".to_string(),
FunctionSignature {
argument_types: vec![
GqlType::String { max_length: None },
GqlType::String { max_length: None },
],
return_type: GqlType::Double,
variadic: true, },
);
self.function_signatures.insert(
"FUZZY_MATCH".to_string(),
FunctionSignature {
argument_types: vec![
GqlType::String { max_length: None },
GqlType::String { max_length: None },
],
return_type: GqlType::Double,
variadic: true, },
);
self.function_signatures.insert(
"TEXT_MATCH".to_string(),
FunctionSignature {
argument_types: vec![
GqlType::String { max_length: None },
GqlType::String { max_length: None },
],
return_type: GqlType::Double,
variadic: true, },
);
self.function_signatures.insert(
"HIGHLIGHT".to_string(),
FunctionSignature {
argument_types: vec![
GqlType::String { max_length: None },
GqlType::String { max_length: None },
],
return_type: GqlType::String { max_length: None },
variadic: true, },
);
self.function_signatures.insert(
"TEXT_SCORE".to_string(),
FunctionSignature {
argument_types: vec![],
return_type: GqlType::Double,
variadic: false,
},
);
self.function_signatures.insert(
"HYBRID_SEARCH".to_string(),
FunctionSignature {
argument_types: vec![
GqlType::String { max_length: None },
GqlType::String { max_length: None },
],
return_type: GqlType::Double,
variadic: true, },
);
self.function_signatures.insert(
"GET_TIMEZONE_NAME".to_string(),
FunctionSignature {
argument_types: vec![GqlType::String { max_length: None }],
return_type: GqlType::String { max_length: None },
variadic: false,
},
);
self.function_signatures.insert(
"GET_TIMEZONE_ABBREVIATION".to_string(),
FunctionSignature {
argument_types: vec![GqlType::String { max_length: None }],
return_type: GqlType::String { max_length: None },
variadic: false,
},
);
self.function_signatures.insert(
"GET_TIMEZONE_OFFSET".to_string(),
FunctionSignature {
argument_types: vec![GqlType::String { max_length: None }],
return_type: GqlType::String { max_length: None },
variadic: false,
},
);
}
}
pub fn validate_query(
document: &Document,
has_graph_context: bool,
) -> Result<(), Vec<ValidationError>> {
let mut errors = Vec::new();
let mut ctx = ValidationContext::new();
ctx.has_graph_context = has_graph_context;
match &document.statement {
Statement::Query(query) => {
validate_query_structure(query, &mut errors);
validate_variable_declarations(query, &mut ctx, &mut errors);
validate_path_patterns(query, &mut ctx, &mut errors);
validate_expressions(query, &mut ctx, &mut errors);
validate_temporal_literals(query, &mut errors);
validate_edge_patterns(query, &mut errors);
}
Statement::Select(select_stmt) => {
validate_select_statement(select_stmt, &mut ctx, &mut errors);
}
Statement::Call(call_stmt) => {
validate_call_statement(call_stmt, &mut ctx, &mut errors);
}
Statement::CatalogStatement(catalog_stmt) => {
validate_catalog_statement(catalog_stmt, &mut ctx, &mut errors);
}
Statement::DataStatement(_) => {
}
Statement::SessionStatement(_) => {
}
Statement::Declare(_) => {
}
Statement::Next(_) => {
}
Statement::AtLocation(_) => {
}
Statement::TransactionStatement(_) => {
}
Statement::ProcedureBody(procedure_body) => {
for var_def in &procedure_body.variable_definitions {
for var_decl in &var_def.variable_declarations {
if var_decl.variable_name.is_empty() {
errors.push(ValidationError {
message: format!("Variable name cannot be empty in procedure body"),
location: Some(var_decl.location.clone()),
error_type: ValidationErrorType::Semantic,
});
}
}
}
validate_procedure_statement(&procedure_body.initial_statement, &ctx, &mut errors);
for chained in &procedure_body.chained_statements {
validate_procedure_statement(&chained.statement, &ctx, &mut errors);
}
}
&Statement::IndexStatement(ref index_stmt) => {
match index_stmt {
crate::ast::ast::IndexStatement::CreateIndex(create_idx) => {
if create_idx.name.is_empty() {
errors.push(ValidationError {
message: "Index name cannot be empty".to_string(),
location: Some(create_idx.location.clone()),
error_type: ValidationErrorType::Semantic,
});
}
if create_idx.table.is_empty() {
errors.push(ValidationError {
message: "Table name cannot be empty for index".to_string(),
location: Some(create_idx.location.clone()),
error_type: ValidationErrorType::Semantic,
});
}
}
crate::ast::ast::IndexStatement::DropIndex(drop_idx) => {
if drop_idx.name.is_empty() {
errors.push(ValidationError {
message: "Index name cannot be empty".to_string(),
location: Some(drop_idx.location.clone()),
error_type: ValidationErrorType::Semantic,
});
}
}
crate::ast::ast::IndexStatement::AlterIndex(alter_idx) => {
if alter_idx.name.is_empty() {
errors.push(ValidationError {
message: "Index name cannot be empty".to_string(),
location: Some(alter_idx.location.clone()),
error_type: ValidationErrorType::Semantic,
});
}
}
crate::ast::ast::IndexStatement::OptimizeIndex(optimize_idx) => {
if optimize_idx.name.is_empty() {
errors.push(ValidationError {
message: "Index name cannot be empty".to_string(),
location: Some(optimize_idx.location.clone()),
error_type: ValidationErrorType::Semantic,
});
}
}
crate::ast::ast::IndexStatement::ReindexIndex(reindex) => {
if reindex.name.is_empty() {
errors.push(ValidationError {
message: "Index name cannot be empty for REINDEX".to_string(),
location: Some(reindex.location.clone()),
error_type: ValidationErrorType::Semantic,
});
}
}
}
}
Statement::Let(let_stmt) => {
for var_def in &let_stmt.variable_definitions {
validate_expression(&var_def.expression, &mut ctx, &mut errors);
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
fn validate_query_structure(query: &Query, errors: &mut Vec<ValidationError>) {
match query {
Query::Basic(basic_query) => {
validate_basic_query_structure(basic_query, errors);
}
Query::SetOperation(set_op) => {
validate_query_structure(&set_op.left, errors);
validate_query_structure(&set_op.right, errors);
}
Query::Limited { query, .. } => {
validate_query_structure(query, errors);
}
Query::WithQuery(_) => {
}
Query::Let(_) => {
}
Query::For(_) => {
}
Query::Filter(_) => {
}
Query::Return(return_query) => {
validate_return_query_structure(return_query, errors);
}
Query::Unwind(_) => {
}
Query::MutationPipeline(_) => {
}
}
}
fn validate_basic_query_structure(query: &BasicQuery, errors: &mut Vec<ValidationError>) {
if query.match_clause.patterns.is_empty() {
errors.push(ValidationError {
message: "Query must have at least one path pattern in MATCH clause".to_string(),
location: None,
error_type: ValidationErrorType::Structural,
});
}
if query.return_clause.items.is_empty() {
errors.push(ValidationError {
message: "Query must have at least one item in RETURN clause".to_string(),
location: None,
error_type: ValidationErrorType::Structural,
});
}
}
fn validate_return_query_structure(query: &ReturnQuery, errors: &mut Vec<ValidationError>) {
if query.return_clause.items.is_empty() {
errors.push(ValidationError {
message: "Query must have at least one item in RETURN clause".to_string(),
location: None,
error_type: ValidationErrorType::Structural,
});
}
}
fn validate_variable_declarations(
query: &Query,
ctx: &mut ValidationContext,
errors: &mut Vec<ValidationError>,
) {
match query {
Query::Basic(basic_query) => {
validate_basic_query_variables(basic_query, ctx, errors);
}
Query::SetOperation(set_op) => {
let mut left_ctx = ctx.clone();
validate_variable_declarations(&set_op.left, &mut left_ctx, errors);
let mut right_ctx = ctx.clone();
validate_variable_declarations(&set_op.right, &mut right_ctx, errors);
}
Query::Limited { query, .. } => {
validate_variable_declarations(query, ctx, errors);
}
Query::WithQuery(_) => {
}
Query::Let(_) => {
}
Query::For(_) => {
}
Query::Filter(_) => {
}
Query::Return(_) => {
}
Query::Unwind(_) => {
}
Query::MutationPipeline(_) => {
}
}
}
fn validate_basic_query_variables(
query: &BasicQuery,
ctx: &mut ValidationContext,
errors: &mut Vec<ValidationError>,
) {
for pattern in &query.match_clause.patterns {
for element in &pattern.elements {
match element {
PatternElement::Node(node) => {
if let Some(ref identifier) = node.identifier {
ctx.declared_variables.insert(identifier.clone());
ctx.variable_types
.insert(identifier.clone(), GqlType::Reference { target_type: None });
}
}
PatternElement::Edge(edge) => {
if let Some(ref identifier) = edge.identifier {
ctx.declared_variables.insert(identifier.clone());
ctx.variable_types
.insert(identifier.clone(), GqlType::Reference { target_type: None });
}
}
}
}
}
if let Some(ref where_clause) = query.where_clause {
validate_expression_variables(&where_clause.condition, ctx, errors);
}
for item in &query.return_clause.items {
validate_expression_variables(&item.expression, ctx, errors);
}
}
fn validate_path_patterns(
query: &Query,
_ctx: &mut ValidationContext,
errors: &mut Vec<ValidationError>,
) {
match query {
Query::Basic(basic_query) => {
validate_basic_query_path_patterns(basic_query, _ctx, errors);
}
Query::SetOperation(set_op) => {
validate_path_patterns(&set_op.left, _ctx, errors);
validate_path_patterns(&set_op.right, _ctx, errors);
}
Query::Limited { query, .. } => {
validate_path_patterns(query, _ctx, errors);
}
Query::WithQuery(_) => {
}
Query::Let(_) => {
}
Query::For(_) => {
}
Query::Filter(_) => {
}
Query::Return(_) => {
}
Query::Unwind(_) => {
}
Query::MutationPipeline(_) => {
}
}
}
fn validate_basic_query_path_patterns(
query: &BasicQuery,
_ctx: &mut ValidationContext,
errors: &mut Vec<ValidationError>,
) {
for pattern in &query.match_clause.patterns {
if pattern.elements.is_empty() {
errors.push(ValidationError {
message: "Path pattern cannot be empty".to_string(),
location: None,
error_type: ValidationErrorType::Structural,
});
continue;
}
for (i, element) in pattern.elements.iter().enumerate() {
match element {
PatternElement::Node(_) => {
if i % 2 != 0 {
errors.push(ValidationError {
message: format!(
"Invalid path pattern: expected edge at position {}",
i
),
location: None,
error_type: ValidationErrorType::Structural,
});
}
}
PatternElement::Edge(_) => {
if i % 2 != 1 {
errors.push(ValidationError {
message: format!(
"Invalid path pattern: expected node at position {}",
i
),
location: None,
error_type: ValidationErrorType::Structural,
});
}
}
}
}
if let Some(first) = pattern.elements.first() {
if matches!(first, PatternElement::Edge(_)) {
errors.push(ValidationError {
message: "Path pattern must start with a node".to_string(),
location: None,
error_type: ValidationErrorType::Structural,
});
}
}
if let Some(last) = pattern.elements.last() {
if matches!(last, PatternElement::Edge(_)) {
errors.push(ValidationError {
message: "Path pattern must end with a node".to_string(),
location: None,
error_type: ValidationErrorType::Structural,
});
}
}
}
}
fn validate_expressions(
query: &Query,
ctx: &mut ValidationContext,
errors: &mut Vec<ValidationError>,
) {
match query {
Query::Basic(basic_query) => {
validate_basic_query_expressions(basic_query, ctx, errors);
}
Query::SetOperation(set_op) => {
validate_expressions(&set_op.left, ctx, errors);
validate_expressions(&set_op.right, ctx, errors);
}
Query::Limited { query, .. } => {
validate_expressions(query, ctx, errors);
}
Query::WithQuery(_) => {
}
Query::Let(_) => {
}
Query::For(_) => {
}
Query::Filter(_) => {
}
Query::Return(_) => {
}
Query::Unwind(_) => {
}
Query::MutationPipeline(_) => {
}
}
}
fn validate_basic_query_expressions(
query: &BasicQuery,
ctx: &mut ValidationContext,
errors: &mut Vec<ValidationError>,
) {
if let Some(ref where_clause) = query.where_clause {
validate_expression(&where_clause.condition, ctx, errors);
}
for item in &query.return_clause.items {
validate_expression(&item.expression, ctx, errors);
}
}
fn validate_expression(
expr: &Expression,
ctx: &mut ValidationContext,
errors: &mut Vec<ValidationError>,
) {
match expr {
Expression::Binary(binary) => {
validate_expression(&binary.left, ctx, errors);
validate_expression(&binary.right, ctx, errors);
validate_binary_operation(&binary.operator, &binary.left, &binary.right, ctx, errors);
}
Expression::Unary(unary) => {
validate_expression(&unary.expression, ctx, errors);
validate_unary_operation(&unary.operator, &unary.expression, ctx, errors);
}
Expression::FunctionCall(func_call) => {
validate_function_call(func_call, ctx, errors);
}
Expression::PropertyAccess(prop_access) => {
validate_property_access(prop_access, ctx, errors);
}
Expression::Variable(variable) => {
validate_variable_usage(variable, ctx, errors);
}
Expression::Literal(literal) => {
validate_literal(literal, errors);
}
Expression::Case(case_expr) => {
validate_case_expression(case_expr, ctx, errors);
}
Expression::PathConstructor(path_constructor) => {
validate_path_constructor(path_constructor, ctx, errors);
}
Expression::Cast(cast_expr) => {
validate_cast_expression(cast_expr, ctx, errors);
}
Expression::Subquery(subquery_expr) => {
validate_subquery(&subquery_expr.query, ctx, errors);
}
Expression::ExistsSubquery(subquery_expr) => {
validate_subquery(&subquery_expr.query, ctx, errors);
}
Expression::NotExistsSubquery(subquery_expr) => {
validate_subquery(&subquery_expr.query, ctx, errors);
}
Expression::InSubquery(subquery_expr) => {
validate_expression(&subquery_expr.expression, ctx, errors);
validate_subquery(&subquery_expr.query, ctx, errors);
}
Expression::NotInSubquery(subquery_expr) => {
validate_expression(&subquery_expr.expression, ctx, errors);
validate_subquery(&subquery_expr.query, ctx, errors);
}
Expression::QuantifiedComparison(quantified_expr) => {
validate_expression(&quantified_expr.left, ctx, errors);
validate_expression(&quantified_expr.subquery, ctx, errors);
}
Expression::IsPredicate(is_predicate) => {
validate_expression(&is_predicate.subject, ctx, errors);
if let Some(ref target) = is_predicate.target {
validate_expression(target, ctx, errors);
}
if let crate::ast::ast::IsPredicateType::Label(ref label_expr) =
is_predicate.predicate_type
{
if label_expr.terms.is_empty() {
errors.push(ValidationError {
message: "Label predicate must have at least one label term".to_string(),
location: Some(is_predicate.location.clone()),
error_type: ValidationErrorType::Syntax,
});
}
}
}
Expression::ArrayIndex(array_index) => {
validate_expression(&array_index.array, ctx, errors);
validate_expression(&array_index.index, ctx, errors);
}
Expression::Parameter(parameter) => {
if parameter.name.is_empty() {
errors.push(ValidationError {
message: "Parameter name cannot be empty".to_string(),
error_type: ValidationErrorType::Syntax,
location: Some(parameter.location.clone()),
});
}
}
Expression::Pattern(pattern_expr) => {
validate_path_pattern(&pattern_expr.pattern, ctx, errors);
}
}
}
fn validate_case_expression(
case_expr: &crate::ast::ast::CaseExpression,
ctx: &mut ValidationContext,
errors: &mut Vec<ValidationError>,
) {
use crate::ast::ast::CaseType;
match &case_expr.case_type {
CaseType::Simple(simple_case) => {
validate_simple_case_expression(simple_case, ctx, errors);
}
CaseType::Searched(searched_case) => {
validate_searched_case_expression(searched_case, ctx, errors);
}
}
}
fn validate_simple_case_expression(
simple_case: &SimpleCaseExpression,
ctx: &mut ValidationContext,
errors: &mut Vec<ValidationError>,
) {
validate_expression(&simple_case.test_expression, ctx, errors);
for when_branch in &simple_case.when_branches {
for when_value in &when_branch.when_values {
validate_expression(when_value, ctx, errors);
}
validate_expression(&when_branch.then_expression, ctx, errors);
}
if let Some(else_expr) = &simple_case.else_expression {
validate_expression(else_expr, ctx, errors);
}
if simple_case.when_branches.is_empty() {
errors.push(ValidationError {
message: "CASE expression must have at least one WHEN branch".to_string(),
location: Some(crate::ast::ast::Location::default()),
error_type: ValidationErrorType::Structural,
});
}
}
fn validate_searched_case_expression(
searched_case: &SearchedCaseExpression,
ctx: &mut ValidationContext,
errors: &mut Vec<ValidationError>,
) {
for when_branch in &searched_case.when_branches {
validate_expression(&when_branch.condition, ctx, errors);
validate_expression(&when_branch.then_expression, ctx, errors);
}
if let Some(else_expr) = &searched_case.else_expression {
validate_expression(else_expr, ctx, errors);
}
if searched_case.when_branches.is_empty() {
errors.push(ValidationError {
message: "CASE expression must have at least one WHEN branch".to_string(),
location: Some(crate::ast::ast::Location::default()),
error_type: ValidationErrorType::Structural,
});
}
}
fn validate_path_constructor(
path_constructor: &crate::ast::ast::PathConstructor,
ctx: &mut ValidationContext,
errors: &mut Vec<ValidationError>,
) {
for element in &path_constructor.elements {
validate_expression(element, ctx, errors);
if let Ok(element_type) = infer_expression_type(element, ctx) {
match element_type {
GqlType::String { .. }
| GqlType::Integer
| GqlType::BigInt
| GqlType::SmallInt
| GqlType::Double
| GqlType::Float { .. }
| GqlType::Real => {
}
_ => {
errors.push(ValidationError {
message: format!(
"PATH constructor element must be a string or number type, got: {:?}",
element_type
),
location: Some(crate::ast::ast::Location::default()),
error_type: ValidationErrorType::Type,
});
}
}
}
}
if path_constructor.elements.len() % 2 == 0 && path_constructor.elements.len() > 2 {
}
}
fn validate_cast_expression(
cast_expr: &crate::ast::ast::CastExpression,
ctx: &mut ValidationContext,
errors: &mut Vec<ValidationError>,
) {
validate_expression(&cast_expr.expression, ctx, errors);
if let Ok(source_type) = infer_expression_type(&cast_expr.expression, ctx) {
match (&source_type, &cast_expr.target_type) {
_ => {} }
}
match &cast_expr.target_type {
GqlType::String { max_length } => {
if let Some(len) = max_length {
if *len == 0 {
errors.push(ValidationError {
message: "String type cannot have max_length of 0".to_string(),
location: Some(crate::ast::ast::Location::default()),
error_type: ValidationErrorType::Type,
});
}
}
}
GqlType::Decimal { precision, scale } => {
if let (Some(p), Some(s)) = (precision, scale) {
if *s > *p {
errors.push(ValidationError {
message: "DECIMAL scale cannot be greater than precision".to_string(),
location: Some(crate::ast::ast::Location::default()),
error_type: ValidationErrorType::Type,
});
}
}
}
_ => {} }
}
fn infer_expression_type(
expression: &Expression,
ctx: &ValidationContext,
) -> Result<GqlType, String> {
match expression {
Expression::Literal(lit) => {
match lit {
Literal::String(_) => Ok(GqlType::String { max_length: None }),
Literal::Integer(_) => Ok(GqlType::BigInt),
Literal::Float(_) => Ok(GqlType::Double),
Literal::Boolean(_) => Ok(GqlType::Boolean),
Literal::Null => Ok(GqlType::String { max_length: None }), Literal::DateTime(_) => Ok(GqlType::ZonedDateTime { precision: None }),
Literal::Duration(_) => Ok(GqlType::Duration { precision: None }),
Literal::TimeWindow(_) => Ok(GqlType::Duration { precision: None }),
Literal::Vector(_) => Ok(GqlType::List {
element_type: Box::new(GqlType::Double),
max_length: None,
}),
Literal::List(list) => {
if list.is_empty() {
Ok(GqlType::List {
element_type: Box::new(GqlType::String { max_length: None }),
max_length: None,
})
} else {
let first_type =
infer_expression_type(&Expression::Literal(list[0].clone()), ctx)?;
Ok(GqlType::List {
element_type: Box::new(first_type),
max_length: None,
})
}
}
}
}
Expression::Variable(var) => ctx
.variable_types
.get(&var.name)
.cloned()
.ok_or_else(|| format!("Unknown variable type: {}", var.name)),
Expression::PropertyAccess(prop_access) => {
if let Some(property_type) = ctx.property_types.get(&prop_access.property) {
Ok(property_type.clone())
} else {
Ok(GqlType::String { max_length: None }) }
}
Expression::FunctionCall(func) => {
let func_name_upper = func.name.to_uppercase();
ctx.function_signatures
.get(&func_name_upper)
.map(|sig| sig.return_type.clone())
.ok_or_else(|| format!("Unknown function: {}", func.name))
}
Expression::PathConstructor(_) => Ok(GqlType::Path),
Expression::Cast(cast_expr) => Ok(cast_expr.target_type.clone()),
Expression::Subquery(_) => {
Ok(GqlType::String { max_length: None })
}
Expression::ExistsSubquery(_) => {
Ok(GqlType::Boolean)
}
Expression::InSubquery(_) => {
Ok(GqlType::Boolean)
}
Expression::NotInSubquery(_) => {
Ok(GqlType::Boolean)
}
Expression::IsPredicate(_) => {
Ok(GqlType::Boolean)
}
Expression::Binary(binary) => {
use crate::ast::ast::Operator;
match &binary.operator {
Operator::Plus
| Operator::Minus
| Operator::Star
| Operator::Slash
| Operator::Percent
| Operator::Caret => {
let left_type = infer_expression_type(&binary.left, ctx)?;
let right_type = infer_expression_type(&binary.right, ctx)?;
match (&left_type, &right_type) {
(
GqlType::Double
| GqlType::Float { .. }
| GqlType::Real
| GqlType::Integer
| GqlType::BigInt
| GqlType::SmallInt
| GqlType::Decimal { .. },
_,
)
| (
_,
GqlType::Double
| GqlType::Float { .. }
| GqlType::Real
| GqlType::Integer
| GqlType::BigInt
| GqlType::SmallInt
| GqlType::Decimal { .. },
) => Ok(GqlType::Double),
_ => Ok(GqlType::Double), }
}
Operator::Equal
| Operator::NotEqual
| Operator::LessThan
| Operator::LessEqual
| Operator::GreaterThan
| Operator::GreaterEqual => Ok(GqlType::Boolean),
Operator::And | Operator::Or => Ok(GqlType::Boolean),
Operator::Concat => Ok(GqlType::String { max_length: None }),
_ => infer_expression_type(&binary.left, ctx),
}
}
_ => {
Ok(GqlType::String { max_length: None })
}
}
}
fn validate_function_call(
func_call: &FunctionCall,
ctx: &mut ValidationContext,
errors: &mut Vec<ValidationError>,
) {
let func_name_upper = func_call.name.to_uppercase();
let signature = match ctx.function_signatures.get(&func_name_upper).cloned() {
Some(sig) => sig,
None => {
errors.push(ValidationError {
message: format!("Unknown function '{}'", func_call.name),
location: None,
error_type: ValidationErrorType::Semantic,
});
return;
}
};
for arg in &func_call.arguments {
validate_expression(arg, ctx, errors);
}
if signature.variadic {
if func_name_upper == "COUNT" {
if func_call.arguments.len() > 1 {
errors.push(ValidationError {
message: format!(
"Function '{}' expects 0 or 1 arguments, got {}",
func_call.name,
func_call.arguments.len()
),
location: None,
error_type: ValidationErrorType::Type,
});
return;
}
}
if func_call.arguments.len() < signature.argument_types.len() {
errors.push(ValidationError {
message: format!(
"Function '{}' expects at least {} arguments, got {}",
func_call.name,
signature.argument_types.len(),
func_call.arguments.len()
),
location: None,
error_type: ValidationErrorType::Type,
});
}
return;
}
if func_name_upper == "ROUND" {
match func_call.arguments.len() {
1 => {
validate_expression(&func_call.arguments[0], ctx, errors);
}
2 => {
validate_expression(&func_call.arguments[0], ctx, errors);
validate_expression(&func_call.arguments[1], ctx, errors);
}
_ => {
errors.push(ValidationError {
message: format!(
"Function 'ROUND' expects 1 or 2 arguments, got {}",
func_call.arguments.len()
),
location: None,
error_type: ValidationErrorType::Type,
});
}
}
return;
}
if func_name_upper == "TRIM" {
match func_call.arguments.len() {
1 => {
validate_expression(&func_call.arguments[0], ctx, errors);
}
2 => {
validate_expression(&func_call.arguments[0], ctx, errors);
validate_expression(&func_call.arguments[1], ctx, errors);
}
3 => {
validate_expression(&func_call.arguments[0], ctx, errors);
validate_expression(&func_call.arguments[1], ctx, errors);
validate_expression(&func_call.arguments[2], ctx, errors);
}
_ => {
errors.push(ValidationError {
message: format!(
"Function 'TRIM' expects 1 to 3 arguments, got {}",
func_call.arguments.len()
),
location: None,
error_type: ValidationErrorType::Type,
});
}
}
return;
}
if func_name_upper == "REPLACE" {
if func_call.arguments.len() != 3 {
errors.push(ValidationError {
message: format!(
"Function 'REPLACE' expects 3 arguments, got {}",
func_call.arguments.len()
),
location: None,
error_type: ValidationErrorType::Type,
});
} else {
validate_expression(&func_call.arguments[0], ctx, errors);
validate_expression(&func_call.arguments[1], ctx, errors);
validate_expression(&func_call.arguments[2], ctx, errors);
}
return;
}
if func_name_upper == "SUBSTRING" {
match func_call.arguments.len() {
2 => {
validate_expression(&func_call.arguments[0], ctx, errors);
validate_expression(&func_call.arguments[1], ctx, errors);
}
3 => {
validate_expression(&func_call.arguments[0], ctx, errors);
validate_expression(&func_call.arguments[1], ctx, errors);
validate_expression(&func_call.arguments[2], ctx, errors);
}
_ => {
errors.push(ValidationError {
message: format!(
"Function 'SUBSTRING' expects 2 or 3 arguments, got {}",
func_call.arguments.len()
),
location: None,
error_type: ValidationErrorType::Type,
});
}
}
return;
}
if func_name_upper == "ROUND" {
match func_call.arguments.len() {
1 => {
validate_expression(&func_call.arguments[0], ctx, errors);
}
2 => {
validate_expression(&func_call.arguments[0], ctx, errors);
validate_expression(&func_call.arguments[1], ctx, errors);
}
_ => {
errors.push(ValidationError {
message: format!(
"Function 'ROUND' expects 1 or 2 arguments, got {}",
func_call.arguments.len()
),
location: None,
error_type: ValidationErrorType::Type,
});
}
}
return;
}
if func_name_upper == "TRIM" {
match func_call.arguments.len() {
1 => {
validate_expression(&func_call.arguments[0], ctx, errors);
}
2 => {
validate_expression(&func_call.arguments[0], ctx, errors);
validate_expression(&func_call.arguments[1], ctx, errors);
}
3 => {
validate_expression(&func_call.arguments[0], ctx, errors);
validate_expression(&func_call.arguments[1], ctx, errors);
validate_expression(&func_call.arguments[2], ctx, errors);
}
_ => {
errors.push(ValidationError {
message: format!(
"Function 'TRIM' expects 1 to 3 arguments, got {}",
func_call.arguments.len()
),
location: None,
error_type: ValidationErrorType::Type,
});
}
}
return;
}
if func_name_upper == "REPLACE" {
if func_call.arguments.len() != 3 {
errors.push(ValidationError {
message: format!(
"Function 'REPLACE' expects 3 arguments, got {}",
func_call.arguments.len()
),
location: None,
error_type: ValidationErrorType::Type,
});
} else {
validate_expression(&func_call.arguments[0], ctx, errors);
validate_expression(&func_call.arguments[1], ctx, errors);
validate_expression(&func_call.arguments[2], ctx, errors);
}
return;
}
if func_name_upper == "SUBSTRING" {
match func_call.arguments.len() {
2 => {
validate_expression(&func_call.arguments[0], ctx, errors);
validate_expression(&func_call.arguments[1], ctx, errors);
}
3 => {
validate_expression(&func_call.arguments[0], ctx, errors);
validate_expression(&func_call.arguments[1], ctx, errors);
validate_expression(&func_call.arguments[2], ctx, errors);
}
_ => {
errors.push(ValidationError {
message: format!(
"Function 'SUBSTRING' expects 2 or 3 arguments, got {}",
func_call.arguments.len()
),
location: None,
error_type: ValidationErrorType::Type,
});
}
}
return;
}
let func_name_upper = func_call.name.to_uppercase();
if let Some(signature) = ctx.function_signatures.get(&func_name_upper) {
if func_call.arguments.len() != signature.argument_types.len() {
errors.push(ValidationError {
message: format!(
"Function '{}' expects {} arguments, got {}",
func_call.name,
signature.argument_types.len(),
func_call.arguments.len()
),
location: None,
error_type: ValidationErrorType::Type,
});
}
for (_i, arg) in func_call.arguments.iter().enumerate() {
validate_expression(arg, ctx, errors);
}
} else {
errors.push(ValidationError {
message: format!("Unknown function '{}'", func_call.name),
location: None,
error_type: ValidationErrorType::Type,
});
}
let mut arg_types = Vec::new();
for arg in &func_call.arguments {
match infer_expression_type(arg, ctx) {
Ok(arg_type) => arg_types.push(arg_type),
Err(err) => {
errors.push(ValidationError {
message: format!("Type inference error: {}", err),
location: None,
error_type: ValidationErrorType::Type,
});
return;
}
}
}
let is_aggregation_function = matches!(
func_name_upper.as_str(),
"SUM" | "AVG" | "MIN" | "MAX" | "COUNT" | "COLLECT"
);
let is_flexible_function = matches!(func_name_upper.as_str(), "TYPE" | "SIZE");
if !is_aggregation_function && !is_flexible_function {
if let Err(type_error) = TypeValidator::validate_function_args(
&func_call.name,
&arg_types,
&signature.argument_types,
signature.variadic,
) {
errors.push(ValidationError {
message: format!("Function validation error: {}", type_error),
location: None,
error_type: ValidationErrorType::Type,
});
}
}
}
fn validate_property_access(
prop_access: &PropertyAccess,
ctx: &mut ValidationContext,
errors: &mut Vec<ValidationError>,
) {
if !ctx.declared_variables.contains(&prop_access.object) {
if !ctx.has_graph_context {
errors.push(ValidationError {
message: format!("Undefined variable '{}'", prop_access.object),
location: None,
error_type: ValidationErrorType::Semantic,
});
}
}
if prop_access.property.is_empty() {
errors.push(ValidationError {
message: "Property name cannot be empty".to_string(),
location: None,
error_type: ValidationErrorType::Syntax,
});
}
}
fn validate_variable_usage(
variable: &Variable,
ctx: &mut ValidationContext,
errors: &mut Vec<ValidationError>,
) {
if !ctx.declared_variables.contains(&variable.name) {
if !ctx.has_graph_context {
errors.push(ValidationError {
message: format!("Undefined variable '{}'", variable.name),
location: None,
error_type: ValidationErrorType::Semantic,
});
}
}
}
fn validate_unary_operation(
operator: &Operator,
expression: &Expression,
ctx: &mut ValidationContext,
errors: &mut Vec<ValidationError>,
) {
validate_expression(expression, ctx, errors);
match operator {
Operator::Not => {
}
Operator::Minus => {
}
_ => {
errors.push(ValidationError {
message: format!("Invalid unary operator '{:?}'", operator),
location: None,
error_type: ValidationErrorType::Type,
});
}
}
}
fn validate_binary_operation(
operator: &Operator,
left: &Expression,
right: &Expression,
ctx: &mut ValidationContext,
errors: &mut Vec<ValidationError>,
) {
validate_expression(left, ctx, errors);
validate_expression(right, ctx, errors);
match operator {
Operator::Plus
| Operator::Minus
| Operator::Star
| Operator::Slash
| Operator::Percent
| Operator::Caret => {
}
Operator::Equal
| Operator::NotEqual
| Operator::LessThan
| Operator::LessEqual
| Operator::GreaterThan
| Operator::GreaterEqual => {
}
Operator::And | Operator::Or | Operator::Not | Operator::Xor => {
}
Operator::In
| Operator::NotIn
| Operator::Contains
| Operator::Starts
| Operator::Ends
| Operator::Like
| Operator::Matches
| Operator::FuzzyEqual
| Operator::Concat => {
}
Operator::Exists => {
}
Operator::Regex => {
}
Operator::Within => {
}
}
}
fn validate_literal(literal: &Literal, errors: &mut Vec<ValidationError>) {
match literal {
Literal::String(s) => {
if s.is_empty() {
errors.push(ValidationError {
message: "String literal cannot be empty".to_string(),
location: None,
error_type: ValidationErrorType::Syntax,
});
}
}
Literal::Vector(v) => {
if v.is_empty() {
errors.push(ValidationError {
message: "Vector literal cannot be empty".to_string(),
location: None,
error_type: ValidationErrorType::Syntax,
});
}
}
Literal::DateTime(dt) => {
validate_datetime_format(dt, errors);
}
Literal::Duration(dur) => {
validate_duration_format(dur, errors);
}
Literal::TimeWindow(tw) => {
validate_timewindow_format(tw, errors);
}
_ => {} }
}
fn validate_datetime_format(dt: &str, errors: &mut Vec<ValidationError>) {
if !dt.contains('T') {
errors.push(ValidationError {
message: "DateTime must be in ISO 8601 format (YYYY-MM-DDTHH:MM:SS)".to_string(),
location: None,
error_type: ValidationErrorType::Syntax,
});
}
}
fn validate_duration_format(dur: &str, errors: &mut Vec<ValidationError>) {
if !dur.starts_with('P') {
errors.push(ValidationError {
message: "Duration must be in ISO 8601 format (P1Y2M3DT4H5M6S)".to_string(),
location: None,
error_type: ValidationErrorType::Syntax,
});
}
}
fn validate_timewindow_format(tw: &str, errors: &mut Vec<ValidationError>) {
if !tw.starts_with("TIME_WINDOW(") || !tw.ends_with(')') {
errors.push(ValidationError {
message: "TimeWindow must be in format TIME_WINDOW(start, end)".to_string(),
location: None,
error_type: ValidationErrorType::Syntax,
});
}
}
fn validate_edge_patterns(query: &Query, errors: &mut Vec<ValidationError>) {
match query {
Query::Basic(basic_query) => {
validate_basic_query_edge_patterns(basic_query, errors);
}
Query::SetOperation(set_op) => {
validate_edge_patterns(&set_op.left, errors);
validate_edge_patterns(&set_op.right, errors);
}
Query::Limited { query, .. } => {
validate_edge_patterns(query, errors);
}
Query::WithQuery(_) => {
}
Query::Let(_) => {
}
Query::For(_) => {
}
Query::Filter(_) => {
}
Query::Return(_) => {
}
Query::Unwind(_) => {
}
Query::MutationPipeline(_) => {
}
}
}
fn validate_basic_query_edge_patterns(query: &BasicQuery, errors: &mut Vec<ValidationError>) {
for pattern in &query.match_clause.patterns {
for element in &pattern.elements {
if let PatternElement::Edge(edge) = element {
match edge.direction {
EdgeDirection::Both => {
}
EdgeDirection::Incoming => {
}
EdgeDirection::Outgoing => {
}
EdgeDirection::Undirected => {
}
}
for label in &edge.labels {
if label.is_empty() {
errors.push(ValidationError {
message: "Edge label cannot be empty".to_string(),
location: None,
error_type: ValidationErrorType::Syntax,
});
}
}
}
}
}
}
fn validate_temporal_literals(_query: &Query, _errors: &mut Vec<ValidationError>) {
}
fn validate_expression_variables(
expr: &Expression,
ctx: &mut ValidationContext,
errors: &mut Vec<ValidationError>,
) {
validate_expression(expr, ctx, errors);
}
fn validate_call_statement(
call_stmt: &CallStatement,
ctx: &mut ValidationContext,
errors: &mut Vec<ValidationError>,
) {
if !crate::catalog::system_procedures::is_system_procedure(&call_stmt.procedure_name) {
errors.push(ValidationError {
message: format!("Unknown system procedure: {}", call_stmt.procedure_name),
location: Some(call_stmt.location.clone()),
error_type: ValidationErrorType::Semantic,
});
}
for arg in &call_stmt.arguments {
validate_expression(arg, ctx, errors);
}
if let Some(yield_clause) = &call_stmt.yield_clause {
validate_yield_clause(yield_clause, errors);
}
if let Some(where_clause) = &call_stmt.where_clause {
if call_stmt.yield_clause.is_none() {
errors.push(ValidationError {
message: "WHERE clause can only be used with YIELD clause in CALL statements"
.to_string(),
location: Some(where_clause.location.clone()),
error_type: ValidationErrorType::Semantic,
});
} else {
if let Some(yield_clause) = &call_stmt.yield_clause {
validate_where_references_yield_columns(where_clause, yield_clause, errors);
}
}
}
}
fn validate_where_references_yield_columns(
where_clause: &WhereClause,
yield_clause: &YieldClause,
errors: &mut Vec<ValidationError>,
) {
let yielded_columns: std::collections::HashSet<String> = yield_clause
.items
.iter()
.map(|item| item.alias.as_ref().unwrap_or(&item.column_name).clone())
.collect();
let mut referenced_columns = std::collections::HashSet::new();
extract_variable_references(&where_clause.condition, &mut referenced_columns);
for column in referenced_columns {
if !yielded_columns.contains(&column) {
errors.push(ValidationError {
message: format!(
"WHERE clause references column '{}' which is not available from YIELD clause",
column
),
location: Some(where_clause.location.clone()),
error_type: ValidationErrorType::Semantic,
});
}
}
}
fn extract_variable_references(
expr: &Expression,
variables: &mut std::collections::HashSet<String>,
) {
match expr {
Expression::Variable(var) => {
variables.insert(var.name.clone());
}
Expression::PropertyAccess(prop_access) => {
variables.insert(prop_access.property.clone());
}
Expression::Binary(binary_expr) => {
extract_variable_references(&binary_expr.left, variables);
extract_variable_references(&binary_expr.right, variables);
}
Expression::Unary(unary_expr) => {
extract_variable_references(&unary_expr.expression, variables);
}
Expression::FunctionCall(func_call) => {
for arg in &func_call.arguments {
extract_variable_references(arg, variables);
}
}
Expression::Case(case_expr) => match &case_expr.case_type {
crate::ast::ast::CaseType::Simple(simple) => {
extract_variable_references(&simple.test_expression, variables);
for branch in &simple.when_branches {
for when_val in &branch.when_values {
extract_variable_references(when_val, variables);
}
extract_variable_references(&branch.then_expression, variables);
}
if let Some(else_expr) = &simple.else_expression {
extract_variable_references(else_expr, variables);
}
}
crate::ast::ast::CaseType::Searched(searched) => {
for branch in &searched.when_branches {
extract_variable_references(&branch.condition, variables);
extract_variable_references(&branch.then_expression, variables);
}
if let Some(else_expr) = &searched.else_expression {
extract_variable_references(else_expr, variables);
}
}
},
_ => {}
}
}
fn validate_yield_clause(yield_clause: &YieldClause, errors: &mut Vec<ValidationError>) {
let mut seen_names = std::collections::HashSet::new();
for item in &yield_clause.items {
let output_name = item.alias.as_ref().unwrap_or(&item.column_name);
if !seen_names.insert(output_name) {
errors.push(ValidationError {
message: format!("Duplicate YIELD column: {}", output_name),
location: Some(item.location.clone()),
error_type: ValidationErrorType::Semantic,
});
}
}
}
fn validate_select_statement(
select_stmt: &SelectStatement,
ctx: &mut ValidationContext,
errors: &mut Vec<ValidationError>,
) {
match &select_stmt.return_items {
SelectItems::Wildcard { .. } => {
}
SelectItems::Explicit { items, .. } => {
if items.is_empty() {
errors.push(ValidationError {
message: "SELECT statement must have at least one return item".to_string(),
location: Some(select_stmt.location.clone()),
error_type: ValidationErrorType::Structural,
});
}
}
}
if let Some(from_clause) = &select_stmt.from_clause {
validate_from_clause(from_clause, ctx, errors);
}
match &select_stmt.return_items {
SelectItems::Wildcard { .. } => {
}
SelectItems::Explicit { items, .. } => {
for item in items {
validate_expression(&item.expression, ctx, errors);
}
}
}
if let Some(where_clause) = &select_stmt.where_clause {
validate_expression(&where_clause.condition, ctx, errors);
}
if let Some(group_clause) = &select_stmt.group_clause {
for expr in &group_clause.expressions {
validate_expression(expr, ctx, errors);
}
}
if let Some(having_clause) = &select_stmt.having_clause {
validate_expression(&having_clause.condition, ctx, errors);
}
if let Some(order_clause) = &select_stmt.order_clause {
for item in &order_clause.items {
validate_expression(&item.expression, ctx, errors);
}
}
}
fn validate_from_clause(
from_clause: &FromClause,
ctx: &mut ValidationContext,
errors: &mut Vec<ValidationError>,
) {
for graph_expr in &from_clause.graph_expressions {
validate_graph_expression(&graph_expr.graph_expression, ctx, errors);
if let Some(match_clause) = &graph_expr.match_statement {
validate_match_clause(match_clause, ctx, errors);
}
}
}
fn validate_graph_expression(
graph_expr: &GraphExpression,
_ctx: &mut ValidationContext,
errors: &mut Vec<ValidationError>,
) {
match graph_expr {
GraphExpression::Reference(path) => {
if path.segments.is_empty() {
errors.push(ValidationError {
message: "Graph reference path cannot be empty".to_string(),
location: Some(path.location.clone()),
error_type: ValidationErrorType::Semantic,
});
}
}
GraphExpression::Union { left, right, .. } => {
validate_graph_expression(left, _ctx, errors);
validate_graph_expression(right, _ctx, errors);
}
GraphExpression::CurrentGraph => {
}
}
}
fn validate_match_clause(
match_clause: &MatchClause,
ctx: &mut ValidationContext,
errors: &mut Vec<ValidationError>,
) {
if match_clause.patterns.is_empty() {
errors.push(ValidationError {
message: "MATCH clause must have at least one pattern".to_string(),
location: Some(match_clause.location.clone()),
error_type: ValidationErrorType::Structural,
});
}
for pattern in &match_clause.patterns {
validate_path_pattern(pattern, ctx, errors);
}
}
fn validate_path_pattern(
pattern: &PathPattern,
ctx: &mut ValidationContext,
errors: &mut Vec<ValidationError>,
) {
if pattern.elements.is_empty() {
errors.push(ValidationError {
message: "Path pattern must have at least one element".to_string(),
location: Some(pattern.location.clone()),
error_type: ValidationErrorType::Structural,
});
}
for element in &pattern.elements {
match element {
PatternElement::Node(node) => {
validate_node(node, ctx, errors);
}
PatternElement::Edge(edge) => {
validate_edge(edge, ctx, errors);
}
}
}
}
fn validate_node(node: &Node, ctx: &mut ValidationContext, errors: &mut Vec<ValidationError>) {
if let Some(ref identifier) = node.identifier {
ctx.declared_variables.insert(identifier.clone());
ctx.variable_types
.insert(identifier.clone(), GqlType::String { max_length: None }); }
for label in &node.labels {
if label.is_empty() {
errors.push(ValidationError {
message: "Node label cannot be empty".to_string(),
location: Some(node.location.clone()),
error_type: ValidationErrorType::Syntax,
});
}
}
if let Some(properties) = &node.properties {
for property in &properties.properties {
validate_expression(&property.value, ctx, errors);
}
}
}
fn validate_edge(edge: &Edge, ctx: &mut ValidationContext, errors: &mut Vec<ValidationError>) {
if let Some(ref identifier) = edge.identifier {
ctx.declared_variables.insert(identifier.clone());
ctx.variable_types
.insert(identifier.clone(), GqlType::String { max_length: None }); }
for label in &edge.labels {
if label.is_empty() {
errors.push(ValidationError {
message: "Edge label cannot be empty".to_string(),
location: Some(edge.location.clone()),
error_type: ValidationErrorType::Syntax,
});
}
}
if let Some(properties) = &edge.properties {
for property in &properties.properties {
validate_expression(&property.value, ctx, errors);
}
}
}
fn validate_subquery(
query: &Query,
ctx: &mut ValidationContext,
errors: &mut Vec<ValidationError>,
) {
let mut subquery_ctx = ctx.clone();
validate_query_structure(query, errors);
validate_variable_declarations(query, &mut subquery_ctx, errors);
validate_path_patterns(query, &mut subquery_ctx, errors);
validate_expressions(query, &mut subquery_ctx, errors);
validate_temporal_literals(query, errors);
validate_edge_patterns(query, errors);
}
fn validate_procedure_statement(
statement: &Statement,
ctx: &ValidationContext,
errors: &mut Vec<ValidationError>,
) {
match statement {
Statement::Query(query) => {
validate_procedure_query(query, ctx, errors);
}
Statement::DataStatement(_data_stmt) => {
}
_ => {
let doc = Document {
statement: statement.clone(),
location: Default::default(),
};
if let Err(mut nested_errors) = validate_query(&doc, ctx.has_graph_context) {
errors.append(&mut nested_errors);
}
}
}
}
fn validate_catalog_statement(
catalog_stmt: &CatalogStatement,
_ctx: &mut ValidationContext,
errors: &mut Vec<ValidationError>,
) {
match catalog_stmt {
CatalogStatement::CreateSchema(create_schema) => {
validate_create_schema_statement(create_schema, errors);
}
CatalogStatement::DropSchema(drop_schema) => {
validate_drop_schema_statement(drop_schema, errors);
}
CatalogStatement::CreateGraph(create_graph) => {
validate_create_graph_statement(create_graph, errors);
}
CatalogStatement::DropGraph(drop_graph) => {
validate_drop_graph_statement(drop_graph, errors);
}
_ => {
}
}
}
fn validate_procedure_query(
query: &Query,
ctx: &ValidationContext,
errors: &mut Vec<ValidationError>,
) {
match query {
Query::Basic(basic_query) => {
if basic_query.match_clause.patterns.is_empty() {
errors.push(ValidationError {
message: "Query must have at least one path pattern in MATCH clause"
.to_string(),
location: None,
error_type: ValidationErrorType::Structural,
});
}
let mut temp_ctx = ctx.clone();
validate_basic_query_variables(basic_query, &mut temp_ctx, errors);
validate_basic_query_path_patterns(basic_query, &mut temp_ctx, errors);
validate_basic_query_expressions(basic_query, &mut temp_ctx, errors);
}
_ => {
}
}
}
fn validate_create_schema_statement(
create_schema: &CreateSchemaStatement,
errors: &mut Vec<ValidationError>,
) {
if create_schema.schema_path.segments.is_empty()
|| create_schema
.schema_path
.segments
.iter()
.any(|s| s.trim().is_empty())
{
errors.push(ValidationError {
message: "Invalid schema name".to_string(),
location: Some(create_schema.location.clone()),
error_type: ValidationErrorType::Syntax,
});
}
}
fn validate_drop_schema_statement(
drop_schema: &DropSchemaStatement,
errors: &mut Vec<ValidationError>,
) {
if drop_schema.schema_path.segments.is_empty()
|| drop_schema
.schema_path
.segments
.iter()
.any(|s| s.trim().is_empty())
{
errors.push(ValidationError {
message: "Invalid schema name".to_string(),
location: Some(drop_schema.location.clone()),
error_type: ValidationErrorType::Syntax,
});
}
}
fn validate_create_graph_statement(
create_graph: &CreateGraphStatement,
errors: &mut Vec<ValidationError>,
) {
if create_graph.graph_path.segments.is_empty()
|| create_graph
.graph_path
.segments
.iter()
.any(|s| s.trim().is_empty())
{
errors.push(ValidationError {
message: "Invalid graph name".to_string(),
location: Some(create_graph.location.clone()),
error_type: ValidationErrorType::Syntax,
});
return;
}
match create_graph.graph_path.segments.len() {
1 => {
}
2 => {
}
_ => {
errors.push(ValidationError {
message: "Invalid graph path: must be either 'graph_name' (when schema is set) or '/schema_name/graph_name'".to_string(),
location: Some(create_graph.location.clone()),
error_type: ValidationErrorType::Syntax,
});
}
}
}
fn validate_drop_graph_statement(
drop_graph: &DropGraphStatement,
errors: &mut Vec<ValidationError>,
) {
if drop_graph.graph_path.segments.is_empty()
|| drop_graph
.graph_path
.segments
.iter()
.any(|s| s.trim().is_empty())
{
errors.push(ValidationError {
message: "Invalid graph name".to_string(),
location: Some(drop_graph.location.clone()),
error_type: ValidationErrorType::Syntax,
});
return;
}
match drop_graph.graph_path.segments.len() {
1 => {
}
2 => {
}
_ => {
errors.push(ValidationError {
message: "Invalid graph path: must be either 'graph_name' (when schema is set) or '/schema_name/graph_name'".to_string(),
location: Some(drop_graph.location.clone()),
error_type: ValidationErrorType::Syntax,
});
}
}
}