From d6977d2a84c7c41d8d6d35f1fdf3c98d7afba4d1 Mon Sep 17 00:00:00 2001 From: Kould <2435992353@qq.com> Date: Fri, 9 Feb 2024 19:13:38 +0800 Subject: [PATCH 1/2] feat: Support `ESCAPE` on `LIKE` --- src/binder/expr.rs | 9 ++-- src/expression/mod.rs | 21 +++++++-- src/expression/value_compute.rs | 22 ++++++++-- tests/slt/filter.slt | 76 +++++++++++++++++++++++---------- 4 files changed, 94 insertions(+), 34 deletions(-) diff --git a/src/binder/expr.rs b/src/binder/expr.rs index 06a94635..90755729 100644 --- a/src/binder/expr.rs +++ b/src/binder/expr.rs @@ -41,8 +41,8 @@ impl<'a, T: Transaction> Binder<'a, T> { negated, expr, pattern, - .. - } => self.bind_like(*negated, expr, pattern), + escape_char, + } => self.bind_like(*negated, expr, pattern, escape_char), Expr::IsNull(expr) => self.bind_is_null(expr, false), Expr::IsNotNull(expr) => self.bind_is_null(expr, true), Expr::InList { @@ -100,13 +100,14 @@ impl<'a, T: Transaction> Binder<'a, T> { negated: bool, expr: &Expr, pattern: &Expr, + escape_char: &Option, ) -> Result { let left_expr = Box::new(self.bind_expr(expr)?); let right_expr = Box::new(self.bind_expr(pattern)?); let op = if negated { - expression::BinaryOperator::NotLike + expression::BinaryOperator::NotLike(*escape_char) } else { - expression::BinaryOperator::Like + expression::BinaryOperator::Like(*escape_char) }; Ok(ScalarExpression::Binary { op, diff --git a/src/expression/mod.rs b/src/expression/mod.rs index 45a3e664..7b2db0b6 100644 --- a/src/expression/mod.rs +++ b/src/expression/mod.rs @@ -351,8 +351,8 @@ pub enum BinaryOperator { Spaceship, Eq, NotEq, - Like, - NotLike, + Like(Option), + NotLike(Option), And, Or, @@ -367,6 +367,13 @@ impl fmt::Display for ScalarExpression { impl fmt::Display for BinaryOperator { fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let like_op = |f: &mut Formatter, escape_char: &Option| { + if let Some(escape_char) = escape_char { + write!(f, "(escape: {})", escape_char)?; + } + Ok(()) + }; + match self { BinaryOperator::Plus => write!(f, "+"), BinaryOperator::Minus => write!(f, "-"), @@ -384,8 +391,14 @@ impl fmt::Display for BinaryOperator { BinaryOperator::And => write!(f, "&&"), BinaryOperator::Or => write!(f, "||"), BinaryOperator::Xor => write!(f, "^"), - BinaryOperator::Like => write!(f, "like"), - BinaryOperator::NotLike => write!(f, "not like"), + BinaryOperator::Like(escape_char) => { + write!(f, "like")?; + like_op(f, escape_char) + } + BinaryOperator::NotLike(escape_char) => { + write!(f, "not like")?; + like_op(f, escape_char) + } } } } diff --git a/src/expression/value_compute.rs b/src/expression/value_compute.rs index 77adad4c..1c3b42c7 100644 --- a/src/expression/value_compute.rs +++ b/src/expression/value_compute.rs @@ -181,18 +181,32 @@ pub fn binary_op( right: &DataValue, op: &BinaryOperator, ) -> Result { - if matches!(op, BinaryOperator::Like | BinaryOperator::NotLike) { + if let BinaryOperator::Like(escape_char) | BinaryOperator::NotLike(escape_char) = op { let value_option = unpack_utf8(left.clone().cast(&LogicalType::Varchar(None))?); let pattern_option = unpack_utf8(right.clone().cast(&LogicalType::Varchar(None))?); let mut is_match = if let (Some(value), Some(pattern)) = (value_option, pattern_option) { - let regex_pattern = pattern.replace('%', ".*").replace('_', "."); + let mut regex_pattern = String::new(); + let mut chars = pattern.chars().peekable(); + while let Some(c) = chars.next() { + if matches!(escape_char.map(|escape_c| escape_c == c), Some(true)) { + if let Some(next_char) = chars.next() { + regex_pattern.push(next_char); + } + } else if c == '%' { + regex_pattern.push_str(".*"); + } else if c == '_' { + regex_pattern.push('.'); + } else { + regex_pattern.push(c); + } + } Regex::new(®ex_pattern).unwrap().is_match(&value) } else { - unreachable!("The left and right values calculated by Like cannot be Null values.") + return Ok(DataValue::Boolean(None)); }; - if op == &BinaryOperator::NotLike { + if matches!(op, BinaryOperator::NotLike(_)) { is_match = !is_match; } return Ok(DataValue::Boolean(Some(is_match))); diff --git a/tests/slt/filter.slt b/tests/slt/filter.slt index 837a0812..6786d2df 100644 --- a/tests/slt/filter.slt +++ b/tests/slt/filter.slt @@ -71,104 +71,136 @@ statement ok create table t1(id int primary key, v1 varchar) statement ok -insert into t1 values (0, 'KipSQL'), (1, 'KipDB'), (2, 'KipBlog'), (3, 'Cool!'); +insert into t1 values (0, 'KipSQL'), (1, 'KipDB'), (2, 'KipBlog'), (3, 'Cool!'), (4, 'F%ck') -query II +query IT select * from t1 where v1 like 'Kip%' ---- 0 KipSQL 1 KipDB 2 KipBlog -query II +query IT select * from t1 where v1 not like 'Kip%' ---- 3 Cool! +4 F%ck -query II +query IT +select * from t1 where v1 like 'F@%ck' escape '@' +---- +4 F%ck + +query IT select * from t1 where v1 like 'KipD_' ---- 1 KipDB -query II +query IT select * from t1 where v1 like 'KipS_L' ---- 0 KipSQL -query II +query IT select * from t1 where v1 like 'K%L' ---- 0 KipSQL -query II +query IT +select * from t1 where v1 like null +---- + +query IT +select * from t1 where null like 'K%L' +---- + +query IT +select * from t1 where null like null +---- + +query IT +select * from t1 where v1 not like null +---- + +query IT +select * from t1 where null not like 'K%L' +---- + +query IT +select * from t1 where null not like null +---- + +query IT select * from t1 where id in (1, 2) ---- 1 KipDB 2 KipBlog -query II +query IT select * from t1 where id not in (1, 2) ---- 0 KipSQL 3 Cool! +4 F%ck -query II +query IT select * from t1 where id in (1, null) ---- -query II +query IT select * from t1 where null in (1, 2) ---- -query II +query IT select * from t1 where null in (1, null) ---- -query II +query IT select * from t1 where id not in (1, null) ---- -query II +query IT select * from t1 where null not in (1, 2) ---- -query II +query IT select * from t1 where null not in (1, null) ---- -query II +query IT select * from t1 where id between 1 and 2 ---- 1 KipDB 2 KipBlog -query II +query IT select * from t1 where id not between 1 and 2 ---- 0 KipSQL 3 Cool! +4 F%ck -query II +query IT select * from t1 where id between 1 and null ---- -query II +query IT select * from t1 where null between 1 and 2 ---- -query II +query IT select * from t1 where null between 1 and null ---- -query II +query IT select * from t1 where id not between 1 and null ---- -query II +query IT select * from t1 where null not between 1 and 2 ---- -query II +query IT select * from t1 where null not between 1 and null ---- From 07dbf2f4c39628a3261417faffdde7736c28d0bb Mon Sep 17 00:00:00 2001 From: Kould <2435992353@qq.com> Date: Fri, 9 Feb 2024 19:17:38 +0800 Subject: [PATCH 2/2] test: pass E061_05 --- tests/slt/sql_2016/E061_05.slt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/slt/sql_2016/E061_05.slt b/tests/slt/sql_2016/E061_05.slt index c8d97987..3d7989f5 100644 --- a/tests/slt/sql_2016/E061_05.slt +++ b/tests/slt/sql_2016/E061_05.slt @@ -1,13 +1,13 @@ # E061-05: LIKE predicate: ESCAPE clause -# TODO: support `ESCAPE` on `LIKE` +statement ok +CREATE TABLE TABLE_E061_05_01_01 ( ID INT PRIMARY KEY, A VARCHAR ( 255 ) ); -# statement ok -# CREATE TABLE TABLE_E061_05_01_01 ( ID INT PRIMARY KEY, A VARCHAR ( 255 ) ); +query T +SELECT A FROM TABLE_E061_05_01_01 WHERE A LIKE 'foo' ESCAPE 'f' -# SELECT A FROM TABLE_E061_05_01_01 WHERE A LIKE 'foo' ESCAPE 'f' +statement ok +CREATE TABLE TABLE_E061_05_01_02 ( ID INT PRIMARY KEY, A VARCHAR ( 255 ) ); -# statement ok -# CREATE TABLE TABLE_E061_05_01_02 ( ID INT PRIMARY KEY, A VARCHAR ( 255 ) ); - -# SELECT A FROM TABLE_E061_05_01_02 WHERE A NOT LIKE 'foo' ESCAPE 'f' +query T +SELECT A FROM TABLE_E061_05_01_02 WHERE A NOT LIKE 'foo' ESCAPE 'f'