use super::super::ast::{BinOp, Expr, ExprSubquery, FieldRef, Span, UnaryOp};
use super::super::lexer::Token;
use super::error::ParseError;
use super::Parser;
use super::PlaceholderMode;
use crate::storage::schema::{DataType, Value};
fn is_duration_unit(unit: &str) -> bool {
matches!(
unit.to_ascii_lowercase().as_str(),
"ms" | "msec"
| "millisecond"
| "milliseconds"
| "s"
| "sec"
| "secs"
| "second"
| "seconds"
| "m"
| "min"
| "mins"
| "minute"
| "minutes"
| "h"
| "hr"
| "hrs"
| "hour"
| "hours"
| "d"
| "day"
| "days"
)
}
fn keyword_function_name(token: &Token) -> Option<&'static str> {
match token {
Token::Count => Some("COUNT"),
Token::Sum => Some("SUM"),
Token::Avg => Some("AVG"),
Token::Min => Some("MIN"),
Token::Max => Some("MAX"),
Token::First => Some("FIRST"),
Token::Last => Some("LAST"),
Token::Left => Some("LEFT"),
Token::Right => Some("RIGHT"),
Token::Contains => Some("CONTAINS"),
Token::Kv => Some("KV"),
_ => None,
}
}
fn bare_zero_arg_function_name(name: &str) -> Option<&'static str> {
match name.to_ascii_uppercase().as_str() {
"CURRENT_TIMESTAMP" => Some("CURRENT_TIMESTAMP"),
"CURRENT_DATE" => Some("CURRENT_DATE"),
"CURRENT_TIME" => Some("CURRENT_TIME"),
_ => None,
}
}
impl<'a> Parser<'a> {
pub fn parse_expr(&mut self) -> Result<Expr, ParseError> {
self.parse_expr_prec(0)
}
pub(crate) fn parse_expr_with_min_precedence(
&mut self,
min_prec: u8,
) -> Result<Expr, ParseError> {
self.parse_expr_prec(min_prec)
}
pub(crate) fn continue_expr(&mut self, left: Expr, min_prec: u8) -> Result<Expr, ParseError> {
self.parse_expr_suffix(left, min_prec)
}
fn parse_expr_prec(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
self.enter_depth()?;
let result = (|| {
let left = self.parse_expr_unary()?;
self.parse_expr_suffix(left, min_prec)
})();
self.exit_depth();
result
}
fn parse_expr_suffix(&mut self, mut left: Expr, min_prec: u8) -> Result<Expr, ParseError> {
loop {
let Some((op, prec)) = self.peek_binop() else {
if min_prec <= 32 {
if let Some(node) = self.try_parse_postfix(&left)? {
left = node;
continue;
}
}
break;
};
if prec < min_prec {
break;
}
self.advance()?; let start_span = self.span_start_of(&left);
let rhs = self.parse_expr_prec(prec + 1)?;
let end_span = self.span_end_of(&rhs);
left = Expr::BinaryOp {
op,
lhs: Box::new(left),
rhs: Box::new(rhs),
span: Span::new(start_span, end_span),
};
}
Ok(left)
}
fn parse_expr_unary(&mut self) -> Result<Expr, ParseError> {
match self.peek() {
Token::Not => {
let start = self.position();
self.advance()?;
let operand = self.parse_expr_prec(25)?;
let end = self.span_end_of(&operand);
Ok(Expr::UnaryOp {
op: UnaryOp::Not,
operand: Box::new(operand),
span: Span::new(start, end),
})
}
Token::Dash => {
let start = self.position();
self.advance()?;
let operand = self.parse_expr_prec(70)?;
let end = self.span_end_of(&operand);
Ok(Expr::UnaryOp {
op: UnaryOp::Neg,
operand: Box::new(operand),
span: Span::new(start, end),
})
}
Token::Plus => {
self.advance()?;
self.parse_expr_prec(70)
}
_ => self.parse_expr_factor(),
}
}
fn parse_expr_factor(&mut self) -> Result<Expr, ParseError> {
let start = self.position();
if self.consume(&Token::LParen)? {
if self.check(&Token::Select) {
let query = self.parse_select_query()?;
self.expect(Token::RParen)?;
return Ok(Expr::Subquery {
query: ExprSubquery {
query: Box::new(query),
},
span: Span::new(start, self.position()),
});
}
let inner = self.parse_expr_prec(0)?;
self.expect(Token::RParen)?;
return Ok(inner);
}
if self.consume(&Token::True)? {
return Ok(Expr::Literal {
value: Value::Boolean(true),
span: Span::new(start, self.position()),
});
}
if self.consume(&Token::False)? {
return Ok(Expr::Literal {
value: Value::Boolean(false),
span: Span::new(start, self.position()),
});
}
if self.consume(&Token::Null)? {
return Ok(Expr::Literal {
value: Value::Null,
span: Span::new(start, self.position()),
});
}
if let Token::Integer(n) = *self.peek() {
self.advance()?;
if let Token::Ident(ref unit) = *self.peek() {
if is_duration_unit(unit) {
let duration = format!("{n}{}", unit.to_ascii_lowercase());
self.advance()?;
return Ok(Expr::Literal {
value: Value::text(duration),
span: Span::new(start, self.position()),
});
}
}
return Ok(Expr::Literal {
value: Value::Integer(n),
span: Span::new(start, self.position()),
});
}
if let Token::Float(n) = *self.peek() {
self.advance()?;
return Ok(Expr::Literal {
value: Value::Float(n),
span: Span::new(start, self.position()),
});
}
if let Token::String(ref s) = *self.peek() {
let text = s.clone();
self.advance()?;
return Ok(Expr::Literal {
value: Value::text(text),
span: Span::new(start, self.position()),
});
}
if matches!(
self.peek(),
Token::LBrace | Token::LBracket | Token::JsonLiteral(_)
) {
let value = self
.parse_literal_value()
.map_err(|e| ParseError::new(e.message, self.position()))?;
return Ok(Expr::Literal {
value,
span: Span::new(start, self.position()),
});
}
if self.check(&Token::Question) {
let (index, span) = self.parse_question_param_index()?;
return Ok(Expr::Parameter { index, span });
}
if self.consume(&Token::Dollar)? {
if let Token::Integer(n) = *self.peek() {
if n < 1 {
return Err(ParseError::new(
"placeholder index must be >= 1".to_string(),
self.position(),
));
}
if self.placeholder_mode == PlaceholderMode::Question {
return Err(ParseError::new(
"cannot mix `?` and `$N` placeholders in one statement".to_string(),
self.position(),
));
}
self.placeholder_mode = PlaceholderMode::Dollar;
self.advance()?;
return Ok(Expr::Parameter {
index: (n - 1) as usize,
span: Span::new(start, self.position()),
});
}
let path = self.parse_dollar_ref_path()?;
let path_lc = path.to_ascii_lowercase();
let (name, key) = if let Some(rest) = path_lc.strip_prefix("secret.") {
("__SECRET_REF", format!("red.vault/{rest}"))
} else if path_lc.starts_with("red.secret.") {
let rest = path_lc.trim_start_matches("red.secret.");
("__SECRET_REF", format!("red.vault/{rest}"))
} else if let Some(rest) = path_lc.strip_prefix("config.") {
("CONFIG", format!("red.config/{rest}"))
} else if path_lc.starts_with("red.config.") {
let rest = path_lc.trim_start_matches("red.config.");
("CONFIG", format!("red.config/{rest}"))
} else {
return Err(ParseError::new(
format!(
"unknown $ reference `${path}`; expected $secret.*, $red.secret.*, $config.*, or $red.config.*"
),
self.position(),
));
};
return Ok(Expr::FunctionCall {
name: name.to_string(),
args: vec![Expr::Literal {
value: Value::text(key),
span: Span::new(start, self.position()),
}],
span: Span::new(start, self.position()),
});
}
if let Some(name) = keyword_function_name(self.peek()) {
if matches!(self.peek_next()?, Token::LParen) {
self.advance()?; return self.parse_function_call_expr_with_name(start, name.to_string());
}
}
if let Token::Ident(ref name) = *self.peek() {
let name_upper = name.to_uppercase();
if name_upper == "CASE" {
return self.parse_case_expr(start);
}
let saved_name = name.clone();
self.advance()?;
if matches!(self.peek(), Token::LParen) {
return self.parse_function_call_expr_with_name(start, saved_name);
}
if let Some(function_name) = bare_zero_arg_function_name(&saved_name) {
let end = self.position();
return Ok(Expr::FunctionCall {
name: function_name.to_string(),
args: Vec::new(),
span: Span::new(start, end),
});
}
if matches!(self.peek(), Token::Dot) {
let mut segments = vec![saved_name];
while self.consume(&Token::Dot)? {
segments.push(self.expect_ident_or_keyword()?);
}
let field = FieldRef::TableColumn {
table: segments.remove(0),
column: segments.join("."),
};
let end = self.position();
return Ok(Expr::Column {
field,
span: Span::new(start, end),
});
}
let field = FieldRef::TableColumn {
table: String::new(),
column: saved_name,
};
let end = self.position();
return Ok(Expr::Column {
field,
span: Span::new(start, end),
});
}
let field = self.parse_field_ref()?;
let end = self.position();
Ok(Expr::Column {
field,
span: Span::new(start, end),
})
}
fn parse_dollar_ref_path(&mut self) -> Result<String, ParseError> {
let mut path = self.expect_ident_or_keyword()?;
while self.consume(&Token::Dot)? {
let next = self.expect_ident_or_keyword()?;
path = format!("{path}.{next}");
}
Ok(path)
}
fn parse_function_call_expr_with_name(
&mut self,
start: crate::storage::query::lexer::Position,
function_name: String,
) -> Result<Expr, ParseError> {
self.expect(Token::LParen)?;
if function_name.eq_ignore_ascii_case("CAST") {
let inner = self.parse_expr_prec(0)?;
self.expect(Token::As)?;
let type_name = self.expect_ident_or_keyword()?;
self.expect(Token::RParen)?;
let end = self.position();
let Some(target) = DataType::from_sql_name(&type_name) else {
return Err(ParseError::new(
format!("unknown type name {type_name:?} in CAST"),
self.position(),
));
};
return Ok(Expr::Cast {
inner: Box::new(inner),
target,
span: Span::new(start, end),
});
}
if function_name.eq_ignore_ascii_case("TRIM") {
let (name, args) = self.parse_trim_expr_args()?;
self.expect(Token::RParen)?;
let end = self.position();
return Ok(Expr::FunctionCall {
name,
args,
span: Span::new(start, end),
});
}
if function_name.eq_ignore_ascii_case("POSITION") {
let args = self.parse_position_expr_args()?;
self.expect(Token::RParen)?;
let end = self.position();
return Ok(Expr::FunctionCall {
name: function_name,
args,
span: Span::new(start, end),
});
}
if function_name.eq_ignore_ascii_case("SUBSTRING") {
let args = self.parse_substring_expr_args()?;
self.expect(Token::RParen)?;
let end = self.position();
return Ok(Expr::FunctionCall {
name: function_name,
args,
span: Span::new(start, end),
});
}
if function_name.eq_ignore_ascii_case("COUNT") {
if self.consume(&Token::Distinct)? {
let arg = self.parse_expr_prec(0)?;
self.expect(Token::RParen)?;
let end = self.position();
return Ok(Expr::FunctionCall {
name: "COUNT_DISTINCT".to_string(),
args: vec![arg],
span: Span::new(start, end),
});
}
if self.consume(&Token::Star)? {
self.expect(Token::RParen)?;
let end = self.position();
return Ok(Expr::FunctionCall {
name: function_name,
args: vec![Expr::Column {
field: FieldRef::TableColumn {
table: String::new(),
column: "*".to_string(),
},
span: Span::synthetic(),
}],
span: Span::new(start, end),
});
}
}
let mut args = Vec::new();
if !self.check(&Token::RParen) {
loop {
args.push(self.parse_expr_prec(0)?);
if !self.consume(&Token::Comma)? {
break;
}
}
}
self.expect(Token::RParen)?;
let end = self.position();
Ok(Expr::FunctionCall {
name: function_name,
args,
span: Span::new(start, end),
})
}
fn parse_case_expr(
&mut self,
start: crate::storage::query::lexer::Position,
) -> Result<Expr, ParseError> {
self.advance()?; let mut branches: Vec<(Expr, Expr)> = Vec::new();
loop {
if !self.consume_ident_ci("WHEN")? {
break;
}
let cond = self.parse_expr_prec(0)?;
if !self.consume_ident_ci("THEN")? {
return Err(ParseError::new(
"expected THEN after CASE WHEN condition".to_string(),
self.position(),
));
}
let then_val = self.parse_expr_prec(0)?;
branches.push((cond, then_val));
}
if branches.is_empty() {
return Err(ParseError::new(
"CASE must have at least one WHEN branch".to_string(),
self.position(),
));
}
let else_ = if self.consume_ident_ci("ELSE")? {
Some(Box::new(self.parse_expr_prec(0)?))
} else {
None
};
if !self.consume_ident_ci("END")? {
return Err(ParseError::new(
"expected END to close CASE expression".to_string(),
self.position(),
));
}
let end = self.position();
Ok(Expr::Case {
branches,
else_,
span: Span::new(start, end),
})
}
fn parse_trim_expr_args(&mut self) -> Result<(String, Vec<Expr>), ParseError> {
let mut function_name = "TRIM".to_string();
if self.consume_ident_ci("LEADING")? {
function_name = "LTRIM".to_string();
} else if self.consume_ident_ci("TRAILING")? {
function_name = "RTRIM".to_string();
} else if self.consume_ident_ci("BOTH")? {
function_name = "TRIM".to_string();
}
if self.consume(&Token::From)? {
let source = self.parse_expr_prec(0)?;
return Ok((function_name, vec![source]));
}
let first = self.parse_expr_prec(0)?;
if self.consume(&Token::Comma)? {
let second = self.parse_expr_prec(0)?;
return Ok((function_name, vec![first, second]));
}
if self.consume(&Token::From)? {
let source = self.parse_expr_prec(0)?;
return Ok((function_name, vec![source, first]));
}
Ok((function_name, vec![first]))
}
fn parse_position_expr_args(&mut self) -> Result<Vec<Expr>, ParseError> {
let needle = self.parse_expr_prec(35)?;
if !self.consume(&Token::Comma)? {
self.expect(Token::In)?;
}
let haystack = self.parse_expr_prec(0)?;
Ok(vec![needle, haystack])
}
fn parse_substring_expr_args(&mut self) -> Result<Vec<Expr>, ParseError> {
let source = self.parse_expr_prec(0)?;
if self.consume(&Token::Comma)? {
let mut args = vec![source];
loop {
args.push(self.parse_expr_prec(0)?);
if !self.consume(&Token::Comma)? {
break;
}
}
return Ok(args);
}
if self.consume(&Token::From)? {
let start = self.parse_expr_prec(0)?;
if self.consume(&Token::For)? {
let count = self.parse_expr_prec(0)?;
return Ok(vec![source, start, count]);
}
return Ok(vec![source, start]);
}
if self.consume(&Token::For)? {
let count = self.parse_expr_prec(0)?;
if self.consume(&Token::From)? {
let start = self.parse_expr_prec(0)?;
return Ok(vec![source, start, count]);
}
return Ok(vec![source, Expr::lit(Value::Integer(1)), count]);
}
Ok(vec![source])
}
fn try_parse_postfix(&mut self, left: &Expr) -> Result<Option<Expr>, ParseError> {
let start = self.span_start_of(left);
if self.consume(&Token::Is)? {
let negated = self.consume(&Token::Not)?;
self.expect(Token::Null)?;
let end = self.position();
return Ok(Some(Expr::IsNull {
operand: Box::new(left.clone()),
negated,
span: Span::new(start, end),
}));
}
let negated = if matches!(self.peek(), Token::Not) {
self.advance()?;
if !matches!(self.peek(), Token::Between | Token::In) {
return Err(ParseError::new(
"expected BETWEEN or IN after postfix NOT".to_string(),
self.position(),
));
}
true
} else {
false
};
if self.consume(&Token::Between)? {
let low = self.parse_expr_prec(34)?;
self.expect(Token::And)?;
let high = self.parse_expr_prec(34)?;
let end = self.position();
return Ok(Some(Expr::Between {
target: Box::new(left.clone()),
low: Box::new(low),
high: Box::new(high),
negated,
span: Span::new(start, end),
}));
}
if self.consume(&Token::In)? {
self.expect(Token::LParen)?;
let mut values = Vec::new();
if self.check(&Token::Select) {
let query = self.parse_select_query()?;
values.push(Expr::Subquery {
query: ExprSubquery {
query: Box::new(query),
},
span: Span::new(self.span_start_of(left), self.position()),
});
} else if !self.check(&Token::RParen) {
loop {
values.push(self.parse_expr_prec(0)?);
if !self.consume(&Token::Comma)? {
break;
}
}
}
self.expect(Token::RParen)?;
let end = self.position();
return Ok(Some(Expr::InList {
target: Box::new(left.clone()),
values,
negated,
span: Span::new(start, end),
}));
}
if negated {
return Err(ParseError::new(
"internal: NOT consumed without BETWEEN/IN follow".to_string(),
self.position(),
));
}
Ok(None)
}
fn peek_binop(&self) -> Option<(BinOp, u8)> {
let op = match self.peek() {
Token::Or => BinOp::Or,
Token::And => BinOp::And,
Token::Eq => BinOp::Eq,
Token::Ne => BinOp::Ne,
Token::Lt => BinOp::Lt,
Token::Le => BinOp::Le,
Token::Gt => BinOp::Gt,
Token::Ge => BinOp::Ge,
Token::DoublePipe => BinOp::Concat,
Token::Plus => BinOp::Add,
Token::Dash => BinOp::Sub,
Token::Star => BinOp::Mul,
Token::Slash => BinOp::Div,
Token::Percent => BinOp::Mod,
_ => return None,
};
Some((op, op.precedence()))
}
fn span_start_of(&self, expr: &Expr) -> crate::storage::query::lexer::Position {
let s = expr.span();
if s.is_synthetic() {
self.position()
} else {
s.start
}
}
fn span_end_of(&self, expr: &Expr) -> crate::storage::query::lexer::Position {
let s = expr.span();
if s.is_synthetic() {
self.position()
} else {
s.end
}
}
}
#[allow(dead_code)]
fn _expr_module_used(_: Expr) {}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::query::ast::FieldRef;
fn parse(input: &str) -> Expr {
let mut parser = Parser::new(input).expect("lexer init");
let expr = parser.parse_expr().expect("parse_expr");
expr
}
#[test]
fn literal_integer() {
let e = parse("42");
match e {
Expr::Literal {
value: Value::Integer(42),
..
} => {}
other => panic!("expected Integer(42), got {other:?}"),
}
}
#[test]
fn literal_float() {
let e = parse("3.14");
match e {
Expr::Literal {
value: Value::Float(f),
..
} => assert!((f - 3.14).abs() < 1e-9),
other => panic!("expected float literal, got {other:?}"),
}
}
#[test]
fn literal_string() {
let e = parse("'hello'");
match e {
Expr::Literal {
value: Value::Text(ref s),
..
} if s.as_ref() == "hello" => {}
other => panic!("expected Text(hello), got {other:?}"),
}
}
#[test]
fn literal_booleans_and_null() {
assert!(matches!(
parse("TRUE"),
Expr::Literal {
value: Value::Boolean(true),
..
}
));
assert!(matches!(
parse("FALSE"),
Expr::Literal {
value: Value::Boolean(false),
..
}
));
assert!(matches!(
parse("NULL"),
Expr::Literal {
value: Value::Null,
..
}
));
}
#[test]
fn bare_column() {
let e = parse("user_id");
match e {
Expr::Column {
field: FieldRef::TableColumn { column, .. },
..
} => {
assert_eq!(column, "user_id");
}
other => panic!("expected column, got {other:?}"),
}
}
#[test]
fn arithmetic_precedence_mul_over_add() {
let e = parse("a + b * c");
let Expr::BinaryOp {
op: BinOp::Add,
rhs,
..
} = e
else {
panic!("root must be Add");
};
let Expr::BinaryOp { op: BinOp::Mul, .. } = *rhs else {
panic!("rhs must be Mul");
};
}
#[test]
fn arithmetic_left_associativity() {
let e = parse("a - b - c");
let Expr::BinaryOp {
op: BinOp::Sub,
lhs,
..
} = e
else {
panic!("root must be Sub");
};
let Expr::BinaryOp { op: BinOp::Sub, .. } = *lhs else {
panic!("lhs must be Sub (left-assoc)");
};
}
#[test]
fn parenthesised_override() {
let e = parse("(a + b) * c");
let Expr::BinaryOp {
op: BinOp::Mul,
lhs,
..
} = e
else {
panic!("root must be Mul");
};
let Expr::BinaryOp { op: BinOp::Add, .. } = *lhs else {
panic!("lhs must be Add");
};
}
#[test]
fn comparison_binds_weaker_than_arith() {
let e = parse("a + 1 = b - 2");
let Expr::BinaryOp {
op: BinOp::Eq,
lhs,
rhs,
..
} = e
else {
panic!("root must be Eq");
};
assert!(matches!(*lhs, Expr::BinaryOp { op: BinOp::Add, .. }));
assert!(matches!(*rhs, Expr::BinaryOp { op: BinOp::Sub, .. }));
}
#[test]
fn and_binds_tighter_than_or() {
let e = parse("a OR b AND c");
let Expr::BinaryOp {
op: BinOp::Or, rhs, ..
} = e
else {
panic!("root must be Or");
};
assert!(matches!(*rhs, Expr::BinaryOp { op: BinOp::And, .. }));
}
#[test]
fn unary_negation() {
let e = parse("-a");
let Expr::UnaryOp {
op: UnaryOp::Neg, ..
} = e
else {
panic!("expected unary Neg");
};
}
#[test]
fn unary_not() {
let e = parse("NOT a");
let Expr::UnaryOp {
op: UnaryOp::Not, ..
} = e
else {
panic!("expected unary Not");
};
}
#[test]
fn concat_operator() {
let e = parse("'hello' || name");
let Expr::BinaryOp {
op: BinOp::Concat, ..
} = e
else {
panic!("expected Concat");
};
}
#[test]
fn cast_expr() {
let e = parse("CAST(age AS TEXT)");
let Expr::Cast { target, .. } = e else {
panic!("expected Cast");
};
assert_eq!(target, DataType::Text);
}
#[test]
fn case_expr() {
let e = parse("CASE WHEN a = 1 THEN 'one' WHEN a = 2 THEN 'two' ELSE 'other' END");
let Expr::Case {
branches, else_, ..
} = e
else {
panic!("expected Case");
};
assert_eq!(branches.len(), 2);
assert!(else_.is_some());
}
#[test]
fn is_null_postfix() {
let e = parse("name IS NULL");
assert!(matches!(e, Expr::IsNull { negated: false, .. }));
}
#[test]
fn is_not_null_postfix() {
let e = parse("name IS NOT NULL");
assert!(matches!(e, Expr::IsNull { negated: true, .. }));
}
#[test]
fn between_with_columns() {
let e = parse("temp BETWEEN min_t AND max_t");
let Expr::Between {
target,
low,
high,
negated,
..
} = e
else {
panic!("expected Between");
};
assert!(!negated);
assert!(matches!(*target, Expr::Column { .. }));
assert!(matches!(*low, Expr::Column { .. }));
assert!(matches!(*high, Expr::Column { .. }));
}
#[test]
fn not_between_negates() {
let e = parse("temp NOT BETWEEN 0 AND 100");
let Expr::Between { negated: true, .. } = e else {
panic!("expected negated Between");
};
}
#[test]
fn in_list_literal() {
let e = parse("status IN (1, 2, 3)");
let Expr::InList {
values, negated, ..
} = e
else {
panic!("expected InList");
};
assert!(!negated);
assert_eq!(values.len(), 3);
}
#[test]
fn not_in_list() {
let e = parse("status NOT IN (1, 2)");
let Expr::InList { negated: true, .. } = e else {
panic!("expected negated InList");
};
}
#[test]
fn function_call_with_args() {
let e = parse("UPPER(name)");
let Expr::FunctionCall { name, args, .. } = e else {
panic!("expected FunctionCall");
};
assert_eq!(name, "UPPER");
assert_eq!(args.len(), 1);
}
#[test]
fn nested_function_call() {
let e = parse("COALESCE(a, UPPER(b))");
let Expr::FunctionCall { name, args, .. } = e else {
panic!("expected FunctionCall");
};
assert_eq!(name, "COALESCE");
assert_eq!(args.len(), 2);
assert!(matches!(&args[1], Expr::FunctionCall { .. }));
}
#[test]
fn duration_literal_parses_as_text() {
let e = parse("time_bucket(5m)");
let Expr::FunctionCall { name, args, .. } = e else {
panic!("expected FunctionCall, got {e:?}");
};
assert_eq!(name.to_uppercase(), "TIME_BUCKET");
assert_eq!(args.len(), 1);
assert!(
matches!(&args[0], Expr::Literal { value: Value::Text(s), .. } if s.as_ref() == "5m"),
"expected Text(\"5m\"), got {:?}",
args[0]
);
}
#[test]
fn placeholder_dollar_one() {
let e = parse("$1");
match e {
Expr::Parameter { index: 0, .. } => {}
other => panic!("expected Parameter(0), got {other:?}"),
}
}
#[test]
fn placeholder_dollar_n() {
let e = parse("$7");
match e {
Expr::Parameter { index: 6, .. } => {}
other => panic!("expected Parameter(6), got {other:?}"),
}
}
#[test]
fn placeholder_in_string_literal_is_text() {
let e = parse("'$1'");
match e {
Expr::Literal {
value: Value::Text(s),
..
} if s.as_ref() == "$1" => {}
other => panic!("expected text literal '$1', got {other:?}"),
}
}
#[test]
fn placeholder_in_comparison() {
let e = parse("id = $1");
let Expr::BinaryOp {
op: BinOp::Eq, rhs, ..
} = e
else {
panic!("root must be Eq");
};
assert!(matches!(*rhs, Expr::Parameter { index: 0, .. }));
}
#[test]
fn placeholder_zero_rejected() {
let mut parser = Parser::new("$0").expect("lexer");
let err = parser.parse_expr().unwrap_err();
assert!(err.to_string().contains("placeholder"));
}
#[test]
fn placeholder_question_single() {
let e = parse("?");
match e {
Expr::Parameter { index: 0, .. } => {}
other => panic!("expected Parameter(0), got {other:?}"),
}
}
#[test]
fn placeholder_question_numbered() {
let e = parse("?7");
match e {
Expr::Parameter { index: 6, .. } => {}
other => panic!("expected Parameter(6), got {other:?}"),
}
}
#[test]
fn placeholder_question_numbered_zero_rejected() {
let mut parser = Parser::new("?0").expect("lexer");
let err = parser.parse_expr().unwrap_err();
assert!(err.to_string().contains("placeholder"));
}
#[test]
fn placeholder_question_left_to_right() {
let e = parse("id = ? AND name = ?");
let Expr::BinaryOp {
op: BinOp::And,
lhs,
rhs,
..
} = e
else {
panic!("root must be And");
};
let Expr::BinaryOp {
op: BinOp::Eq,
rhs: r1,
..
} = *lhs
else {
panic!("lhs must be Eq");
};
assert!(matches!(*r1, Expr::Parameter { index: 0, .. }));
let Expr::BinaryOp {
op: BinOp::Eq,
rhs: r2,
..
} = *rhs
else {
panic!("rhs must be Eq");
};
assert!(matches!(*r2, Expr::Parameter { index: 1, .. }));
}
#[test]
fn placeholder_question_in_string_literal_is_text() {
let e = parse("'?'");
match e {
Expr::Literal {
value: Value::Text(s),
..
} if s.as_ref() == "?" => {}
other => panic!("expected text literal '?', got {other:?}"),
}
}
#[test]
fn placeholder_mixing_question_then_dollar_rejected() {
let mut parser = Parser::new("id = ? AND x = $2").expect("lexer");
let err = parser.parse_expr().err().expect("should fail");
assert!(
err.to_string().contains("mix"),
"expected mixing error, got: {err}"
);
}
#[test]
fn placeholder_mixing_dollar_then_question_rejected() {
let mut parser = Parser::new("id = $1 AND x = ?").expect("lexer");
let err = parser.parse_expr().err().expect("should fail");
assert!(
err.to_string().contains("mix"),
"expected mixing error, got: {err}"
);
}
#[test]
fn placeholder_question_in_comment_ignored() {
let mut parser = Parser::new("-- ? ignored\n ?").expect("lexer");
let e = parser.parse_expr().expect("parse_expr");
match e {
Expr::Parameter { index: 0, .. } => {}
other => panic!("expected Parameter(0), got {other:?}"),
}
}
#[test]
fn span_tracks_token_range() {
let mut parser = Parser::new("123 + 456").expect("lexer");
let e = parser.parse_expr().expect("parse_expr");
let span = e.span();
assert!(!span.is_synthetic(), "root span must be real");
assert!(span.start.offset < span.end.offset);
}
}