Skip to content

Commit

Permalink
accept empty string as escape char
Browse files Browse the repository at this point in the history
Signed-off-by: TennyZhuang <zty0826@gmail.com>
  • Loading branch information
TennyZhuang committed Mar 29, 2024
1 parent 034efe4 commit b05cb06
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 21 deletions.
13 changes: 8 additions & 5 deletions src/frontend/src/binder/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -461,7 +461,7 @@ impl Binder {
expr: Expr,
negated: bool,
pattern: Expr,
escape_char: Option<char>,
escape_char: Option<EscapeChar>,
) -> Result<ExprImpl> {
if matches!(pattern, Expr::AllOp(_) | Expr::SomeOp(_)) {
if escape_char.is_some() {
Expand Down Expand Up @@ -511,13 +511,16 @@ impl Binder {
expr: Expr,
negated: bool,
pattern: Expr,
escape_char: Option<char>,
escape_char: Option<EscapeChar>,
) -> Result<ExprImpl> {
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]
Expand Down
34 changes: 31 additions & 3 deletions src/sqlparser/src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<char>);

impl EscapeChar {
pub fn escape(ch: char) -> Self {
Self(Some(ch))
}

pub fn empty() -> Self {
Self(None)
}

pub fn as_char(&self) -> Option<char> {
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
Expand Down Expand Up @@ -334,21 +362,21 @@ pub enum Expr {
negated: bool,
expr: Box<Expr>,
pattern: Box<Expr>,
escape_char: Option<char>,
escape_char: Option<EscapeChar>,
},
/// ILIKE (case-insensitive LIKE)
ILike {
negated: bool,
expr: Box<Expr>,
pattern: Box<Expr>,
escape_char: Option<char>,
escape_char: Option<EscapeChar>,
},
/// `<expr> [ NOT ] SIMILAR TO <pat> ESCAPE <esc_text>`
SimilarTo {
negated: bool,
expr: Box<Expr>,
pattern: Box<Expr>,
escape_char: Option<char>,
escape_char: Option<EscapeChar>,
},
/// Binary operation e.g. `1 + 1` or `foo > bar`
BinaryOp {
Expand Down
19 changes: 9 additions & 10 deletions src/sqlparser/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<char>, ParserError> {
pub fn parse_escape_char(&mut self) -> Result<Option<EscapeChar>, 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)
}
Expand Down Expand Up @@ -3503,14 +3510,6 @@ impl Parser {
})
}

fn parse_literal_char(&mut self) -> Result<char, ParserError> {
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<Option<String>> {
Expand Down
2 changes: 1 addition & 1 deletion src/sqlparser/tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
);
Expand Down
4 changes: 2 additions & 2 deletions src/sqlparser/tests/testdata/select.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 })'

0 comments on commit b05cb06

Please sign in to comment.