Support the string concat operator (#178)

The selected precedence is based on BigQuery documentation, where it is equal to `*` and `/`:

https://cloud.google.com/bigquery/docs/reference/standard-sql/operators
This commit is contained in:
Daniël Heres 2020-06-02 20:24:30 +02:00 committed by GitHub
parent 5f3c1bda01
commit 00dc490f72
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 52 additions and 1 deletions

View file

@ -38,6 +38,7 @@ pub enum BinaryOperator {
Multiply, Multiply,
Divide, Divide,
Modulus, Modulus,
StringConcat,
Gt, Gt,
Lt, Lt,
GtEq, GtEq,
@ -58,6 +59,7 @@ impl fmt::Display for BinaryOperator {
BinaryOperator::Multiply => "*", BinaryOperator::Multiply => "*",
BinaryOperator::Divide => "/", BinaryOperator::Divide => "/",
BinaryOperator::Modulus => "%", BinaryOperator::Modulus => "%",
BinaryOperator::StringConcat => "||",
BinaryOperator::Gt => ">", BinaryOperator::Gt => ">",
BinaryOperator::Lt => "<", BinaryOperator::Lt => "<",
BinaryOperator::GtEq => ">=", BinaryOperator::GtEq => ">=",

View file

@ -577,6 +577,7 @@ impl Parser {
Token::Minus => Some(BinaryOperator::Minus), Token::Minus => Some(BinaryOperator::Minus),
Token::Mult => Some(BinaryOperator::Multiply), Token::Mult => Some(BinaryOperator::Multiply),
Token::Mod => Some(BinaryOperator::Modulus), Token::Mod => Some(BinaryOperator::Modulus),
Token::StringConcat => Some(BinaryOperator::StringConcat),
Token::Div => Some(BinaryOperator::Divide), Token::Div => Some(BinaryOperator::Divide),
Token::Word(ref k) => match k.keyword.as_ref() { Token::Word(ref k) => match k.keyword.as_ref() {
"AND" => Some(BinaryOperator::And), "AND" => Some(BinaryOperator::And),
@ -708,7 +709,7 @@ impl Parser {
Ok(20) Ok(20)
} }
Token::Plus | Token::Minus => Ok(Self::PLUS_MINUS_PREC), Token::Plus | Token::Minus => Ok(Self::PLUS_MINUS_PREC),
Token::Mult | Token::Div | Token::Mod => Ok(40), Token::Mult | Token::Div | Token::Mod | Token::StringConcat => Ok(40),
Token::DoubleColon => Ok(50), Token::DoubleColon => Ok(50),
_ => Ok(0), _ => Ok(0),
} }

View file

@ -64,6 +64,8 @@ pub enum Token {
Div, Div,
/// Modulo Operator `%` /// Modulo Operator `%`
Mod, Mod,
/// String concatenation `||`
StringConcat,
/// Left parenthesis `(` /// Left parenthesis `(`
LParen, LParen,
/// Right parenthesis `)` /// Right parenthesis `)`
@ -111,6 +113,7 @@ impl fmt::Display for Token {
Token::Minus => f.write_str("-"), Token::Minus => f.write_str("-"),
Token::Mult => f.write_str("*"), Token::Mult => f.write_str("*"),
Token::Div => f.write_str("/"), Token::Div => f.write_str("/"),
Token::StringConcat => f.write_str("||"),
Token::Mod => f.write_str("%"), Token::Mod => f.write_str("%"),
Token::LParen => f.write_str("("), Token::LParen => f.write_str("("),
Token::RParen => f.write_str(")"), Token::RParen => f.write_str(")"),
@ -374,6 +377,16 @@ impl<'a> Tokenizer<'a> {
'+' => self.consume_and_return(chars, Token::Plus), '+' => self.consume_and_return(chars, Token::Plus),
'*' => self.consume_and_return(chars, Token::Mult), '*' => self.consume_and_return(chars, Token::Mult),
'%' => self.consume_and_return(chars, Token::Mod), '%' => self.consume_and_return(chars, Token::Mod),
'|' => {
chars.next(); // consume the '|'
match chars.peek() {
Some('|') => self.consume_and_return(chars, Token::StringConcat),
_ => Err(TokenizerError(format!(
"Expecting to see `||`. Bitwise or operator `|` is not supported. \nError at Line: {}, Col: {}",
self.line, self.col
))),
}
}
'=' => self.consume_and_return(chars, Token::Eq), '=' => self.consume_and_return(chars, Token::Eq),
'.' => self.consume_and_return(chars, Token::Period), '.' => self.consume_and_return(chars, Token::Period),
'!' => { '!' => {
@ -562,6 +575,26 @@ mod tests {
compare(expected, tokens); compare(expected, tokens);
} }
#[test]
fn tokenize_string_string_concat() {
let sql = String::from("SELECT 'a' || 'b'");
let dialect = GenericDialect {};
let mut tokenizer = Tokenizer::new(&dialect, &sql);
let tokens = tokenizer.tokenize().unwrap();
let expected = vec![
Token::make_keyword("SELECT"),
Token::Whitespace(Whitespace::Space),
Token::SingleQuotedString(String::from("a")),
Token::Whitespace(Whitespace::Space),
Token::StringConcat,
Token::Whitespace(Whitespace::Space),
Token::SingleQuotedString(String::from("b")),
];
compare(expected, tokens);
}
#[test] #[test]
fn tokenize_simple_select() { fn tokenize_simple_select() {
let sql = String::from("SELECT * FROM customer WHERE id = 1 LIMIT 5"); let sql = String::from("SELECT * FROM customer WHERE id = 1 LIMIT 5");

View file

@ -665,6 +665,21 @@ fn parse_in_subquery() {
); );
} }
#[test]
fn parse_string_agg() {
let sql = "SELECT a || b";
let select = verified_only_select(sql);
assert_eq!(
SelectItem::UnnamedExpr(Expr::BinaryOp {
left: Box::new(Expr::Identifier(Ident::new("a"))),
op: BinaryOperator::StringConcat,
right: Box::new(Expr::Identifier(Ident::new("b"))),
}),
select.projection[0]
);
}
#[test] #[test]
fn parse_between() { fn parse_between() {
fn chk(negated: bool) { fn chk(negated: bool) {