Skip to content

Commit 249f47a

Browse files
authored
fix(query): handle empty LIKE ESCAPE in planner (#19595)
* fix(query): handle empty LIKE ESCAPE in planner (#19562) * fix(query): validate LIKE ESCAPE literals (#19562) * fix(query): preserve LIKE ESCAPE fallback semantics * test(query): cover empty LIKE ESCAPE fallback (#19562) * test(query): align empty LIKE ESCAPE regression output * test(query): remove cluster-unstable explain assertion
1 parent f4061b1 commit 249f47a

3 files changed

Lines changed: 60 additions & 7 deletions

File tree

src/query/sql/src/planner/semantic/type_check.rs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5646,13 +5646,22 @@ impl<'a> TypeChecker<'a> {
56465646
like_str: &str,
56475647
escape: &Option<String>,
56485648
) -> Result<Box<(ScalarExpr, DataType)>> {
5649-
let new_like_str = if let Some(escape) = escape {
5650-
Cow::Owned(convert_escape_pattern(
5651-
like_str,
5652-
escape.chars().next().unwrap(),
5653-
))
5654-
} else {
5655-
Cow::Borrowed(like_str)
5649+
let new_like_str = match escape.as_ref() {
5650+
Some(escape_literal) => {
5651+
let mut chars = escape_literal.chars();
5652+
let Some(escape_char) = chars.next() else {
5653+
// Empty escape literals must stay on the builtin path to match runtime behavior.
5654+
return self.resolve_like_escape(op, span, left, right, escape);
5655+
};
5656+
5657+
if chars.next().is_some() {
5658+
// Preserve existing builtin behavior for non-single-character escape literals.
5659+
return self.resolve_like_escape(op, span, left, right, escape);
5660+
}
5661+
5662+
Cow::Owned(convert_escape_pattern(like_str, escape_char))
5663+
}
5664+
None => Cow::Borrowed(like_str),
56565665
};
56575666
if check_percent(&new_like_str) {
56585667
// Convert to `a is not null`

src/query/sql/tests/it/planner.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,24 @@ async fn test_lite_replay_service_optimizer_cases() -> Result<()> {
142142
Ok(())
143143
}
144144

145+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
146+
async fn test_like_escape_preserves_existing_binding_semantics() -> Result<()> {
147+
let ctx = LiteTableContext::create().await?;
148+
149+
for sql in [
150+
"SELECT 'a' LIKE 'a' ESCAPE ''",
151+
"SELECT 'a' LIKE concat('a') ESCAPE ''",
152+
"SELECT '%' LIKE '\\\\%' ESCAPE ''",
153+
"SELECT like_any('%', '\\\\%', '')",
154+
"SELECT 'a' LIKE ANY ('a', 'b') ESCAPE ''",
155+
"SELECT 'a' LIKE ANY (SELECT 'a') ESCAPE ''",
156+
] {
157+
ctx.bind_sql(sql).await?;
158+
}
159+
160+
Ok(())
161+
}
162+
145163
async fn setup_tables(ctx: &Arc<LiteTableContext>, case: &TestCase) -> Result<()> {
146164
for sql in case.tables.values() {
147165
for statement in sql.split(';').filter(|s| !s.trim().is_empty()) {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# GitHub issue: https://github.com/databendlabs/databend/issues/19562
2+
3+
query B
4+
SELECT 'a' LIKE 'a' ESCAPE ''
5+
----
6+
1
7+
8+
query B
9+
SELECT 'a' LIKE concat('a') ESCAPE ''
10+
----
11+
1
12+
13+
query B
14+
SELECT '%' LIKE '\\\\%' ESCAPE ''
15+
----
16+
0
17+
18+
query B
19+
SELECT like_any('%', '\\\\%', '')
20+
----
21+
0
22+
23+
query B
24+
SELECT 'a' LIKE ANY (SELECT 'a') ESCAPE ''
25+
----
26+
1

0 commit comments

Comments
 (0)