You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by jh...@apache.org on 2020/07/29 00:59:18 UTC

[calcite] 05/09: In SqlReturnType, add methods orElse and andThen

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

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

commit e17a5c9f640be399b1afeaccfbc660482e5de187
Author: Julian Hyde <jh...@apache.org>
AuthorDate: Mon Jul 20 15:57:09 2020 -0700

    In SqlReturnType, add methods orElse and andThen
    
    Given `SqlReturnType rule`, `rule.andThen(transform)` is shorthand for
    `ReturnTypes.cascade(rule, transform)` and `rule.orElse(rule2)` is
    shorthand for `ReturnTypes.chain(rule, rule2)`.
    
    Refactor existing code to use these new methods. There are no
    changes in functionality.
---
 .../calcite/sql/fun/SqlCoalesceFunction.java       |   3 +-
 .../calcite/sql/fun/SqlJsonDepthFunction.java      |   3 +-
 .../calcite/sql/fun/SqlJsonExistsFunction.java     |   5 +-
 .../calcite/sql/fun/SqlJsonKeysFunction.java       |  10 +-
 .../calcite/sql/fun/SqlJsonLengthFunction.java     |   3 +-
 .../calcite/sql/fun/SqlJsonPrettyFunction.java     |   9 +-
 .../calcite/sql/fun/SqlJsonQueryFunction.java      |   7 +-
 .../calcite/sql/fun/SqlJsonRemoveFunction.java     |  13 +--
 .../sql/fun/SqlJsonStorageSizeFunction.java        |  10 +-
 .../calcite/sql/fun/SqlJsonTypeFunction.java       |  12 +--
 .../sql/fun/SqlJsonValueExpressionOperator.java    |   5 +-
 .../calcite/sql/fun/SqlLeadLagAggFunction.java     |  25 +++--
 .../calcite/sql/fun/SqlLibraryOperators.java       | 105 ++++++++++-----------
 .../calcite/sql/fun/SqlRegexpReplaceFunction.java  |  14 ++-
 .../apache/calcite/sql/fun/SqlTrimFunction.java    |   8 +-
 .../org/apache/calcite/sql/type/ReturnTypes.java   |  99 +++++++++++--------
 .../calcite/sql/type/SqlReturnTypeInference.java   |  13 +++
 .../sql/type/SqlReturnTypeInferenceChain.java      |   7 +-
 .../calcite/sql/type/SqlTypeTransformCascade.java  |   8 +-
 19 files changed, 181 insertions(+), 178 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlCoalesceFunction.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlCoalesceFunction.java
