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

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

commit 2a96512c352bda4a5d9c0c80730f5c115ac363d6
Author: Julian Hyde <[email protected]>
AuthorDate: Tue Aug 22 23:22:13 2023 -0700

    [CALCITE-5914] Cache compiled regular expressions in SQL function runtime
    
    Functions/operators that now cache include REGEXP_CONTAINS,
    PARSE_URL, SIMILAR TO, LIKE, ILIKE, RLIKE, posix regex,
    PARSE_TIMESTAMP, FORMAT_DATE, TO_CHAR.
    
    Introduce `class RegexFunction` and use it to implement
    variants of REGEXP_REPLACE.
    
    Add CalciteSystemProperty.FUNCTION_LEVEL_CACHE_MAX_SIZE to
    limit the number of entries in a function's cache.
    
    Close apache/calcite#3394
---
 .../calcite/adapter/enumerable/RexImpTable.java    | 271 ++++-----
 .../calcite/config/CalciteSystemProperty.java      |  46 +-
 .../org/apache/calcite/rex/RexInterpreter.java     |  19 +-
 .../main/java/org/apache/calcite/runtime/Like.java |  12 +-
 .../org/apache/calcite/runtime/SqlFunctions.java   | 647 ++++++++++++++-------
 .../org/apache/calcite/util/BuiltInMethod.java     |  69 ++-
 .../apache/calcite/util/format/FormatModel.java    |   3 +
 .../apache/calcite/util/format/FormatModels.java   |   4 +-
 .../org/apache/calcite/test/SqlFunctionsTest.java  | 119 ++--
 .../main/java/org/apache/calcite/linq4j/Ord.java   |  19 +-
 10 files changed, 721 insertions(+), 488 deletions(-)

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 df3073699f..bfc6342a35 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
@@ -385,6 +385,8 @@ import static 
org.apache.calcite.sql.fun.SqlStdOperatorTable.MULTISET_INTERSECT;
 import static 
org.apache.calcite.sql.fun.SqlStdOperatorTable.MULTISET_INTERSECT_DISTINCT;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.MULTISET_UNION;
 import static 
org.apache.calcite.sql.fun.SqlStdOperatorTable.MULTISET_UNION_DISTINCT;
+import static 
org.apache.calcite.sql.fun.SqlStdOperatorTable.NEGATED_POSIX_REGEX_CASE_INSENSITIVE;
+import static 
org.apache.calcite.sql.fun.SqlStdOperatorTable.NEGATED_POSIX_REGEX_CASE_SENSITIVE;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.NEXT_VALUE;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.NOT;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.NOT_EQUALS;
@@ -397,6 +399,8 @@ import static 
org.apache.calcite.sql.fun.SqlStdOperatorTable.OVERLAY;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.PI;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.PLUS;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.POSITION;
+import static 
org.apache.calcite.sql.fun.SqlStdOperatorTable.POSIX_REGEX_CASE_INSENSITIVE;
+import static 
org.apache.calcite.sql.fun.SqlStdOperatorTable.POSIX_REGEX_CASE_SENSITIVE;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.POWER;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.RADIANS;
 import static org.apache.calcite.sql.fun.SqlStdOperatorTable.RAND;
