#![allow(dead_code)]
use std::iter::Peekable;
use std::vec;
use super::token::Token;
use crate::expressions::ColumnName;
use crate::{DeltaResult, Error};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum Operand {
Column(ColumnName),
Literal(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum CmpOp {
Lt,
Le,
Gt,
Ge,
Eq,
Ne,
NullSafeEq,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct Comparison {
pub(super) op: CmpOp,
pub(super) left: Operand,
pub(super) right: Operand,
}
pub(super) fn parse(tokens: Vec<Token>) -> DeltaResult<Comparison> {
let mut parser = Parser {
tokens: tokens.into_iter().peekable(),
};
let comparison = parser.parse_comparison()?;
if parser.advance().is_some() {
return Err(Error::generic(
"unexpected trailing input: only a single comparison is supported",
));
}
Ok(comparison)
}
struct Parser {
tokens: Peekable<vec::IntoIter<Token>>,
}
impl Parser {
fn peek(&mut self) -> Option<&Token> {
self.tokens.peek()
}
fn advance(&mut self) -> Option<Token> {
self.tokens.next()
}
fn parse_comparison(&mut self) -> DeltaResult<Comparison> {
let left = self.parse_operand()?;
let op = self.parse_cmp_op()?;
let right = self.parse_operand()?;
Ok(Comparison { op, left, right })
}
fn parse_cmp_op(&mut self) -> DeltaResult<CmpOp> {
let op = match self.advance() {
Some(Token::Lt) => CmpOp::Lt,
Some(Token::Le) => CmpOp::Le,
Some(Token::Gt) => CmpOp::Gt,
Some(Token::Ge) => CmpOp::Ge,
Some(Token::Eq) => CmpOp::Eq,
Some(Token::Ne) => CmpOp::Ne,
Some(Token::NullSafeEq) => CmpOp::NullSafeEq,
other => return Err(expected("a comparison operator", other)),
};
Ok(op)
}
fn parse_operand(&mut self) -> DeltaResult<Operand> {
match self.advance() {
Some(sign @ (Token::Plus | Token::Minus)) => {
let sign = if sign == Token::Minus { '-' } else { '+' };
match self.advance() {
Some(Token::Number(raw)) => Ok(Operand::Literal(format!("{sign}{raw}"))),
other => Err(expected(&format!("a number after '{sign}'"), other)),
}
}
Some(Token::Number(raw)) | Some(Token::Literal(raw)) => Ok(Operand::Literal(raw)),
Some(Token::Ident(first)) => self.parse_column_path(first),
other => Err(expected("a column or literal", other)),
}
}
fn parse_column_path(&mut self, first: String) -> DeltaResult<Operand> {
let mut path = vec![first];
while self.peek() == Some(&Token::Dot) {
self.advance();
match self.advance() {
Some(Token::Ident(segment)) => path.push(segment),
other => return Err(expected("an identifier after '.'", other)),
}
}
Ok(Operand::Column(ColumnName::new(path)))
}
}
fn expected(what: &str, found: Option<Token>) -> Error {
Error::generic(format!("expected {what}, found {found:?}"))
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::super::token::{tokenize, Keyword, Token};
use super::{parse, CmpOp, Comparison, Operand};
use crate::expressions::ColumnName;
fn ident(s: &str) -> Token {
Token::Ident(s.to_string())
}
fn col(segments: &[&str]) -> Operand {
Operand::Column(ColumnName::new(segments.iter().map(|s| s.to_string())))
}
fn lit(s: &str) -> Operand {
Operand::Literal(s.to_string())
}
#[rstest]
#[case(Token::Lt, CmpOp::Lt)]
#[case(Token::Le, CmpOp::Le)]
#[case(Token::Gt, CmpOp::Gt)]
#[case(Token::Ge, CmpOp::Ge)]
#[case(Token::Eq, CmpOp::Eq)]
#[case(Token::Ne, CmpOp::Ne)]
#[case(Token::NullSafeEq, CmpOp::NullSafeEq)]
fn parses_each_operator(#[case] tok: Token, #[case] expected: CmpOp) {
let got = parse(vec![ident("a"), tok, Token::Number("1".into())]).unwrap();
assert_eq!(
got,
Comparison {
op: expected,
left: col(&["a"]),
right: Operand::Literal("1".into()),
}
);
}
#[rstest]
#[case(vec![ident("a"), Token::Gt, ident("b")], col(&["a"]), col(&["b"]))]
#[case(
vec![Token::Number("0".into()), Token::Lt, ident("a")],
Operand::Literal("0".into()),
col(&["a"])
)]
#[case(
vec![ident("a"), Token::Dot, ident("b"), Token::Eq, Token::Number("1".into())],
col(&["a", "b"]),
Operand::Literal("1".into())
)]
#[case(
vec![
ident("a"), Token::Dot, ident("b"), Token::Dot, ident("c"),
Token::Eq, Token::Number("1".into()),
],
col(&["a", "b", "c"]),
Operand::Literal("1".into())
)]
#[case(
vec![ident("a"), Token::Gt, Token::Minus, Token::Number("234".into())],
col(&["a"]),
Operand::Literal("-234".into())
)]
#[case(
vec![Token::Plus, Token::Number("5".into()), Token::Lt, ident("a")],
Operand::Literal("+5".into()),
col(&["a"])
)]
#[case(
vec![ident("a"), Token::Gt, Token::Minus, Token::Number(".5".into())],
col(&["a"]),
Operand::Literal("-.5".into())
)]
fn parses_operand_shapes(
#[case] tokens: Vec<Token>,
#[case] left: Operand,
#[case] right: Operand,
) {
let got = parse(tokens).unwrap();
assert_eq!(got.left, left);
assert_eq!(got.right, right);
}
#[rstest]
#[case("a > -.5", CmpOp::Gt, col(&["a"]), lit("-.5"))]
#[case("amount >= -234", CmpOp::Ge, col(&["amount"]), lit("-234"))]
#[case("ratio == .25", CmpOp::Eq, col(&["ratio"]), lit(".25"))]
#[case("a.b.c <= 100", CmpOp::Le, col(&["a", "b", "c"]), lit("100"))]
#[case("x != y", CmpOp::Ne, col(&["x"]), col(&["y"]))]
#[case("n <=> NULL", CmpOp::NullSafeEq, col(&["n"]), lit("NULL"))]
#[case("+5 < a", CmpOp::Lt, lit("+5"), col(&["a"]))]
#[case("status = 'active'", CmpOp::Eq, col(&["status"]), lit("'active'"))]
fn tokenize_then_parse_yields_expected_comparison(
#[case] sql: &str,
#[case] op: CmpOp,
#[case] left: Operand,
#[case] right: Operand,
) {
let got = parse(tokenize(sql).unwrap()).unwrap();
assert_eq!(got, Comparison { op, left, right });
}
#[rstest]
#[case::junction_and("a > 0 AND b < 9")]
#[case::junction_or("a > 0 OR b > 0")]
#[case::is_not_null("a IS NOT NULL")]
#[case::parens("(amount > 0)")]
#[case::bare_bool_column("is_active")]
fn tokenize_then_parse_rejects_non_single_comparison(#[case] sql: &str) {
assert!(tokenize(sql).and_then(parse).is_err());
}
#[rstest]
#[case::trailing(vec![ident("a"), Token::Gt, Token::Number("0".into()), ident("extra")])]
#[case::missing_operator(vec![ident("a"), ident("b")])]
#[case::dangling_dot(vec![ident("a"), Token::Dot, Token::Gt, Token::Number("0".into())])]
#[case::operator_first(vec![Token::Gt, Token::Number("0".into())])]
#[case::empty(vec![])]
#[case::keyword_operand(vec![ident("a"), Token::Gt, Token::Keyword(Keyword::And)])]
#[case::keyword_junction(vec![
ident("a"), Token::Gt, Token::Number("0".into()),
Token::Keyword(Keyword::And), ident("b"), Token::Lt, Token::Number("9".into()),
])]
#[case::sign_before_column(vec![Token::Minus, ident("a"), Token::Gt, Token::Number("0".into())])]
#[case::sign_before_string(vec![
ident("a"), Token::Eq, Token::Minus, Token::Literal("'foo'".into()),
])]
#[case::sign_then_eof(vec![ident("a"), Token::Gt, Token::Minus])]
#[case::double_sign(vec![
ident("a"), Token::Gt, Token::Minus, Token::Minus, Token::Number("5".into()),
])]
fn rejects_non_single_comparison(#[case] tokens: Vec<Token>) {
assert!(parse(tokens).is_err());
}
}