Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions src/query/sql/src/planner/semantic/type_check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5620,13 +5620,22 @@ impl<'a> TypeChecker<'a> {
like_str: &str,
escape: &Option<String>,
) -> Result<Box<(ScalarExpr, DataType)>> {
let new_like_str = if let Some(escape) = escape {
Cow::Owned(convert_escape_pattern(
like_str,
escape.chars().next().unwrap(),
))
} else {
Cow::Borrowed(like_str)
let new_like_str = match escape.as_ref() {
Some(escape_literal) => {
let mut chars = escape_literal.chars();
let Some(escape_char) = chars.next() else {
// Empty escape literals must stay on the builtin path to match runtime behavior.
return self.resolve_like_escape(op, span, left, right, escape);
};

if chars.next().is_some() {
// Preserve existing builtin behavior for non-single-character escape literals.
return self.resolve_like_escape(op, span, left, right, escape);
}

Cow::Owned(convert_escape_pattern(like_str, escape_char))
}
None => Cow::Borrowed(like_str),
};
if check_percent(&new_like_str) {
// Convert to `a is not null`
Expand Down
18 changes: 18 additions & 0 deletions src/query/sql/tests/it/planner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,24 @@ async fn test_lite_replay_service_optimizer_cases() -> Result<()> {
Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_like_escape_preserves_existing_binding_semantics() -> Result<()> {
let ctx = LiteTableContext::create().await?;

for sql in [
"SELECT 'a' LIKE 'a' ESCAPE ''",
"SELECT 'a' LIKE concat('a') ESCAPE ''",
"SELECT '%' LIKE '\\\\%' ESCAPE ''",
"SELECT like_any('%', '\\\\%', '')",
"SELECT 'a' LIKE ANY ('a', 'b') ESCAPE ''",
"SELECT 'a' LIKE ANY (SELECT 'a') ESCAPE ''",
] {
ctx.bind_sql(sql).await?;
}

Ok(())
}

async fn setup_tables(ctx: &Arc<LiteTableContext>, case: &TestCase) -> Result<()> {
for sql in case.tables.values() {
for statement in sql.split(';').filter(|s| !s.trim().is_empty()) {
Expand Down
44 changes: 44 additions & 0 deletions tests/sqllogictests/suites/query/issues/issue_19562.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# GitHub issue: https://github.com/databendlabs/databend/issues/19562

query B
SELECT 'a' LIKE 'a' ESCAPE ''
----
1

query B
SELECT 'a' LIKE concat('a') ESCAPE ''
----
1

query B
SELECT '%' LIKE '\\\\%' ESCAPE ''
----
1

query B
SELECT like_any('%', '\\\\%', '')
----
1

query T
EXPLAIN SELECT * FROM numbers(1) WHERE to_string(number) LIKE '\\\\%' ESCAPE ''
----
Filter
├── output columns: [numbers.number (#0)]
├── filters: [like(CAST(numbers.number (#0) AS String), '\\\\%', '')]
├── estimated rows: 1.00
└── TableScan
├── table: default.system.numbers
├── scan id: 0
├── output columns: [number (#0)]
├── read rows: 1
├── read size: < 1 KiB
├── partitions total: 1
├── partitions scanned: 1
├── push downs: [filters: [like(CAST(numbers.number (#0) AS String), '\\\\%', '')], limit: NONE]
└── estimated rows: 1.00

query B
SELECT 'a' LIKE ANY (SELECT 'a') ESCAPE ''
----
1
Loading