diff --git a/src/query/sql/src/planner/semantic/type_check.rs b/src/query/sql/src/planner/semantic/type_check.rs index 9d94f00e486c3..90d4d661af4b0 100644 --- a/src/query/sql/src/planner/semantic/type_check.rs +++ b/src/query/sql/src/planner/semantic/type_check.rs @@ -5646,13 +5646,22 @@ impl<'a> TypeChecker<'a> { like_str: &str, escape: &Option, ) -> Result> { - 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` diff --git a/src/query/sql/tests/it/planner.rs b/src/query/sql/tests/it/planner.rs index 9fc5d93869758..070fc980ec0d6 100644 --- a/src/query/sql/tests/it/planner.rs +++ b/src/query/sql/tests/it/planner.rs @@ -142,6 +142,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, case: &TestCase) -> Result<()> { for sql in case.tables.values() { for statement in sql.split(';').filter(|s| !s.trim().is_empty()) { diff --git a/tests/sqllogictests/suites/query/issues/issue_19562.test b/tests/sqllogictests/suites/query/issues/issue_19562.test new file mode 100644 index 0000000000000..135b9570363b4 --- /dev/null +++ b/tests/sqllogictests/suites/query/issues/issue_19562.test @@ -0,0 +1,26 @@ +# 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 '' +---- +0 + +query B +SELECT like_any('%', '\\\\%', '') +---- +0 + +query B +SELECT 'a' LIKE ANY (SELECT 'a') ESCAPE '' +---- +1