use crate::ast;
use crate::error::ParseError;
use crate::grammar;
use pest::iterators::{Pair, Pairs};
use pest::Parser;
#[cfg(test)]
fn print_tree(pair: Pair<grammar::Rule>, indent: usize) {
if matches!(pair.as_rule(), grammar::Rule::EOI) {
return;
}
println!("{:indent$}{:?}: '{}'", "", pair.as_rule(), pair.as_str().trim(), indent = indent);
for inner in pair.into_inner() {
print_tree(inner, indent + 2);
}
}
#[cfg(test)]
fn debug_print_pairs(pairs: Pairs<grammar::Rule>) {
println!("Parse tree:");
for pair in pairs {
print_tree(pair, 0);
}
}
pub fn parse_selection(input: &str) -> Result<ast::Selection, ParseError> {
if input.trim().is_empty() {
return Ok(ast::Selection { predicate: ast::Predicate::True, order_by: None, limit: None });
}
let pairs = grammar::AnkqlParser::parse(grammar::Rule::Selection, input).map_err(|e| ParseError::SyntaxError(format!("{}", e)))?;
#[cfg(test)]
debug_print_pairs(pairs.clone());
let mut predicate = None;
let mut order_by = None;
let mut limit = None;
for pair in pairs {
match pair.as_rule() {
grammar::Rule::Expr => {
predicate = Some(parse_expr(pair)?);
}
grammar::Rule::OrderByClause => {
order_by = Some(parse_order_by_clause(pair)?);
}
grammar::Rule::LimitClause => {
limit = Some(parse_limit_clause(pair)?);
}
grammar::Rule::EOI => {} _ => {
return Err(ParseError::UnexpectedRule { expected: "Expr, OrderByClause, or LimitClause", got: pair.as_rule() });
}
}
}
let predicate = predicate.ok_or(ParseError::EmptyExpression)?;
Ok(ast::Selection { predicate, order_by, limit })
}
fn parse_expr(pair: Pair<grammar::Rule>) -> Result<ast::Predicate, ParseError> {
assert_eq!(pair.as_rule(), grammar::Rule::Expr, "Expected Expr rule");
let mut pairs = pair.into_inner();
let first = pairs.next().ok_or(ParseError::MissingOperand("first"))?;
if first.as_rule() == grammar::Rule::UnaryNot {
let next: Pair<'_, grammar::Rule> = pairs.next().ok_or(ParseError::EmptyExpression)?;
return Ok(ast::Predicate::Not(Box::new(match next.as_rule() {
grammar::Rule::ExpressionInParentheses => {
parse_expr(next.into_inner().next().ok_or(ParseError::EmptyExpression)?)?
}
_ => {
return Err(ParseError::UnexpectedRule { expected: "ExpressionInParentheses", got: next.as_rule() });
}
})));
}
let mut result = parse_atomic_expr(first)?;
while let Some(op) = pairs.next() {
match op.as_rule() {
grammar::Rule::IsNullPostfix => {
let is_not = op.as_str().to_uppercase().contains("NOT");
let is_null = ast::Expr::Predicate(ast::Predicate::IsNull(Box::new(result)));
result = if is_not { ast::Expr::Predicate(ast::Predicate::Not(Box::new(is_null.try_into()?))) } else { is_null };
}
_ => {
let right = pairs.next().ok_or(ParseError::MissingOperand("right"))?;
result = match op.as_rule() {
grammar::Rule::Eq
| grammar::Rule::GtEq
| grammar::Rule::Gt
| grammar::Rule::LtEq
| grammar::Rule::Lt
| grammar::Rule::NotEq
| grammar::Rule::In => create_comparison(result, op.as_rule(), right)?,
grammar::Rule::And | grammar::Rule::Or => create_logical_op(op.as_rule(), result, right, &mut pairs)?,
_ => {
return Err(ParseError::UnexpectedRule { expected: "comparison operator, And, or Or", got: op.as_rule() });
}
}
}
};
}
result.try_into()
}
fn create_comparison(left: ast::Expr, op: grammar::Rule, right: Pair<grammar::Rule>) -> Result<ast::Expr, ParseError> {
let right_expr = parse_atomic_expr(right)?;
let operator = match op {
grammar::Rule::Eq => ast::ComparisonOperator::Equal,
grammar::Rule::GtEq => ast::ComparisonOperator::GreaterThanOrEqual,
grammar::Rule::Gt => ast::ComparisonOperator::GreaterThan,
grammar::Rule::LtEq => ast::ComparisonOperator::LessThanOrEqual,
grammar::Rule::Lt => ast::ComparisonOperator::LessThan,
grammar::Rule::NotEq => ast::ComparisonOperator::NotEqual,
grammar::Rule::In => ast::ComparisonOperator::In,
_ => {
return Err(ParseError::UnexpectedRule { expected: "comparison operator", got: op });
}
};
Ok(ast::Expr::Predicate(ast::Predicate::Comparison { left: Box::new(left), operator, right: Box::new(right_expr) }))
}
fn create_logical_op(
op: grammar::Rule,
left: ast::Expr,
right: Pair<grammar::Rule>,
rest: &mut Pairs<grammar::Rule>,
) -> Result<ast::Expr, ParseError> {
let left_pred = left.try_into()?;
let right_expr = parse_atomic_expr(right)?;
let right_pred = if let Some(next_op) = rest.next() {
match next_op.as_rule() {
grammar::Rule::Eq
| grammar::Rule::GtEq
| grammar::Rule::Gt
| grammar::Rule::LtEq
| grammar::Rule::Lt
| grammar::Rule::NotEq
| grammar::Rule::In => {
let next_right = rest.next().ok_or(ParseError::MissingOperand("comparison right"))?;
let next_right_expr = parse_atomic_expr(next_right)?;
ast::Predicate::Comparison {
left: Box::new(right_expr),
operator: match next_op.as_rule() {
grammar::Rule::Eq => ast::ComparisonOperator::Equal,
grammar::Rule::GtEq => ast::ComparisonOperator::GreaterThanOrEqual,
grammar::Rule::Gt => ast::ComparisonOperator::GreaterThan,
grammar::Rule::LtEq => ast::ComparisonOperator::LessThanOrEqual,
grammar::Rule::Lt => ast::ComparisonOperator::LessThan,
grammar::Rule::NotEq => ast::ComparisonOperator::NotEqual,
grammar::Rule::In => ast::ComparisonOperator::In,
_ => unimplemented!("rule not implemented: {:?}", next_op.as_rule()),
},
right: Box::new(next_right_expr),
}
}
_ => {
return Err(ParseError::UnexpectedRule { expected: "comparison operator", got: next_op.as_rule() });
}
}
} else {
right_expr.try_into()?
};
Ok(ast::Expr::Predicate(match op {
grammar::Rule::And => ast::Predicate::And(Box::new(left_pred), Box::new(right_pred)),
grammar::Rule::Or => ast::Predicate::Or(Box::new(left_pred), Box::new(right_pred)),
_ => unimplemented!("rule not implemented: {:?}", op),
}))
}
fn parse_atomic_expr(pair: Pair<grammar::Rule>) -> Result<ast::Expr, ParseError> {
match pair.as_rule() {
grammar::Rule::PathExpr => parse_path_expr(pair),
grammar::Rule::SingleQuotedString => parse_string_literal(pair),
grammar::Rule::True => Ok(ast::Expr::Literal(ast::Literal::Bool(true))),
grammar::Rule::False => Ok(ast::Expr::Literal(ast::Literal::Bool(false))),
grammar::Rule::Unsigned => parse_number(pair),
grammar::Rule::QuestionParameter => Ok(ast::Expr::Placeholder),
grammar::Rule::ExpressionInParentheses => {
let inner = pair.into_inner().next().ok_or(ParseError::EmptyExpression)?;
let pred = parse_expr(inner)?;
Ok(ast::Expr::Predicate(pred))
}
grammar::Rule::Row => {
let mut exprs = Vec::new();
for expr_pair in pair.into_inner() {
if expr_pair.as_rule() == grammar::Rule::Expr {
let expr = parse_atomic_expr(expr_pair.into_inner().next().ok_or(ParseError::EmptyExpression)?)?;
exprs.push(expr);
} else {
exprs.push(parse_atomic_expr(expr_pair)?);
}
}
Ok(ast::Expr::ExprList(exprs))
}
_ => Err(ParseError::UnexpectedRule { expected: "atomic expression", got: pair.as_rule() }),
}
}
fn parse_path_expr(pair: Pair<grammar::Rule>) -> Result<ast::Expr, ParseError> {
if pair.as_rule() != grammar::Rule::PathExpr {
return Err(ParseError::UnexpectedRule { expected: "PathExpr", got: pair.as_rule() });
}
let steps: Vec<String> =
pair.into_inner().filter(|p| p.as_rule() == grammar::Rule::Identifier).map(|p| p.as_str().trim().to_string()).collect();
if steps.is_empty() {
return Err(ParseError::InvalidPredicate("Empty path expression".into()));
}
Ok(ast::Expr::Path(ast::PathExpr { steps }))
}
fn parse_string_literal(pair: Pair<grammar::Rule>) -> Result<ast::Expr, ParseError> {
if pair.as_rule() != grammar::Rule::SingleQuotedString {
return Err(ParseError::UnexpectedRule { expected: "SingleQuotedString", got: pair.as_rule() });
}
let s = pair.as_str();
if !s.starts_with('\'') || !s.ends_with('\'') {
return Err(ParseError::InvalidPredicate("String literal must be quoted".into()));
}
let s = &s[1..s.len() - 1];
Ok(ast::Expr::Literal(ast::Literal::String(s.to_string())))
}
fn parse_number(pair: Pair<grammar::Rule>) -> Result<ast::Expr, ParseError> {
if pair.as_rule() != grammar::Rule::Unsigned {
return Err(ParseError::UnexpectedRule { expected: "Unsigned", got: pair.as_rule() });
}
let num = pair.as_str().trim().parse::<i64>().map_err(|e| ParseError::InvalidPredicate(format!("Failed to parse number: {}", e)))?;
if num < i32::MAX as i64 && num > i32::MIN as i64 {
return Ok(ast::Expr::Literal(ast::Literal::I32(num as i32)));
}
Ok(ast::Expr::Literal(ast::Literal::I64(num)))
}
fn parse_limit_clause(pair: Pair<grammar::Rule>) -> Result<u64, ParseError> {
if pair.as_rule() != grammar::Rule::LimitClause {
return Err(ParseError::UnexpectedRule { expected: "LimitClause", got: pair.as_rule() });
}
let unsigned_pair = pair
.into_inner()
.find(|p| p.as_rule() == grammar::Rule::Unsigned)
.ok_or(ParseError::InvalidPredicate("Missing limit value".into()))?;
let limit =
unsigned_pair.as_str().trim().parse::<u64>().map_err(|e| ParseError::InvalidPredicate(format!("Failed to parse limit: {}", e)))?;
Ok(limit)
}
fn parse_order_by_clause(pair: Pair<grammar::Rule>) -> Result<Vec<ast::OrderByItem>, ParseError> {
if pair.as_rule() != grammar::Rule::OrderByClause {
return Err(ParseError::UnexpectedRule { expected: "OrderByClause", got: pair.as_rule() });
}
let mut order_by_items = Vec::new();
for inner_pair in pair.into_inner() {
if inner_pair.as_rule() == grammar::Rule::OrderByItem {
let order_by_item = parse_order_by_item(inner_pair)?;
order_by_items.push(order_by_item);
}
}
if order_by_items.is_empty() {
return Err(ParseError::InvalidPredicate("Missing ORDER BY items".into()));
}
Ok(order_by_items)
}
fn parse_order_by_item(pair: Pair<grammar::Rule>) -> Result<ast::OrderByItem, ParseError> {
if pair.as_rule() != grammar::Rule::OrderByItem {
return Err(ParseError::UnexpectedRule { expected: "OrderByItem", got: pair.as_rule() });
}
let inner_pairs: Vec<_> = pair.into_inner().collect();
let identifier_pair = inner_pairs
.iter()
.find(|p| p.as_rule() == grammar::Rule::Identifier)
.ok_or(ParseError::InvalidPredicate("Missing column name in ORDER BY item".into()))?;
let identifier_str = identifier_pair.as_str().trim();
if identifier_str.contains('.') {
return Err(ParseError::InvalidPredicate("Dotted identifiers are not supported in ORDER BY clauses".into()));
}
let path = ast::PathExpr::simple(identifier_str);
let direction = inner_pairs
.iter()
.find(|p| p.as_rule() == grammar::Rule::OrderDirection)
.map(|p| match p.as_str().trim().to_uppercase().as_str() {
"ASC" => Ok(ast::OrderDirection::Asc),
"DESC" => Ok(ast::OrderDirection::Desc),
_ => Err(ParseError::InvalidPredicate(format!("Invalid order direction: {}", p.as_str()))),
})
.transpose()?
.unwrap_or(ast::OrderDirection::Asc);
Ok(ast::OrderByItem { path, direction })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_selection_status_active() {
let input = r#"status = 'active'"#;
let selection = parse_selection(input).unwrap();
assert_eq!(
selection,
ast::Selection {
predicate: ast::Predicate::Comparison {
left: Box::new(ast::Expr::Path(ast::PathExpr::simple("status".to_string()))),
operator: ast::ComparisonOperator::Equal,
right: Box::new(ast::Expr::Literal(ast::Literal::String("active".to_string())))
},
order_by: None,
limit: None,
}
);
}
#[test]
fn test_parse_selection_user_and_status() {
let input = r#"user = 123 AND status = 'active'"#;
let selection = parse_selection(input).unwrap();
assert_eq!(
selection,
ast::Selection {
predicate: ast::Predicate::And(
Box::new(ast::Predicate::Comparison {
left: Box::new(ast::Expr::Path(ast::PathExpr::simple("user".to_string()))),
operator: ast::ComparisonOperator::Equal,
right: Box::new(ast::Expr::Literal(ast::Literal::I32(123)))
}),
Box::new(ast::Predicate::Comparison {
left: Box::new(ast::Expr::Path(ast::PathExpr::simple("status".to_string()))),
operator: ast::ComparisonOperator::Equal,
right: Box::new(ast::Expr::Literal(ast::Literal::String("active".to_string())))
})
),
order_by: None,
limit: None,
}
);
}
#[test]
fn test_parse_selection_user_or_and_status() {
let input = r#"(user = 123 OR user = 456) AND status = 'active'"#;
let selection = parse_selection(input).unwrap();
assert_eq!(
selection.predicate,
ast::Predicate::And(
Box::new(ast::Predicate::Or(
Box::new(ast::Predicate::Comparison {
left: Box::new(ast::Expr::Path(ast::PathExpr::simple("user".to_string()))),
operator: ast::ComparisonOperator::Equal,
right: Box::new(ast::Expr::Literal(ast::Literal::I32(123)))
}),
Box::new(ast::Predicate::Comparison {
left: Box::new(ast::Expr::Path(ast::PathExpr::simple("user".to_string()))),
operator: ast::ComparisonOperator::Equal,
right: Box::new(ast::Expr::Literal(ast::Literal::I32(456)))
})
)),
Box::new(ast::Predicate::Comparison {
left: Box::new(ast::Expr::Path(ast::PathExpr::simple("status".to_string()))),
operator: ast::ComparisonOperator::Equal,
right: Box::new(ast::Expr::Literal(ast::Literal::String("active".to_string())))
})
)
);
}
#[test]
fn test_parse_selection_status_is_null() {
let input = r#"status IS NULL"#;
let selection = parse_selection(input).unwrap();
assert_eq!(selection.predicate, ast::Predicate::IsNull(Box::new(ast::Expr::Path(ast::PathExpr::simple("status".to_string())))));
}
#[test]
fn test_parse_selection_status_is_not_null() {
let input = r#"status IS NOT NULL"#;
let selection = parse_selection(input).unwrap();
assert_eq!(
selection.predicate,
ast::Predicate::Not(Box::new(ast::Predicate::IsNull(Box::new(ast::Expr::Path(ast::PathExpr::simple("status".to_string()))))))
);
}
#[test]
fn unary_not_parenthesized() {
let input = r#"NOT (status = 'active')"#;
let selection = parse_selection(input).unwrap();
assert_eq!(
selection.predicate,
ast::Predicate::Not(Box::new(ast::Predicate::Comparison {
left: Box::new(ast::Expr::Path(ast::PathExpr::simple("status".to_string()))),
operator: ast::ComparisonOperator::Equal,
right: Box::new(ast::Expr::Literal(ast::Literal::String("active".to_string())))
}))
);
}
#[test]
fn unary_not_unparenthesized() {
let input = r#"NOT status = 'active'"#;
matches!(
parse_selection(input),
Err(ParseError::UnexpectedRule { expected: "ExpressionInParentheses", got: grammar::Rule::ExpressionInParentheses })
);
}
#[test]
fn test_parse_empty_string() {
let input = "";
let selection = parse_selection(input).unwrap();
assert_eq!(selection.predicate, ast::Predicate::True);
}
#[test]
fn test_parse_true_literal() {
let input = "true";
let selection = parse_selection(input).unwrap();
assert_eq!(selection.predicate, ast::Predicate::True);
}
#[test]
fn test_parse_selection_in_clause() {
let input = r#"status IN ('active', 'pending')"#;
let selection = parse_selection(input).unwrap();
assert_eq!(
selection.predicate,
ast::Predicate::Comparison {
left: Box::new(ast::Expr::Path(ast::PathExpr::simple("status".to_string()))),
operator: ast::ComparisonOperator::In,
right: Box::new(ast::Expr::ExprList(vec![
ast::Expr::Literal(ast::Literal::String("active".to_string())),
ast::Expr::Literal(ast::Literal::String("pending".to_string())),
]))
}
);
}
#[test]
fn test_parse_selection_in_clause_numbers() {
let input = r#"user_id IN (1, 2, 3)"#;
let selection = parse_selection(input).unwrap();
assert_eq!(
selection.predicate,
ast::Predicate::Comparison {
left: Box::new(ast::Expr::Path(ast::PathExpr::simple("user_id".to_string()))),
operator: ast::ComparisonOperator::In,
right: Box::new(ast::Expr::ExprList(vec![
ast::Expr::Literal(ast::Literal::I32(1)),
ast::Expr::Literal(ast::Literal::I32(2)),
ast::Expr::Literal(ast::Literal::I32(3)),
]))
}
);
}
#[test]
fn test_comparison_to_true() {
let input = r#"bool_field = true"#;
let selection = parse_selection(input).unwrap();
assert_eq!(
selection.predicate,
ast::Predicate::Comparison {
left: Box::new(ast::Expr::Path(ast::PathExpr::simple("bool_field".to_string()))),
operator: ast::ComparisonOperator::Equal,
right: Box::new(ast::Expr::Literal(ast::Literal::Bool(true)))
}
);
}
#[test]
fn test_comparison_to_false() {
let input = r#"bool_field <> false"#;
let selection = parse_selection(input).unwrap();
assert_eq!(
selection.predicate,
ast::Predicate::Comparison {
left: Box::new(ast::Expr::Path(ast::PathExpr::simple("bool_field".to_string()))),
operator: ast::ComparisonOperator::NotEqual,
right: Box::new(ast::Expr::Literal(ast::Literal::Bool(false)))
}
);
}
#[test]
fn test_comparison_to_left_operand_boolean() {
let input = r#"false <> bool_field"#;
let selection = parse_selection(input).unwrap();
assert_eq!(
selection.predicate,
ast::Predicate::Comparison {
left: Box::new(ast::Expr::Literal(ast::Literal::Bool(false))),
operator: ast::ComparisonOperator::NotEqual,
right: Box::new(ast::Expr::Path(ast::PathExpr::simple("bool_field".to_string())))
}
);
}
#[test]
fn test_placeholders() -> anyhow::Result<()> {
assert_eq!(
parse_selection("user_id = ?")?.predicate,
ast::Predicate::Comparison {
left: Box::new(ast::Expr::Path(ast::PathExpr::simple("user_id".to_string()))),
operator: ast::ComparisonOperator::Equal,
right: Box::new(ast::Expr::Placeholder)
}
);
assert_eq!(
parse_selection("user_id = ? AND status = ?")?.predicate,
ast::Predicate::And(
Box::new(ast::Predicate::Comparison {
left: Box::new(ast::Expr::Path(ast::PathExpr::simple("user_id".to_string()))),
operator: ast::ComparisonOperator::Equal,
right: Box::new(ast::Expr::Placeholder)
}),
Box::new(ast::Predicate::Comparison {
left: Box::new(ast::Expr::Path(ast::PathExpr::simple("status".to_string()))),
operator: ast::ComparisonOperator::Equal,
right: Box::new(ast::Expr::Placeholder)
})
)
);
assert_eq!(
parse_selection("status IN (?, ?, ?)")?.predicate,
ast::Predicate::Comparison {
left: Box::new(ast::Expr::Path(ast::PathExpr::simple("status".to_string()))),
operator: ast::ComparisonOperator::In,
right: Box::new(ast::Expr::ExprList(vec![ast::Expr::Placeholder, ast::Expr::Placeholder, ast::Expr::Placeholder,]))
}
);
assert_eq!(
parse_selection("? AND ?")?.predicate,
ast::Predicate::And(Box::new(ast::Predicate::Placeholder), Box::new(ast::Predicate::Placeholder))
);
assert_eq!(
parse_selection("? OR ?")?.predicate,
ast::Predicate::Or(Box::new(ast::Predicate::Placeholder), Box::new(ast::Predicate::Placeholder))
);
assert_eq!(parse_selection("?")?.predicate, ast::Predicate::Placeholder);
assert_eq!(
parse_selection("? AND foo = ?")?.predicate,
ast::Predicate::And(
Box::new(ast::Predicate::Placeholder),
Box::new(ast::Predicate::Comparison {
left: Box::new(ast::Expr::Path(ast::PathExpr::simple("foo".to_string()))),
operator: ast::ComparisonOperator::Equal,
right: Box::new(ast::Expr::Placeholder),
})
)
);
Ok(())
}
#[test]
fn test_boolean_literals() -> anyhow::Result<()> {
assert_eq!(parse_selection("true")?.predicate, ast::Predicate::True);
assert_eq!(parse_selection("false")?.predicate, ast::Predicate::False);
Ok(())
}
#[test]
fn test_order_by_basic() -> anyhow::Result<()> {
let selection = parse_selection("status = 'active' ORDER BY name")?;
assert_eq!(
selection.predicate,
ast::Predicate::Comparison {
left: Box::new(ast::Expr::Path(ast::PathExpr::simple("status".to_string()))),
operator: ast::ComparisonOperator::Equal,
right: Box::new(ast::Expr::Literal(ast::Literal::String("active".to_string())))
}
);
assert_eq!(
selection.order_by,
Some(vec![ast::OrderByItem { path: ast::PathExpr::simple("name".to_string()), direction: ast::OrderDirection::Asc }])
);
assert_eq!(selection.limit, None);
Ok(())
}
#[test]
fn test_order_by_with_direction() -> anyhow::Result<()> {
let selection = parse_selection("true ORDER BY created_at DESC")?;
assert_eq!(selection.predicate, ast::Predicate::True);
assert_eq!(
selection.order_by,
Some(vec![ast::OrderByItem { path: ast::PathExpr::simple("created_at".to_string()), direction: ast::OrderDirection::Desc }])
);
Ok(())
}
#[test]
fn test_order_by_dotted_identifier_not_supported() -> anyhow::Result<()> {
let result = parse_selection("true ORDER BY user.name ASC");
assert!(result.is_err());
Ok(())
}
#[test]
fn test_limit_basic() -> anyhow::Result<()> {
let selection = parse_selection("status = 'active' LIMIT 10")?;
assert_eq!(
selection.predicate,
ast::Predicate::Comparison {
left: Box::new(ast::Expr::Path(ast::PathExpr::simple("status".to_string()))),
operator: ast::ComparisonOperator::Equal,
right: Box::new(ast::Expr::Literal(ast::Literal::String("active".to_string())))
}
);
assert_eq!(selection.order_by, None);
assert_eq!(selection.limit, Some(10));
Ok(())
}
#[test]
fn test_order_by_and_limit() -> anyhow::Result<()> {
let selection = parse_selection("user_id > 100 ORDER BY created_at DESC LIMIT 5")?;
assert_eq!(
selection.predicate,
ast::Predicate::Comparison {
left: Box::new(ast::Expr::Path(ast::PathExpr::simple("user_id".to_string()))),
operator: ast::ComparisonOperator::GreaterThan,
right: Box::new(ast::Expr::Literal(ast::Literal::I32(100)))
}
);
assert_eq!(
selection.order_by,
Some(vec![ast::OrderByItem { path: ast::PathExpr::simple("created_at".to_string()), direction: ast::OrderDirection::Desc }])
);
assert_eq!(selection.limit, Some(5));
Ok(())
}
#[test]
fn test_limit_only() -> anyhow::Result<()> {
let selection = parse_selection("true LIMIT 100")?;
assert_eq!(selection.predicate, ast::Predicate::True);
assert_eq!(selection.order_by, None);
assert_eq!(selection.limit, Some(100));
Ok(())
}
#[test]
fn test_order_by_only() -> anyhow::Result<()> {
let selection = parse_selection("true ORDER BY score")?;
assert_eq!(selection.predicate, ast::Predicate::True);
assert_eq!(
selection.order_by,
Some(vec![ast::OrderByItem { path: ast::PathExpr::simple("score".to_string()), direction: ast::OrderDirection::Asc }])
);
assert_eq!(selection.limit, None);
Ok(())
}
#[test]
fn test_order_by_multiple_items() -> anyhow::Result<()> {
let selection = parse_selection("true ORDER BY name ASC, created_at DESC, id")?;
assert_eq!(selection.predicate, ast::Predicate::True);
assert_eq!(
selection.order_by,
Some(vec![
ast::OrderByItem { path: ast::PathExpr::simple("name".to_string()), direction: ast::OrderDirection::Asc },
ast::OrderByItem { path: ast::PathExpr::simple("created_at".to_string()), direction: ast::OrderDirection::Desc },
ast::OrderByItem {
path: ast::PathExpr::simple("id".to_string()),
direction: ast::OrderDirection::Asc }
])
);
assert_eq!(selection.limit, None);
Ok(())
}
#[test]
fn test_pathological_keyword_cases() -> anyhow::Result<()> {
let selection = parse_selection("limit = 1")?;
assert_eq!(
selection.predicate,
ast::Predicate::Comparison {
left: Box::new(ast::Expr::Path(ast::PathExpr::simple("limit".to_string()))),
operator: ast::ComparisonOperator::Equal,
right: Box::new(ast::Expr::Literal(ast::Literal::I32(1)))
}
);
let selection = parse_selection("order = 2 ORDER BY name")?;
assert_eq!(
selection.predicate,
ast::Predicate::Comparison {
left: Box::new(ast::Expr::Path(ast::PathExpr::simple("order".to_string()))),
operator: ast::ComparisonOperator::Equal,
right: Box::new(ast::Expr::Literal(ast::Literal::I32(2)))
}
);
assert_eq!(
selection.order_by,
Some(vec![ast::OrderByItem { path: ast::PathExpr::simple("name".to_string()), direction: ast::OrderDirection::Asc }])
);
Ok(())
}
#[test]
fn test_raw_parsing() {
println!("=== Testing Raw Grammar Parsing ===\n");
let test_cases = vec![
"true",
"true or false",
"true and false",
"true LIMIT 10",
"status = 'active'",
"status = 'active' LIMIT 5",
"limit = 1", "limit = 1 LIMIT 10", "foo = 1 order by name",
"true ORDER BY name ASC",
"true ORDER BY name DESC",
"true ORDER BY name LIMIT 10",
"order = 1", "by = 2", "order = 1 ORDER BY name", ];
for (i, input) in test_cases.iter().enumerate() {
println!("Test {}: '{}'", i + 1, input);
match grammar::AnkqlParser::parse(grammar::Rule::Selection, input) {
Ok(pairs) => {
println!("✅ Grammar parsed successfully!");
for pair in pairs {
print_parse_tree(pair, 0);
}
}
Err(e) => {
panic!("❌ Grammar parse error: {}", e);
}
}
}
}
fn print_parse_tree(pair: pest::iterators::Pair<grammar::Rule>, indent: usize) {
let indent_str = " ".repeat(indent);
println!("{}Rule::{:?} -> '{}'", indent_str, pair.as_rule(), pair.as_str());
for inner_pair in pair.into_inner() {
print_parse_tree(inner_pair, indent + 1);
}
}
}