@@ -562,9 +566,9 @@ public class RexImpTable {
       defineMethod(REVERSE, BuiltInMethod.REVERSE.method, NullPolicy.STRICT);
       defineMethod(LEVENSHTEIN, BuiltInMethod.LEVENSHTEIN.method, 
NullPolicy.STRICT);
       defineMethod(SPLIT, BuiltInMethod.SPLIT.method, NullPolicy.STRICT);
-      defineMethod(PARSE_URL, BuiltInMethod.PARSE_URL.method, 
NullPolicy.STRICT);
-      defineMethod(REGEXP_CONTAINS, BuiltInMethod.REGEXP_CONTAINS.method,
-          NullPolicy.STRICT);
+      defineReflective(PARSE_URL, BuiltInMethod.PARSE_URL2.method,
+          BuiltInMethod.PARSE_URL3.method);
+      defineReflective(REGEXP_CONTAINS, BuiltInMethod.REGEXP_CONTAINS.method);
 
       map.put(TRIM, new TrimImplementor());
 
@@ -603,8 +607,10 @@ public class RexImpTable {
       map.put(LOG, new LogImplementor());
       map.put(LOG10, new LogImplementor());
 
-      map.put(RAND, new RandImplementor());
-      map.put(RAND_INTEGER, new RandIntegerImplementor());
+      defineReflective(RAND, BuiltInMethod.RAND.method,
+          BuiltInMethod.RAND_SEED.method);
+      defineReflective(RAND_INTEGER, BuiltInMethod.RAND_INTEGER.method,
+          BuiltInMethod.RAND_INTEGER_SEED.method);
 
       defineMethod(ACOS, BuiltInMethod.ACOS.method, NullPolicy.STRICT);
       defineMethod(ACOSH, BuiltInMethod.ACOSH.method, NullPolicy.STRICT);
@@ -721,17 +727,13 @@ public class RexImpTable {
           NullPolicy.STRICT);
 
       // Datetime parsing methods
-      defineMethod(PARSE_DATE, BuiltInMethod.PARSE_DATE.method,
-          NullPolicy.STRICT);
-      defineMethod(PARSE_DATETIME, BuiltInMethod.PARSE_DATETIME.method,
-          NullPolicy.STRICT);
-      defineMethod(PARSE_TIME, BuiltInMethod.PARSE_TIME.method,
-          NullPolicy.STRICT);
-      defineMethod(PARSE_TIMESTAMP, BuiltInMethod.PARSE_TIMESTAMP.method,
-          NullPolicy.STRICT);
+      defineReflective(PARSE_DATE, BuiltInMethod.PARSE_DATE.method);
+      defineReflective(PARSE_DATETIME, BuiltInMethod.PARSE_DATETIME.method);
+      defineReflective(PARSE_TIME, BuiltInMethod.PARSE_TIME.method);
+      defineReflective(PARSE_TIMESTAMP, BuiltInMethod.PARSE_TIMESTAMP.method);
 
       // Datetime formatting methods
-      defineMethod(TO_CHAR, BuiltInMethod.TO_CHAR.method, NullPolicy.STRICT);
+      defineReflective(TO_CHAR, BuiltInMethod.TO_CHAR.method);
       final FormatDatetimeImplementor datetimeFormatImpl =
           new FormatDatetimeImplementor();
       map.put(FORMAT_DATE, datetimeFormatImpl);
@@ -747,34 +749,31 @@ public class RexImpTable {
       map.put(IS_FALSE, new IsFalseImplementor());
       map.put(IS_NOT_FALSE, new IsNotFalseImplementor());
 
-      // LIKE, ILIKE and SIMILAR
-      map.put(LIKE,
-          new MethodImplementor(BuiltInMethod.LIKE.method, NullPolicy.STRICT,
-              false));
-      map.put(ILIKE,
-          new MethodImplementor(BuiltInMethod.ILIKE.method, NullPolicy.STRICT,
-              false));
-      map.put(RLIKE,
-          new MethodImplementor(BuiltInMethod.RLIKE.method, NullPolicy.STRICT,
-              false));
-      map.put(SIMILAR_TO,
-          new MethodImplementor(BuiltInMethod.SIMILAR.method, 
NullPolicy.STRICT,
-              false));
+      // LIKE, ILIKE, RLIKE and SIMILAR
+      defineReflective(LIKE, BuiltInMethod.LIKE.method,
+          BuiltInMethod.LIKE_ESCAPE.method);
+      defineReflective(ILIKE, BuiltInMethod.ILIKE.method,
+          BuiltInMethod.ILIKE_ESCAPE.method);
+      defineReflective(RLIKE, BuiltInMethod.RLIKE.method);
+      defineReflective(SIMILAR_TO, BuiltInMethod.SIMILAR.method,
+          BuiltInMethod.SIMILAR_ESCAPE.method);
 
       // POSIX REGEX
-      final MethodImplementor posixRegexImplementorCaseSensitive =
-          new PosixRegexMethodImplementor(true);
-      final MethodImplementor posixRegexImplementorCaseInsensitive =
-          new PosixRegexMethodImplementor(false);
-      map.put(SqlStdOperatorTable.POSIX_REGEX_CASE_INSENSITIVE,
-          posixRegexImplementorCaseInsensitive);
-      map.put(SqlStdOperatorTable.POSIX_REGEX_CASE_SENSITIVE,
-          posixRegexImplementorCaseSensitive);
-      map.put(SqlStdOperatorTable.NEGATED_POSIX_REGEX_CASE_INSENSITIVE,
-          NotImplementor.of(posixRegexImplementorCaseInsensitive));
-      map.put(SqlStdOperatorTable.NEGATED_POSIX_REGEX_CASE_SENSITIVE,
-          NotImplementor.of(posixRegexImplementorCaseSensitive));
-      map.put(REGEXP_REPLACE, new RegexpReplaceImplementor());
+      ReflectiveImplementor insensitiveImplementor =
+          defineReflective(POSIX_REGEX_CASE_INSENSITIVE,
+              BuiltInMethod.POSIX_REGEX_INSENSITIVE.method);
+      ReflectiveImplementor sensitiveImplementor =
+          defineReflective(POSIX_REGEX_CASE_SENSITIVE,
+              BuiltInMethod.POSIX_REGEX_SENSITIVE.method);
+      map.put(NEGATED_POSIX_REGEX_CASE_INSENSITIVE,
+          NotImplementor.of(insensitiveImplementor));
+      map.put(NEGATED_POSIX_REGEX_CASE_SENSITIVE,
+          NotImplementor.of(sensitiveImplementor));
+      defineReflective(REGEXP_REPLACE,
+          BuiltInMethod.REGEXP_REPLACE3.method,
+          BuiltInMethod.REGEXP_REPLACE4.method,
+          BuiltInMethod.REGEXP_REPLACE5.method,
+          BuiltInMethod.REGEXP_REPLACE6.method);
 
       // Multisets & arrays
       defineMethod(CARDINALITY, BuiltInMethod.COLLECTION_SIZE.method,
@@ -1062,6 +1061,14 @@ public class RexImpTable {
       map.put(operator, new MethodImplementor(method, nullPolicy, false));
     }
 
+    private ReflectiveImplementor defineReflective(SqlOperator operator,
+        Method... methods) {
+      final ReflectiveImplementor implementor =
+          new ReflectiveImplementor(ImmutableList.copyOf(methods));
+      map.put(operator, implementor);
+      return implementor;
+    }
+
     private void defineUnary(SqlOperator operator, ExpressionType 
expressionType,
         NullPolicy nullPolicy, @Nullable String backupMethodName) {
       map.put(operator, new UnaryImplementor(expressionType, nullPolicy, 
backupMethodName));
@@ -2478,7 +2485,7 @@ public class RexImpTable {
 
     // Because BigQuery treats all int types as aliases for BIGINT (Java's 
long)
     // they can all be converted to LONG to minimize entries in the 
SqlFunctions class.
-    private Expression convertType(Expression arg, RexNode node) {
+    static Expression convertType(Expression arg, RexNode node) {
       if (SqlTypeName.INT_TYPES.contains(node.getType().getSqlTypeName())) {
         return Expressions.convert_(arg, long.class);
       } else {
@@ -2659,9 +2666,12 @@ public class RexImpTable {
    * {@code FORMAT_TIME} and {@code FORMAT_DATETIME} functions.
    */
   private static class FormatDatetimeImplementor
-      extends AbstractRexCallImplementor {
+      extends ReflectiveImplementor {
     FormatDatetimeImplementor() {
-      super("formatDatetime", NullPolicy.STRICT, false);
+      super(
+          ImmutableList.of(BuiltInMethod.FORMAT_DATE.method,
+              BuiltInMethod.FORMAT_TIME.method,
+              BuiltInMethod.FORMAT_TIMESTAMP.method));
     }
 
     @Override Expression implementSafe(final RexToLixTranslator translator,
@@ -2679,7 +2689,8 @@ public class RexImpTable {
       default:
         method = BuiltInMethod.FORMAT_TIMESTAMP.method;
       }
-      return Expressions.call(method, translator.getRoot(), operand0, 
operand1);
+      return implementSafe(method,
+          ImmutableList.of(translator.getRoot(), operand0, operand1));
     }
   }
 
@@ -2687,13 +2698,13 @@ public class RexImpTable {
   private static class MethodImplementor extends AbstractRexCallImplementor {
     protected final Method method;
 
-    MethodImplementor(String variableName, Method method,
-        @Nullable NullPolicy nullPolicy, boolean harmonize) {
+    MethodImplementor(String variableName, Method method, NullPolicy 
nullPolicy,
+        boolean harmonize) {
       super(variableName, nullPolicy, harmonize);
       this.method = method;
     }
 
-    MethodImplementor(Method method, @Nullable NullPolicy nullPolicy,
+    MethodImplementor(Method method, NullPolicy nullPolicy,
         boolean harmonize) {
       this("method_call", method, nullPolicy, harmonize);
     }
@@ -2725,25 +2736,6 @@ public class RexImpTable {
     }
   }
 
-  /** Implementor for {@link 
org.apache.calcite.sql.fun.SqlPosixRegexOperator}s. */
-  private static class PosixRegexMethodImplementor extends MethodImplementor {
-    protected final boolean caseSensitive;
-
-    PosixRegexMethodImplementor(boolean caseSensitive) {
-      super(BuiltInMethod.POSIX_REGEX.method, NullPolicy.STRICT, false);
-      this.caseSensitive = caseSensitive;
-    }
-
-    @Override Expression implementSafe(RexToLixTranslator translator,
-        RexCall call, List<Expression> argValueList) {
-      assert argValueList.size() == 2;
-      // Add extra parameter (caseSensitive boolean flag), required by 
SqlFunctions#posixRegex.
-      final List<Expression> newOperands = new ArrayList<>(argValueList);
-      newOperands.add(Expressions.constant(caseSensitive));
-      return super.implementSafe(translator, call, newOperands);
-    }
-  }
-
   /**
    * Implementor for JSON_VALUE function, convert to solid format
    * "JSON_VALUE(json_doc, path, empty_behavior, empty_default, 
error_behavior, error default)"
@@ -3473,7 +3465,7 @@ public class RexImpTable {
     private final AbstractRexCallImplementor implementor;
 
     private NotImplementor(AbstractRexCallImplementor implementor) {
-      super("not", implementor.getNullPolicy(), false);
+      super("not", implementor.nullPolicy, false);
       this.implementor = implementor;
     }
 
@@ -3713,13 +3705,13 @@ public class RexImpTable {
     /** Variable name should be meaningful. It helps us debug issues. */
     final String variableName;
 
-    final @Nullable NullPolicy nullPolicy;
-    private final boolean harmonize;
+    final NullPolicy nullPolicy;
+    final boolean harmonize;
 
     AbstractRexCallImplementor(String variableName,
-        @Nullable NullPolicy nullPolicy, boolean harmonize) {
+        NullPolicy nullPolicy, boolean harmonize) {
       this.variableName = requireNonNull(variableName, "variableName");
-      this.nullPolicy = nullPolicy;
+      this.nullPolicy = requireNonNull(nullPolicy, "nullPolicy");
       this.harmonize = harmonize;
     }
 
@@ -3741,19 +3733,9 @@ public class RexImpTable {
       return new RexToLixTranslator.Result(isNullVariable, valueVariable);
     }
 
-    // Variable name facilitates reasoning about issues when necessary
-    String getVariableName() {
-      return variableName;
-    }
-
-    @Nullable NullPolicy getNullPolicy() {
-      return nullPolicy;
-    }
-
     /** Figures out conditional expression according to NullPolicy. */
     Expression getCondition(final List<Expression> argIsNullList) {
       if (argIsNullList.isEmpty()
-          || nullPolicy == null
           || nullPolicy == NullPolicy.NONE) {
         return FALSE_EXPR;
       }
@@ -3807,7 +3789,7 @@ public class RexImpTable {
               convertedCallValue);
       final ParameterExpression value =
           Expressions.parameter(convertedCallValue.getType(),
-              translator.getBlockBuilder().newName(getVariableName() + 
"_value"));
+              translator.getBlockBuilder().newName(variableName + "_value"));
       translator.getBlockBuilder().add(
           Expressions.declare(Modifier.FINAL, value, valueExpression));
       return value;
@@ -3822,7 +3804,7 @@ public class RexImpTable {
         final RexToLixTranslator translator, final ParameterExpression value) {
       final ParameterExpression isNullVariable =
           Expressions.parameter(Boolean.TYPE,
-              translator.getBlockBuilder().newName(getVariableName() + 
"_isNull"));
+              translator.getBlockBuilder().newName(variableName + "_isNull"));
       final Expression isNullExpression = translator.checkNull(value);
       translator.getBlockBuilder().add(
           Expressions.declare(Modifier.FINAL, isNullVariable, 
isNullExpression));
@@ -3872,18 +3854,21 @@ public class RexImpTable {
     /** Under null check, it is safe to unbox the operands before entering the
      * implementor. */
     private List<Expression> unboxIfNecessary(final List<Expression> 
argValueList) {
-      List<Expression> unboxValueList = argValueList;
-      if (nullPolicy == NullPolicy.STRICT || nullPolicy == NullPolicy.ANY
-          || nullPolicy == NullPolicy.SEMI_STRICT) {
-        unboxValueList = argValueList.stream()
-            .map(AbstractRexCallImplementor::unboxExpression)
-            .collect(Collectors.toList());
-      }
-      if (nullPolicy == NullPolicy.ARG0 && !argValueList.isEmpty()) {
-        final Expression unboxArg0 = unboxExpression(unboxValueList.get(0));
-        unboxValueList.set(0, unboxArg0);
+      switch (nullPolicy) {
+      case STRICT:
+      case ANY:
+      case SEMI_STRICT:
+        return Util.transform(argValueList,
+            AbstractRexCallImplementor::unboxExpression);
+      case ARG0:
+        if (!argValueList.isEmpty()) {
+          final Expression unboxArg0 = unboxExpression(argValueList.get(0));
+          argValueList.set(0, unboxArg0);
+        }
+        // fall through
+      default:
+        return argValueList;
       }
-      return unboxValueList;
     }
 
     private static Expression unboxExpression(final Expression argValue) {
@@ -3944,11 +3929,11 @@ public class RexImpTable {
       final Expression valueExpression = nullAs.handle(callExpression);
       final ParameterExpression valueVariable =
           Expressions.parameter(valueExpression.getType(),
-              translator.getBlockBuilder().newName(getVariableName() + 
"_value"));
+              translator.getBlockBuilder().newName(variableName + "_value"));
       final Expression isNullExpression = translator.checkNull(valueVariable);
       final ParameterExpression isNullVariable =
           Expressions.parameter(Boolean.TYPE,
-              translator.getBlockBuilder().newName(getVariableName() + 
"_isNull"));
+              translator.getBlockBuilder().newName(variableName + "_isNull"));
       translator.getBlockBuilder().add(
           Expressions.declare(Modifier.FINAL, valueVariable, valueExpression));
       translator.getBlockBuilder().add(
@@ -3997,11 +3982,11 @@ public class RexImpTable {
       final Expression valueExpression = nullAs.handle(callExpression);
       final ParameterExpression valueVariable =
           Expressions.parameter(valueExpression.getType(),
-              translator.getBlockBuilder().newName(getVariableName() + 
"_value"));
+              translator.getBlockBuilder().newName(variableName + "_value"));
       final Expression isNullExpression = 
translator.checkNull(valueExpression);
       final ParameterExpression isNullVariable =
           Expressions.parameter(Boolean.TYPE,
-              translator.getBlockBuilder().newName(getVariableName() + 
"_isNull"));
+              translator.getBlockBuilder().newName(variableName + "_isNull"));
       translator.getBlockBuilder().add(
           Expressions.declare(Modifier.FINAL, valueVariable, valueExpression));
       translator.getBlockBuilder().add(
@@ -4110,70 +4095,48 @@ public class RexImpTable {
   }
 
   /**
-   * Implementation that calls a given {@link java.lang.reflect.Method}.
+   * Implementation that a {@link java.lang.reflect.Method}.
+   *
+   * <p>If there are several methods in the list, calls the first that has the
+   * right number of arguments.
    *
    * <p>When method is not static, a new instance of the required class is
    * created.
    */
   private static class ReflectiveImplementor extends 
AbstractRexCallImplementor {
-    protected final Method method;
+    protected final ImmutableList<? extends Method> methods;
 
-    ReflectiveImplementor(Method method, @Nullable NullPolicy nullPolicy) {
-      super("reflective_" + method.getName(), nullPolicy, false);
-      this.method = requireNonNull(method, "method");
+    ReflectiveImplementor(List<? extends Method> methods) {
+      super("reflective_" + methods.get(0).getName(), NullPolicy.STRICT, 
false);
+      this.methods = ImmutableList.copyOf(methods);
     }
 
     @Override Expression implementSafe(RexToLixTranslator translator,
         RexCall call, List<Expression> argValueList) {
+      for (Method method : methods) {
+        if (method.getParameterCount() == argValueList.size()) {
+          return implementSafe(method, argValueList);
+        }
+      }
+      throw new IllegalArgumentException("no matching method");
+    }
+
+    protected MethodCallExpression implementSafe(Method method,
+        List<Expression> argValueList) {
       List<Expression> argValueList0 =
-          EnumUtils.fromInternal(method.getParameterTypes(), argValueList);
+          EnumUtils.fromInternal(method.getParameterTypes(),
+              argValueList);
       if (isStatic(method)) {
         return Expressions.call(method, argValueList0);
       } else {
-        // The UDF class must have a public zero-args constructor.
-        // Assume that the validator checked already.
-        final Expression target = Expressions.new_(method.getDeclaringClass());
+        // The class must have a public zero-args constructor.
+        final Expression target =
+            Expressions.new_(method.getDeclaringClass());
         return Expressions.call(target, method, argValueList0);
       }
     }
   }
 
-  /** Implementor for the {@code RAND} function. */
-  private static class RandImplementor extends AbstractRexCallImplementor {
-    private final AbstractRexCallImplementor[] implementors = {
-        new ReflectiveImplementor(BuiltInMethod.RAND.method, nullPolicy),
-        new ReflectiveImplementor(BuiltInMethod.RAND_SEED.method, nullPolicy)
-    };
-
-    RandImplementor() {
-      super("rand", NullPolicy.STRICT, false);
-    }
-
-    @Override Expression implementSafe(final RexToLixTranslator translator,
-        final RexCall call, final List<Expression> argValueList) {
-      return implementors[call.getOperands().size()]
-          .implementSafe(translator, call, argValueList);
-    }
-  }
-
-  /** Implementor for the {@code RAND_INTEGER} function. */
-  private static class RandIntegerImplementor extends 
AbstractRexCallImplementor {
-    private final AbstractRexCallImplementor[] implementors = {
-        new ReflectiveImplementor(BuiltInMethod.RAND_INTEGER.method, 
nullPolicy),
-        new ReflectiveImplementor(BuiltInMethod.RAND_INTEGER_SEED.method, 
nullPolicy)
-    };
-
-    RandIntegerImplementor() {
-      super("rand_integer", NullPolicy.STRICT, false);
-    }
-
-    @Override Expression implementSafe(final RexToLixTranslator translator,
-        final RexCall call, final List<Expression> argValueList) {
-      return implementors[call.getOperands().size() - 1]
-          .implementSafe(translator, call, argValueList);
-    }
-  }
-
   /** Implementor for the {@code PI} operator. */
   private static class PiImplementor extends AbstractRexCallImplementor {
     PiImplementor() {
@@ -4282,26 +4245,6 @@ public class RexImpTable {
     }
   }
 
-  /** Implementor for the {@code REGEXP_REPLACE} function. */
-  private static class RegexpReplaceImplementor extends 
AbstractRexCallImplementor {
-    private final AbstractRexCallImplementor[] implementors = {
-        new ReflectiveImplementor(BuiltInMethod.REGEXP_REPLACE3.method, 
nullPolicy),
-        new ReflectiveImplementor(BuiltInMethod.REGEXP_REPLACE4.method, 
nullPolicy),
-        new ReflectiveImplementor(BuiltInMethod.REGEXP_REPLACE5.method, 
nullPolicy),
-        new ReflectiveImplementor(BuiltInMethod.REGEXP_REPLACE6.method, 
nullPolicy),
-    };
-
-    RegexpReplaceImplementor() {
-      super("regexp_replace", NullPolicy.STRICT, false);
-    }
-
-    @Override Expression implementSafe(RexToLixTranslator translator,
-        RexCall call, List<Expression> argValueList) {
-      return implementors[call.getOperands().size() - 3]
-          .implementSafe(translator, call, argValueList);
-    }
-  }
-
   /** Implementor for the {@code DEFAULT} function. */
   private static class DefaultImplementor extends AbstractRexCallImplementor {
     DefaultImplementor() {
@@ -4423,7 +4366,7 @@ public class RexImpTable {
       BlockBuilder lambdaBuilder = new BlockBuilder();
       final ParameterExpression leftExpr =
           Expressions.parameter(left.getType(),
-              translator.getBlockBuilder().newName("_" + getVariableName() + 
"_left_value"));
+              translator.getBlockBuilder().newName("_" + variableName + 
"_left_value"));
       // left should have final modifier otherwise it can not be passed to 
lambda
       translator.getBlockBuilder().add(Expressions.declare(Modifier.FINAL, 
leftExpr, left));
       RexNode leftRex = call.getOperands().get(0);
diff --git 
a/core/src/main/java/org/apache/calcite/config/CalciteSystemProperty.java 
b/core/src/main/java/org/apache/calcite/config/CalciteSystemProperty.java
index 0e7281f11a..963626fd10 100644
--- a/core/src/main/java/org/apache/calcite/config/CalciteSystemProperty.java
+++ b/core/src/main/java/org/apache/calcite/config/CalciteSystemProperty.java
@@ -251,7 +251,8 @@ public final class CalciteSystemProperty<T> {
       booleanProperty("calcite.test.redis", true);
 
   /**
-   * Whether to use Docker containers (https://www.testcontainers.org/) in 
tests.
+   * Whether to use
+   * <a href="https://www.testcontainers.org/";>Docker containers</a> in tests.
    *
    * <p>If the property is set to <code>true</code>, affected tests will 
attempt
    * to start Docker containers; when Docker is not available tests fallback to
@@ -303,7 +304,7 @@ public final class CalciteSystemProperty<T> {
    * The name of the default national character set.
    *
    * <p>It is used with the N'string' construct in
-   * {@link org.apache.calcite.sql.SqlLiteral#SqlLiteral}
+   * {@link org.apache.calcite.sql.SqlLiteral}
    * and may be different from the {@link #DEFAULT_CHARSET}.
    */
   // TODO review zabetak:
@@ -315,7 +316,7 @@ public final class CalciteSystemProperty<T> {
    * The name of the default collation.
    *
    * <p>It is used in {@link org.apache.calcite.sql.SqlCollation} and
-   * {@link org.apache.calcite.sql.SqlLiteral#SqlLiteral}.
+   * {@link org.apache.calcite.sql.SqlLiteral}.
    */
   // TODO review zabetak:
   // What happens if a wrong value is specified?
@@ -328,7 +329,7 @@ public final class CalciteSystemProperty<T> {
    * tertiary, identical.
    *
    * <p>It is used in {@link org.apache.calcite.sql.SqlCollation} and
-   * {@link org.apache.calcite.sql.SqlLiteral#SqlLiteral}.
+   * {@link org.apache.calcite.sql.SqlLiteral}.
    */
   // TODO review zabetak:
   // What happens if a wrong value is specified?
@@ -364,7 +365,7 @@ public final class CalciteSystemProperty<T> {
    * <p>Setting this property to 0 disables the cache.
    */
   public static final CalciteSystemProperty<Integer> BINDABLE_CACHE_MAX_SIZE =
-      intProperty("calcite.bindable.cache.maxSize", 0, v -> v >= 0 && v <= 
Integer.MAX_VALUE);
+      intProperty("calcite.bindable.cache.maxSize", 0, v -> v >= 0);
 
   /**
    * The concurrency level of the cache used for storing Bindable objects,
@@ -380,15 +381,44 @@ public final class CalciteSystemProperty<T> {
    * {@link #BINDABLE_CACHE_MAX_SIZE} set to 0.
    */
   public static final CalciteSystemProperty<Integer> 
BINDABLE_CACHE_CONCURRENCY_LEVEL =
-      intProperty("calcite.bindable.cache.concurrencyLevel", 1,
-          v -> v >= 1 && v <= Integer.MAX_VALUE);
+      intProperty("calcite.bindable.cache.concurrencyLevel", 1, v -> v >= 1);
+
+  /**
+   * The maximum number of items in a function-level cache.
+   *
+   * <p>A few SQL functions have expensive processing that, if its results are
+   * cached, can be reused by future calls to the function. One such function
+   * is {@code RLIKE}, whose arguments are a regular expression and a string.
+   * The regular expression needs to be compiled to a
+   * {@link java.util.regex.Pattern}. Compilation is expensive, and within a
+   * particular query, the arguments are often the same string, or a small
+   * number of distinct strings, so caching makes sense.
+   *
+   * <p>Therefore, functions such as {@code RLIKE}, {@code SIMILAR TO},
+   * {@code PARSE_URL}, {@code PARSE_TIMESTAMP}, {@code FORMAT_DATE} have a
+   * function-level cache. The cache is created in the code generated for the
+   * query, at the call site of the function, and expires when the query has
+   * finished executing. Such caches do not need time-based expiration, but
+   * we need to cap the size of the cache to deal with scenarios such as a
+   * billion-row table where every row has a distinct regular expression.
+   *
+   * <p>Because of how Calcite generates and executes code in Enumerable
+   * convention, each function object is used from a single thread. Therefore,
+   * non thread-safe objects such as {@link java.text.DateFormat} can be safely
+   * cached.
+   *
+   * <p>The value of this parameter limits the size of every function-level
+   * cache in Calcite. The default value is 1,000.
+   */
+  public static final CalciteSystemProperty<Integer> 
FUNCTION_LEVEL_CACHE_MAX_SIZE =
+      intProperty("calcite.function.cache.maxSize", 0, v -> v >= 0);
 
   private static CalciteSystemProperty<Boolean> booleanProperty(String key,
       boolean defaultValue) {
     // Note that "" -> true (convenient for command-lines flags like '-Dflag')
     return new CalciteSystemProperty<>(key,
         v -> v == null ? defaultValue
-            : "".equals(v) || Boolean.parseBoolean(v));
+            : v.isEmpty() || Boolean.parseBoolean(v));
   }
 
   private static CalciteSystemProperty<Integer> intProperty(String key, int 
defaultValue) {
diff --git a/core/src/main/java/org/apache/calcite/rex/RexInterpreter.java 
b/core/src/main/java/org/apache/calcite/rex/RexInterpreter.java
index 80adc5ca5f..c495dddd35 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexInterpreter.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexInterpreter.java
@@ -68,6 +68,13 @@ public class RexInterpreter implements 
RexVisitor<Comparable> {
           SqlKind.DIVIDE, SqlKind.COALESCE, SqlKind.CEIL,
           SqlKind.FLOOR, SqlKind.EXTRACT);
 
+  private final SqlFunctions.LikeFunction likeFunction =
+      new SqlFunctions.LikeFunction();
+  private final SqlFunctions.SimilarFunction similarFunction =
+      new SqlFunctions.SimilarFunction();
+  private final SqlFunctions.SimilarEscapeFunction similarEscapeFunction =
+      new SqlFunctions.SimilarEscapeFunction();
+
   private final Map<RexNode, Comparable> environment;
 
   /** Creates an interpreter.
@@ -245,7 +252,7 @@ public class RexInterpreter implements 
RexVisitor<Comparable> {
     return DateTimeUtils.unixDateExtract(timeUnitRange, v2);
   }
 
-  private static Comparable like(List<Comparable> values) {
+  private Comparable like(List<Comparable> values) {
     if (containsNull(values)) {
       return N;
     }
@@ -253,17 +260,17 @@ public class RexInterpreter implements 
RexVisitor<Comparable> {
     final NlsString pattern = (NlsString) values.get(1);
     switch (values.size()) {
     case 2:
-      return SqlFunctions.like(value.getValue(), pattern.getValue());
+      return likeFunction.like(value.getValue(), pattern.getValue());
     case 3:
       final NlsString escape = (NlsString) values.get(2);
-      return SqlFunctions.like(value.getValue(), pattern.getValue(),
+      return likeFunction.like(value.getValue(), pattern.getValue(),
           escape.getValue());
     default:
       throw new AssertionError();
     }
   }
 
-  private static Comparable similar(List<Comparable> values) {
+  private Comparable similar(List<Comparable> values) {
     if (containsNull(values)) {
       return N;
     }
@@ -271,10 +278,10 @@ public class RexInterpreter implements 
RexVisitor<Comparable> {
     final NlsString pattern = (NlsString) values.get(1);
     switch (values.size()) {
     case 2:
-      return SqlFunctions.similar(value.getValue(), pattern.getValue());
+      return similarFunction.similar(value.getValue(), pattern.getValue());
     case 3:
       final NlsString escape = (NlsString) values.get(2);
-      return SqlFunctions.similar(value.getValue(), pattern.getValue(),
+      return similarEscapeFunction.similar(value.getValue(), 
pattern.getValue(),
           escape.getValue());
     default:
       throw new AssertionError();
diff --git a/core/src/main/java/org/apache/calcite/runtime/Like.java 
b/core/src/main/java/org/apache/calcite/runtime/Like.java
index 2fa8665629..ac074afa3d 100644
--- a/core/src/main/java/org/apache/calcite/runtime/Like.java
+++ b/core/src/main/java/org/apache/calcite/runtime/Like.java
@@ -310,16 +310,22 @@ public class Like {
     return javaPattern.toString();
   }
 
-  static java.util.regex.Pattern posixRegexToPattern(String regex, boolean 
caseSensitive) {
+  static java.util.regex.Pattern posixRegexToPattern(String regex,
+      boolean caseSensitive) {
+    int flags = caseSensitive ? 0 : java.util.regex.Pattern.CASE_INSENSITIVE;
+    return posixRegexToPattern(regex, flags);
+  }
+
+  static java.util.regex.Pattern posixRegexToPattern(String regex, int flags) {
     // Replace existing character classes with java equivalent ones
     String originalRegex = regex;
     String[] existingExpressions = Arrays.stream(POSIX_CHARACTER_CLASSES)
-        .filter(v -> 
originalRegex.contains(v.toLowerCase(Locale.ROOT))).toArray(String[]::new);
+        .filter(v -> originalRegex.contains(v.toLowerCase(Locale.ROOT)))
+        .toArray(String[]::new);
     for (String v : existingExpressions) {
       regex = regex.replace(v.toLowerCase(Locale.ROOT), "\\p{" + v + "}");
     }
 
-    int flags = caseSensitive ? 0 : java.util.regex.Pattern.CASE_INSENSITIVE;
     return java.util.regex.Pattern.compile(regex, flags);
   }
 }
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 6837fbb9dc..648093b57f 100644
--- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
+++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
@@ -27,6 +27,7 @@ import org.apache.calcite.linq4j.CartesianProductEnumerator;
 import org.apache.calcite.linq4j.Enumerable;
 import org.apache.calcite.linq4j.Enumerator;
 import org.apache.calcite.linq4j.Linq4j;
+import org.apache.calcite.linq4j.Ord;
 import org.apache.calcite.linq4j.function.Deterministic;
 import org.apache.calcite.linq4j.function.Experimental;
 import org.apache.calcite.linq4j.function.Function1;
@@ -43,6 +44,7 @@ import org.apache.calcite.util.TimestampWithTimeZoneString;
 import org.apache.calcite.util.Unsafe;
 import org.apache.calcite.util.Util;
 import org.apache.calcite.util.format.FormatElement;
+import org.apache.calcite.util.format.FormatModel;
 import org.apache.calcite.util.format.FormatModels;
 
 import org.apache.commons.codec.DecoderException;
@@ -55,7 +57,11 @@ import 
org.apache.commons.text.similarity.LevenshteinDistance;
 
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
 import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.UncheckedExecutionException;
 
 import org.checkerframework.checker.nullness.qual.Nullable;
 import org.checkerframework.checker.nullness.qual.PolyNull;
@@ -72,6 +78,7 @@ import java.nio.charset.CharacterCodingException;
 import java.nio.charset.Charset;
 import java.nio.charset.CharsetDecoder;
 import java.sql.SQLException;
+import java.sql.Time;
 import java.sql.Timestamp;
 import java.text.DateFormat;
 import java.text.DecimalFormat;
@@ -106,14 +113,18 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.TimeZone;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.function.BinaryOperator;
+import java.util.function.Consumer;
+import java.util.function.Function;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.regex.PatternSyntaxException;
 
+import static 
org.apache.calcite.config.CalciteSystemProperty.FUNCTION_LEVEL_CACHE_MAX_SIZE;
 import static org.apache.calcite.linq4j.Nullness.castNonNull;
 import static org.apache.calcite.util.Static.RESOURCE;
 
@@ -354,54 +365,87 @@ 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();
+  /** State for {@code REGEXP_CONTAINS}, {@code REGEXP_REPLACE}, {@code RLIKE}.
+   *
+   * <p>Marked deterministic so that the code generator instantiates one once
+   * per query, not once per row. */
+  @Deterministic
+  public static class RegexFunction {
+    /** Cache key. */
+    private static class Key extends Ord<String> {
+      Key(int flags, String regex) {
+        super(flags, regex);
+      }
+
+      @SuppressWarnings("MagicConstant")
+      Pattern toPattern() {
+        return Pattern.compile(e, i);
+      }
     }
-  }
 
-  /** SQL {@code REGEXP_REPLACE} function with 3 arguments. */
-  public static String regexpReplace(String s, String regex,
-      String replacement) {
-    return regexpReplace(s, regex, replacement, 1, 0, null);
-  }
+    private final LoadingCache<Key, Pattern> cache =
+        CacheBuilder.newBuilder()
+            .maximumSize(FUNCTION_LEVEL_CACHE_MAX_SIZE.value())
+            .build(CacheLoader.from(Key::toPattern));
 
-  /** SQL {@code REGEXP_REPLACE} function with 4 arguments. */
-  public static String regexpReplace(String s, String regex, String 
replacement,
-      int pos) {
-    return regexpReplace(s, regex, replacement, pos, 0, null);
-  }
+    /** SQL {@code REGEXP_CONTAINS(value, regexp)} function.
+     * Throws a runtime exception for invalid regular expressions.*/
+    public boolean regexpContains(String value, String regex) {
+      final Pattern pattern;
+      try {
+        // Uses java.util.regex as a standard for regex processing
+        // in Calcite instead of RE2 used by BigQuery/GoogleSQL
+        pattern = cache.getUnchecked(new Key(0, regex));
+      } catch (UncheckedExecutionException e) {
+        if (e.getCause() instanceof PatternSyntaxException) {
+          throw RESOURCE.invalidInputForRegexpContains(
+              stripLineEndings(
+                  requireNonNull(e.getCause().getMessage(), "message"))).ex();
+        }
+        throw e;
+      }
+      return pattern.matcher(value).find();
+    }
 
-  /** SQL {@code REGEXP_REPLACE} function with 5 arguments. */
-  public static String regexpReplace(String s, String regex, String 
replacement,
-      int pos, int occurrence) {
-    return regexpReplace(s, regex, replacement, pos, occurrence, null);
-  }
+    private static String stripLineEndings(String message) {
+      return message.replace("\r\n", " ")
+          .replace("\n", " ")
+          .replace("\r", " ");
+    }
 
-  /** SQL {@code REGEXP_REPLACE} function with 6 arguments. */
-  public static String regexpReplace(String s, String regex, String 
replacement,
-      int pos, int occurrence, @Nullable String matchType) {
-    if (pos < 1 || pos > s.length()) {
-      throw RESOURCE.invalidInputForRegexpReplace(Integer.toString(pos)).ex();
+    /** SQL {@code REGEXP_REPLACE} function with 3 arguments. */
+    public String regexpReplace(String s, String regex,
+        String replacement) {
+      return regexpReplace(s, regex, replacement, 1, 0, null);
     }
 
-    final int flags = makeRegexpFlags(matchType);
-    final Pattern pattern = Pattern.compile(regex, flags);
+    /** SQL {@code REGEXP_REPLACE} function with 4 arguments. */
+    public String regexpReplace(String s, String regex, String replacement,
+        int pos) {
+      return regexpReplace(s, regex, replacement, pos, 0, null);
+    }
 
-    return Unsafe.regexpReplace(s, pattern, replacement, pos, occurrence);
-  }
+    /** SQL {@code REGEXP_REPLACE} function with 5 arguments. */
+    public String regexpReplace(String s, String regex, String replacement,
+        int pos, int occurrence) {
+      return regexpReplace(s, regex, replacement, pos, occurrence, null);
+    }
+
+    /** SQL {@code REGEXP_REPLACE} function with 6 arguments. */
+    public String regexpReplace(String s, String regex, String replacement,
+        int pos, int occurrence, @Nullable String matchType) {
+      if (pos < 1 || pos > s.length()) {
+        throw 
RESOURCE.invalidInputForRegexpReplace(Integer.toString(pos)).ex();
+      }
 
-  private static int makeRegexpFlags(@Nullable String stringFlags) {
-    int flags = 0;
-    if (stringFlags != null) {
+      final int flags = matchType == null ? 0 : makeRegexpFlags(matchType);
+      final Pattern pattern = cache.getUnchecked(new Key(flags, regex));
+
+      return Unsafe.regexpReplace(s, pattern, replacement, pos, occurrence);
+    }
+
+    private static int makeRegexpFlags(String stringFlags) {
+      int flags = 0;
       for (int i = 0; i < stringFlags.length(); ++i) {
         switch (stringFlags.charAt(i)) {
         case 'i':
@@ -420,8 +464,13 @@ public class SqlFunctions {
           throw RESOURCE.invalidInputForRegexpReplace(stringFlags).ex();
         }
       }
+      return flags;
+    }
+
+    /** SQL {@code RLIKE} function. */
+    public boolean rlike(String s, String pattern) {
+      return cache.getUnchecked(new Key(0, pattern)).matcher(s).find();
     }
-    return flags;
   }
 
   /** SQL {@code LPAD(string, integer, string)} function. */
@@ -1069,78 +1118,76 @@ public class SqlFunctions {
     }
   }
 
-  /** SQL {@code PARSE_URL(urlStr, partToExtract, keyToExtract)} function. */
-  public static @Nullable String parseUrl(@Nullable String urlStr,
-      @Nullable String partToExtract, @Nullable String keyToExtract) {
-    if (partToExtract == null || !partToExtract.equals("QUERY")) {
-      return null;
+  /** State for {@code PARSE_URL}. */
+  @Deterministic
+  public static class ParseUrlFunction {
+    static Pattern keyToPattern(String keyToExtract) {
+      return Pattern.compile("(&|^)" + keyToExtract + "=([^&]*)");
     }
 
-    String query = parseUrl(urlStr, partToExtract);
-    if (query == null) {
-      return null;
-    }
+    private final LoadingCache<String, Pattern> cache =
+        CacheBuilder.newBuilder()
+            .maximumSize(FUNCTION_LEVEL_CACHE_MAX_SIZE.value())
+            .build(CacheLoader.from(ParseUrlFunction::keyToPattern));
 
-    Pattern p = Pattern.compile("(&|^)" + keyToExtract + "=([^&]*)");
-    Matcher m = p.matcher(query);
-    return m.find() ? m.group(2) : null;
-  }
+    /** SQL {@code PARSE_URL(urlStr, partToExtract, keyToExtract)} function. */
+    public @Nullable String parseUrl(String urlStr, String partToExtract,
+        String keyToExtract) {
+      if (!partToExtract.equals("QUERY")) {
+        return null;
+      }
 
-  /** SQL {@code PARSE_URL(urlStr, partToExtract)} function. */
-  public static @Nullable String parseUrl(@Nullable String urlStr,
-      @Nullable String partToExtract) {
-    if (urlStr == null || partToExtract == null) {
-      return null;
-    }
+      String query = parseUrl(urlStr, partToExtract);
+      if (query == null) {
+        return null;
+      }
 
-    URI uri;
-    try {
-      uri = new URI(urlStr);
-    } catch (URISyntaxException e) {
-      return null;
+      Pattern p = cache.getUnchecked(keyToExtract);
+      Matcher m = p.matcher(query);
+      return m.find() ? m.group(2) : null;
     }
 
-    String extractValue;
-    PartToExtract part;
-    try {
-      part = PartToExtract.valueOf(partToExtract);
-    } catch (IllegalArgumentException e) {
-      return null;
-    }
+    /** SQL {@code PARSE_URL(urlStr, partToExtract)} function. */
+    public @Nullable String parseUrl(String urlStr, String partToExtract) {
+      URI uri;
+      try {
+        uri = new URI(urlStr);
+      } catch (URISyntaxException e) {
+        return null;
+      }
 
-    switch (part) {
-    case HOST:
-      extractValue = uri.getHost();
-      break;
-    case PATH:
-      extractValue = uri.getRawPath();
-      break;
-    case QUERY:
-      extractValue = uri.getRawQuery();
-      break;
-    case REF:
-      extractValue = uri.getRawFragment();
-      break;
-    case PROTOCOL:
-      extractValue = uri.getScheme();
-      break;
-    case FILE:
-      if (uri.getRawQuery() != null) {
-        extractValue = uri.getRawPath() + "?" + uri.getRawQuery();
-      } else {
-        extractValue = uri.getRawPath();
+      PartToExtract part;
+      try {
+        part = PartToExtract.valueOf(partToExtract);
+      } catch (IllegalArgumentException e) {
+        return null;
+      }
+
+      switch (part) {
+      case HOST:
+        return uri.getHost();
+      case PATH:
+        return uri.getRawPath();
+      case QUERY:
+        return uri.getRawQuery();
+      case REF:
+        return uri.getRawFragment();
+      case PROTOCOL:
+        return uri.getScheme();
+      case FILE:
+        if (uri.getRawQuery() != null) {
+          return uri.getRawPath() + "?" + uri.getRawQuery();
+        } else {
+          return uri.getRawPath();
+        }
+      case AUTHORITY:
+        return uri.getRawAuthority();
+      case USERINFO:
+        return uri.getRawUserInfo();
+      default:
+        return null;
       }
-      break;
-    case AUTHORITY:
-      extractValue = uri.getRawAuthority();
-      break;
-    case USERINFO:
-      extractValue = uri.getRawUserInfo();
-      break;
-    default:
-      extractValue = null;
     }
-    return extractValue;
   }
 
   /** SQL {@code RTRIM} function applied to string. */
@@ -1259,50 +1306,136 @@ public class SqlFunctions {
         .concat(s.substring(start - 1 + length));
   }
 
-  /** SQL {@code LIKE} function. */
-  public static boolean like(String s, String pattern) {
-    final String regex = Like.sqlToRegexLike(pattern, null);
-    return Pattern.matches(regex, s);
-  }
+  /** State for {@code LIKE}, {@code ILIKE}. */
+  @Deterministic
+  public static class LikeFunction {
+    /** Key for cache of compiled regular expressions. */
+    private static final class Key {
+      final String pattern;
+      final @Nullable String escape;
+      final int flags;
+
+      Key(String pattern, @Nullable String escape, int flags) {
+        this.pattern = pattern;
+        this.escape = escape;
+        this.flags = flags;
+      }
 
-  /** SQL {@code LIKE} function with escape. */
-  public static boolean like(String s, String pattern, String escape) {
-    final String regex = Like.sqlToRegexLike(pattern, escape);
-    return Pattern.matches(regex, s);
-  }
+      @Override public int hashCode() {
+        return pattern.hashCode()
+            ^ (escape == null ? 0 : escape.hashCode())
+            ^ flags;
+      }
 
-  /** SQL {@code ILIKE} function. */
-  public static boolean ilike(String s, String pattern) {
-    final String regex = Like.sqlToRegexLike(pattern, null);
-    return Pattern.compile(regex, 
Pattern.CASE_INSENSITIVE).matcher(s).matches();
-  }
+      @Override public boolean equals(@Nullable Object obj) {
+        return this == obj
+            || obj instanceof Key
+            && pattern.equals(((Key) obj).pattern)
+            && Objects.equals(escape, ((Key) obj).escape)
+            && flags == ((Key) obj).flags;
+      }
 
-  /** SQL {@code ILIKE} function with escape. */
-  public static boolean ilike(String s, String pattern, String escape) {
-    final String regex = Like.sqlToRegexLike(pattern, escape);
-    return Pattern.compile(regex, 
Pattern.CASE_INSENSITIVE).matcher(s).matches();
-  }
+      Pattern toPattern() {
+        String regex = Like.sqlToRegexLike(pattern, escape);
+        return Pattern.compile(regex, flags);
+      }
+    }
 
-  /** SQL {@code RLIKE} function. */
-  public static boolean rlike(String s, String pattern) {
-    return Pattern.compile(pattern).matcher(s).find();
-  }
+    private final LoadingCache<Key, Pattern> cache =
+        CacheBuilder.newBuilder()
+            .maximumSize(FUNCTION_LEVEL_CACHE_MAX_SIZE.value())
+            .build(CacheLoader.from(Key::toPattern));
+
+    /** SQL {@code LIKE} function. */
+    public boolean like(String s, String pattern) {
+      final Key key = new Key(pattern, null, 0);
+      return cache.getUnchecked(key).matcher(s).matches();
+    }
+
+    /** SQL {@code LIKE} function with escape. */
+    public boolean like(String s, String pattern, String escape) {
+      final Key key = new Key(pattern, escape, 0);
+      return cache.getUnchecked(key).matcher(s).matches();
+    }
+
+    /** SQL {@code ILIKE} function. */
+    public boolean ilike(String s, String pattern) {
+      final Key key = new Key(pattern, null, Pattern.CASE_INSENSITIVE);
+      return cache.getUnchecked(key).matcher(s).matches();
+    }
 
-  /** SQL {@code SIMILAR} function. */
-  public static boolean similar(String s, String pattern) {
-    final String regex = Like.sqlToRegexSimilar(pattern, null);
-    return Pattern.matches(regex, s);
+    /** SQL {@code ILIKE} function with escape. */
+    public boolean ilike(String s, String pattern, String escape) {
+      final Key key = new Key(pattern, escape, Pattern.CASE_INSENSITIVE);
+      return cache.getUnchecked(key).matcher(s).matches();
+    }
   }
 
-  /** SQL {@code SIMILAR} function with escape. */
-  public static boolean similar(String s, String pattern, String escape) {
-    final String regex = Like.sqlToRegexSimilar(pattern, escape);
-    return Pattern.matches(regex, s);
+  /** State for {@code SIMILAR} function. */
+  @Deterministic
+  public static class SimilarFunction {
+    private final LoadingCache<String, Pattern> cache =
+        CacheBuilder.newBuilder()
+            .maximumSize(FUNCTION_LEVEL_CACHE_MAX_SIZE.value())
+            .build(
+                CacheLoader.from(pattern ->
+                    Pattern.compile(Like.sqlToRegexSimilar(pattern, null))));
+
+    /** SQL {@code SIMILAR} function. */
+    public boolean similar(String s, String pattern) {
+      return cache.getUnchecked(pattern).matcher(s).matches();
+    }
+  }
+
+  /** State for {@code SIMILAR} function with escape. */
+  public static class SimilarEscapeFunction {
+    /** Cache key. */
+    private static class Key extends MapEntry<String, String> {
+      Key(String formatModel, String format) {
+        super(formatModel, format);
+      }
+
+      Pattern toPattern() {
+        return Pattern.compile(Like.sqlToRegexSimilar(t, u));
+      }
+    }
+
+    private final LoadingCache<Key, Pattern> cache =
+        CacheBuilder.newBuilder()
+            .maximumSize(FUNCTION_LEVEL_CACHE_MAX_SIZE.value())
+            .build(CacheLoader.from(Key::toPattern));
+
+    /** SQL {@code SIMILAR} function with escape. */
+    public boolean similar(String s, String pattern, String escape) {
+      return cache.getUnchecked(new Key(pattern, escape))
+          .matcher(s).matches();
+    }
   }
 
-  public static boolean posixRegex(String s, String regex, boolean 
caseSensitive) {
-    final Pattern pattern = Like.posixRegexToPattern(regex, caseSensitive);
-    return pattern.matcher(s).find();
+  /** State for posix regex function. */
+  @Deterministic
+  public static class PosixRegexFunction {
+    private final LoadingCache<Ord<String>, Pattern> cache =
+        CacheBuilder.newBuilder()
+            .maximumSize(FUNCTION_LEVEL_CACHE_MAX_SIZE.value())
+            .build(
+                CacheLoader.from(pattern ->
+                    Like.posixRegexToPattern(pattern.e, pattern.i)));
+
+    boolean posixRegex(String s, String regex, int flags) {
+      final Ord<String> key = Ord.of(flags, regex);
+      return cache.getUnchecked(key).matcher(s).find();
+    }
+
+    /** Posix regex, case-insensitive. */
+    public boolean posixRegexInsensitive(String s, String regex) {
+      return posixRegex(s, regex, Pattern.CASE_INSENSITIVE);
+    }
+
+    /** Posix regex, case-sensitive. */
+    public boolean posixRegexSensitive(String s, String regex) {
+      return posixRegex(s, regex, 0);
+    }
   }
 
   // =
@@ -3339,97 +3472,176 @@ public class SqlFunctions {
         .toString();
   }
 
-  private static String internalFormatDatetime(String fmtString, 
java.util.Date date) {
-    StringBuilder sb = new StringBuilder();
-    List<FormatElement> elements = FormatModels.BIG_QUERY.parse(fmtString);
-    elements.forEach(ele -> ele.format(sb, date));
-    return sb.toString();
-  }
+  /** State for {@code FORMAT_DATE}, {@code FORMAT_TIMESTAMP},
+   * {@code FORMAT_DATETIME}, {@code FORMAT_TIME}, {@code TO_CHAR} functions. 
*/
+  @Deterministic
+  public static class DateFormatFunction {
+    /** Work space for various functions. Clear it before you use it. */
+    final StringBuilder sb = new StringBuilder();
+
+    /** Cache key. */
+    private static class Key extends MapEntry<FormatModel, String> {
+      Key(FormatModel formatModel, String format) {
+        super(formatModel, format);
+      }
+    }
 
-  public static String formatTimestamp(DataContext ctx, String fmtString, long 
timestamp) {
-    return internalFormatDatetime(fmtString, internalToTimestamp(timestamp));
-  }
+    private final LoadingCache<Key, List<FormatElement>> formatCache =
+        CacheBuilder.newBuilder()
+            .maximumSize(FUNCTION_LEVEL_CACHE_MAX_SIZE.value())
+            .build(CacheLoader.from(key -> key.t.parseNoCache(key.u)));
 
-  public static String toChar(long timestamp, String pattern) {
-    List<FormatElement> elements = FormatModels.POSTGRESQL.parse(pattern);
-    StringBuilder sb = new StringBuilder();
-    elements.forEach(ele ->  ele.format(sb, internalToTimestamp(timestamp)));
-    return sb.toString().trim();
-  }
+    /** Given a format string and a format model, calls an action with the
+     * list of elements obtained by parsing that format string. */
+    protected final void withElements(FormatModel formatModel, String format,
+        Consumer<List<FormatElement>> consumer) {
+      List<FormatElement> elements =
+          formatCache.getUnchecked(new Key(formatModel, format));
+      consumer.accept(elements);
+    }
 
-  public static String formatDate(DataContext ctx, String fmtString, int date) 
{
-    return internalFormatDatetime(fmtString, internalToDate(date));
-  }
+    private String internalFormatDatetime(String fmtString,
+        java.util.Date date) {
+      sb.setLength(0);
+      withElements(FormatModels.BIG_QUERY, fmtString, elements ->
+          elements.forEach(element -> element.format(sb, date)));
+      return sb.toString();
+    }
 
-  public static String formatTime(DataContext ctx, String fmtString, int time) 
{
-    return internalFormatDatetime(fmtString, internalToTime(time));
-  }
+    public String formatTimestamp(DataContext ctx, String fmtString,
+        long timestamp) {
+      return internalFormatDatetime(fmtString, internalToTimestamp(timestamp));
+    }
 
-  private static String parseDatetimePattern(String fmtString) {
-    StringBuilder sb = new StringBuilder();
-    List<FormatElement> elements = FormatModels.BIG_QUERY.parse(fmtString);
-    elements.forEach(ele -> ele.toPattern(sb));
-    return sb.toString();
-  }
+    public String toChar(long timestamp, String pattern) {
+      final Timestamp sqlTimestamp = internalToTimestamp(timestamp);
+      sb.setLength(0);
+      withElements(FormatModels.POSTGRESQL, pattern, elements ->
+          elements.forEach(element -> element.format(sb, sqlTimestamp)));
+      return sb.toString().trim();
+    }
 
-  private static long internalParseDatetime(String fmtString, String datetime) 
{
-    return internalParseDatetime(fmtString, datetime,
-        DateTimeUtils.DEFAULT_ZONE);
-  }
-
-  private static long internalParseDatetime(String fmt, String datetime,
-      TimeZone tz) {
-    final String javaFmt = parseDatetimePattern(fmt);
-    // TODO: make Locale configurable. ENGLISH set for weekday parsing (e.g.
-    // Thursday, Friday).
-    final DateFormat parser = new SimpleDateFormat(javaFmt, Locale.ENGLISH);
-    final ParsePosition pos = new ParsePosition(0);
-    parser.setLenient(false);
-    parser.setCalendar(Calendar.getInstance(tz, Locale.ROOT));
-    Date parsed = parser.parse(datetime, pos);
-    // Throw if either the parse was unsuccessful, or the format string did not
-    // contain enough elements to parse the datetime string completely.
-    if (pos.getErrorIndex() >= 0 || pos.getIndex() != datetime.length()) {
-      SQLException e =
-          new SQLException(
-              String.format(Locale.ROOT,
-                  "Invalid format: '%s' for datetime string: '%s'.", fmt,
-                  datetime));
-      throw Util.toUnchecked(e);
+    public String formatDate(DataContext ctx, String fmtString, int date) {
+      return internalFormatDatetime(fmtString, internalToDate(date));
     }
-    // Suppress the Errorprone warning "[JavaUtilDate] Date has a bad API that
-    // leads to bugs; prefer java.time.Instant or LocalDate" because we know
-    // what we're doing.
-    @SuppressWarnings("JavaUtilDate")
-    final long millisSinceEpoch = parsed.getTime();
-    return millisSinceEpoch;
-  }
 
-  public static int parseDate(String fmtString, String date) {
-    final long millisSinceEpoch = internalParseDatetime(fmtString, date);
-    return toInt(new java.sql.Date(millisSinceEpoch));
+    public String formatTime(DataContext ctx, String fmtString, int time) {
+      return internalFormatDatetime(fmtString, internalToTime(time));
+    }
   }
 
-  public static long parseDatetime(String fmtString, String datetime) {
-    final long millisSinceEpoch = internalParseDatetime(fmtString, datetime);
-    return toLong(new java.sql.Timestamp(millisSinceEpoch));
-  }
+  /** State for {@code PARSE_DATE}, {@code PARSE_TIMESTAMP},
+   * {@code PARSE_DATETIME}, {@code PARSE_TIME} functions. */
+  @Deterministic
+  public static class DateParseFunction {
+    /** Use a {@link DateFormatFunction} for its cache of parsed
+     * format strings. */
+    final DateFormatFunction f = new DateFormatFunction();
+
+    private final LoadingCache<Key, DateFormat> cache =
+        CacheBuilder.newBuilder()
+            .maximumSize(FUNCTION_LEVEL_CACHE_MAX_SIZE.value())
+            .build(CacheLoader.from(key -> key.toDateFormat(f)));
+
+    private <T> T withParser(String fmt, String timeZone,
+        Function<DateFormat, T> action) {
+      final DateFormat dateFormat = cache.getUnchecked(new Key(fmt, timeZone));
+      return action.apply(dateFormat);
+    }
+
+    private long internalParseDatetime(String fmtString, String datetime) {
+      return internalParseDatetime(fmtString, datetime,
+          DateTimeUtils.DEFAULT_ZONE.getID());
+    }
+
+    private long internalParseDatetime(String fmt, String datetime,
+        String timeZone) {
+      final ParsePosition pos = new ParsePosition(0);
+      Date parsed =
+          withParser(fmt, timeZone, parser -> parser.parse(datetime, pos));
+      // Throw if either the parse was unsuccessful, or the format string did
+      // not contain enough elements to parse the datetime string completely.
+      if (pos.getErrorIndex() >= 0 || pos.getIndex() != datetime.length()) {
+        SQLException e =
+            new SQLException(
+                String.format(Locale.ROOT,
+                    "Invalid format: '%s' for datetime string: '%s'.", fmt,
+                    datetime));
+        throw Util.toUnchecked(e);
+      }
+      // Suppress the Errorprone warning "[JavaUtilDate] Date has a bad API 
that
+      // leads to bugs; prefer java.time.Instant or LocalDate" because we know
+      // what we're doing.
+      @SuppressWarnings("JavaUtilDate")
+      final long millisSinceEpoch = parsed.getTime();
+      return millisSinceEpoch;
+    }
 
-  public static int parseTime(String fmtString, String time) {
-    final long millisSinceEpoch = internalParseDatetime(fmtString, time);
-    return toInt(new java.sql.Time(millisSinceEpoch));
-  }
+    public int parseDate(String fmtString, String date) {
+      final long millisSinceEpoch = internalParseDatetime(fmtString, date);
+      return toInt(new java.sql.Date(millisSinceEpoch));
+    }
 
-  public static long parseTimestamp(String fmtString, String timestamp) {
-    return parseTimestamp(fmtString, timestamp, "UTC");
-  }
+    public long parseDatetime(String fmtString, String datetime) {
+      final long millisSinceEpoch = internalParseDatetime(fmtString, datetime);
+      return toLong(new Timestamp(millisSinceEpoch));
+    }
 
-  public static long parseTimestamp(String fmtString, String timestamp,
-      String timeZone) {
-    TimeZone tz = TimeZone.getTimeZone(timeZone);
-    final long millisSinceEpoch =
-        internalParseDatetime(fmtString, timestamp, tz);
-    return toLong(new java.sql.Timestamp(millisSinceEpoch), tz);
+    public int parseTime(String fmtString, String time) {
+      final long millisSinceEpoch = internalParseDatetime(fmtString, time);
+      return toInt(new Time(millisSinceEpoch));
+    }
+
+    public long parseTimestamp(String fmtString, String timestamp) {
+      return parseTimestamp(fmtString, timestamp, "UTC");
+    }
+
+    public long parseTimestamp(String fmtString, String timestamp,
+        String timeZone) {
+      TimeZone tz = TimeZone.getTimeZone(timeZone);
+      final long millisSinceEpoch =
+          internalParseDatetime(fmtString, timestamp, timeZone);
+      return toLong(new java.sql.Timestamp(millisSinceEpoch), tz);
+    }
+
+    /** Key for cache of parsed format strings. */
+    private static final class Key {
+      final String fmt;
+      final String timeZone;
+
+      Key(String fmt, String timeZone) {
+        this.fmt = fmt;
+        this.timeZone = timeZone;
+      }
+
+      @Override public int hashCode() {
+        return fmt.hashCode()
+            + timeZone.hashCode() * 37;
+      }
+
+      @Override public boolean equals(@Nullable Object obj) {
+        return this == obj
+            || obj instanceof Key
+            && fmt.equals(((Key) obj).fmt)
+            && timeZone.equals(((Key) obj).timeZone);
+      }
+
+      DateFormat toDateFormat(DateFormatFunction f) {
+        f.sb.setLength(0);
+        f.withElements(FormatModels.BIG_QUERY, fmt, elements ->
+            elements.forEach(ele -> ele.toPattern(f.sb)));
+        final String javaFmt = f.sb.toString();
+
+        // TODO: make Locale configurable. ENGLISH set for weekday
+        // parsing (e.g. Thursday, Friday).
+        final DateFormat parser =
+            new SimpleDateFormat(javaFmt, Locale.ENGLISH);
+        parser.setLenient(false);
+        TimeZone tz = TimeZone.getTimeZone(timeZone);
+        parser.setCalendar(Calendar.getInstance(tz, Locale.ROOT));
+        return parser;
+      }
+    }
   }
 
   /**
@@ -5141,8 +5353,8 @@ public class SqlFunctions {
     SCALAR, LIST, MAP
   }
 
-  /** Type of part to extract passed into {@link #parseUrl}. */
-  public enum PartToExtract {
+  /** Type of part to extract passed into {@link ParseUrlFunction#parseUrl}. */
+  private enum PartToExtract {
     HOST,
     PATH,
     QUERY,
@@ -5152,5 +5364,4 @@ public class SqlFunctions {
     AUTHORITY,
     USERINFO;
   }
-
 }
diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java 
b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
index a4b5e56881..fe50d2c8cc 100644
--- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
+++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
@@ -361,8 +361,8 @@ public enum BuiltInMethod {
   EXP(SqlFunctions.class, "exp", double.class),
   MOD(SqlFunctions.class, "mod", long.class, long.class),
   POWER(SqlFunctions.class, "power", double.class, double.class),
-  REGEXP_CONTAINS(SqlFunctions.class, "regexpContains", String.class,
-      String.class),
+  REGEXP_CONTAINS(SqlFunctions.RegexFunction.class, "regexpContains",
+      String.class, String.class),
   REPEAT(SqlFunctions.class, "repeat", String.class, int.class),
   SPACE(SqlFunctions.class, "space", int.class),
   SPLIT(SqlFunctions.class, "split", String.class),
@@ -442,7 +442,10 @@ public enum BuiltInMethod {
   STRING_CONCAT(SqlFunctions.class, "concat", String.class, String.class),
   STRING_CONCAT_WITH_NULL(SqlFunctions.class, "concatWithNull", String.class,
       String.class),
-  PARSE_URL(SqlFunctions.class, "parseUrl", String.class, String.class, 
String.class),
+  PARSE_URL2(SqlFunctions.ParseUrlFunction.class, "parseUrl", String.class,
+      String.class),
+  PARSE_URL3(SqlFunctions.ParseUrlFunction.class, "parseUrl", String.class,
+      String.class, String.class),
   MULTI_STRING_CONCAT(SqlFunctions.class, "concatMulti", String[].class),
   MULTI_STRING_CONCAT_WITH_NULL(SqlFunctions.class, "concatMultiWithNull",
       String[].class),
@@ -505,19 +508,30 @@ public enum BuiltInMethod {
   TRANSLATE3(SqlFunctions.class, "translate3", String.class, String.class, 
String.class),
   LTRIM(SqlFunctions.class, "ltrim", String.class),
   RTRIM(SqlFunctions.class, "rtrim", String.class),
-  LIKE(SqlFunctions.class, "like", String.class, String.class),
-  ILIKE(SqlFunctions.class, "ilike", String.class, String.class),
-  RLIKE(SqlFunctions.class, "rlike", String.class, String.class),
-  SIMILAR(SqlFunctions.class, "similar", String.class, String.class),
-  POSIX_REGEX(SqlFunctions.class, "posixRegex", String.class, String.class, 
boolean.class),
-  REGEXP_REPLACE3(SqlFunctions.class, "regexpReplace", String.class,
+  LIKE(SqlFunctions.LikeFunction.class, "like", String.class, String.class),
+  LIKE_ESCAPE(SqlFunctions.LikeFunction.class, "like", String.class,
       String.class, String.class),
-  REGEXP_REPLACE4(SqlFunctions.class, "regexpReplace", String.class,
-      String.class, String.class, int.class),
-  REGEXP_REPLACE5(SqlFunctions.class, "regexpReplace", String.class,
-      String.class, String.class, int.class, int.class),
-  REGEXP_REPLACE6(SqlFunctions.class, "regexpReplace", String.class,
-      String.class, String.class, int.class, int.class, String.class),
+  ILIKE(SqlFunctions.LikeFunction.class, "ilike", String.class, String.class),
+  ILIKE_ESCAPE(SqlFunctions.LikeFunction.class, "ilike", String.class,
+      String.class, String.class),
+  RLIKE(SqlFunctions.RegexFunction.class, "rlike", String.class, String.class),
+  SIMILAR(SqlFunctions.SimilarFunction.class, "similar", String.class,
+      String.class),
+  SIMILAR_ESCAPE(SqlFunctions.SimilarEscapeFunction.class, "similar",
+      String.class, String.class, String.class),
+  POSIX_REGEX_INSENSITIVE(SqlFunctions.PosixRegexFunction.class,
+      "posixRegexInsensitive", String.class, String.class),
+  POSIX_REGEX_SENSITIVE(SqlFunctions.PosixRegexFunction.class,
+      "posixRegexSensitive", String.class, String.class),
+  REGEXP_REPLACE3(SqlFunctions.RegexFunction.class, "regexpReplace",
+      String.class, String.class, String.class),
+  REGEXP_REPLACE4(SqlFunctions.RegexFunction.class, "regexpReplace",
+      String.class, String.class, String.class, int.class),
+  REGEXP_REPLACE5(SqlFunctions.RegexFunction.class, "regexpReplace",
+      String.class, String.class, String.class, int.class, int.class),
+  REGEXP_REPLACE6(SqlFunctions.RegexFunction.class, "regexpReplace",
+      String.class, String.class, String.class, int.class, int.class,
+      String.class),
   IS_TRUE(SqlFunctions.class, "isTrue", Boolean.class),
   IS_NOT_FALSE(SqlFunctions.class, "isNotFalse", Boolean.class),
   NOT(SqlFunctions.class, "not", Boolean.class),
@@ -580,19 +594,22 @@ public enum BuiltInMethod {
   DATETIME(SqlFunctions.class, "datetime", long.class),
   TIME(SqlFunctions.class, "time", int.class),
   TIMESTAMP(SqlFunctions.class, "timestamp", long.class),
-  PARSE_DATE(SqlFunctions.class, "parseDate", String.class, String.class),
-  PARSE_DATETIME(SqlFunctions.class, "parseDatetime", String.class,
+  PARSE_DATE(SqlFunctions.DateParseFunction.class, "parseDate", String.class,
       String.class),
-  PARSE_TIME(SqlFunctions.class, "parseTime", String.class, String.class),
-  PARSE_TIMESTAMP(SqlFunctions.class, "parseTimestamp", String.class,
+  PARSE_DATETIME(SqlFunctions.DateParseFunction.class, "parseDatetime",
+      String.class, String.class),
+  PARSE_TIME(SqlFunctions.DateParseFunction.class, "parseTime", String.class,
       String.class),
-  FORMAT_TIMESTAMP(SqlFunctions.class, "formatTimestamp", DataContext.class,
-      String.class, long.class),
-  TO_CHAR(SqlFunctions.class, "toChar", long.class, String.class),
-  FORMAT_DATE(SqlFunctions.class, "formatDate", DataContext.class, 
String.class,
-      int.class),
-  FORMAT_TIME(SqlFunctions.class, "formatTime", DataContext.class, 
String.class,
-      int.class),
+  PARSE_TIMESTAMP(SqlFunctions.DateParseFunction.class, "parseTimestamp",
+      String.class, String.class),
+  FORMAT_TIMESTAMP(SqlFunctions.DateFormatFunction.class, "formatTimestamp",
+      DataContext.class, String.class, long.class),
+  TO_CHAR(SqlFunctions.DateFormatFunction.class, "toChar", long.class,
+      String.class),
+  FORMAT_DATE(SqlFunctions.DateFormatFunction.class, "formatDate",
+      DataContext.class, String.class, int.class),
+  FORMAT_TIME(SqlFunctions.DateFormatFunction.class, "formatTime",
+      DataContext.class, String.class, int.class),
   UNIX_DATE_TO_STRING(DateTimeUtils.class, "unixDateToString", int.class),
   UNIX_TIME_TO_STRING(DateTimeUtils.class, "unixTimeToString", int.class),
   UNIX_TIMESTAMP_TO_STRING(DateTimeUtils.class, "unixTimestampToString",
diff --git a/core/src/main/java/org/apache/calcite/util/format/FormatModel.java 
b/core/src/main/java/org/apache/calcite/util/format/FormatModel.java
index 39bac2f053..248ae5accb 100644
--- a/core/src/main/java/org/apache/calcite/util/format/FormatModel.java
+++ b/core/src/main/java/org/apache/calcite/util/format/FormatModel.java
@@ -34,4 +34,7 @@ public interface FormatModel {
   /** Parses a format string using element identifiers supplied by
    * {@code format}. */
   List<FormatElement> parse(String format);
+
+  /** As {@link #parse}, but does not cache. */
+  List<FormatElement> parseNoCache(String format);
 }
diff --git 
a/core/src/main/java/org/apache/calcite/util/format/FormatModels.java 
b/core/src/main/java/org/apache/calcite/util/format/FormatModels.java
index 545e315b58..2839e661b7 100644
--- a/core/src/main/java/org/apache/calcite/util/format/FormatModels.java
+++ b/core/src/main/java/org/apache/calcite/util/format/FormatModels.java
@@ -237,7 +237,7 @@ public class FormatModels {
       return elementMap;
     }
 
-    private List<FormatElement> internalParse(String format) {
+    @Override public List<FormatElement> parseNoCache(String format) {
       final ImmutableList.Builder<FormatElement> elements =
           ImmutableList.builder();
       final Matcher matcher = pattern.matcher(format);
@@ -263,7 +263,7 @@ public class FormatModels {
     }
 
     @Override public List<FormatElement> parse(String format) {
-      return memoizedElements.computeIfAbsent(format, this::internalParse);
+      return memoizedElements.computeIfAbsent(format, this::parseNoCache);
     }
   }
 
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 e6eb29b938..e308297898 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
@@ -58,15 +58,11 @@ import static org.apache.calcite.runtime.SqlFunctions.lower;
 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;
 import static org.apache.calcite.runtime.SqlFunctions.sha256;
 import static org.apache.calcite.runtime.SqlFunctions.sha512;
 import static org.apache.calcite.runtime.SqlFunctions.toBase64;
-import static org.apache.calcite.runtime.SqlFunctions.toChar;
 import static org.apache.calcite.runtime.SqlFunctions.toInt;
 import static org.apache.calcite.runtime.SqlFunctions.toIntOptional;
 import static org.apache.calcite.runtime.SqlFunctions.toLong;
@@ -216,76 +212,86 @@ class SqlFunctionsTest {
   }
 
   @Test void testPosixRegex() {
-    assertThat(posixRegex("abc", "abc", true), is(true));
-    assertThat(posixRegex("abc", "^a", true), is(true));
-    assertThat(posixRegex("abc", "(b|d)", true), is(true));
-    assertThat(posixRegex("abc", "^(b|c)", true), is(false));
+    final SqlFunctions.PosixRegexFunction f =
+        new SqlFunctions.PosixRegexFunction();
+    assertThat(f.posixRegexSensitive("abc", "abc"), is(true));
+    assertThat(f.posixRegexSensitive("abc", "^a"), is(true));
+    assertThat(f.posixRegexSensitive("abc", "(b|d)"), is(true));
+    assertThat(f.posixRegexSensitive("abc", "^(b|c)"), is(false));
 
-    assertThat(posixRegex("abc", "ABC", false), is(true));
-    assertThat(posixRegex("abc", "^A", false), is(true));
-    assertThat(posixRegex("abc", "(B|D)", false), is(true));
-    assertThat(posixRegex("abc", "^(B|C)", false), is(false));
+    assertThat(f.posixRegexInsensitive("abc", "ABC"), is(true));
+    assertThat(f.posixRegexInsensitive("abc", "^A"), is(true));
+    assertThat(f.posixRegexInsensitive("abc", "(B|D)"), is(true));
+    assertThat(f.posixRegexInsensitive("abc", "^(B|C)"), is(false));
 
-    assertThat(posixRegex("abc", "^[[:xdigit:]]$", false), is(false));
-    assertThat(posixRegex("abc", "^[[:xdigit:]]+$", false), is(true));
-    assertThat(posixRegex("abcq", "^[[:xdigit:]]+$", false), is(false));
+    assertThat(f.posixRegexInsensitive("abc", "^[[:xdigit:]]$"), is(false));
+    assertThat(f.posixRegexInsensitive("abc", "^[[:xdigit:]]+$"), is(true));
+    assertThat(f.posixRegexInsensitive("abcq", "^[[:xdigit:]]+$"), is(false));
 
-    assertThat(posixRegex("abc", "[[:xdigit:]]", false), is(true));
-    assertThat(posixRegex("abc", "[[:xdigit:]]+", false), is(true));
-    assertThat(posixRegex("abcq", "[[:xdigit:]]", false), is(true));
+    assertThat(f.posixRegexInsensitive("abc", "[[:xdigit:]]"), is(true));
+    assertThat(f.posixRegexInsensitive("abc", "[[:xdigit:]]+"), is(true));
+    assertThat(f.posixRegexInsensitive("abcq", "[[:xdigit:]]"), is(true));
   }
 
   @Test void testRegexpContains() {
+    final SqlFunctions.RegexFunction f = new SqlFunctions.RegexFunction();
+
+    // Use same regex; should hit cache
+    assertThat(f.regexpContains("abcdef", "abz*"), is(true));
+    assertThat(f.regexpContains("zabzz", "abz*"), is(true));
+    assertThat(f.regexpContains("zazbbzz", "abz*"), is(false));
+
     try {
-      regexpContains("abc def ghi", "(abc");
-      fail("'regexp_contains' on an invalid regex input '(abc' is not 
possible");
+      final boolean b = f.regexpContains("abc def ghi", "(abc");
+      fail("expected error, got " + b);
     } catch (RuntimeException e) {
-      assertThat(
-          e.getMessage(), is("Invalid regular expression for REGEXP_CONTAINS: 
'Unclosed "
+      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");
+      final boolean b = f.regexpContains("abc def ghi", "[z-a]");
+      fail("expected error, got " + b);
     } catch (RuntimeException e) {
-      assertThat(
-          e.getMessage(), is("Invalid regular expression for REGEXP_CONTAINS: 
'Illegal "
+      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");
+      final boolean b = f.regexpContains("abc def ghi", "{2,1}");
+      fail("expected error, got " + b);
     } catch (RuntimeException e) {
-      assertThat(
-          e.getMessage(), is("Invalid regular expression for REGEXP_CONTAINS: 
'Illegal "
+      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"));
-    assertThat(regexpReplace("abc def ghi", "[a-z]+", "X"), is("X X X"));
-    assertThat(regexpReplace("a b c", "a|b", "X"), is("X X c"));
-    assertThat(regexpReplace("a b c", "y", "X"), is("a b c"));
-
-    assertThat(regexpReplace("100-200", "(\\d+)", "num"), is("num-num"));
-    assertThat(regexpReplace("100-200", "(\\d+)", "###"), is("###-###"));
-    assertThat(regexpReplace("100-200", "(-)", "###"), is("100###200"));
-
-    assertThat(regexpReplace("abc def ghi", "[a-z]+", "X", 1), is("X X X"));
-    assertThat(regexpReplace("abc def ghi", "[a-z]+", "X", 2), is("aX X X"));
-    assertThat(regexpReplace("abc def ghi", "[a-z]+", "X", 1, 3),
+    final SqlFunctions.RegexFunction f = new SqlFunctions.RegexFunction();
+    assertThat(f.regexpReplace("a b c", "b", "X"), is("a X c"));
+    assertThat(f.regexpReplace("abc def ghi", "[g-z]+", "X"), is("abc def X"));
+    assertThat(f.regexpReplace("abc def ghi", "[a-z]+", "X"), is("X X X"));
+    assertThat(f.regexpReplace("a b c", "a|b", "X"), is("X X c"));
+    assertThat(f.regexpReplace("a b c", "y", "X"), is("a b c"));
+
+    assertThat(f.regexpReplace("100-200", "(\\d+)", "num"), is("num-num"));
+    assertThat(f.regexpReplace("100-200", "(\\d+)", "###"), is("###-###"));
+    assertThat(f.regexpReplace("100-200", "(-)", "###"), is("100###200"));
+
+    assertThat(f.regexpReplace("abc def ghi", "[a-z]+", "X", 1), is("X X X"));
+    assertThat(f.regexpReplace("abc def ghi", "[a-z]+", "X", 2), is("aX X X"));
+    assertThat(f.regexpReplace("abc def ghi", "[a-z]+", "X", 1, 3),
         is("abc def X"));
-    assertThat(regexpReplace("abc def GHI", "[a-z]+", "X", 1, 3, "c"),
+    assertThat(f.regexpReplace("abc def GHI", "[a-z]+", "X", 1, 3, "c"),
         is("abc def GHI"));
-    assertThat(regexpReplace("abc def GHI", "[a-z]+", "X", 1, 3, "i"),
+    assertThat(f.regexpReplace("abc def GHI", "[a-z]+", "X", 1, 3, "i"),
         is("abc def X"));
 
     try {
-      regexpReplace("abc def ghi", "[a-z]+", "X", 0);
+      f.regexpReplace("abc def ghi", "[a-z]+", "X", 0);
       fail("'regexp_replace' on an invalid pos is not possible");
     } catch (CalciteException e) {
       assertThat(e.getMessage(),
@@ -293,7 +299,7 @@ class SqlFunctionsTest {
     }
 
     try {
-      regexpReplace("abc def ghi", "[a-z]+", "X", 1, 3, "WWW");
+      f.regexpReplace("abc def ghi", "[a-z]+", "X", 1, 3, "WWW");
       fail("'regexp_replace' on an invalid matchType is not possible");
     } catch (CalciteException e) {
       assertThat(e.getMessage(),
@@ -1492,24 +1498,23 @@ class SqlFunctionsTest {
     String pattern1 = "YYYY-MM-DD HH24:MI:SS.MS";
     String pattern2 = "Day, DD HH12:MI:SS";
 
-    assertThat(
-        toChar(0, pattern1),
+    final SqlFunctions.DateFormatFunction f =
+        new SqlFunctions.DateFormatFunction();
+    assertThat(f.toChar(0, pattern1),
         is("1970-01-01 00:00:00.000"));
 
-    assertThat(
-        toChar(0, pattern2),
+    assertThat(f.toChar(0, pattern2),
         is("Thursday, 01 12:00:00"));
 
-    assertThat(
-        toChar(timestampStringToUnixDate("2014-09-30 15:28:27.356"), pattern1),
+    final long ts0 = timestampStringToUnixDate("2014-09-30 15:28:27.356");
+    assertThat(f.toChar(ts0, pattern1),
         is("2014-09-30 15:28:27.356"));
 
-    assertThat(
-        toChar(timestampStringToUnixDate("2014-09-30 15:28:27.356"), pattern2),
+    assertThat(f.toChar(ts0, pattern2),
         is("Tuesday, 30 03:28:27"));
 
-    assertThat(
-        toChar(timestampStringToUnixDate("1500-04-30 12:00:00.123"), pattern1),
+    final long ts1 = timestampStringToUnixDate("1500-04-30 12:00:00.123");
+    assertThat(f.toChar(ts1, pattern1),
         is("1500-04-30 12:00:00.123"));
   }
 
diff --git a/linq4j/src/main/java/org/apache/calcite/linq4j/Ord.java 
b/linq4j/src/main/java/org/apache/calcite/linq4j/Ord.java
index 7e92686a1f..f30c7ba256 100644
--- a/linq4j/src/main/java/org/apache/calcite/linq4j/Ord.java
+++ b/linq4j/src/main/java/org/apache/calcite/linq4j/Ord.java
@@ -18,10 +18,13 @@ package org.apache.calcite.linq4j;
 
 import com.google.common.collect.ImmutableList;
 
+import org.checkerframework.checker.nullness.qual.Nullable;
+
 import java.util.AbstractList;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.RandomAccess;
 import java.util.function.ObjIntConsumer;
 
@@ -49,6 +52,17 @@ public class Ord<E> implements Map.Entry<Integer, E> {
     return new Ord<>(n, e);
   }
 
+  @Override public int hashCode() {
+    return Objects.hash(e, i);
+  }
+
+  @Override public boolean equals(@Nullable Object obj) {
+    return this == obj
+        || obj instanceof Ord
+        && i == ((Ord<?>) obj).i
+        && Objects.equals(e, ((Ord<?>) obj).e);
+  }
+
   /**
    * Creates an iterable of {@code Ord}s over an iterable.
    */
@@ -99,6 +113,7 @@ public class Ord<E> implements Map.Entry<Integer, E> {
    * <p>Given the array ["a", "b", "c"], returns (2, "c") then (1, "b") then
    * (0, "a").
    */
+  @SafeVarargs // heap pollution is not possible because we only read
   public static <E> Iterable<Ord<E>> reverse(E... elements) {
     return reverse(ImmutableList.copyOf(elements));
   }
@@ -121,10 +136,6 @@ public class Ord<E> implements Map.Entry<Integer, E> {
       @Override public Ord<E> next() {
         return Ord.of(i, elementList.get(i--));
       }
-
-      @Override public void remove() {
-        throw new UnsupportedOperationException("remove");
-      }
     };
   }
 

Reply via email to