Repository: jorgecarleitao/arrow2 (rust)

Issue:
Fix LIKE escape issues
Due to using `regex` crate directly, it's sensitive to not escape `like` patterns`.
A solution to it can be using the `escape` function in `regex`: https://docs.rs/regex/1.5.4/regex/fn.escape.html

Next, there should be a possibility to escape the `%` and `_` characters.

Arrow-rs issues/PRs:

https://github.com/apache/arrow-rs/pull/1085
https://github.com/apache/arrow-rs/pull/1042
https://github.com/apache/arrow-rs/issues/415

Pull request:
Fix escaped like wildcards
# Which issue does this PR close?

Closes #702 

# Rationale for this change
 
It is explained in the Issues linked above.

# What changes are included in this PR?

Added a new function that replaces the like wildcards '%' and '_' for the regex counterparts before executing them. It also takes into account that the wildcards can be escaped, in that case, it does remove the escape characters and leaves the wildcards so that they are matched against the raw character.

This is implemented iterating over all the characters of the pattern to figure out when it needs to be transformed or not.

# Are there any user-facing changes?
N/A

Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 3fe3cb06c7..1d246a80be 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -38,6 +38,7 @@ csv-core = { version = "0.1", optional = true }
 csv-async = { version = "^1.1", optional = true }
 
 regex = { version = "^1.3", optional = true }
+regex-syntax = { version = "^0.6", optional = true }
 streaming-iterator = { version = "0.1", optional = true }
 fallible-streaming-iterator = { version = "0.1", optional = true }
 
@@ -135,6 +136,7 @@ full = [
     "io_avro_compression",
     "io_avro_async",
     "regex",
+    "regex-syntax",
     "compute",
     # parses timezones used in timestamp conversions
     "chrono-tz",
@@ -190,7 +192,7 @@ compute_filter = []
 compute_hash = ["multiversion"]
 compute_if_then_else = []
 compute_length = []
-compute_like = ["regex"]
+compute_like = ["regex", "regex-syntax"]
 compute_limit = []
 compute_merge_sort = ["itertools", "compute_sort"]
 compute_nullif = ["compute_comparison"]
diff --git a/src/compute/like.rs b/src/compute/like.rs
index cc7517e823..bf363972a1 100644
--- a/src/compute/like.rs
+++ b/src/compute/like.rs
@@ -17,8 +17,41 @@ fn is_like_pattern(c: char) -> bool {
     c == '%' || c == '_'
 }
 
+/// Transforms a like `pattern` to a regex compatible pattern. To achieve that, it does:
+///
+/// 1. Replace like wildcards for regex expressions as the pattern will be evaluated using regex match: `%` => `.*` and `_` => `.`
+/// 2. Escape regex meta characters to match them and not be evaluated as regex special chars. For example: `.` => `\\.`
+/// 3. Replace escaped like wildcards removing the escape characters to be able to match it as a regex. For example: `\\%` => `%`
 fn replace_pattern(pattern: &str) -> String {
-    pattern.replace('%', ".*").replace('_', ".")
+    let mut result = String::new();
+    let text = String::from(pattern);
+    let mut chars_iter = text.chars().peekable();
+    while let Some(c) = chars_iter.next() {
+        if c == '\\' {
+            let next = chars_iter.peek();
+            match next {
+                Some(next) if is_like_pattern(*next) => {
+                    result.push(*next);
+                    // Skipping the next char as it is already appended
+                    chars_iter.next();
+                }
+                _ => {
+                    result.push('\\');
+                    result.push('\\');
+                }
+            }
+        } else if regex_syntax::is_meta_character(c) {
+            result.push('\\');
+            result.push(c);
+        } else if c == '%' {
+            result.push_str(".*");
+        } else if c == '_' {
+            result.push('.');
+        } else {
+            result.push(c);
+        }
+    }
+    result
 }
 
 #[inline]
@@ -108,7 +141,10 @@ fn a_like_utf8_scalar<O: Offset, F: Fn(bool) -> bool>(
 
     let values = if !rhs.contains(is_like_pattern) {
         Bitmap::from_trusted_len_iter(lhs.values_iter().map(|x| op(x == rhs)))
-    } else if rhs.ends_with('%') && !rhs[..rhs.len() - 1].contains(is_like_pattern) {
+    } else if rhs.ends_with('%')
+        && !rhs.ends_with("\\%")
+        && !rhs[..rhs.len() - 1].contains(is_like_pattern)
+    {
         // fast path, can use starts_with
         let starts_with = &rhs[..rhs.len() - 1];
         Bitmap::from_trusted_len_iter(lhs.values_iter().map(|x| op(x.starts_with(starts_with))))
@@ -260,7 +296,10 @@ fn a_like_binary_scalar<O: Offset, F: Fn(bool) -> bool>(
 
     let values = if !pattern.contains(is_like_pattern) {
         Bitmap::from_trusted_len_iter(lhs.values_iter().map(|x| op(x == rhs)))
-    } else if pattern.ends_with('%') && !pattern[..pattern.len() - 1].contains(is_like_pattern) {
+    } else if pattern.ends_with('%')
+        && !pattern.ends_with("\\%")
+        && !pattern[..pattern.len() - 1].contains(is_like_pattern)
+    {
         // fast path, can use starts_with
         let starts_with = &rhs[..rhs.len() - 1];
         Bitmap::from_trusted_len_iter(lhs.values_iter().map(|x| op(x.starts_with(starts_with))))
diff --git a/src/io/orc/read/mod.rs b/src/io/orc/read/mod.rs
index 123addf3f9..597ecbc9fb 100644
--- a/src/io/orc/read/mod.rs
+++ b/src/io/orc/read/mod.rs
@@ -256,7 +256,6 @@ fn try_extend<O: Offset + TryFrom<u64>, I: Iterator<Item = u64>>(
     iter: I,
 ) -> Result<(), orc_format::error::Error> {
     for item in iter {
-        println!("{item}");
         let item: O = item
             .try_into()
             .map_err(|_| orc_format::error::Error::OutOfSpec)?;


Test patch:
diff --git a/tests/it/compute/like.rs b/tests/it/compute/like.rs
index 61cb01cc13..3bd4c6aaf3 100644
--- a/tests/it/compute/like.rs
+++ b/tests/it/compute/like.rs
@@ -39,6 +39,28 @@ fn test_like_binary_scalar() -> Result<()> {
     Ok(())
 }
 
+#[test]
+fn test_like_utf8_scalar() -> Result<()> {
+    let array = Utf8Array::<i32>::from_slice(&["Arrow", "Arrow", "Arrow", "BA"]);
+
+    let result = like_utf8_scalar(&array, "A%").unwrap();
+    assert_eq!(result, BooleanArray::from_slice(&[true, true, true, false]));
+
+    let result = like_utf8_scalar(&array, "Arrow").unwrap();
+    assert_eq!(result, BooleanArray::from_slice(&[true, true, true, false]));
+
+    let array = Utf8Array::<i32>::from_slice(&["A%", "Arrow"]);
+
+    let result = like_utf8_scalar(&array, "A\\%").unwrap();
+    assert_eq!(result, BooleanArray::from_slice(&[true, false]));
+
+    let array = Utf8Array::<i32>::from_slice(&["A_row", "Arrow"]);
+    let result = like_utf8_scalar(&array, "A\\_row").unwrap();
+    assert_eq!(result, BooleanArray::from_slice(&[true, false]));
+
+    Ok(())
+}
+
 #[test]
 fn test_nlike_binary_scalar() -> Result<()> {
     let array = BinaryArray::<i32>::from_slice(&["Arrow", "Arrow", "Arrow", "BA"]);