From b05cb06719d668ce86a4eb5882e9f284b3c03251 Mon Sep 17 00:00:00 2001 From: TennyZhuang Date: Fri, 29 Mar 2024 17:46:57 +0800 Subject: [PATCH] accept empty string as escape char Signed-off-by: TennyZhuang --- src/frontend/src/binder/expr/mod.rs | 13 +++++---- src/sqlparser/src/ast/mod.rs | 34 +++++++++++++++++++++--- src/sqlparser/src/parser.rs | 19 +++++++------ src/sqlparser/tests/sqlparser_common.rs | 2 +- src/sqlparser/tests/testdata/select.yaml | 4 +-- 5 files changed, 51 insertions(+), 21 deletions(-) diff --git a/src/frontend/src/binder/expr/mod.rs b/src/frontend/src/binder/expr/mod.rs index 51b712209b586..5abcc2857243a 100644 --- a/src/frontend/src/binder/expr/mod.rs +++ b/src/frontend/src/binder/expr/mod.rs @@ -19,8 +19,8 @@ use risingwave_common::util::iter_util::zip_eq_fast; use risingwave_common::{bail_no_function, bail_not_implemented, not_implemented}; use risingwave_pb::plan_common::{AdditionalColumn, ColumnDescVersion}; use risingwave_sqlparser::ast::{ - Array, BinaryOperator, DataType as AstDataType, Expr, Function, JsonPredicateType, ObjectName, - Query, StructField, TrimWhereField, UnaryOperator, + Array, BinaryOperator, DataType as AstDataType, EscapeChar, Expr, Function, JsonPredicateType, + ObjectName, Query, StructField, TrimWhereField, UnaryOperator, }; use crate::binder::expr::function::SYS_FUNCTION_WITHOUT_ARGS; @@ -461,7 +461,7 @@ impl Binder { expr: Expr, negated: bool, pattern: Expr, - escape_char: Option, + escape_char: Option, ) -> Result { if matches!(pattern, Expr::AllOp(_) | Expr::SomeOp(_)) { if escape_char.is_some() { @@ -511,13 +511,16 @@ impl Binder { expr: Expr, negated: bool, pattern: Expr, - escape_char: Option, + escape_char: Option, ) -> Result { let expr = self.bind_expr_inner(expr)?; let pattern = self.bind_expr_inner(pattern)?; let esc_inputs = if let Some(escape_char) = escape_char { - let escape_char = ExprImpl::literal_varchar(escape_char.to_string()); + let escape_char = ExprImpl::literal_varchar(match escape_char.as_char() { + Some(c) => c.to_string(), + None => "".to_string(), + }); vec![pattern, escape_char] } else { vec![pattern] diff --git a/src/sqlparser/src/ast/mod.rs b/src/sqlparser/src/ast/mod.rs index ad53558b0a3e4..c0df8e3335b6f 100644 --- a/src/sqlparser/src/ast/mod.rs +++ b/src/sqlparser/src/ast/mod.rs @@ -256,6 +256,34 @@ impl fmt::Display for Array { } } +/// An escape character, to represent '' or a single character. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct EscapeChar(Option); + +impl EscapeChar { + pub fn escape(ch: char) -> Self { + Self(Some(ch)) + } + + pub fn empty() -> Self { + Self(None) + } + + pub fn as_char(&self) -> Option { + self.0 + } +} + +impl fmt::Display for EscapeChar { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + Some(ch) => write!(f, "'{}'", ch), + None => f.write_str("''"), + } + } +} + /// An SQL expression of any type. /// /// The parser does not distinguish between expressions of different types @@ -334,21 +362,21 @@ pub enum Expr { negated: bool, expr: Box, pattern: Box, - escape_char: Option, + escape_char: Option, }, /// ILIKE (case-insensitive LIKE) ILike { negated: bool, expr: Box, pattern: Box, - escape_char: Option, + escape_char: Option, }, /// ` [ NOT ] SIMILAR TO ESCAPE ` SimilarTo { negated: bool, expr: Box, pattern: Box, - escape_char: Option, + escape_char: Option, }, /// Binary operation e.g. `1 + 1` or `foo > bar` BinaryOp { diff --git a/src/sqlparser/src/parser.rs b/src/sqlparser/src/parser.rs index 64e504213c304..e8fdcb87710c9 100644 --- a/src/sqlparser/src/parser.rs +++ b/src/sqlparser/src/parser.rs @@ -1625,9 +1625,16 @@ impl Parser { } /// parse the ESCAPE CHAR portion of LIKE, ILIKE, and SIMILAR TO - pub fn parse_escape_char(&mut self) -> Result, ParserError> { + pub fn parse_escape_char(&mut self) -> Result, ParserError> { if self.parse_keyword(Keyword::ESCAPE) { - Ok(Some(self.parse_literal_char()?)) + let s = self.parse_literal_string()?; + if s.len() == 0 { + Ok(Some(EscapeChar::empty())) + } else if s.len() == 1 { + Ok(Some(EscapeChar::escape(s.chars().next().unwrap()))) + } else { + parser_err!(format!("Expect a char or an empty string, found {s:?}")) + } } else { Ok(None) } @@ -3503,14 +3510,6 @@ impl Parser { }) } - fn parse_literal_char(&mut self) -> Result { - let s = self.parse_literal_string()?; - if s.len() != 1 { - return parser_err!(format!("Expect a char, found {s:?}")); - } - Ok(s.chars().next().unwrap()) - } - /// Parse a tab separated values in /// COPY payload fn parse_tsv(&mut self) -> Vec> { diff --git a/src/sqlparser/tests/sqlparser_common.rs b/src/sqlparser/tests/sqlparser_common.rs index addc615505df6..f09cb56b4ce7d 100644 --- a/src/sqlparser/tests/sqlparser_common.rs +++ b/src/sqlparser/tests/sqlparser_common.rs @@ -667,7 +667,7 @@ fn parse_like() { expr: Box::new(Expr::Identifier(Ident::new_unchecked("name"))), negated, pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('\\') + escape_char: Some(EscapeChar::escape('\\')) }, select.selection.unwrap() ); diff --git a/src/sqlparser/tests/testdata/select.yaml b/src/sqlparser/tests/testdata/select.yaml index 9acff6923b99a..f4469aa5443d7 100644 --- a/src/sqlparser/tests/testdata/select.yaml +++ b/src/sqlparser/tests/testdata/select.yaml @@ -166,8 +166,8 @@ formatted_sql: SELECT 'a' LIKE 'a' formatted_ast: 'Query(Query { with: None, body: Select(Select { distinct: All, projection: [UnnamedExpr(Like { negated: false, expr: Value(SingleQuotedString("a")), pattern: Value(SingleQuotedString("a")), escape_char: None })], from: [], lateral_views: [], selection: None, group_by: [], having: None }), order_by: [], limit: None, offset: None, fetch: None })' - input: select 'a' like 'a' escape '\'; - formatted_sql: SELECT 'a' LIKE 'a' ESCAPE '\' - formatted_ast: 'Query(Query { with: None, body: Select(Select { distinct: All, projection: [UnnamedExpr(Like { negated: false, expr: Value(SingleQuotedString("a")), pattern: Value(SingleQuotedString("a")), escape_char: Some(''\\'') })], from: [], lateral_views: [], selection: None, group_by: [], having: None }), order_by: [], limit: None, offset: None, fetch: None })' + formatted_sql: SELECT 'a' LIKE 'a' ESCAPE ''\'' + formatted_ast: 'Query(Query { with: None, body: Select(Select { distinct: All, projection: [UnnamedExpr(Like { negated: false, expr: Value(SingleQuotedString("a")), pattern: Value(SingleQuotedString("a")), escape_char: Some(EscapeChar(Some(''\\''))) })], from: [], lateral_views: [], selection: None, group_by: [], having: None }), order_by: [], limit: None, offset: None, fetch: None })' - input: select 'a' not like ANY(array['a', null]); formatted_sql: SELECT 'a' NOT LIKE SOME(ARRAY['a', NULL]) formatted_ast: 'Query(Query { with: None, body: Select(Select { distinct: All, projection: [UnnamedExpr(Like { negated: true, expr: Value(SingleQuotedString("a")), pattern: SomeOp(Array(Array { elem: [Value(SingleQuotedString("a")), Value(Null)], named: true })), escape_char: None })], from: [], lateral_views: [], selection: None, group_by: [], having: None }), order_by: [], limit: None, offset: None, fetch: None })'