use sqlparser::ast::{ Expr, Join, JoinConstraint, JoinOperator, Query, Select, SelectItem, SetExpr, Statement, TableFactor, TableWithJoins, Ident, }; use sqlparser::dialect::PostgreSqlDialect; use sqlparser::parser::Parser; use std::collections::HashSet; pub fn validate_semantic_sql(sql: &str) -> Result<(), String> { let dialect = PostgreSqlDialect {}; let statements = match Parser::parse_sql(&dialect, sql) { Ok(s) => s, Err(e) => return Err(format!("SQL Syntax Error: {}\nSQL: {}", e, sql)), }; for statement in statements { validate_statement(&statement, sql)?; } Ok(()) } fn validate_statement(stmt: &Statement, original_sql: &str) -> Result<(), String> { match stmt { Statement::Query(query) => validate_query(query, original_sql)?, Statement::Insert(insert) => { if let Some(query) = &insert.source { validate_query(query, original_sql)? } } Statement::Update(update) => { if let Some(expr) = &update.selection { validate_expr(expr, &HashSet::new(), original_sql)?; } } Statement::Delete(delete) => { if let Some(expr) = &delete.selection { validate_expr(expr, &HashSet::new(), original_sql)?; } } _ => {} } Ok(()) } fn validate_query(query: &Query, original_sql: &str) -> Result<(), String> { if let SetExpr::Select(select) = &*query.body { validate_select(select, original_sql)?; } Ok(()) } fn validate_select(select: &Select, original_sql: &str) -> Result<(), String> { let mut available_aliases = HashSet::new(); // 1. Collect all declared table aliases in the FROM clause and JOINs for table_with_joins in &select.from { collect_aliases_from_table_factor(&table_with_joins.relation, &mut available_aliases); for join in &table_with_joins.joins { collect_aliases_from_table_factor(&join.relation, &mut available_aliases); } } // 2. Validate all SELECT projection fields for projection in &select.projection { if let SelectItem::UnnamedExpr(expr) | SelectItem::ExprWithAlias { expr, .. } = projection { validate_expr(expr, &available_aliases, original_sql)?; } } // 3. Validate ON conditions in joins for table_with_joins in &select.from { for join in &table_with_joins.joins { if let JoinOperator::Inner(JoinConstraint::On(expr)) | JoinOperator::LeftOuter(JoinConstraint::On(expr)) | JoinOperator::RightOuter(JoinConstraint::On(expr)) | JoinOperator::FullOuter(JoinConstraint::On(expr)) | JoinOperator::Join(JoinConstraint::On(expr)) = &join.join_operator { validate_expr(expr, &available_aliases, original_sql)?; } } } // 4. Validate WHERE conditions if let Some(selection) = &select.selection { validate_expr(selection, &available_aliases, original_sql)?; } Ok(()) } fn collect_aliases_from_table_factor(tf: &TableFactor, aliases: &mut HashSet) { match tf { TableFactor::Table { name, alias, .. } => { if let Some(table_alias) = alias { aliases.insert(table_alias.name.value.clone()); } else if let Some(last) = name.0.last() { match last { sqlparser::ast::ObjectNamePart::Identifier(i) => { aliases.insert(i.value.clone()); } _ => {} } } } TableFactor::Derived { alias: Some(table_alias), .. } => { aliases.insert(table_alias.name.value.clone()); } _ => {} } } fn validate_expr(expr: &Expr, available_aliases: &HashSet, sql: &str) -> Result<(), String> { match expr { Expr::CompoundIdentifier(idents) => { if idents.len() == 2 { let alias = &idents[0].value; if !available_aliases.is_empty() && !available_aliases.contains(alias) { return Err(format!( "Semantic Error: Orchestrated query referenced table alias '{}' but it was not declared in the query's FROM/JOIN clauses.\nAvailable aliases: {:?}\nSQL: {}", alias, available_aliases, sql )); } } else if idents.len() > 2 { let alias = &idents[1].value; // In form schema.table.column, 'table' is idents[1] if !available_aliases.is_empty() && !available_aliases.contains(alias) { return Err(format!( "Semantic Error: Orchestrated query referenced table '{}' but it was not mapped.\nAvailable aliases: {:?}\nSQL: {}", alias, available_aliases, sql )); } } } Expr::BinaryOp { left, right, .. } => { validate_expr(left, available_aliases, sql)?; validate_expr(right, available_aliases, sql)?; } Expr::IsFalse(e) | Expr::IsNotFalse(e) | Expr::IsTrue(e) | Expr::IsNotTrue(e) | Expr::IsNull(e) | Expr::IsNotNull(e) | Expr::InList { expr: e, .. } | Expr::Nested(e) | Expr::UnaryOp { expr: e, .. } | Expr::Cast { expr: e, .. } | Expr::Like { expr: e, .. } | Expr::ILike { expr: e, .. } | Expr::AnyOp { left: e, .. } | Expr::AllOp { left: e, .. } => { validate_expr(e, available_aliases, sql)?; } Expr::Function(func) => { if let sqlparser::ast::FunctionArguments::List(args) = &func.args { if let Some(sqlparser::ast::FunctionArg::Unnamed(sqlparser::ast::FunctionArgExpr::Expr(e))) = args.args.get(0) { validate_expr(e, available_aliases, sql)?; } } } _ => {} } Ok(()) }