This is an automated email from the ASF dual-hosted git repository.

tanner pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/main by this push:
     new 9404c8c99a [CALCITE-5873] Add REGEXP_CONTAINS function (enabled in 
BigQuery library)
9404c8c99a is described below

commit 9404c8c99ab6f847b8c788476f0047368aba7c49
Author: Jerin John <[email protected]>
AuthorDate: Tue Jul 25 15:29:31 2023 -0700

    [CALCITE-5873] Add REGEXP_CONTAINS function (enabled in BigQuery library)
---
 babel/src/test/resources/sql/big-query.iq          | 65 +++++++++++++++++++++-
 .../calcite/adapter/enumerable/RexImpTable.java    |  2 +
 .../apache/calcite/runtime/CalciteResource.java    |  3 +
 .../org/apache/calcite/runtime/SqlFunctions.java   | 15 +++++
 .../calcite/sql/fun/SqlLibraryOperators.java       |  8 +++
 .../calcite/runtime/CalciteResource.properties     |  1 +
 .../org/apache/calcite/test/SqlFunctionsTest.java  | 30 ++++++++++
 site/_docs/reference.md                            |  1 +
 .../org/apache/calcite/test/SqlOperatorTest.java   | 21 +++++++
 9 files changed, 145 insertions(+), 1 deletion(-)

diff --git a/babel/src/test/resources/sql/big-query.iq 
b/babel/src/test/resources/sql/big-query.iq
index a0e7782a30..82a37386e9 100755
--- a/babel/src/test/resources/sql/big-query.iq
+++ b/babel/src/test/resources/sql/big-query.iq
@@ -717,6 +717,70 @@ SELECT (19 % 19) as result;
 
 !ok
 
