diff --git a/datafusion/physical-expr/src/expressions/binary.rs b/datafusion/physical-expr/src/expressions/binary.rs index 6f0b60556a751..51aa781478024 100644 --- a/datafusion/physical-expr/src/expressions/binary.rs +++ b/datafusion/physical-expr/src/expressions/binary.rs @@ -18,6 +18,8 @@ mod kernels; use crate::PhysicalExpr; +use crate::expressions::SqlSimilarToPattern; +use crate::expressions::translate_scalar; use crate::intervals::cp_solver::{propagate_arithmetic, propagate_comparison}; use std::hash::Hash; use std::sync::Arc; @@ -1112,7 +1114,19 @@ pub fn similar_to( (true, false) => Operator::RegexNotMatch, (true, true) => Operator::RegexNotIMatch, }; - Ok(Arc::new(BinaryExpr::new(expr, binary_op, pattern))) + + let translated_pattern = match pattern.downcast_ref::() { + Some(literal) => Arc::new(crate::expressions::Literal::new(translate_scalar( + literal.value(), + )?)) as Arc, + None => Arc::new(SqlSimilarToPattern::new(pattern)) as Arc, + }; + + Ok(Arc::new(BinaryExpr::new( + expr, + binary_op, + translated_pattern, + ))) } #[cfg(test)] @@ -4800,25 +4814,17 @@ mod tests { Ok(()) } - /// Test helper for SIMILAR TO binary operation fn apply_similar_to( schema: &SchemaRef, va: Vec<&str>, - vb: Vec<&str>, + pattern: &str, negated: bool, case_insensitive: bool, expected: &BooleanArray, ) -> Result<()> { let a = StringArray::from(va); - let b = StringArray::from(vb); - let op = similar_to( - negated, - case_insensitive, - col("a", schema)?, - col("b", schema)?, - )?; - let batch = - RecordBatch::try_new(Arc::clone(schema), vec![Arc::new(a), Arc::new(b)])?; + let op = similar_to(negated, case_insensitive, col("a", schema)?, lit(pattern))?; + let batch = RecordBatch::try_new(Arc::clone(schema), vec![Arc::new(a)])?; let result = op .evaluate(&batch)? .into_array(batch.num_rows()) @@ -4830,32 +4836,237 @@ mod tests { #[test] fn test_similar_to() { - let schema = Arc::new(Schema::new(vec![ - Field::new("a", DataType::Utf8, false), - Field::new("b", DataType::Utf8, false), - ])); + let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Utf8, false)])); + // `%` matches any sequence; case-sensitive let expected = [Some(true), Some(false)].iter().collect(); - // case-sensitive apply_similar_to( &schema, vec!["hello world", "Hello World"], - vec!["hello.*", "hello.*"], + "hello%", false, false, &expected, ) .unwrap(); - // case-insensitive + + // `%` matches any sequence; case-insensitive + let expected = [Some(true), Some(false)].iter().collect(); apply_similar_to( &schema, vec!["hello world", "bye"], - vec!["hello.*", "hello.*"], + "hello%", false, true, &expected, ) .unwrap(); + + // `_` matches exactly one character + let expected = [Some(true), Some(false), Some(false)].iter().collect(); + apply_similar_to(&schema, vec!["x", "xy", ""], "_", false, false, &expected) + .unwrap(); + + // Match must cover the entire string (no implicit substring match) + let expected = [Some(false), Some(true)].iter().collect(); + apply_similar_to(&schema, vec!["abc", "a"], "a", false, false, &expected) + .unwrap(); + + // `%` matches zero or more, so the empty string matches. + let expected = [Some(true), Some(true)].iter().collect(); + apply_similar_to(&schema, vec!["", "anything"], "%", false, false, &expected) + .unwrap(); + + // `_` requires exactly one character, so the empty string does not + // match. + let expected = [Some(false), Some(true)].iter().collect(); + apply_similar_to(&schema, vec!["", "x"], "_", false, false, &expected).unwrap(); + + // `%` at the start of the pattern is still anchored: the string + // must end where the trailing literal begins. + let expected = [Some(true), Some(false)].iter().collect(); + apply_similar_to(&schema, vec!["abc", "abd"], "%c", false, false, &expected) + .unwrap(); + + // `%` and `_` together: `%` matches zero or more (including the + // empty string), `_` matches exactly one character. + let expected = [Some(true), Some(true)].iter().collect(); + apply_similar_to(&schema, vec!["a", "abc"], "a%", false, false, &expected) + .unwrap(); + let expected = [Some(true), Some(false)].iter().collect(); + apply_similar_to(&schema, vec!["axb", "abc"], "a_b", false, false, &expected) + .unwrap(); + } + + // Regression: regex metacharacters that are NOT SIMILAR TO metacharacters + // (`. ^ $ \`) must be treated as SQL literals. Without escaping, `a.` + // would match any `a` followed by any character (`ab`, `a1`, ...). + #[test] + fn test_similar_to_sql_literal_metachars() { + let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Utf8, false)])); + + // `.` is a literal, not the regex "any character" operator. + let expected = [Some(true), Some(false), Some(false)].iter().collect(); + apply_similar_to( + &schema, + vec!["a.", "ab", "a"], + "a.", + false, + false, + &expected, + ) + .unwrap(); + + // `^` and `$` are literals and only match the literal `^` and `$`. + let expected = [Some(true), Some(false)].iter().collect(); + apply_similar_to(&schema, vec!["^x$", "x"], r"^x$", false, false, &expected) + .unwrap(); + + // `\` is a literal backslash (we don't support the ESCAPE clause). + let expected = [Some(true), Some(false)].iter().collect(); + apply_similar_to(&schema, vec![r"a\b", "ab"], r"a\b", false, false, &expected) + .unwrap(); + } + + // SIMILAR TO borrows POSIX metacharacters from regular expressions: + // `| * + ? ( ) { } [ ]`. The translator passes them through to the + // underlying regex engine. + #[test] + fn test_similar_to_posix_metachars() { + let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Utf8, false)])); + + // `|` alternation. + let expected = [Some(true), Some(false), Some(true)].iter().collect(); + apply_similar_to(&schema, vec!["a", "c", "b"], "a|b", false, false, &expected) + .unwrap(); + + // `*` zero or more. + let expected = [Some(true), Some(true), Some(false)].iter().collect(); + apply_similar_to(&schema, vec!["", "aa", "ab"], "a*", false, false, &expected) + .unwrap(); + + // `+` one or more. + let expected = [Some(false), Some(true)].iter().collect(); + apply_similar_to(&schema, vec!["", "aa"], "a+", false, false, &expected).unwrap(); + + // `?` zero or one. + let expected = [Some(true), Some(true), Some(false)].iter().collect(); + apply_similar_to(&schema, vec!["", "a", "aa"], "a?", false, false, &expected) + .unwrap(); + + // `()` grouping. + let expected = [Some(true), Some(true), Some(false)].iter().collect(); + apply_similar_to( + &schema, + vec!["ab", "abc", "ac"], + "(ab)c?", + false, + false, + &expected, + ) + .unwrap(); + + // `{m}` exact count. + let expected = [Some(true), Some(false)].iter().collect(); + apply_similar_to(&schema, vec!["aaa", "aa"], "a{3}", false, false, &expected) + .unwrap(); + + // `[...]` character class. + let expected = [Some(true), Some(false)].iter().collect(); + apply_similar_to(&schema, vec!["a", "c"], "[ab]", false, false, &expected) + .unwrap(); + + // `[^...]` negated character class. + let expected = [Some(true), Some(false)].iter().collect(); + apply_similar_to(&schema, vec!["c", "a"], "[^ab]", false, false, &expected) + .unwrap(); + + // `[a-z]` range inside a character class. + let expected = [Some(true), Some(false)].iter().collect(); + apply_similar_to(&schema, vec!["m", "1"], "[a-z]", false, false, &expected) + .unwrap(); + } + + // Regression: `%` and `_` must match newlines, matching SQL semantics + // where these wildcards match "any character". + #[test] + fn test_similar_to_wildcards_match_newlines() { + let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Utf8, false)])); + + // `%` crosses a newline. (`%` also matches zero characters, so `ab` + // matches `a%b` as well.) + let expected = [Some(true), Some(true)].iter().collect(); + apply_similar_to(&schema, vec!["a\nb", "ab"], "a%b", false, false, &expected) + .unwrap(); + + // `_` matches a single newline. (`_` requires exactly one character, + // so `ab` does not match `a_b`.) + let expected = [Some(true), Some(false)].iter().collect(); + apply_similar_to(&schema, vec!["a\nb", "ab"], "a_b", false, false, &expected) + .unwrap(); + } + + #[test] + fn test_similar_to_non_literal_pattern_errors() { + let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Utf8, false)])); + // Non-string literal patterns still error. + let err = similar_to(false, false, col("a", &schema).unwrap(), lit(1i32)) + .expect_err("non-string literal pattern should error"); + assert!( + err.to_string() + .contains("SIMILAR TO pattern must be a string type, got Int32(1)"), + "unexpected error message: {err}" + ); + } + + #[test] + fn test_similar_to_dynamic_pattern() { + let schema = Arc::new(Schema::new(vec![ + Field::new("text", DataType::Utf8, false), + Field::new("pattern", DataType::Utf8, false), + ])); + let text = StringArray::from(vec!["abc", "ab", "x"]); + let pattern = StringArray::from(vec!["a%", "a.", "_"]); + let batch = RecordBatch::try_new( + Arc::clone(&schema), + vec![Arc::new(text), Arc::new(pattern)], + ) + .unwrap(); + + let op = similar_to( + false, + false, + col("text", &schema).unwrap(), + col("pattern", &schema).unwrap(), + ) + .unwrap(); + let result = op.evaluate(&batch).unwrap(); + let result_array = result.into_array(batch.num_rows()).unwrap(); + let result = as_boolean_array(&result_array).unwrap(); + assert!(result.value(0)); // "abc" ~ ^(?:a(?s:.*))$ + assert!(!result.value(1)); // "ab" ~ ^(?:a\.)$ + assert!(result.value(2)); // "x" ~ ^(?:a(?s:.))$ + } + + #[test] + fn test_similar_to_null_pattern() { + let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Utf8, false)])); + let a = StringArray::from(vec!["hello"]); + let op = similar_to( + false, + false, + col("a", &schema).unwrap(), + lit(ScalarValue::Utf8(None)), + ) + .unwrap(); + let batch = RecordBatch::try_new(Arc::clone(&schema), vec![Arc::new(a)]).unwrap(); + let result = op + .evaluate(&batch) + .unwrap() + .into_array(batch.num_rows()) + .unwrap(); + let expected: BooleanArray = [None].iter().collect(); + assert_eq!(result.as_ref(), &expected); } pub fn binary_expr( diff --git a/datafusion/physical-expr/src/expressions/mod.rs b/datafusion/physical-expr/src/expressions/mod.rs index 05a04f88dcadf..ae39186bd9ef8 100644 --- a/datafusion/physical-expr/src/expressions/mod.rs +++ b/datafusion/physical-expr/src/expressions/mod.rs @@ -33,6 +33,7 @@ mod literal; mod negative; mod no_op; mod not; +mod similar_to_pattern; mod try_cast; mod unknown_column; @@ -59,6 +60,8 @@ pub use literal::{Literal, lit}; pub use negative::{NegativeExpr, negative}; pub use no_op::NoOp; pub use not::{NotExpr, not}; +pub(crate) use similar_to_pattern::translate_scalar; +pub use similar_to_pattern::{SqlSimilarToPattern, sql_similar_to_regex}; pub use try_cast::{TryCastExpr, try_cast}; pub use unknown_column::UnKnownColumn; diff --git a/datafusion/physical-expr/src/expressions/similar_to_pattern.rs b/datafusion/physical-expr/src/expressions/similar_to_pattern.rs new file mode 100644 index 0000000000000..6f3115b70af68 --- /dev/null +++ b/datafusion/physical-expr/src/expressions/similar_to_pattern.rs @@ -0,0 +1,232 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::fmt; +use std::hash::Hash; +use std::sync::Arc; + +use crate::PhysicalExpr; + +use arrow::array::{ArrayRef, LargeStringArray, StringArray, StringViewArray}; +use arrow::datatypes::{DataType, FieldRef}; +use arrow::record_batch::RecordBatch; +use datafusion_common::{Result, ScalarValue, exec_err}; +use datafusion_expr::ColumnarValue; +use datafusion_physical_expr_common::physical_expr::fmt_sql; + +pub fn sql_similar_to_regex(pattern: &str) -> String { + let mut result = String::with_capacity(pattern.len() + 10); + result.push_str("^(?:"); + let mut in_bracket = false; + for ch in pattern.chars() { + match (ch, in_bracket) { + ('%', false) => result.push_str("(?s:.*)"), + ('_', false) => result.push_str("(?s:.)"), + ('[', false) => { + result.push('['); + in_bracket = true; + } + (']', true) => { + result.push(']'); + in_bracket = false; + } + // `. ^ $` are SQL literals but regex metachars when not inside a + // bracket expression; `\` is a regex escape character in all + // positions, so it always needs escaping. + ('.' | '^' | '$', false) | ('\\', _) => { + result.push('\\'); + result.push(ch); + } + (c, _) => result.push(c), + } + } + result.push_str(")$"); + result +} + +#[derive(Debug, Eq)] +pub struct SqlSimilarToPattern { + expr: Arc, +} + +// Manually derive PartialEq and Hash to work around https://github.com/rust-lang/rust/issues/78808 +impl PartialEq for SqlSimilarToPattern { + fn eq(&self, other: &Self) -> bool { + self.expr.eq(&other.expr) + } +} + +impl Hash for SqlSimilarToPattern { + fn hash(&self, state: &mut H) { + self.expr.hash(state); + } +} + +impl SqlSimilarToPattern { + pub fn new(expr: Arc) -> Self { + Self { expr } + } + + pub fn expr(&self) -> &Arc { + &self.expr + } +} + +impl fmt::Display for SqlSimilarToPattern { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "sql_similar_to_regex({})", self.expr) + } +} + +impl PhysicalExpr for SqlSimilarToPattern { + fn data_type(&self, input_schema: &arrow::datatypes::Schema) -> Result { + self.expr.data_type(input_schema) + } + + fn nullable(&self, input_schema: &arrow::datatypes::Schema) -> Result { + self.expr.nullable(input_schema) + } + + fn evaluate(&self, batch: &RecordBatch) -> Result { + match self.expr.evaluate(batch)? { + ColumnarValue::Array(array) => translate_array(&array), + ColumnarValue::Scalar(scalar) => { + Ok(ColumnarValue::Scalar(translate_scalar(&scalar)?)) + } + } + } + + fn return_field(&self, input_schema: &arrow::datatypes::Schema) -> Result { + self.expr.return_field(input_schema) + } + + fn children(&self) -> Vec<&Arc> { + vec![&self.expr] + } + + fn with_new_children( + self: Arc, + children: Vec>, + ) -> Result> { + Ok(Arc::new(SqlSimilarToPattern::new(Arc::clone(&children[0])))) + } + + fn fmt_sql(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "sql_similar_to_regex({})", fmt_sql(self.expr.as_ref())) + } +} + +pub(crate) fn translate_scalar(scalar: &ScalarValue) -> Result { + match scalar { + ScalarValue::Utf8(Some(s)) => { + Ok(ScalarValue::Utf8(Some(sql_similar_to_regex(s)))) + } + ScalarValue::LargeUtf8(Some(s)) => { + Ok(ScalarValue::LargeUtf8(Some(sql_similar_to_regex(s)))) + } + ScalarValue::Utf8View(Some(s)) => { + Ok(ScalarValue::Utf8View(Some(sql_similar_to_regex(s)))) + } + ScalarValue::Utf8(None) + | ScalarValue::LargeUtf8(None) + | ScalarValue::Utf8View(None) + | ScalarValue::Null => Ok(ScalarValue::Utf8(None)), + other => exec_err!("SIMILAR TO pattern must be a string type, got {other:?}"), + } +} + +fn translate_array(array: &ArrayRef) -> Result { + match array.data_type() { + DataType::Utf8 => { + let arr = array.as_any().downcast_ref::().unwrap(); + let translated: StringArray = arr + .iter() + .map(|opt| opt.map(sql_similar_to_regex)) + .collect(); + Ok(ColumnarValue::Array(Arc::new(translated))) + } + DataType::LargeUtf8 => { + let arr = array.as_any().downcast_ref::().unwrap(); + let translated: LargeStringArray = arr + .iter() + .map(|opt| opt.map(sql_similar_to_regex)) + .collect(); + Ok(ColumnarValue::Array(Arc::new(translated))) + } + DataType::Utf8View => { + let arr = array.as_any().downcast_ref::().unwrap(); + let translated: StringViewArray = arr + .iter() + .map(|opt| opt.map(sql_similar_to_regex)) + .collect(); + Ok(ColumnarValue::Array(Arc::new(translated))) + } + other => exec_err!("SIMILAR TO pattern must be a string type, got {other:?}"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::expressions::{col, lit}; + use arrow::array::StringArray; + use arrow::datatypes::{DataType, Field, Schema}; + use datafusion_common::cast::as_string_array; + + #[test] + fn test_translate_scalar() -> Result<()> { + assert_eq!( + translate_scalar(&ScalarValue::Utf8(Some("a%".to_string())))?, + ScalarValue::Utf8(Some(r"^(?:a(?s:.*))$".to_string())) + ); + assert_eq!( + translate_scalar(&ScalarValue::Utf8(Some("a.b".to_string())))?, + ScalarValue::Utf8(Some(r"^(?:a\.b)$".to_string())) + ); + assert!( + translate_scalar(&ScalarValue::Null).is_ok(), + "NULL pattern should pass through" + ); + assert_eq!( + translate_scalar(&ScalarValue::Null).unwrap(), + ScalarValue::Utf8(None) + ); + assert!(translate_scalar(&ScalarValue::Int32(Some(1))).is_err()); + Ok(()) + } + + #[test] + fn test_translate_array() -> Result<()> { + let schema = Schema::new(vec![Field::new("a", DataType::Utf8, false)]); + let input = StringArray::from(vec!["a%", "a.b", "a_b"]); + let expr = SqlSimilarToPattern::new(col("a", &schema)?); + let batch = RecordBatch::try_new(Arc::new(schema), vec![Arc::new(input)])?; + let result = expr.evaluate(&batch)?.into_array(batch.num_rows())?; + let result = as_string_array(&result)?; + assert_eq!(result.value(0), r"^(?:a(?s:.*))$"); + assert_eq!(result.value(1), r"^(?:a\.b)$"); + assert_eq!(result.value(2), r"^(?:a(?s:.)b)$"); + Ok(()) + } + + #[test] + fn test_display_and_sql() -> Result<()> { + let expr = SqlSimilarToPattern::new(lit("a%")); + assert_eq!("sql_similar_to_regex(a%)", format!("{expr}")); + Ok(()) + } +} diff --git a/datafusion/sql/src/expr/mod.rs b/datafusion/sql/src/expr/mod.rs index c00dcb82ff3a9..742cb778f157e 100644 --- a/datafusion/sql/src/expr/mod.rs +++ b/datafusion/sql/src/expr/mod.rs @@ -1009,7 +1009,10 @@ impl SqlToRel<'_, S> { ) -> Result { let pattern = self.sql_expr_to_logical_expr(pattern, schema, planner_context)?; let pattern_type = pattern.get_type(schema)?; - if pattern_type != DataType::Utf8 && pattern_type != DataType::Null { + if !matches!( + pattern_type, + DataType::Utf8 | DataType::LargeUtf8 | DataType::Utf8View | DataType::Null + ) { return plan_err!("Invalid pattern in SIMILAR TO expression"); } let escape_char = match escape_char.map(|v| v.value) { diff --git a/datafusion/sqllogictest/test_files/strings.slt b/datafusion/sqllogictest/test_files/strings.slt index 9fa453fa02523..a5fbc998cb2ca 100644 --- a/datafusion/sqllogictest/test_files/strings.slt +++ b/datafusion/sqllogictest/test_files/strings.slt @@ -71,25 +71,204 @@ p2 p2e1 p2m1e1 -# SIMILAR TO +# SIMILAR TO with `%` wildcard (zero or more characters) query T rowsort -SELECT s FROM test WHERE s SIMILAR TO 'p[12].*'; +SELECT s FROM test WHERE s SIMILAR TO 'p1%'; ---- p1 p1e1 p1m1e1 + +# SIMILAR TO with `_` wildcard (exactly one character) +query T rowsort +SELECT s FROM test WHERE s SIMILAR TO 'p_'; +---- +p1 p2 -p2e1 -p2m1e1 -# NOT SIMILAR TO +# SIMILAR TO requires full-string match (no implicit substring) query T rowsort -SELECT s FROM test WHERE s NOT SIMILAR TO 'p[12].*'; +SELECT s FROM test WHERE s SIMILAR TO 'p1'; ---- -P1 -P1e1 -P1m1e1 -e1 +p1 + +# SIMILAR TO treats `. ^ $` as SQL literals (not as regex metachars). +query B +SELECT 'a.' SIMILAR TO 'a.'; +---- +true + +query B +SELECT 'ab' SIMILAR TO 'a.'; +---- +false + +query B +SELECT '^x$' SIMILAR TO '^x$'; +---- +true + +query B +SELECT 'x' SIMILAR TO '^x$'; +---- +false + +# SIMILAR TO supports the POSIX metacharacters `| * + ? ( ) { } [ ]`. +query B +SELECT 'a' SIMILAR TO 'a|b'; +---- +true + +query B +SELECT 'c' SIMILAR TO 'a|b'; +---- +false + +query B +SELECT '' SIMILAR TO 'a*'; +---- +true + +query B +SELECT 'aa' SIMILAR TO 'a*'; +---- +true + +query B +SELECT 'ab' SIMILAR TO 'a*'; +---- +false + +query B +SELECT 'a' SIMILAR TO 'a+'; +---- +true + +query B +SELECT '' SIMILAR TO 'a+'; +---- +false + +query B +SELECT '' SIMILAR TO 'a?'; +---- +true + +query B +SELECT 'a' SIMILAR TO 'a?'; +---- +true + +query B +SELECT 'aa' SIMILAR TO 'a?'; +---- +false + +query B +SELECT 'ab' SIMILAR TO '(ab)'; +---- +true + +query B +SELECT 'a' SIMILAR TO 'a{2}'; +---- +false + +query B +SELECT 'aa' SIMILAR TO 'a{2}'; +---- +true + +query B +SELECT 'a' SIMILAR TO '[ab]'; +---- +true + +query B +SELECT 'c' SIMILAR TO '[ab]'; +---- +false + +query B +SELECT 'c' SIMILAR TO '[^ab]'; +---- +true + +query B +SELECT 'a' SIMILAR TO '[^ab]'; +---- +false + +# SIMILAR TO wildcards match newlines. +query B +SELECT 'a' || chr(10) || 'b' SIMILAR TO 'a%b'; +---- +true + +query B +SELECT 'a' || chr(10) || 'b' SIMILAR TO 'a_b'; +---- +true + +query B +SELECT 'ab' SIMILAR TO 'a_b'; +---- +false + +# SIMILAR TO with a dynamic (column) pattern: wildcards and regex-only +# metacharacters are translated the same way as for literal patterns. +statement ok +CREATE TABLE dynamic_patterns(text TEXT, pattern TEXT) AS VALUES + ('abc', 'a%'), + ('ab', 'a.'), + ('x', '_'), + ('hello world', 'hello%'); + +query T rowsort +SELECT text FROM dynamic_patterns WHERE text SIMILAR TO pattern; +---- +abc +hello world +x + +query T rowsort +SELECT text FROM dynamic_patterns WHERE text NOT SIMILAR TO pattern; +---- +ab + +statement ok +DROP TABLE dynamic_patterns; + +# SIMILAR TO with a dynamic pattern produced by a function. +query B +SELECT 'abc' SIMILAR TO concat('a', '%'); +---- +true + +query B +SELECT 'ab' SIMILAR TO concat('a', '.'); +---- +false + +query B +SELECT 'x' SIMILAR TO lower('X'); +---- +true + +query B +SELECT 'X' SIMILAR TO lower('X'); +---- +false + +# SIMILAR TO NULL pattern returns NULL. +query B +SELECT 'a' SIMILAR TO NULL; +---- +NULL + +# SIMILAR TO with a non-string pattern is rejected at plan time. +statement error Invalid pattern in SIMILAR TO expression +SELECT 'a' SIMILAR TO 1; # NOT LIKE query T rowsort