use sqlparser::{ ast::{Query, SetExpr, Statement}, dialect::PostgreSqlDialect, parser::Parser, }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SqlRoute { Read, Write, } pub fn route_sql(sql: &str) -> SqlRoute { let trimmed = sql.trim_start(); if starts_with_hint(trimmed, "write") { return SqlRoute::Write; } if starts_with_hint(trimmed, "read") { return SqlRoute::Read; } if is_read_query_by_ast(sql) { SqlRoute::Read } else { SqlRoute::Write } } fn starts_with_hint(sql: &str, hint: &str) -> bool { let expected = format!("/*+ {hint} */"); sql.starts_with(&expected) } fn is_read_query_by_ast(sql: &str) -> bool { let dialect = PostgreSqlDialect {}; let Ok(statements) = Parser::parse_sql(&dialect, sql) else { return false; }; if statements.is_empty() { return false; } statements.iter().all(is_read_statement) } fn is_read_statement(statement: &Statement) -> bool { match statement { Statement::Query(query) => is_read_query_ast(query), Statement::ShowVariable { .. } | Statement::ShowVariables { .. } | Statement::ShowCreate { .. } | Statement::ShowColumns { .. } | Statement::ShowTables { .. } => true, _ => false, } } fn is_read_query_ast(query: &Query) -> bool { if !query.locks.is_empty() { return false; } match query.body.as_ref() { SetExpr::Select(_) => true, SetExpr::Query(inner) => is_read_query_ast(inner), SetExpr::SetOperation { left, right, .. } => { is_read_set_expr(left) && is_read_set_expr(right) } SetExpr::Values(_) => true, _ => false, } } fn is_read_set_expr(expr: &SetExpr) -> bool { match expr { SetExpr::Select(_) => true, SetExpr::Query(query) => is_read_query_ast(query), SetExpr::SetOperation { left, right, .. } => { is_read_set_expr(left) && is_read_set_expr(right) } SetExpr::Values(_) => true, _ => false, } }