+#####################################################################
+# REGEXP_CONTAINS(value, regexp)
+#
+# Takes two STRING values. Returns TRUE if value is a partial match
+# for the regular expression, regexp.
+# If the regexp argument is invalid, the function returns an error.
+# Uses java.util.regex as a standard for regex processing
+# in Calcite instead of RE2 used by BigQuery/GoogleSQL.
+
+SELECT
+  email,
+  REGEXP_CONTAINS(email, '@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+') AS is_valid
+FROM
+  (SELECT
+    ARRAY['[email protected]', '[email protected]', 'www.example.net']
+    AS addresses),
+  UNNEST(addresses) AS email;
++-----------------+----------+
+| email           | is_valid |
++-----------------+----------+
+| [email protected] | true     |
+| [email protected] | true     |
+| www.example.net | false    |
++-----------------+----------+
+(3 rows)
+
+!ok
+
+SELECT
+  email,
+  REGEXP_CONTAINS(email, '^([\w.+-]+@foo\.com|[\w.+-]+@bar\.org)$')
+    AS valid_email_address,
+  REGEXP_CONTAINS(email, '^[\w.+-]+@foo\.com|[\w.+-]+@bar\.org$')
+    AS without_parentheses
+FROM
+  (SELECT
+    ARRAY['[email protected]', '[email protected]', '[email protected]', '[email protected]', 
'[email protected]']
+    AS addresses),
+  UNNEST(addresses) AS email;
++----------------+---------------------+---------------------+
+| email          | valid_email_address | without_parentheses |
++----------------+---------------------+---------------------+
+| [email protected]      | true                | true                |
+| [email protected] | false               | true                |
+| [email protected]      | true                | true                |
+| [email protected]     | false               | true                |
+| [email protected]      | false               | false               |
++----------------+---------------------+---------------------+
+(5 rows)
+
+!ok
+
+SELECT REGEXP_CONTAINS('abc def ghi', '(abc');
+Invalid regular expression for REGEXP_CONTAINS: 'Unclosed group near index 4 
(abc'
+!error
+
+SELECT REGEXP_CONTAINS('abc def ghi', '[z-a]');
+Invalid regular expression for REGEXP_CONTAINS: 'Illegal character range near 
index 3 [z-a]    ^'
+!error
+
+SELECT REGEXP_CONTAINS('abc def ghi', '{2,1}');
+Invalid regular expression for REGEXP_CONTAINS: 'Illegal repetition range near 
index 4 {2,1}     ^'
+!error
+
 #####################################################################
 # SPLIT
 #
@@ -3593,5 +3657,4 @@ FROM items;
 
 !ok
 
-
 # End big-query.iq
diff --git 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
index 531a0ce013..7614eb784a 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
@@ -215,6 +215,7 @@ import static 
org.apache.calcite.sql.fun.SqlLibraryOperators.PARSE_TIME;
 import static org.apache.calcite.sql.fun.SqlLibraryOperators.PARSE_TIMESTAMP;
 import static org.apache.calcite.sql.fun.SqlLibraryOperators.PARSE_URL;
 import static org.apache.calcite.sql.fun.SqlLibraryOperators.POW;
+import static org.apache.calcite.sql.fun.SqlLibraryOperators.REGEXP_CONTAINS;
 import static org.apache.calcite.sql.fun.SqlLibraryOperators.REGEXP_REPLACE;
 import static org.apache.calcite.sql.fun.SqlLibraryOperators.REPEAT;
 import static org.apache.calcite.sql.fun.SqlLibraryOperators.REVERSE;
@@ -552,6 +553,7 @@ public class RexImpTable {
       defineMethod(LEVENSHTEIN, BuiltInMethod.LEVENSHTEIN.method, 
NullPolicy.STRICT);
       defineMethod(SPLIT, "split", NullPolicy.STRICT);
       defineMethod(PARSE_URL, BuiltInMethod.PARSE_URL.method, 
NullPolicy.STRICT);
+      defineMethod(REGEXP_CONTAINS, "regexpContains", NullPolicy.STRICT);
 
       map.put(TRIM, new TrimImplementor());
 
diff --git a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java 
b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
index c09c309ccc..bb33ae9cf0 100644
--- a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
+++ b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
@@ -1005,6 +1005,9 @@ public interface CalciteResource {
   @BaseMessage("Invalid input for JSON_STORAGE_SIZE: ''{0}''")
   ExInst<CalciteException> invalidInputForJsonStorageSize(String value);
 
+  @BaseMessage("Invalid regular expression for REGEXP_CONTAINS: ''{0}''")
+  ExInst<RuntimeException> invalidInputForRegexpContains(String value);
+
   @BaseMessage("Invalid input for REGEXP_REPLACE: ''{0}''")
   ExInst<CalciteException> invalidInputForRegexpReplace(String value);
 
diff --git a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java 
b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
index 79def765a2..15fbc472fa 100644
--- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
+++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
@@ -110,6 +110,7 @@ import java.util.concurrent.atomic.AtomicLong;
 import java.util.function.BinaryOperator;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
 
 import static org.apache.calcite.linq4j.Nullness.castNonNull;
 import static org.apache.calcite.util.Static.RESOURCE;
@@ -345,6 +346,20 @@ public class SqlFunctions {
     return DigestUtils.sha512Hex(string.getBytes());
   }
 
+  /** SQL {@code REGEXP_CONTAINS(value, regexp)} function.
+   * Throws a runtime exception for invalid regular expressions.*/
+  public static boolean regexpContains(String value, String regex) {
+    try {
+      // Uses java.util.regex as a standard for regex processing
+      // in Calcite instead of RE2 used by BigQuery/GoogleSQL
+      Pattern regexp = Pattern.compile(regex);
+      return regexp.matcher(value).find();
+    } catch (PatternSyntaxException ex) {
+      throw 
RESOURCE.invalidInputForRegexpContains(ex.getMessage().replace("\r\n", " ")
+          .replace("\n", " ").replace("\r", " ")).ex();
+    }
+  }
+
   /** SQL {@code REGEXP_REPLACE} function with 3 arguments. */
   public static String regexpReplace(String s, String regex,
       String replacement) {
diff --git 
a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java 
b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
index c3bcb53607..6159d1162c 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
@@ -478,6 +478,14 @@ public abstract class SqlLibraryOperators {
   @LibraryOperator(libraries = {MYSQL, ORACLE})
   public static final SqlFunction REGEXP_REPLACE = new 
SqlRegexpReplaceFunction();
 
+  /** The "REGEXP_CONTAINS(value, regexp)" function.
+   * Returns TRUE if value is a partial match for the regular expression, 
regexp. */
+  @LibraryOperator(libraries = {BIG_QUERY})
+  public static final SqlFunction REGEXP_CONTAINS =
+      SqlBasicFunction.create("REGEXP_CONTAINS", ReturnTypes.BOOLEAN_NULLABLE,
+          OperandTypes.STRING_STRING,
+          SqlFunctionCategory.STRING);
+
   @LibraryOperator(libraries = {MYSQL})
   public static final SqlFunction COMPRESS =
       SqlBasicFunction.create("COMPRESS",
diff --git 
a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties 
b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
index 8dfd6fb9f3..8c67a7538b 100644
--- 
a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
+++ 
b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
@@ -328,6 +328,7 @@ InvalidInputForJsonLength=Invalid input for JSON_LENGTH: 
''{0}''
 InvalidInputForJsonKeys=Invalid input for JSON_KEYS: ''{0}''
 InvalidInputForJsonRemove=Invalid input for JSON_REMOVE: document: ''{0}'', 
jsonpath expressions: ''{1}''
 InvalidInputForJsonStorageSize=Invalid input for JSON_STORAGE_SIZE: ''{0}''
+InvalidInputForRegexpContains=Invalid regular expression for REGEXP_CONTAINS: 
''{0}''
 InvalidInputForRegexpReplace=Invalid input for REGEXP_REPLACE: ''{0}''
 InvalidInputForJsonInsert=Invalid input for JSON_INSERT: jsonDoc: ''{0}'', 
kvs: ''{1}''
 InvalidInputForJsonReplace=Invalid input for JSON_REPLACE: jsonDoc: ''{0}'', 
kvs: ''{1}''
diff --git a/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java 
b/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
index b973d8c2b8..e6eb29b938 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
@@ -59,6 +59,7 @@ import static org.apache.calcite.runtime.SqlFunctions.ltrim;
 import static org.apache.calcite.runtime.SqlFunctions.md5;
 import static org.apache.calcite.runtime.SqlFunctions.position;
 import static org.apache.calcite.runtime.SqlFunctions.posixRegex;
+import static org.apache.calcite.runtime.SqlFunctions.regexpContains;
 import static org.apache.calcite.runtime.SqlFunctions.regexpReplace;
 import static org.apache.calcite.runtime.SqlFunctions.rtrim;
 import static org.apache.calcite.runtime.SqlFunctions.sha1;
@@ -234,6 +235,35 @@ class SqlFunctionsTest {
     assertThat(posixRegex("abcq", "[[:xdigit:]]", false), is(true));
   }
 
+  @Test void testRegexpContains() {
+    try {
+      regexpContains("abc def ghi", "(abc");
+      fail("'regexp_contains' on an invalid regex input '(abc' is not 
possible");
+    } catch (RuntimeException e) {
+      assertThat(
+          e.getMessage(), is("Invalid regular expression for REGEXP_CONTAINS: 
'Unclosed "
+              + "group near index 4 (abc'"));
+    }
+
+    try {
+      regexpContains("abc def ghi", "[z-a]");
+      fail("'regexp_contains' on an invalid regex input '[z-a]' is not 
possible");
+    } catch (RuntimeException e) {
+      assertThat(
+          e.getMessage(), is("Invalid regular expression for REGEXP_CONTAINS: 
'Illegal "
+              + "character range near index" + " 3 [z-a]    ^'"));
+    }
+
+    try {
+      regexpContains("abc def ghi", "{2,1}");
+      fail("'regexp_contains' on an invalid regex input '{2,1}' is not 
possible");
+    } catch (RuntimeException e) {
+      assertThat(
+          e.getMessage(), is("Invalid regular expression for REGEXP_CONTAINS: 
'Illegal "
+              + "repetition range near " + "index 4 {2,1}     ^'"));
+    }
+  }
+
   @Test void testRegexpReplace() {
     assertThat(regexpReplace("a b c", "b", "X"), is("a X c"));
     assertThat(regexpReplace("abc def ghi", "[g-z]+", "X"), is("abc def X"));
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index abd151131f..ef80f5ea03 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -2778,6 +2778,7 @@ BigQuery's type system uses confusingly different names 
for types and functions:
 | b | PARSE_TIMESTAMP(format, string[, timeZone])    | Uses format specified 
by *format* to convert *string* representation of timestamp to a TIMESTAMP WITH 
LOCAL TIME ZONE value in *timeZone*
 | h s | PARSE_URL(urlString, partToExtract [, keyToExtract] ) | Returns the 
specified *partToExtract* from the *urlString*. Valid values for 
*partToExtract* include HOST, PATH, QUERY, REF, PROTOCOL, AUTHORITY, FILE, and 
USERINFO. *keyToExtract* specifies which query to extract
 | b | POW(numeric1, numeric2)                        | Returns *numeric1* 
raised to the power *numeric2*
+| b | REGEXP_CONTAINS(string, regexp)                | Returns whether 
*string* is a partial match for the *regexp*
 | m o | REGEXP_REPLACE(string, regexp, rep [, pos [, occurrence [, 
matchType]]]) | Replaces all substrings of *string* that match *regexp* with 
*rep* at the starting *pos* in expr (if omitted, the default is 1), 
*occurrence* means which occurrence of a match to search for (if omitted, the 
default is 1), *matchType* specifies how to perform matching
 | b m p | REPEAT(string, integer)                    | Returns a string 
consisting of *string* repeated of *integer* times; returns an empty string if 
*integer* is less than 1
 | b m | REVERSE(string)                              | Returns *string* with 
the order of the characters reversed
diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java 
b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
index 35f23a8d3f..82a2aa56be 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -4547,6 +4547,27 @@ public class SqlOperatorTest {
     f0.forEachLibrary(list(SqlLibrary.MYSQL, SqlLibrary.POSTGRESQL), consumer);
   }
 
+  @Test void testRegexpContainsFunc() {
+    final SqlOperatorFixture f = 
fixture().setFor(SqlLibraryOperators.REGEXP_CONTAINS)
+        .withLibrary(SqlLibrary.BIG_QUERY);
+    f.checkBoolean("regexp_contains('abc def ghi', 'abc')", true);
+    f.checkBoolean("regexp_contains('abc def ghi', '[a-z]+')", true);
+    f.checkBoolean("regexp_contains('[email protected]', 
'@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+')", true);
+    f.checkBoolean("regexp_contains('[email protected]', 
'@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+')", false);
+    f.checkBoolean("regexp_contains('5556664422', '^\\d{10}$')", true);
+    f.checkBoolean("regexp_contains('11555666442233', '^\\d{10}$')", false);
+    f.checkBoolean("regexp_contains('55566644221133', '\\d{10}')", true);
+    f.checkBoolean("regexp_contains('55as56664as422', '\\d{10}')", false);
+
+    f.checkQuery("select regexp_contains('abc def ghi', 'abc')");
+    f.checkQuery("select regexp_contains('[email protected]', 
'@[a-zA-Z0-9-]+\\\\.[a-zA-Z0-9-.]+')");
+    f.checkQuery("select regexp_contains('55as56664as422', '\\d{10}')");
+
+    f.checkNull("regexp_contains('abc def ghi', cast(null as varchar))");
+    f.checkNull("regexp_contains(cast(null as varchar), 'abc')");
+    f.checkNull("regexp_contains(cast(null as varchar), cast(null as 
varchar))");
+  }
+
   @Test void testRegexpReplaceFunc() {
     final SqlOperatorFixture f0 = fixture();
     final Consumer<SqlOperatorFixture> consumer = f -> {

Reply via email to