Skip to content

Commit 508dbf3

Browse files
authored
feat: impl Escape <escape> for LIKE/NOT LIKE/LIKE ANY (#18120)
* feat: impl `Escape <escape>` for LIKE/NOT LIKE/LIKE ANY * chore: codefmt * chore: fix unit test `test_convert_escape_pattern` * chore: codefmt * chore: more test cases on `02_0005_function_compare.test`
1 parent 858acde commit 508dbf3

File tree

21 files changed

+1036
-222
lines changed

21 files changed

+1036
-222
lines changed

src/query/ast/src/ast/expr.rs

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,28 @@ pub enum Expr {
8787
subquery: Box<Query>,
8888
not: bool,
8989
},
90-
/// `LIKE (SELECT ...)`
90+
/// `LIKE (SELECT ...) [ESCAPE '<escape>']`
9191
LikeSubquery {
9292
span: Span,
9393
expr: Box<Expr>,
9494
subquery: Box<Query>,
9595
modifier: SubqueryModifier,
96+
escape: Option<String>,
97+
},
98+
/// `<left> LIKE ANY <right> ESCAPE '<escape>'`
99+
LikeAnyWithEscape {
100+
span: Span,
101+
left: Box<Expr>,
102+
right: Box<Expr>,
103+
escape: String,
104+
},
105+
/// `<left> [NOT] LIKE <right> ESCAPE '<escape>'`
106+
LikeWithEscape {
107+
span: Span,
108+
left: Box<Expr>,
109+
right: Box<Expr>,
110+
is_not: bool,
111+
escape: String,
96112
},
97113
/// `BETWEEN ... AND ...`
98114
Between {
@@ -295,6 +311,8 @@ impl Expr {
295311
| Expr::InList { span, .. }
296312
| Expr::InSubquery { span, .. }
297313
| Expr::LikeSubquery { span, .. }
314+
| Expr::LikeAnyWithEscape { span, .. }
315+
| Expr::LikeWithEscape { span, .. }
298316
| Expr::Between { span, .. }
299317
| Expr::BinaryOp { span, .. }
300318
| Expr::JsonOp { span, .. }
@@ -370,6 +388,12 @@ impl Expr {
370388
),
371389
Expr::BinaryOp {
372390
span, left, right, ..
391+
}
392+
| Expr::LikeWithEscape {
393+
span, left, right, ..
394+
}
395+
| Expr::LikeAnyWithEscape {
396+
span, left, right, ..
373397
} => merge_span(merge_span(*span, left.whole_span()), right.whole_span()),
374398
Expr::JsonOp {
375399
span, left, right, ..
@@ -612,10 +636,36 @@ impl Display for Expr {
612636
expr,
613637
subquery,
614638
modifier,
639+
escape,
615640
..
616641
} => {
617642
write_expr(expr, Some(affix), true, f)?;
618643
write!(f, " LIKE {modifier} ({subquery})")?;
644+
if let Some(escape) = escape {
645+
write!(f, " ESCAPE '{escape}'")?;
646+
}
647+
}
648+
Expr::LikeAnyWithEscape {
649+
left,
650+
right,
651+
escape,
652+
..
653+
} => {
654+
write_expr(left, Some(affix), true, f)?;
655+
write!(f, " LIKE ANY {right} ESCAPE '{escape}'")?;
656+
}
657+
Expr::LikeWithEscape {
658+
left,
659+
right,
660+
is_not,
661+
escape,
662+
..
663+
} => {
664+
write_expr(left, Some(affix), true, f)?;
665+
if *is_not {
666+
write!(f, " NOT")?;
667+
}
668+
write!(f, " LIKE {right} ESCAPE '{escape}'")?;
619669
}
620670
Expr::Between {
621671
expr,
@@ -1480,9 +1530,9 @@ pub enum BinaryOperator {
14801530
And,
14811531
Or,
14821532
Xor,
1483-
Like,
1484-
LikeAny,
1485-
NotLike,
1533+
Like(Option<String>),
1534+
NotLike(Option<String>),
1535+
LikeAny(Option<String>),
14861536
Regexp,
14871537
RLike,
14881538
NotRegexp,
@@ -1522,7 +1572,8 @@ impl BinaryOperator {
15221572
BinaryOperator::BitwiseShiftRight => "bit_shift_right".to_string(),
15231573
BinaryOperator::Caret => "pow".to_string(),
15241574
BinaryOperator::L2Distance => "l2_distance".to_string(),
1525-
BinaryOperator::LikeAny => "like_any".to_string(),
1575+
BinaryOperator::LikeAny(_) => "like_any".to_string(),
1576+
BinaryOperator::Like(_) => "like".to_string(),
15261577
_ => {
15271578
let name = format!("{:?}", self);
15281579
name.to_lowercase()
@@ -1588,13 +1639,13 @@ impl Display for BinaryOperator {
15881639
BinaryOperator::Xor => {
15891640
write!(f, "XOR")
15901641
}
1591-
BinaryOperator::Like => {
1642+
BinaryOperator::Like(_) => {
15921643
write!(f, "LIKE")
15931644
}
1594-
BinaryOperator::LikeAny => {
1645+
BinaryOperator::LikeAny(_) => {
15951646
write!(f, "LIKE ANY")
15961647
}
1597-
BinaryOperator::NotLike => {
1648+
BinaryOperator::NotLike(_) => {
15981649
write!(f, "NOT LIKE")
15991650
}
16001651
BinaryOperator::Regexp => {
@@ -2086,7 +2137,9 @@ impl ExprReplacer {
20862137
Expr::UnaryOp { expr, .. } => {
20872138
self.replace_expr(expr);
20882139
}
2089-
Expr::BinaryOp { left, right, .. } => {
2140+
Expr::BinaryOp { left, right, .. }
2141+
| Expr::LikeWithEscape { left, right, .. }
2142+
| Expr::LikeAnyWithEscape { left, right, .. } => {
20902143
self.replace_expr(left);
20912144
self.replace_expr(right);
20922145
}

src/query/ast/src/parser/expr.rs

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,15 @@ pub enum ExprElement {
168168
subquery: Box<Query>,
169169
not: bool,
170170
},
171-
/// `LIKE (SELECT ...)`
171+
/// `LIKE (SELECT ...) [ESCAPE '<escape>']`
172172
LikeSubquery {
173173
modifier: SubqueryModifier,
174174
subquery: Box<Query>,
175+
escape: Option<String>,
176+
},
177+
/// `ESCAPE '<escape>'`
178+
Escape {
179+
escape: String,
175180
},
176181
/// `BETWEEN ... AND ...`
177182
Between {
@@ -353,6 +358,9 @@ const IS_DISTINCT_FROM_AFFIX: Affix = Affix::Infix(Precedence(BETWEEN_PREC), Ass
353358
const IN_LIST_AFFIX: Affix = Affix::Postfix(Precedence(BETWEEN_PREC));
354359
const IN_SUBQUERY_AFFIX: Affix = Affix::Postfix(Precedence(BETWEEN_PREC));
355360
const LIKE_SUBQUERY_AFFIX: Affix = Affix::Postfix(Precedence(BETWEEN_PREC));
361+
const LIKE_ANY_WITH_ESCAPE_AFFIX: Affix = Affix::Postfix(Precedence(BETWEEN_PREC));
362+
const LIKE_WITH_ESCAPE_AFFIX: Affix = Affix::Postfix(Precedence(BETWEEN_PREC));
363+
const ESCAPE_AFFIX: Affix = Affix::Postfix(Precedence(BETWEEN_PREC));
356364
const JSON_OP_AFFIX: Affix = Affix::Infix(Precedence(40), Associativity::Left);
357365
const PG_CAST_AFFIX: Affix = Affix::Postfix(Precedence(60));
358366

@@ -379,9 +387,9 @@ const fn binary_affix(op: &BinaryOperator) -> Affix {
379387
BinaryOperator::Lt => Affix::Infix(Precedence(20), Associativity::Left),
380388
BinaryOperator::Gte => Affix::Infix(Precedence(20), Associativity::Left),
381389
BinaryOperator::Lte => Affix::Infix(Precedence(20), Associativity::Left),
382-
BinaryOperator::Like => Affix::Infix(Precedence(20), Associativity::Left),
383-
BinaryOperator::LikeAny => Affix::Infix(Precedence(20), Associativity::Left),
384-
BinaryOperator::NotLike => Affix::Infix(Precedence(20), Associativity::Left),
390+
BinaryOperator::Like(_) => Affix::Infix(Precedence(20), Associativity::Left),
391+
BinaryOperator::LikeAny(_) => Affix::Infix(Precedence(20), Associativity::Left),
392+
BinaryOperator::NotLike(_) => Affix::Infix(Precedence(20), Associativity::Left),
385393
BinaryOperator::Regexp => Affix::Infix(Precedence(20), Associativity::Left),
386394
BinaryOperator::NotRegexp => Affix::Infix(Precedence(20), Associativity::Left),
387395
BinaryOperator::RLike => Affix::Infix(Precedence(20), Associativity::Left),
@@ -418,6 +426,7 @@ impl ExprElement {
418426
ExprElement::InList { .. } => IN_LIST_AFFIX,
419427
ExprElement::InSubquery { .. } => IN_SUBQUERY_AFFIX,
420428
ExprElement::LikeSubquery { .. } => LIKE_SUBQUERY_AFFIX,
429+
ExprElement::Escape { .. } => ESCAPE_AFFIX,
421430
ExprElement::UnaryOp { op } => unary_affix(op),
422431
ExprElement::BinaryOp { op } => binary_affix(op),
423432
ExprElement::JsonOp { .. } => JSON_OP_AFFIX,
@@ -467,6 +476,8 @@ impl Expr {
467476
Expr::InList { .. } => IN_LIST_AFFIX,
468477
Expr::InSubquery { .. } => IN_SUBQUERY_AFFIX,
469478
Expr::LikeSubquery { .. } => LIKE_SUBQUERY_AFFIX,
479+
Expr::LikeAnyWithEscape { .. } => LIKE_ANY_WITH_ESCAPE_AFFIX,
480+
Expr::LikeWithEscape { .. } => LIKE_WITH_ESCAPE_AFFIX,
470481
Expr::UnaryOp { op, .. } => unary_affix(op),
471482
Expr::BinaryOp { op, .. } => binary_affix(op),
472483
Expr::JsonOp { .. } => JSON_OP_AFFIX,
@@ -847,11 +858,54 @@ impl<'a, I: Iterator<Item = WithSpan<'a, ExprElement>>> PrattParser<I> for ExprP
847858
subquery,
848859
not,
849860
},
850-
ExprElement::LikeSubquery { subquery, modifier } => Expr::LikeSubquery {
861+
ExprElement::LikeSubquery {
862+
subquery,
863+
modifier,
864+
escape,
865+
} => Expr::LikeSubquery {
851866
span: transform_span(elem.span.tokens),
852867
expr: Box::new(lhs),
853868
subquery,
854869
modifier,
870+
escape,
871+
},
872+
ExprElement::Escape { escape } => match lhs {
873+
Expr::BinaryOp {
874+
span,
875+
op: BinaryOperator::Like(_),
876+
left,
877+
right,
878+
} => Expr::LikeWithEscape {
879+
span,
880+
left,
881+
right,
882+
is_not: false,
883+
escape,
884+
},
885+
Expr::BinaryOp {
886+
span,
887+
op: BinaryOperator::NotLike(_),
888+
left,
889+
right,
890+
} => Expr::LikeWithEscape {
891+
span,
892+
left,
893+
right,
894+
is_not: true,
895+
escape,
896+
},
897+
Expr::BinaryOp {
898+
span,
899+
op: BinaryOperator::LikeAny(_),
900+
left,
901+
right,
902+
} => Expr::LikeAnyWithEscape {
903+
span,
904+
left,
905+
right,
906+
escape,
907+
},
908+
_ => return Err("escape clause must be after LIKE/NOT LIKE/LIKE ANY binary expr"),
855909
},
856910
ExprElement::Between { low, high, not } => Expr::Between {
857911
span: transform_span(elem.span.tokens),
@@ -913,9 +967,9 @@ pub fn expr_element(i: Input) -> IResult<WithSpan<ExprElement>> {
913967
);
914968
let like_subquery = map(
915969
rule! {
916-
LIKE ~ ( ANY | SOME | ALL ) ~ "(" ~ #query ~ ^")"
970+
LIKE ~ ( ANY | SOME | ALL ) ~ "(" ~ #query ~ ^")" ~ (ESCAPE ~ ^#literal_string)?
917971
},
918-
|(_, m, _, subquery, _)| {
972+
|(_, m, _, subquery, _, option_escape)| {
919973
let modifier = match m.kind {
920974
TokenKind::ALL => SubqueryModifier::All,
921975
TokenKind::ANY => SubqueryModifier::Any,
@@ -925,9 +979,16 @@ pub fn expr_element(i: Input) -> IResult<WithSpan<ExprElement>> {
925979
ExprElement::LikeSubquery {
926980
modifier,
927981
subquery: Box::new(subquery),
982+
escape: option_escape.map(|(_, escape)| escape),
928983
}
929984
},
930985
);
986+
let escape = map(
987+
rule! {
988+
ESCAPE ~ ^#literal_string
989+
},
990+
|(_, escape)| ExprElement::Escape { escape },
991+
);
931992
let between = map(
932993
rule! {
933994
NOT? ~ BETWEEN ~ ^#subexpr(BETWEEN_PREC) ~ ^AND ~ ^#subexpr(BETWEEN_PREC)
@@ -1438,6 +1499,7 @@ pub fn expr_element(i: Input) -> IResult<WithSpan<ExprElement>> {
14381499
| #list_comprehensions: "[expr for x in ... [if ...]]"
14391500
| #count_all_with_window : "`COUNT(*) OVER ...`"
14401501
| #function_call
1502+
| #escape: "`ESCAPE '<escape>'`"
14411503
),
14421504
rule!(
14431505
#case : "`CASE ... END`"
@@ -1492,9 +1554,9 @@ pub fn binary_op(i: Input) -> IResult<BinaryOperator> {
14921554
value(BinaryOperator::And, rule! { AND }),
14931555
value(BinaryOperator::Or, rule! { OR }),
14941556
value(BinaryOperator::Xor, rule! { XOR }),
1495-
value(BinaryOperator::LikeAny, rule! { LIKE ~ ANY }),
1496-
value(BinaryOperator::Like, rule! { LIKE }),
1497-
value(BinaryOperator::NotLike, rule! { NOT ~ LIKE }),
1557+
value(BinaryOperator::LikeAny(None), rule! { LIKE ~ ANY }),
1558+
value(BinaryOperator::Like(None), rule! { LIKE }),
1559+
value(BinaryOperator::NotLike(None), rule! { NOT ~ LIKE }),
14981560
value(BinaryOperator::Regexp, rule! { REGEXP }),
14991561
value(BinaryOperator::NotRegexp, rule! { NOT ~ REGEXP }),
15001562
value(BinaryOperator::RLike, rule! { RLIKE }),

src/query/ast/tests/it/parser.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,6 +1285,8 @@ fn test_expr() {
12851285
r#"(arr[0]:a).b"#,
12861286
r#"arr[4]["k"]"#,
12871287
r#"a rlike '^11'"#,
1288+
r#"a like '%1$%1%' escape '$'"#,
1289+
r#"a not like '%1$%1%' escape '$'"#,
12881290
r#"'中文'::text not in ('a', 'b')"#,
12891291
r#"G.E.B IS NOT NULL AND col1 not between col2 and (1 + col3) DIV sum(col4)"#,
12901292
r#"sum(CASE WHEN n2.n_name = 'GERMANY' THEN ol_amount ELSE 0 END) / CASE WHEN sum(ol_amount) = 0 THEN 1 ELSE sum(ol_amount) END"#,
@@ -1296,9 +1298,11 @@ fn test_expr() {
12961298
AND l_shipmode IN ('AIR', 'AIR REG')
12971299
AND l_shipinstruct = 'DELIVER IN PERSON'"#,
12981300
r#"'中文'::text LIKE ANY ('a', 'b')"#,
1301+
r#"'中文'::text LIKE ANY ('a', 'b') ESCAPE '$'"#,
12991302
r#"'中文'::text LIKE ANY (SELECT 'a', 'b')"#,
13001303
r#"'中文'::text LIKE ALL (SELECT 'a', 'b')"#,
13011304
r#"'中文'::text LIKE SOME (SELECT 'a', 'b')"#,
1305+
r#"'中文'::text LIKE ANY (SELECT 'a', 'b') ESCAPE '$'"#,
13021306
r#"nullif(1, 1)"#,
13031307
r#"nullif(a, b)"#,
13041308
r#"coalesce(1, 2, 3)"#,
@@ -1357,6 +1361,8 @@ fn test_expr_error() {
13571361
AND col1 NOT BETWEEN col2 AND
13581362
AND 1 + col3 DIV sum(col4)
13591363
"#,
1364+
r#"CAST(1 AS STRING) ESCAPE '$'"#,
1365+
r#"1 + 1 ESCAPE '$'"#,
13601366
];
13611367

13621368
for case in cases {

src/query/ast/tests/it/testdata/expr-error.txt

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ error:
5252
--> SQL:1:10
5353
|
5454
1 | CAST(col1)
55-
| ---- ^ unexpected `)`, expecting `AS`, `,`, `(`, `IS`, `NOT`, `IN`, `LIKE`, `EXISTS`, `BETWEEN`, `+`, `-`, `*`, `/`, `//`, `DIV`, `%`, `||`, `<->`, `>`, `<`, `>=`, `<=`, `=`, `<>`, `!=`, `^`, `AND`, `OR`, `XOR`, `REGEXP`, `RLIKE`, `SOUNDS`, <BitWiseOr>, <BitWiseAnd>, <BitWiseXor>, <ShiftLeft>, <ShiftRight>, `->`, `->>`, `#>`, `#>>`, `?`, `?|`, `?&`, `@>`, `<@`, `@?`, `@@`, `#-`, <Factorial>, <SquareRoot>, <BitWiseNot>, <CubeRoot>, <Abs>, `CAST`, `TRY_CAST`, `::`, `POSITION`, `IdentVariable`, `DATE_ADD`, or 42 more ...
55+
| ---- ^ unexpected `)`, expecting `AS`, `,`, `(`, `IS`, `NOT`, `IN`, `LIKE`, `EXISTS`, `BETWEEN`, `+`, `-`, `*`, `/`, `//`, `DIV`, `%`, `||`, `<->`, `>`, `<`, `>=`, `<=`, `=`, `<>`, `!=`, `^`, `AND`, `OR`, `XOR`, `REGEXP`, `RLIKE`, `SOUNDS`, <BitWiseOr>, <BitWiseAnd>, <BitWiseXor>, <ShiftLeft>, <ShiftRight>, `->`, `->>`, `#>`, `#>>`, `?`, `?|`, `?&`, `@>`, `<@`, `@?`, `@@`, `#-`, <Factorial>, <SquareRoot>, <BitWiseNot>, <CubeRoot>, <Abs>, `CAST`, `TRY_CAST`, `::`, `POSITION`, `IdentVariable`, `DATE_ADD`, or 43 more ...
5656
| |
5757
| while parsing `CAST(... AS ...)`
5858
| while parsing expression
@@ -81,7 +81,7 @@ error:
8181
1 | $ abc + 3
8282
| ^
8383
| |
84-
| unexpected `$`, expecting `IS`, `IN`, `LIKE`, `EXISTS`, `BETWEEN`, `+`, `-`, `*`, `/`, `//`, `DIV`, `%`, `||`, `<->`, `>`, `<`, `>=`, `<=`, `=`, `<>`, `!=`, `^`, `AND`, `OR`, `XOR`, `NOT`, `REGEXP`, `RLIKE`, `SOUNDS`, <BitWiseOr>, <BitWiseAnd>, <BitWiseXor>, <ShiftLeft>, <ShiftRight>, `->`, `->>`, `#>`, `#>>`, `?`, `?|`, `?&`, `@>`, `<@`, `@?`, `@@`, `#-`, <Factorial>, <SquareRoot>, <BitWiseNot>, <CubeRoot>, <Abs>, `CAST`, `TRY_CAST`, `::`, `POSITION`, `IdentVariable`, `DATE_ADD`, `DATE_DIFF`, `DATEDIFF`, `DATESUB`, or 40 more ...
84+
| unexpected `$`, expecting `IS`, `IN`, `LIKE`, `EXISTS`, `BETWEEN`, `+`, `-`, `*`, `/`, `//`, `DIV`, `%`, `||`, `<->`, `>`, `<`, `>=`, `<=`, `=`, `<>`, `!=`, `^`, `AND`, `OR`, `XOR`, `NOT`, `REGEXP`, `RLIKE`, `SOUNDS`, <BitWiseOr>, <BitWiseAnd>, <BitWiseXor>, <ShiftLeft>, <ShiftRight>, `->`, `->>`, `#>`, `#>>`, `?`, `?|`, `?&`, `@>`, `<@`, `@?`, `@@`, `#-`, <Factorial>, <SquareRoot>, <BitWiseNot>, <CubeRoot>, <Abs>, `CAST`, `TRY_CAST`, `::`, `POSITION`, `IdentVariable`, `DATE_ADD`, `DATE_DIFF`, `DATEDIFF`, `DATESUB`, or 41 more ...
8585
| while parsing expression
8686

8787

@@ -115,3 +115,27 @@ error:
115115
| ^^^ expected more tokens for expression
116116

117117

118+
---------- Input ----------
119+
CAST(1 AS STRING) ESCAPE '$'
120+
---------- Output ---------
121+
error:
122+
--> SQL:1:19
123+
|
124+
1 | CAST(1 AS STRING) ESCAPE '$'
125+
| ---- ^^^^^^ escape clause must be after LIKE/NOT LIKE/LIKE ANY binary expr
126+
| |
127+
| while parsing expression
128+
129+
130+
---------- Input ----------
131+
1 + 1 ESCAPE '$'
132+
---------- Output ---------
133+
error:
134+
--> SQL:1:7
135+
|
136+
1 | 1 + 1 ESCAPE '$'
137+
| - ^^^^^^ escape clause must be after LIKE/NOT LIKE/LIKE ANY binary expr
138+
| |
139+
| while parsing expression
140+
141+

0 commit comments

Comments
 (0)