index 65c069e..79f058e 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlCoalesceFunction.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlCoalesceFunction.java
@@ -45,8 +45,7 @@ public class SqlCoalesceFunction extends SqlFunction {
     // strategies are used.
     super("COALESCE",
         SqlKind.COALESCE,
-        ReturnTypes.cascade(ReturnTypes.LEAST_RESTRICTIVE,
-            SqlTypeTransforms.LEAST_NULLABLE),
+        ReturnTypes.LEAST_RESTRICTIVE.andThen(SqlTypeTransforms.LEAST_NULLABLE),
         null,
         OperandTypes.SAME_VARIADIC,
         SqlFunctionCategory.SYSTEM);
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonDepthFunction.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonDepthFunction.java
index 7f16ac8..94dbb7b 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonDepthFunction.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonDepthFunction.java
@@ -38,8 +38,7 @@ public class SqlJsonDepthFunction extends SqlFunction {
   public SqlJsonDepthFunction() {
     super("JSON_DEPTH",
         SqlKind.OTHER_FUNCTION,
-        ReturnTypes.cascade(ReturnTypes.INTEGER,
-            SqlTypeTransforms.FORCE_NULLABLE),
+        ReturnTypes.INTEGER.andThen(SqlTypeTransforms.FORCE_NULLABLE),
         null,
         OperandTypes.ANY,
         SqlFunctionCategory.SYSTEM);
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonExistsFunction.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonExistsFunction.java
index b91b334..f547b45 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonExistsFunction.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonExistsFunction.java
@@ -32,10 +32,11 @@ import org.apache.calcite.sql.type.SqlTypeTransforms;
 public class SqlJsonExistsFunction extends SqlFunction {
   public SqlJsonExistsFunction() {
     super("JSON_EXISTS", SqlKind.OTHER_FUNCTION,
-        ReturnTypes.cascade(ReturnTypes.BOOLEAN, SqlTypeTransforms.FORCE_NULLABLE), null,
+        ReturnTypes.BOOLEAN.andThen(SqlTypeTransforms.FORCE_NULLABLE), null,
         OperandTypes.or(
             OperandTypes.family(SqlTypeFamily.ANY, SqlTypeFamily.CHARACTER),
-            OperandTypes.family(SqlTypeFamily.ANY, SqlTypeFamily.CHARACTER, SqlTypeFamily.ANY)),
+            OperandTypes.family(SqlTypeFamily.ANY, SqlTypeFamily.CHARACTER,
+                SqlTypeFamily.ANY)),
         SqlFunctionCategory.SYSTEM);
   }
 
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonKeysFunction.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonKeysFunction.java
index 5251d3e..f71ae0e 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonKeysFunction.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonKeysFunction.java
@@ -30,10 +30,10 @@ import org.apache.calcite.sql.type.SqlTypeTransforms;
 public class SqlJsonKeysFunction extends SqlFunction {
   public SqlJsonKeysFunction() {
     super("JSON_KEYS", SqlKind.OTHER_FUNCTION,
-          ReturnTypes.cascade(ReturnTypes.VARCHAR_2000, SqlTypeTransforms.FORCE_NULLABLE),
-          null,
-          OperandTypes.or(OperandTypes.ANY,
-              OperandTypes.family(SqlTypeFamily.ANY, SqlTypeFamily.CHARACTER)),
-          SqlFunctionCategory.SYSTEM);
+        ReturnTypes.VARCHAR_2000.andThen(SqlTypeTransforms.FORCE_NULLABLE),
+        null,
+        OperandTypes.or(OperandTypes.ANY,
+            OperandTypes.family(SqlTypeFamily.ANY, SqlTypeFamily.CHARACTER)),
+        SqlFunctionCategory.SYSTEM);
   }
 }
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonLengthFunction.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonLengthFunction.java
index 552cc1c..27f33ed 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonLengthFunction.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonLengthFunction.java
@@ -30,8 +30,7 @@ import org.apache.calcite.sql.type.SqlTypeTransforms;
 public class SqlJsonLengthFunction extends SqlFunction {
   public SqlJsonLengthFunction() {
     super("JSON_LENGTH", SqlKind.OTHER_FUNCTION,
-        ReturnTypes.cascade(ReturnTypes.INTEGER,
-            SqlTypeTransforms.FORCE_NULLABLE),
+        ReturnTypes.INTEGER.andThen(SqlTypeTransforms.FORCE_NULLABLE),
         null,
         OperandTypes.or(OperandTypes.ANY,
             OperandTypes.family(SqlTypeFamily.ANY, SqlTypeFamily.CHARACTER)),
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonPrettyFunction.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonPrettyFunction.java
index b5b1b3d..23d77b6 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonPrettyFunction.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonPrettyFunction.java
@@ -37,12 +37,9 @@ import org.apache.calcite.sql.validate.SqlValidator;
 public class SqlJsonPrettyFunction extends SqlFunction {
 
   public SqlJsonPrettyFunction() {
-    super("JSON_PRETTY",
-        SqlKind.OTHER_FUNCTION,
-        ReturnTypes.cascade(ReturnTypes.VARCHAR_2000, SqlTypeTransforms.FORCE_NULLABLE),
-        null,
-        OperandTypes.ANY,
-        SqlFunctionCategory.SYSTEM);
+    super("JSON_PRETTY", SqlKind.OTHER_FUNCTION,
+        ReturnTypes.VARCHAR_2000.andThen(SqlTypeTransforms.FORCE_NULLABLE),
+        null, OperandTypes.ANY, SqlFunctionCategory.SYSTEM);
   }
 
   @Override public SqlOperandCountRange getOperandCountRange() {
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonQueryFunction.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonQueryFunction.java
index 52dd020..83a210e 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonQueryFunction.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonQueryFunction.java
@@ -37,11 +37,10 @@ import org.apache.calcite.sql.type.SqlTypeTransforms;
 public class SqlJsonQueryFunction extends SqlFunction {
   public SqlJsonQueryFunction() {
     super("JSON_QUERY", SqlKind.OTHER_FUNCTION,
-        ReturnTypes.cascade(ReturnTypes.VARCHAR_2000,
-            SqlTypeTransforms.FORCE_NULLABLE),
+        ReturnTypes.VARCHAR_2000.andThen(SqlTypeTransforms.FORCE_NULLABLE),
         null,
-        OperandTypes.family(SqlTypeFamily.ANY,
-            SqlTypeFamily.CHARACTER, SqlTypeFamily.ANY, SqlTypeFamily.ANY, SqlTypeFamily.ANY),
+        OperandTypes.family(SqlTypeFamily.ANY, SqlTypeFamily.CHARACTER,
+            SqlTypeFamily.ANY, SqlTypeFamily.ANY, SqlTypeFamily.ANY),
         SqlFunctionCategory.SYSTEM);
   }
 
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonRemoveFunction.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonRemoveFunction.java
index 59cd4b7..6860f20 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonRemoveFunction.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonRemoveFunction.java
@@ -35,20 +35,17 @@ import java.util.Locale;
 public class SqlJsonRemoveFunction extends SqlFunction {
 
   public SqlJsonRemoveFunction() {
-    super("JSON_REMOVE",
-        SqlKind.OTHER_FUNCTION,
-        ReturnTypes.cascade(ReturnTypes.VARCHAR_2000,
-            SqlTypeTransforms.FORCE_NULLABLE),
-        null,
-        null,
-        SqlFunctionCategory.SYSTEM);
+    super("JSON_REMOVE", SqlKind.OTHER_FUNCTION,
+        ReturnTypes.VARCHAR_2000.andThen(SqlTypeTransforms.FORCE_NULLABLE),
+        null, null, SqlFunctionCategory.SYSTEM);
   }
 
   @Override public SqlOperandCountRange getOperandCountRange() {
     return SqlOperandCountRanges.from(2);
   }
 
-  @Override public boolean checkOperandTypes(SqlCallBinding callBinding, boolean throwOnFailure) {
+  @Override public boolean checkOperandTypes(SqlCallBinding callBinding,
+      boolean throwOnFailure) {
     final int operandCount = callBinding.getOperandCount();
     assert operandCount >= 2;
     if (!OperandTypes.ANY.checkSingleOperandType(
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonStorageSizeFunction.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonStorageSizeFunction.java
index 8608e9c..5abbfea 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonStorageSizeFunction.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonStorageSizeFunction.java
@@ -29,12 +29,8 @@ import org.apache.calcite.sql.type.SqlTypeTransforms;
 public class SqlJsonStorageSizeFunction extends SqlFunction {
 
   public SqlJsonStorageSizeFunction() {
-    super("JSON_STORAGE_SIZE",
-        SqlKind.OTHER_FUNCTION,
-        ReturnTypes.cascade(ReturnTypes.INTEGER,
-            SqlTypeTransforms.FORCE_NULLABLE),
-        null,
-        OperandTypes.ANY,
-        SqlFunctionCategory.SYSTEM);
+    super("JSON_STORAGE_SIZE", SqlKind.OTHER_FUNCTION,
+        ReturnTypes.INTEGER.andThen(SqlTypeTransforms.FORCE_NULLABLE),
+        null, OperandTypes.ANY, SqlFunctionCategory.SYSTEM);
   }
 }
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonTypeFunction.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonTypeFunction.java
index 90ff7c0..e477194 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonTypeFunction.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonTypeFunction.java
@@ -37,14 +37,10 @@ import org.apache.calcite.sql.validate.SqlValidator;
  */
 public class SqlJsonTypeFunction extends SqlFunction {
   public SqlJsonTypeFunction() {
-    super("JSON_TYPE",
-        SqlKind.OTHER_FUNCTION,
-        ReturnTypes.cascade(
-            ReturnTypes.explicit(SqlTypeName.VARCHAR, 20),
-            SqlTypeTransforms.FORCE_NULLABLE),
-        null,
-        OperandTypes.ANY,
-        SqlFunctionCategory.SYSTEM);
+    super("JSON_TYPE", SqlKind.OTHER_FUNCTION,
+        ReturnTypes.explicit(SqlTypeName.VARCHAR, 20)
+            .andThen(SqlTypeTransforms.FORCE_NULLABLE),
+        null, OperandTypes.ANY, SqlFunctionCategory.SYSTEM);
   }
 
   @Override public SqlOperandCountRange getOperandCountRange() {
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonValueExpressionOperator.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonValueExpressionOperator.java
index 4215b26..450b758 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonValueExpressionOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlJsonValueExpressionOperator.java
@@ -31,7 +31,8 @@ public class SqlJsonValueExpressionOperator extends SqlPostfixOperator {
 
   public SqlJsonValueExpressionOperator() {
     super("FORMAT JSON", SqlKind.JSON_VALUE_EXPRESSION, 28,
-        ReturnTypes.cascade(ReturnTypes.explicit(SqlTypeName.ANY),
-            SqlTypeTransforms.TO_NULLABLE), null, OperandTypes.CHARACTER);
+        ReturnTypes.explicit(SqlTypeName.ANY)
+            .andThen(SqlTypeTransforms.TO_NULLABLE),
+        null, OperandTypes.CHARACTER);
   }
 }
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlLeadLagAggFunction.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlLeadLagAggFunction.java
index ba41584..7d8f7f2 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlLeadLagAggFunction.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlLeadLagAggFunction.java
@@ -20,6 +20,7 @@ import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.sql.SqlAggFunction;
 import org.apache.calcite.sql.SqlFunctionCategory;
 import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlOperatorBinding;
 import org.apache.calcite.sql.type.OperandTypes;
 import org.apache.calcite.sql.type.ReturnTypes;
 import org.apache.calcite.sql.type.SameOperandTypeChecker;
@@ -56,19 +57,7 @@ public class SqlLeadLagAggFunction extends SqlAggFunction {
               }));
 
   private static final SqlReturnTypeInference RETURN_TYPE =
-      ReturnTypes.cascade(ReturnTypes.ARG0, (binding, type) -> {
-        // Result is NOT NULL if NOT NULL default value is provided
-        SqlTypeTransform transform;
-        if (binding.getOperandCount() < 3) {
-          transform = SqlTypeTransforms.FORCE_NULLABLE;
-        } else {
-          RelDataType defValueType = binding.getOperandType(2);
-          transform = defValueType.isNullable()
-              ? SqlTypeTransforms.FORCE_NULLABLE
-              : SqlTypeTransforms.TO_NOT_NULLABLE;
-        }
-        return transform.transformType(binding, type);
-      });
+      ReturnTypes.ARG0.andThen(SqlLeadLagAggFunction::transformType);
 
   public SqlLeadLagAggFunction(SqlKind kind) {
     super(kind.name(),
@@ -90,6 +79,16 @@ public class SqlLeadLagAggFunction extends SqlAggFunction {
     this(isLead ? SqlKind.LEAD : SqlKind.LAG);
   }
 
+  // Result is NOT NULL if NOT NULL default value is provided
+  private static RelDataType transformType(SqlOperatorBinding binding,
+      RelDataType type) {
+    SqlTypeTransform transform =
+        binding.getOperandCount() < 3 || binding.getOperandType(2).isNullable()
+            ? SqlTypeTransforms.FORCE_NULLABLE
+            : SqlTypeTransforms.TO_NOT_NULLABLE;
+    return transform.transformType(binding, type);
+  }
+
   @Override public boolean allowsFraming() {
     return false;
   }
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 f23c884..d578496 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
@@ -91,24 +91,24 @@ public abstract class SqlLibraryOperators {
   @LibraryOperator(libraries = {ORACLE})
   public static final SqlFunction NVL =
       new SqlFunction("NVL", SqlKind.NVL,
-          ReturnTypes.cascade(ReturnTypes.LEAST_RESTRICTIVE,
-              SqlTypeTransforms.TO_NULLABLE_ALL),
+          ReturnTypes.LEAST_RESTRICTIVE
+              .andThen(SqlTypeTransforms.TO_NULLABLE_ALL),
           null, OperandTypes.SAME_SAME, SqlFunctionCategory.SYSTEM);
 
   /** The "LTRIM(string)" function. */
   @LibraryOperator(libraries = {ORACLE})
   public static final SqlFunction LTRIM =
       new SqlFunction("LTRIM", SqlKind.LTRIM,
-          ReturnTypes.cascade(ReturnTypes.ARG0, SqlTypeTransforms.TO_NULLABLE,
-              SqlTypeTransforms.TO_VARYING), null,
+          ReturnTypes.ARG0.andThen(SqlTypeTransforms.TO_NULLABLE)
+              .andThen(SqlTypeTransforms.TO_VARYING), null,
           OperandTypes.STRING, SqlFunctionCategory.STRING);
 
   /** The "RTRIM(string)" function. */
   @LibraryOperator(libraries = {ORACLE})
   public static final SqlFunction RTRIM =
       new SqlFunction("RTRIM", SqlKind.RTRIM,
-          ReturnTypes.cascade(ReturnTypes.ARG0, SqlTypeTransforms.TO_NULLABLE,
-              SqlTypeTransforms.TO_VARYING), null,
+          ReturnTypes.ARG0.andThen(SqlTypeTransforms.TO_NULLABLE)
+              .andThen(SqlTypeTransforms.TO_VARYING), null,
           OperandTypes.STRING, SqlFunctionCategory.STRING);
 
   /** Oracle's "SUBSTR(string, position [, substringLength ])" function.
@@ -125,17 +125,15 @@ public abstract class SqlLibraryOperators {
   @LibraryOperator(libraries = {ORACLE})
   public static final SqlFunction GREATEST =
       new SqlFunction("GREATEST", SqlKind.GREATEST,
-          ReturnTypes.cascade(ReturnTypes.LEAST_RESTRICTIVE,
-              SqlTypeTransforms.TO_NULLABLE), null,
-          OperandTypes.SAME_VARIADIC, SqlFunctionCategory.SYSTEM);
+          ReturnTypes.LEAST_RESTRICTIVE.andThen(SqlTypeTransforms.TO_NULLABLE),
+          null, OperandTypes.SAME_VARIADIC, SqlFunctionCategory.SYSTEM);
 
   /** The "LEAST(value, value)" function. */
   @LibraryOperator(libraries = {ORACLE})
   public static final SqlFunction LEAST =
       new SqlFunction("LEAST", SqlKind.LEAST,
-          ReturnTypes.cascade(ReturnTypes.LEAST_RESTRICTIVE,
-              SqlTypeTransforms.TO_NULLABLE), null,
-          OperandTypes.SAME_VARIADIC, SqlFunctionCategory.SYSTEM);
+          ReturnTypes.LEAST_RESTRICTIVE.andThen(SqlTypeTransforms.TO_NULLABLE),
+          null, OperandTypes.SAME_VARIADIC, SqlFunctionCategory.SYSTEM);
 
   /**
    * The <code>TRANSLATE(<i>string_expr</i>, <i>search_chars</i>,
@@ -174,34 +172,38 @@ public abstract class SqlLibraryOperators {
   public static final SqlFunction REGEXP_REPLACE = new SqlRegexpReplaceFunction();
 
   @LibraryOperator(libraries = {MYSQL})
-  public static final SqlFunction COMPRESS = new SqlFunction("COMPRESS", SqlKind.OTHER_FUNCTION,
-      ReturnTypes.cascade(ReturnTypes.explicit(SqlTypeName.VARBINARY),
-          SqlTypeTransforms.TO_NULLABLE), null, OperandTypes.STRING, SqlFunctionCategory.STRING);
+  public static final SqlFunction COMPRESS =
+      new SqlFunction("COMPRESS", SqlKind.OTHER_FUNCTION,
+          ReturnTypes.explicit(SqlTypeName.VARBINARY)
+              .andThen(SqlTypeTransforms.TO_NULLABLE),
+          null, OperandTypes.STRING, SqlFunctionCategory.STRING);
 
 
   @LibraryOperator(libraries = {MYSQL})
-  public static final SqlFunction EXTRACT_VALUE = new SqlFunction(
-      "EXTRACTVALUE", SqlKind.OTHER_FUNCTION,
-      ReturnTypes.cascade(ReturnTypes.VARCHAR_2000, SqlTypeTransforms.FORCE_NULLABLE),
-      null, OperandTypes.STRING_STRING, SqlFunctionCategory.SYSTEM);
+  public static final SqlFunction EXTRACT_VALUE =
+      new SqlFunction("EXTRACTVALUE", SqlKind.OTHER_FUNCTION,
+          ReturnTypes.VARCHAR_2000.andThen(SqlTypeTransforms.FORCE_NULLABLE),
+          null, OperandTypes.STRING_STRING, SqlFunctionCategory.SYSTEM);
 
   @LibraryOperator(libraries = {ORACLE})
-  public static final SqlFunction XML_TRANSFORM = new SqlFunction(
-      "XMLTRANSFORM", SqlKind.OTHER_FUNCTION,
-      ReturnTypes.cascade(ReturnTypes.VARCHAR_2000, SqlTypeTransforms.FORCE_NULLABLE),
-      null, OperandTypes.STRING_STRING, SqlFunctionCategory.SYSTEM);
+  public static final SqlFunction XML_TRANSFORM =
+      new SqlFunction("XMLTRANSFORM", SqlKind.OTHER_FUNCTION,
+          ReturnTypes.VARCHAR_2000.andThen(SqlTypeTransforms.FORCE_NULLABLE),
+          null, OperandTypes.STRING_STRING, SqlFunctionCategory.SYSTEM);
 
   @LibraryOperator(libraries = {ORACLE})
-  public static final SqlFunction EXTRACT_XML = new SqlFunction(
-      "EXTRACT", SqlKind.OTHER_FUNCTION,
-      ReturnTypes.cascade(ReturnTypes.VARCHAR_2000, SqlTypeTransforms.FORCE_NULLABLE),
-      null, OperandTypes.STRING_STRING_OPTIONAL_STRING, SqlFunctionCategory.SYSTEM);
+  public static final SqlFunction EXTRACT_XML =
+      new SqlFunction("EXTRACT", SqlKind.OTHER_FUNCTION,
+          ReturnTypes.VARCHAR_2000.andThen(SqlTypeTransforms.FORCE_NULLABLE),
+          null, OperandTypes.STRING_STRING_OPTIONAL_STRING,
+          SqlFunctionCategory.SYSTEM);
 
   @LibraryOperator(libraries = {ORACLE})
-  public static final SqlFunction EXISTS_NODE = new SqlFunction(
-      "EXISTSNODE", SqlKind.OTHER_FUNCTION,
-      ReturnTypes.cascade(ReturnTypes.INTEGER_NULLABLE, SqlTypeTransforms.FORCE_NULLABLE),
-      null, OperandTypes.STRING_STRING_OPTIONAL_STRING, SqlFunctionCategory.SYSTEM);
+  public static final SqlFunction EXISTS_NODE =
+      new SqlFunction("EXISTSNODE", SqlKind.OTHER_FUNCTION,
+          ReturnTypes.INTEGER_NULLABLE
+              .andThen(SqlTypeTransforms.FORCE_NULLABLE), null,
+          OperandTypes.STRING_STRING_OPTIONAL_STRING, SqlFunctionCategory.SYSTEM);
 
   /** The "DATE(string)" function, equivalent to "CAST(string AS DATE). */
   @LibraryOperator(libraries = {BIG_QUERY})
@@ -334,22 +336,17 @@ public abstract class SqlLibraryOperators {
 
   @LibraryOperator(libraries = {MYSQL})
   public static final SqlFunction FROM_BASE64 =
-      new SqlFunction("FROM_BASE64",
-          SqlKind.OTHER_FUNCTION,
-          ReturnTypes.cascade(ReturnTypes.explicit(SqlTypeName.VARBINARY),
-              SqlTypeTransforms.TO_NULLABLE),
-          null,
-          OperandTypes.STRING,
-          SqlFunctionCategory.STRING);
+      new SqlFunction("FROM_BASE64", SqlKind.OTHER_FUNCTION,
+          ReturnTypes.explicit(SqlTypeName.VARBINARY)
+              .andThen(SqlTypeTransforms.TO_NULLABLE),
+          null, OperandTypes.STRING, SqlFunctionCategory.STRING);
 
   @LibraryOperator(libraries = {MYSQL})
   public static final SqlFunction TO_BASE64 =
-      new SqlFunction("TO_BASE64",
-          SqlKind.OTHER_FUNCTION,
-          ReturnTypes.cascade(ReturnTypes.explicit(SqlTypeName.VARCHAR),
-              SqlTypeTransforms.TO_NULLABLE),
-          null,
-          OperandTypes.or(OperandTypes.STRING, OperandTypes.BINARY),
+      new SqlFunction("TO_BASE64", SqlKind.OTHER_FUNCTION,
+          ReturnTypes.explicit(SqlTypeName.VARCHAR)
+              .andThen(SqlTypeTransforms.TO_NULLABLE),
+          null, OperandTypes.or(OperandTypes.STRING, OperandTypes.BINARY),
           SqlFunctionCategory.STRING);
 
   /** The "TO_DATE(string1, string2)" function; casts string1
@@ -460,22 +457,18 @@ public abstract class SqlLibraryOperators {
 
   @LibraryOperator(libraries = {MYSQL, POSTGRESQL})
   public static final SqlFunction MD5 =
-      new SqlFunction("MD5",
-          SqlKind.OTHER_FUNCTION,
-          ReturnTypes.cascade(ReturnTypes.explicit(SqlTypeName.VARCHAR),
-              SqlTypeTransforms.TO_NULLABLE),
-          null,
-          OperandTypes.or(OperandTypes.STRING, OperandTypes.BINARY),
+      new SqlFunction("MD5", SqlKind.OTHER_FUNCTION,
+          ReturnTypes.explicit(SqlTypeName.VARCHAR)
+              .andThen(SqlTypeTransforms.TO_NULLABLE),
+          null, OperandTypes.or(OperandTypes.STRING, OperandTypes.BINARY),
           SqlFunctionCategory.STRING);
 
   @LibraryOperator(libraries = {MYSQL, POSTGRESQL})
   public static final SqlFunction SHA1 =
-      new SqlFunction("SHA1",
-          SqlKind.OTHER_FUNCTION,
-          ReturnTypes.cascade(ReturnTypes.explicit(SqlTypeName.VARCHAR),
-              SqlTypeTransforms.TO_NULLABLE),
-          null,
-          OperandTypes.or(OperandTypes.STRING, OperandTypes.BINARY),
+      new SqlFunction("SHA1", SqlKind.OTHER_FUNCTION,
+          ReturnTypes.explicit(SqlTypeName.VARCHAR)
+              .andThen(SqlTypeTransforms.TO_NULLABLE),
+          null, OperandTypes.or(OperandTypes.STRING, OperandTypes.BINARY),
           SqlFunctionCategory.STRING);
 
   /** Infix "::" cast operator used by PostgreSQL, for example
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlRegexpReplaceFunction.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlRegexpReplaceFunction.java
index 7ee4d42..777e184 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlRegexpReplaceFunction.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlRegexpReplaceFunction.java
@@ -39,20 +39,18 @@ import java.util.List;
 public class SqlRegexpReplaceFunction extends SqlFunction {
 
   public SqlRegexpReplaceFunction() {
-    super("REGEXP_REPLACE",
-        SqlKind.OTHER_FUNCTION,
-        ReturnTypes.cascade(ReturnTypes.explicit(SqlTypeName.VARCHAR),
-            SqlTypeTransforms.TO_NULLABLE),
-        null,
-        null,
-        SqlFunctionCategory.STRING);
+    super("REGEXP_REPLACE", SqlKind.OTHER_FUNCTION,
+        ReturnTypes.explicit(SqlTypeName.VARCHAR)
+            .andThen(SqlTypeTransforms.TO_NULLABLE),
+        null, null, SqlFunctionCategory.STRING);
   }
 
   @Override public SqlOperandCountRange getOperandCountRange() {
     return SqlOperandCountRanges.between(3, 6);
   }
 
-  @Override public boolean checkOperandTypes(SqlCallBinding callBinding, boolean throwOnFailure) {
+  @Override public boolean checkOperandTypes(SqlCallBinding callBinding,
+      boolean throwOnFailure) {
     final int operandCount = callBinding.getOperandCount();
     assert operandCount >= 3;
     if (operandCount == 3) {
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlTrimFunction.java b/core/src/main/java/org/apache/calcite/sql/fun/SqlTrimFunction.java
index 1702659..aff3235 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlTrimFunction.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlTrimFunction.java
@@ -29,9 +29,9 @@ import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.sql.type.OperandTypes;
 import org.apache.calcite.sql.type.ReturnTypes;
 import org.apache.calcite.sql.type.SameOperandTypeChecker;
+import org.apache.calcite.sql.type.SqlReturnTypeInference;
 import org.apache.calcite.sql.type.SqlSingleOperandTypeChecker;
 import org.apache.calcite.sql.type.SqlTypeFamily;
-import org.apache.calcite.sql.type.SqlTypeTransformCascade;
 import org.apache.calcite.sql.type.SqlTypeTransforms;
 import org.apache.calcite.sql.type.SqlTypeUtil;
 
@@ -46,8 +46,8 @@ import java.util.List;
 public class SqlTrimFunction extends SqlFunction {
   protected static final SqlTrimFunction INSTANCE =
       new SqlTrimFunction("TRIM", SqlKind.TRIM,
-          ReturnTypes.cascade(ReturnTypes.ARG2, SqlTypeTransforms.TO_NULLABLE,
-              SqlTypeTransforms.TO_VARYING),
+          ReturnTypes.ARG2.andThen(SqlTypeTransforms.TO_NULLABLE)
+              .andThen(SqlTypeTransforms.TO_VARYING),
           OperandTypes.and(
               OperandTypes.family(SqlTypeFamily.ANY, SqlTypeFamily.STRING,
                   SqlTypeFamily.STRING),
@@ -87,7 +87,7 @@ public class SqlTrimFunction extends SqlFunction {
   //~ Constructors -----------------------------------------------------------
 
   public SqlTrimFunction(String name, SqlKind kind,
-      SqlTypeTransformCascade returnTypeInference,
+      SqlReturnTypeInference returnTypeInference,
       SqlSingleOperandTypeChecker operandTypeChecker) {
     super(name, kind, returnTypeInference, null, operandTypeChecker,
         SqlFunctionCategory.STRING);
diff --git a/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java b/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java
index 7c76f65..98bfcb3 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java
@@ -43,13 +43,19 @@ public abstract class ReturnTypes {
   private ReturnTypes() {
   }
 
+  /** Creates a return-type inference that applies a rule then a sequence of
+   * rules, returning the first non-null result.
+   *
+   * @see SqlReturnTypeInference#orElse(SqlReturnTypeInference) */
   public static SqlReturnTypeInferenceChain chain(
       SqlReturnTypeInference... rules) {
     return new SqlReturnTypeInferenceChain(rules);
   }
 
   /** Creates a return-type inference that applies a rule then a sequence of
-   * transforms. */
+   * transforms.
+   *
+   * @see SqlReturnTypeInference#andThen(SqlTypeTransform) */
   public static SqlTypeTransformCascade cascade(SqlReturnTypeInference rule,
       SqlTypeTransform... transforms) {
     return new SqlTypeTransformCascade(rule, transforms);
@@ -90,6 +96,7 @@ public abstract class ReturnTypes {
    */
   public static final SqlReturnTypeInference ARG0 =
       new OrdinalReturnTypeInference(0);
+
   /**
    * Type-inference strategy whereby the result type of a call is VARYING the
    * type of the first argument. The length returned is the same as length of
@@ -97,8 +104,8 @@ public abstract class ReturnTypes {
    * returned type will also be nullable. First Arg must be of string type.
    */
   public static final SqlReturnTypeInference ARG0_NULLABLE_VARYING =
-      cascade(ARG0, SqlTypeTransforms.TO_NULLABLE,
-          SqlTypeTransforms.TO_VARYING);
+      ARG0.andThen(SqlTypeTransforms.TO_NULLABLE)
+          .andThen(SqlTypeTransforms.TO_VARYING);
 
   /**
    * Type-inference strategy whereby the result type of a call is the type of
@@ -106,21 +113,21 @@ public abstract class ReturnTypes {
    * returned type will also be nullable.
    */
   public static final SqlReturnTypeInference ARG0_NULLABLE =
-      cascade(ARG0, SqlTypeTransforms.TO_NULLABLE);
+      ARG0.andThen(SqlTypeTransforms.TO_NULLABLE);
 
   /**
    * Type-inference strategy whereby the result type of a call is the type of
    * the operand #0 (0-based), with nulls always allowed.
    */
   public static final SqlReturnTypeInference ARG0_FORCE_NULLABLE =
-      cascade(ARG0, SqlTypeTransforms.FORCE_NULLABLE);
+      ARG0.andThen(SqlTypeTransforms.FORCE_NULLABLE);
 
   public static final SqlReturnTypeInference ARG0_INTERVAL =
       new MatchReturnTypeInference(0,
           SqlTypeFamily.DATETIME_INTERVAL.getTypeNames());
 
   public static final SqlReturnTypeInference ARG0_INTERVAL_NULLABLE =
-      cascade(ARG0_INTERVAL, SqlTypeTransforms.TO_NULLABLE);
+      ARG0_INTERVAL.andThen(SqlTypeTransforms.TO_NULLABLE);
 
   /**
    * Type-inference strategy whereby the result type of a call is the type of
@@ -148,26 +155,30 @@ public abstract class ReturnTypes {
    */
   public static final SqlReturnTypeInference ARG1 =
       new OrdinalReturnTypeInference(1);
+
   /**
    * Type-inference strategy whereby the result type of a call is the type of
    * the operand #1 (0-based). If any of the other operands are nullable the
    * returned type will also be nullable.
    */
   public static final SqlReturnTypeInference ARG1_NULLABLE =
-      cascade(ARG1, SqlTypeTransforms.TO_NULLABLE);
+      ARG1.andThen(SqlTypeTransforms.TO_NULLABLE);
+
   /**
    * Type-inference strategy whereby the result type of a call is the type of
    * operand #2 (0-based).
    */
   public static final SqlReturnTypeInference ARG2 =
       new OrdinalReturnTypeInference(2);
+
   /**
    * Type-inference strategy whereby the result type of a call is the type of
    * operand #2 (0-based). If any of the other operands are nullable the
    * returned type will also be nullable.
    */
   public static final SqlReturnTypeInference ARG2_NULLABLE =
-      cascade(ARG2, SqlTypeTransforms.TO_NULLABLE);
+      ARG2.andThen(SqlTypeTransforms.TO_NULLABLE);
+
   /**
    * Type-inference strategy whereby the result type of a call is Boolean.
    */
@@ -178,7 +189,7 @@ public abstract class ReturnTypes {
    * with nulls allowed if any of the operands allow nulls.
    */
   public static final SqlReturnTypeInference BOOLEAN_NULLABLE =
-      cascade(BOOLEAN, SqlTypeTransforms.TO_NULLABLE);
+      BOOLEAN.andThen(SqlTypeTransforms.TO_NULLABLE);
 
   /**
    * Type-inference strategy with similar effect to {@link #BOOLEAN_NULLABLE},
@@ -207,14 +218,14 @@ public abstract class ReturnTypes {
    * Boolean.
    */
   public static final SqlReturnTypeInference BOOLEAN_FORCE_NULLABLE =
-      cascade(BOOLEAN, SqlTypeTransforms.FORCE_NULLABLE);
+      BOOLEAN.andThen(SqlTypeTransforms.FORCE_NULLABLE);
 
   /**
    * Type-inference strategy whereby the result type of a call is BOOLEAN
    * NOT NULL.
    */
   public static final SqlReturnTypeInference BOOLEAN_NOT_NULL =
-      cascade(BOOLEAN, SqlTypeTransforms.TO_NOT_NULLABLE);
+      BOOLEAN.andThen(SqlTypeTransforms.TO_NOT_NULLABLE);
 
   /**
    * Type-inference strategy whereby the result type of a call is DATE.
@@ -227,7 +238,7 @@ public abstract class ReturnTypes {
    * DATE.
    */
   public static final SqlReturnTypeInference DATE_NULLABLE =
-      cascade(DATE, SqlTypeTransforms.TO_NULLABLE);
+      DATE.andThen(SqlTypeTransforms.TO_NULLABLE);
 
   /**
    * Type-inference strategy whereby the result type of a call is TIME(0).
@@ -240,7 +251,7 @@ public abstract class ReturnTypes {
    * TIME(0).
    */
   public static final SqlReturnTypeInference TIME_NULLABLE =
-      cascade(TIME, SqlTypeTransforms.TO_NULLABLE);
+      TIME.andThen(SqlTypeTransforms.TO_NULLABLE);
 
   /**
    * Type-inference strategy whereby the result type of a call is TIMESTAMP.
@@ -253,19 +264,20 @@ public abstract class ReturnTypes {
    * TIMESTAMP.
    */
   public static final SqlReturnTypeInference TIMESTAMP_NULLABLE =
-      cascade(TIMESTAMP, SqlTypeTransforms.TO_NULLABLE);
+      TIMESTAMP.andThen(SqlTypeTransforms.TO_NULLABLE);
 
   /**
    * Type-inference strategy whereby the result type of a call is Double.
    */
   public static final SqlReturnTypeInference DOUBLE =
       explicit(SqlTypeName.DOUBLE);
+
   /**
    * Type-inference strategy whereby the result type of a call is Double with
    * nulls allowed if any of the operands allow nulls.
    */
   public static final SqlReturnTypeInference DOUBLE_NULLABLE =
-      cascade(DOUBLE, SqlTypeTransforms.TO_NULLABLE);
+      DOUBLE.andThen(SqlTypeTransforms.TO_NULLABLE);
 
   /**
    * Type-inference strategy whereby the result type of a call is a Char.
@@ -284,7 +296,7 @@ public abstract class ReturnTypes {
    * with nulls allowed if any of the operands allow nulls.
    */
   public static final SqlReturnTypeInference INTEGER_NULLABLE =
-      cascade(INTEGER, SqlTypeTransforms.TO_NULLABLE);
+      INTEGER.andThen(SqlTypeTransforms.TO_NULLABLE);
 
   /**
    * Type-inference strategy whereby the result type of a call is a BIGINT.
@@ -297,13 +309,14 @@ public abstract class ReturnTypes {
    * BIGINT.
    */
   public static final SqlReturnTypeInference BIGINT_FORCE_NULLABLE =
-      cascade(BIGINT, SqlTypeTransforms.FORCE_NULLABLE);
+      BIGINT.andThen(SqlTypeTransforms.FORCE_NULLABLE);
+
   /**
-   * Type-inference strategy whereby the result type of a call is an Bigint
+   * Type-inference strategy whereby the result type of a call is a BIGINT
    * with nulls allowed if any of the operands allow nulls.
    */
   public static final SqlReturnTypeInference BIGINT_NULLABLE =
-      cascade(BIGINT, SqlTypeTransforms.TO_NULLABLE);
+      BIGINT.andThen(SqlTypeTransforms.TO_NULLABLE);
 
   /**
    * Type-inference strategy that always returns "VARCHAR(4)".
@@ -316,7 +329,7 @@ public abstract class ReturnTypes {
    * allowed if any of the operands allow nulls.
    */
   public static final SqlReturnTypeInference VARCHAR_4_NULLABLE =
-      cascade(VARCHAR_4, SqlTypeTransforms.TO_NULLABLE);
+      VARCHAR_4.andThen(SqlTypeTransforms.TO_NULLABLE);
 
   /**
    * Type-inference strategy that always returns "VARCHAR(2000)".
@@ -329,7 +342,7 @@ public abstract class ReturnTypes {
    * allowed if any of the operands allow nulls.
    */
   public static final SqlReturnTypeInference VARCHAR_2000_NULLABLE =
-      cascade(VARCHAR_2000, SqlTypeTransforms.TO_NULLABLE);
+      VARCHAR_2000.andThen(SqlTypeTransforms.TO_NULLABLE);
 
   /**
    * Type-inference strategy for Histogram agg support.
@@ -348,6 +361,7 @@ public abstract class ReturnTypes {
    */
   public static final SqlReturnTypeInference COLUMN_LIST =
       explicit(SqlTypeName.COLUMN_LIST);
+
   /**
    * Type-inference strategy whereby the result type of a call is using its
    * operands biggest type, using the SQL:1999 rules described in "Data types
@@ -359,6 +373,7 @@ public abstract class ReturnTypes {
   public static final SqlReturnTypeInference LEAST_RESTRICTIVE =
       opBinding -> opBinding.getTypeFactory().leastRestrictive(
           opBinding.collectOperandTypes());
+
   /**
    * Returns the same type as the multiset carries. The multiset type returned
    * is the least restrictive of the call's multiset operands
@@ -395,20 +410,20 @@ public abstract class ReturnTypes {
    * <code>INTEGER MULTISET</code>.
    */
   public static final SqlReturnTypeInference TO_MULTISET =
-      cascade(ARG0, SqlTypeTransforms.TO_MULTISET);
+      ARG0.andThen(SqlTypeTransforms.TO_MULTISET);
 
   /**
    * Returns the element type of a MULTISET.
    */
   public static final SqlReturnTypeInference MULTISET_ELEMENT_NULLABLE =
-      cascade(MULTISET, SqlTypeTransforms.TO_MULTISET_ELEMENT_TYPE);
+      MULTISET.andThen(SqlTypeTransforms.TO_MULTISET_ELEMENT_TYPE);
 
   /**
    * Same as {@link #MULTISET} but returns with nullability if any of the
    * operands is nullable.
    */
   public static final SqlReturnTypeInference MULTISET_NULLABLE =
-      cascade(MULTISET, SqlTypeTransforms.TO_NULLABLE);
+      MULTISET.andThen(SqlTypeTransforms.TO_NULLABLE);
 
   /**
    * Returns the type of the only column of a multiset.
@@ -417,7 +432,7 @@ public abstract class ReturnTypes {
    * <code>INTEGER MULTISET</code>.
    */
   public static final SqlReturnTypeInference MULTISET_PROJECT_ONLY =
-      cascade(MULTISET, SqlTypeTransforms.ONLY_COLUMN);
+      MULTISET.andThen(SqlTypeTransforms.ONLY_COLUMN);
 
   /**
    * Type-inference strategy whereby the result type of a call is
@@ -425,7 +440,7 @@ public abstract class ReturnTypes {
    * are used for integer division.
    */
   public static final SqlReturnTypeInference INTEGER_QUOTIENT_NULLABLE =
-      chain(ARG0_INTERVAL_NULLABLE, LEAST_RESTRICTIVE);
+      ARG0_INTERVAL_NULLABLE.orElse(LEAST_RESTRICTIVE);
 
   /**
    * Type-inference strategy for a call where the first argument is a decimal.
@@ -462,7 +477,7 @@ public abstract class ReturnTypes {
    * is used for floor, ceiling.
    */
   public static final SqlReturnTypeInference ARG0_OR_EXACT_NO_SCALE =
-      chain(DECIMAL_SCALE0, ARG0);
+      DECIMAL_SCALE0.orElse(ARG0);
 
   /**
    * Type-inference strategy whereby the result type of a call is the decimal
@@ -482,7 +497,7 @@ public abstract class ReturnTypes {
    * {@link org.apache.calcite.sql.type.SqlTypeTransforms#TO_NULLABLE}.
    */
   public static final SqlReturnTypeInference DECIMAL_PRODUCT_NULLABLE =
-      cascade(DECIMAL_PRODUCT, SqlTypeTransforms.TO_NULLABLE);
+      DECIMAL_PRODUCT.andThen(SqlTypeTransforms.TO_NULLABLE);
 
   /**
    * Type-inference strategy whereby the result type of a call is
@@ -492,8 +507,8 @@ public abstract class ReturnTypes {
    * These rules are used for multiplication.
    */
   public static final SqlReturnTypeInference PRODUCT_NULLABLE =
-      chain(DECIMAL_PRODUCT_NULLABLE, ARG0_INTERVAL_NULLABLE,
-          LEAST_RESTRICTIVE);
+      DECIMAL_PRODUCT_NULLABLE.orElse(ARG0_INTERVAL_NULLABLE)
+          .orElse(LEAST_RESTRICTIVE);
 
   /**
    * Type-inference strategy whereby the result type of a call is the decimal
@@ -513,7 +528,7 @@ public abstract class ReturnTypes {
    * {@link org.apache.calcite.sql.type.SqlTypeTransforms#TO_NULLABLE}.
    */
   public static final SqlReturnTypeInference DECIMAL_QUOTIENT_NULLABLE =
-      cascade(DECIMAL_QUOTIENT, SqlTypeTransforms.TO_NULLABLE);
+      DECIMAL_QUOTIENT.andThen(SqlTypeTransforms.TO_NULLABLE);
 
   /**
    * Type-inference strategy whereby the result type of a call is
@@ -522,8 +537,8 @@ public abstract class ReturnTypes {
    * are used for division.
    */
   public static final SqlReturnTypeInference QUOTIENT_NULLABLE =
-      chain(DECIMAL_QUOTIENT_NULLABLE, ARG0_INTERVAL_NULLABLE,
-          LEAST_RESTRICTIVE);
+      DECIMAL_QUOTIENT_NULLABLE.orElse(ARG0_INTERVAL_NULLABLE)
+          .orElse(LEAST_RESTRICTIVE);
 
   /**
    * Type-inference strategy whereby the result type of a call is the decimal
@@ -543,7 +558,7 @@ public abstract class ReturnTypes {
    * {@link org.apache.calcite.sql.type.SqlTypeTransforms#TO_NULLABLE}.
    */
   public static final SqlReturnTypeInference DECIMAL_SUM_NULLABLE =
-      cascade(DECIMAL_SUM, SqlTypeTransforms.TO_NULLABLE);
+      DECIMAL_SUM.andThen(SqlTypeTransforms.TO_NULLABLE);
 
   /**
    * Type-inference strategy whereby the result type of a call is
@@ -566,14 +581,15 @@ public abstract class ReturnTypes {
    * decimal.
    */
   public static final SqlReturnTypeInference DECIMAL_MOD_NULLABLE =
-          cascade(DECIMAL_MOD, SqlTypeTransforms.TO_NULLABLE);
+      DECIMAL_MOD.andThen(SqlTypeTransforms.TO_NULLABLE);
+
   /**
    * Type-inference strategy whereby the result type of a call is
    * {@link #DECIMAL_MOD_NULLABLE} with a fallback to {@link #ARG1_NULLABLE}
    * These rules are used for modulus.
    */
   public static final SqlReturnTypeInference NULLABLE_MOD =
-          chain(DECIMAL_MOD_NULLABLE, ARG1_NULLABLE);
+      DECIMAL_MOD_NULLABLE.orElse(ARG1_NULLABLE);
 
   /**
    * Type-inference strategy for concatenating two string arguments. The result
@@ -674,7 +690,6 @@ public abstract class ReturnTypes {
         return ret;
       };
 
-
   /**
    * Type-inference strategy for String concatenation.
    * Result is varying if either input is; otherwise fixed.
@@ -725,7 +740,7 @@ public abstract class ReturnTypes {
    * {@link org.apache.calcite.sql.type.SqlTypeTransforms#TO_NULLABLE}.
    */
   public static final SqlReturnTypeInference MULTIVALENT_STRING_SUM_PRECISION_NULLABLE =
-      cascade(MULTIVALENT_STRING_SUM_PRECISION, SqlTypeTransforms.TO_NULLABLE);
+      MULTIVALENT_STRING_SUM_PRECISION.andThen(SqlTypeTransforms.TO_NULLABLE);
 
   /**
    * Same as {@link #DYADIC_STRING_SUM_PRECISION} and using
@@ -733,15 +748,15 @@ public abstract class ReturnTypes {
    * {@link org.apache.calcite.sql.type.SqlTypeTransforms#TO_VARYING}.
    */
   public static final SqlReturnTypeInference DYADIC_STRING_SUM_PRECISION_NULLABLE_VARYING =
-      cascade(DYADIC_STRING_SUM_PRECISION, SqlTypeTransforms.TO_NULLABLE,
-          SqlTypeTransforms.TO_VARYING);
+      DYADIC_STRING_SUM_PRECISION.andThen(SqlTypeTransforms.TO_NULLABLE)
+          .andThen(SqlTypeTransforms.TO_VARYING);
 
   /**
    * Same as {@link #DYADIC_STRING_SUM_PRECISION} and using
    * {@link org.apache.calcite.sql.type.SqlTypeTransforms#TO_NULLABLE}.
    */
   public static final SqlReturnTypeInference DYADIC_STRING_SUM_PRECISION_NULLABLE =
-      cascade(DYADIC_STRING_SUM_PRECISION, SqlTypeTransforms.TO_NULLABLE);
+      DYADIC_STRING_SUM_PRECISION.andThen(SqlTypeTransforms.TO_NULLABLE);
 
   /**
    * Type-inference strategy where the expression is assumed to be registered
@@ -775,6 +790,7 @@ public abstract class ReturnTypes {
         firstColType,
         -1);
   };
+
   /**
    * Returns a multiset of the first column of a multiset. For example, given
    * <code>INTEGER MULTISET</code>, returns <code>RECORD(x INTEGER)
@@ -791,6 +807,7 @@ public abstract class ReturnTypes {
         .add(SqlUtil.deriveAliasFromOrdinal(0), componentType).build();
     return typeFactory.createMultisetType(type, -1);
   };
+
   /**
    * Returns the field type of a structured type which has only one field. For
    * example, given {@code RECORD(x INTEGER)} returns {@code INTEGER}.
diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlReturnTypeInference.java b/core/src/main/java/org/apache/calcite/sql/type/SqlReturnTypeInference.java
index 3e56728..c9e31d2 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SqlReturnTypeInference.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SqlReturnTypeInference.java
@@ -30,6 +30,7 @@ import org.apache.calcite.sql.SqlOperatorBinding;
  * sense because many operators have similar, straightforward strategies, such
  * as to take the type of the first operand.</p>
  */
+@FunctionalInterface
 public interface SqlReturnTypeInference {
   //~ Methods ----------------------------------------------------------------
 
@@ -41,4 +42,16 @@ public interface SqlReturnTypeInference {
    */
   RelDataType inferReturnType(
       SqlOperatorBinding opBinding);
+
+  /** Returns a return-type inference that applies this rule then a
+   * transform. */
+  default SqlReturnTypeInference andThen(SqlTypeTransform transform) {
+    return ReturnTypes.cascade(this, transform);
+  }
+
+  /** Returns a return-type inference that applies this rule then another
+   * rule, until one of them returns a not-null result. */
+  default SqlReturnTypeInference orElse(SqlReturnTypeInference transform) {
+    return ReturnTypes.chain(this, transform);
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlReturnTypeInferenceChain.java b/core/src/main/java/org/apache/calcite/sql/type/SqlReturnTypeInferenceChain.java
index a0db292..67163d9 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SqlReturnTypeInferenceChain.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SqlReturnTypeInferenceChain.java
@@ -19,6 +19,7 @@ package org.apache.calcite.sql.type;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.sql.SqlOperatorBinding;
 
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 
 /**
@@ -41,11 +42,7 @@ public class SqlReturnTypeInferenceChain implements SqlReturnTypeInference {
    * Use {@link org.apache.calcite.sql.type.ReturnTypes#chain}.</p>
    */
   SqlReturnTypeInferenceChain(SqlReturnTypeInference... rules) {
-    assert rules != null;
-    assert rules.length > 1;
-    for (SqlReturnTypeInference rule : rules) {
-      assert rule != null;
-    }
+    Preconditions.checkArgument(rules.length > 1);
     this.rules = ImmutableList.copyOf(rules);
   }
 
diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeTransformCascade.java b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeTransformCascade.java
index 8b60f48..474a0f0 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeTransformCascade.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeTransformCascade.java
@@ -19,8 +19,11 @@ package org.apache.calcite.sql.type;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.sql.SqlOperatorBinding;
 
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 
+import java.util.Objects;
+
 /**
  * Strategy to infer the type of an operator call from the type of the operands
  * by using one {@link SqlReturnTypeInference} rule and a combination of
@@ -41,9 +44,8 @@ public class SqlTypeTransformCascade implements SqlReturnTypeInference {
   public SqlTypeTransformCascade(
       SqlReturnTypeInference rule,
       SqlTypeTransform... transforms) {
-    assert rule != null;
-    assert transforms.length > 0;
-    this.rule = rule;
+    Preconditions.checkArgument(transforms.length > 0);
+    this.rule = Objects.requireNonNull(rule);
     this.transforms = ImmutableList.copyOf(transforms);
   }