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


The following commit(s) were added to refs/heads/master by this push:
     new 0327135  [CALCITE-4408] Implement Oracle SUBSTR function (James Starr)
0327135 is described below

commit 0327135376246eb58e1703fc13d555efaf389ada
Author: James Starr <[email protected]>
AuthorDate: Wed Nov 18 14:16:27 2020 -0800

    [CALCITE-4408] Implement Oracle SUBSTR function (James Starr)
    
    Add an operand type checker for the Oracle SUBSTR. Becauses
    semantics are different to standard SUBSTRING, implement by
    translating to SUBSTRING with a CASE expression to handle the
    differences.
    
    The implementation assumes that SUBSTRING is consistent with
    BigQuery, not ISO Standard SQL, and may need rework when
      [CALCITE-4427] Make SUBSTRING operator comply with ISO
      standard SQL
    is fixed.
    
    Rework so that it gives the right result for very negative
    start; add tests for MySQL and PostgreSQL variants of SUBSTR
    (Julian Hyde).
    
    Close apache/calcite#2271
---
 .../calcite/sql/dialect/OracleSqlDialect.java      |  3 +-
 .../calcite/sql/fun/OracleSqlOperatorTable.java    |  2 +-
 .../calcite/sql/fun/SqlLibraryOperators.java       | 19 ++++--
 .../org/apache/calcite/sql/type/OperandTypes.java  |  5 ++
 .../calcite/sql2rel/StandardConvertletTable.java   | 59 +++++++++++++++++
 .../calcite/sql/test/SqlOperatorBaseTest.java      | 73 ++++++++++++++++++++++
 6 files changed, 155 insertions(+), 6 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/sql/dialect/OracleSqlDialect.java 
b/core/src/main/java/org/apache/calcite/sql/dialect/OracleSqlDialect.java
index 7de88da..9b1f4e4 100644
--- a/core/src/main/java/org/apache/calcite/sql/dialect/OracleSqlDialect.java
+++ b/core/src/main/java/org/apache/calcite/sql/dialect/OracleSqlDialect.java
@@ -142,7 +142,8 @@ public class OracleSqlDialect extends SqlDialect {
   @Override public void unparseCall(SqlWriter writer, SqlCall call,
       int leftPrec, int rightPrec) {
     if (call.getOperator() == SqlStdOperatorTable.SUBSTRING) {
-      SqlUtil.unparseFunctionSyntax(SqlLibraryOperators.SUBSTR, writer, call, 
false);
+      SqlUtil.unparseFunctionSyntax(SqlLibraryOperators.ORACLE_SUBSTR, writer,
+          call, false);
     } else {
       switch (call.getKind()) {
       case FLOOR:
diff --git 
a/core/src/main/java/org/apache/calcite/sql/fun/OracleSqlOperatorTable.java 
b/core/src/main/java/org/apache/calcite/sql/fun/OracleSqlOperatorTable.java
index 6dae93b..d39a1ad 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/OracleSqlOperatorTable.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/OracleSqlOperatorTable.java
@@ -50,7 +50,7 @@ public class OracleSqlOperatorTable extends 
ReflectiveSqlOperatorTable {
   public static final SqlFunction RTRIM = SqlLibraryOperators.RTRIM;
 
   @Deprecated // to be removed before 2.0
-  public static final SqlFunction SUBSTR = SqlLibraryOperators.SUBSTR;
+  public static final SqlFunction SUBSTR = SqlLibraryOperators.ORACLE_SUBSTR;
 
   @Deprecated // to be removed before 2.0
   public static final SqlFunction GREATEST = SqlLibraryOperators.GREATEST;
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 2f8b4cb..f42e05f 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
@@ -157,12 +157,23 @@ public abstract class SqlLibraryOperators {
 
   /** Oracle's "SUBSTR(string, position [, substringLength ])" function.
    *
-   * <p>It has similar semantics to standard SQL's
-   * {@link SqlStdOperatorTable#SUBSTRING} function but different syntax. */
+   * <p>It has different semantics to standard SQL's
+   * {@link SqlStdOperatorTable#SUBSTRING} function:
+   *
+   * <ul>
+   *   <li>If {@code substringLength} &le; 0, result is the empty string
+   *   (Oracle would return null, because it treats the empty string as null,
+   *   but Calcite does not have these semantics);
+   *   <li>If {@code position} = 0, treat {@code position} as 1;
+   *   <li>If {@code position} &lt; 0, treat {@code position} as
+   *       "length(string) + position + 1".
+   * </ul>
+   */
   @LibraryOperator(libraries = {ORACLE})
-  public static final SqlFunction SUBSTR =
+  public static final SqlFunction ORACLE_SUBSTR =
       new SqlFunction("SUBSTR", SqlKind.OTHER_FUNCTION,
-          ReturnTypes.ARG0_NULLABLE_VARYING, null, null,
+          ReturnTypes.ARG0_NULLABLE_VARYING, null,
+          OperandTypes.STRING_INTEGER_OPTIONAL_INTEGER,
           SqlFunctionCategory.STRING);
 
   /** The "GREATEST(value, value)" function. */
diff --git a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java 
b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
index c78704a..4020de6 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
@@ -442,6 +442,11 @@ public abstract class OperandTypes {
   public static final SqlSingleOperandTypeChecker STRING_INTEGER =
       family(SqlTypeFamily.STRING, SqlTypeFamily.INTEGER);
 
+  public static final SqlSingleOperandTypeChecker 
STRING_INTEGER_OPTIONAL_INTEGER =
+      family(
+          ImmutableList.of(SqlTypeFamily.STRING, SqlTypeFamily.INTEGER,
+              SqlTypeFamily.INTEGER), i -> i == 2);
+
   /** Operand type-checking strategy where the first operand is a character or
    * binary string (CHAR, VARCHAR, BINARY or VARBINARY), and the second operand
    * is INTEGER. */
diff --git 
a/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java 
b/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java
index f5ed894..6eff2b4 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java
@@ -145,6 +145,7 @@ public class StandardConvertletTable extends 
ReflectiveConvertletTable {
 
     registerOp(SqlLibraryOperators.GREATEST, new GreatestConvertlet());
     registerOp(SqlLibraryOperators.LEAST, new GreatestConvertlet());
+    registerOp(SqlLibraryOperators.ORACLE_SUBSTR, new 
OracleSubstrConvertlet());
 
     registerOp(SqlLibraryOperators.NVL, StandardConvertletTable::convertNvl);
     registerOp(SqlLibraryOperators.DECODE,
@@ -1542,6 +1543,64 @@ public class StandardConvertletTable extends 
ReflectiveConvertletTable {
     }
   }
 
+  /** Convertlet that handles Oracle's {@code SUBSTR} function. */
+  private static class OracleSubstrConvertlet implements SqlRexConvertlet {
+    @Override public RexNode convertCall(SqlRexContext cx, SqlCall call) {
+      // Translate
+      //   SUBSTR(value, start, length)
+      // to
+      //   SUBSTRING(
+      //     value
+      //     FROM CASE
+      //          WHEN start = 0 THEN 1
+      //          WHEN start + length(value) < 0
+      //          THEN length(value) + 1
+      //          ELSE start)
+      //     FOR CASE WHEN length < 0 THEN 0 ELSE length END)
+
+      final RexBuilder rexBuilder = cx.getRexBuilder();
+      final List<RexNode> exprs =
+          convertOperands(cx, call, SqlOperandTypeChecker.Consistency.NONE);
+      final RexNode value = exprs.get(0);
+      final RexNode start = exprs.get(1);
+      final RelDataType startType = start.getType();
+      final RexNode zeroLiteral = rexBuilder.makeLiteral(0, startType, false);
+      final RexNode oneLiteral = rexBuilder.makeLiteral(1, startType, false);
+
+      final RexNode valueLength =
+          rexBuilder.makeCall(SqlStdOperatorTable.CHAR_LENGTH, value);
+      final RexNode newStart =
+          rexBuilder.makeCall(SqlStdOperatorTable.CASE,
+              rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, start,
+                  zeroLiteral),
+              oneLiteral,
+              rexBuilder.makeCall(SqlStdOperatorTable.LESS_THAN,
+                  rexBuilder.makeCall(SqlStdOperatorTable.PLUS, start,
+                      valueLength),
+                  zeroLiteral),
+              rexBuilder.makeCall(SqlStdOperatorTable.PLUS, valueLength,
+                  oneLiteral),
+              start);
+
+      switch (call.operandCount()) {
+      case 2:
+        return rexBuilder.makeCall(SqlStdOperatorTable.SUBSTRING, value,
+            newStart);
+      case 3:
+        final RexNode length = exprs.get(2);
+        final RexNode newLength =
+            rexBuilder.makeCall(SqlStdOperatorTable.CASE,
+                rexBuilder.makeCall(SqlStdOperatorTable.LESS_THAN, length,
+                    zeroLiteral),
+                zeroLiteral, length);
+        return rexBuilder.makeCall(SqlStdOperatorTable.SUBSTRING, value,
+            newStart, newLength);
+      default:
+        throw new AssertionError();
+      }
+    }
+  }
+
   /** Convertlet that handles the {@code TIMESTAMPADD} function. */
   private static class TimestampAddConvertlet implements SqlRexConvertlet {
     @Override public RexNode convertCall(SqlRexContext cx, SqlCall call) {
diff --git 
a/core/src/test/java/org/apache/calcite/sql/test/SqlOperatorBaseTest.java 
b/core/src/test/java/org/apache/calcite/sql/test/SqlOperatorBaseTest.java
index ab44440..a9899f1 100644
--- a/core/src/test/java/org/apache/calcite/sql/test/SqlOperatorBaseTest.java
+++ b/core/src/test/java/org/apache/calcite/sql/test/SqlOperatorBaseTest.java
@@ -65,6 +65,7 @@ import org.apache.calcite.util.trace.CalciteTrace;
 
 import com.google.common.base.Throwables;
 
+import org.checkerframework.checker.nullness.qual.Nullable;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Tag;
@@ -6713,6 +6714,78 @@ public abstract class SqlOperatorBaseTest {
     tester.checkNull("substring(cast(null as varchar(1)),1,2)");
   }
 
+  /** Tests the non-standard SUBSTR function, that has syntax
+   * "SUBSTR(value, start [, length ])", as used in Oracle. */
+  @Test void testOracleSubstrFunction() {
+    checkSubstrFunction(SqlLibrary.ORACLE);
+  }
+
+  void checkSubstrFunction(SqlLibrary library) {
+    SqlTester t = tester(library);
+    t.setFor(SqlLibraryOperators.ORACLE_SUBSTR);
+    // The following tests have been checked on Oracle 11g R2, PostgreSQL 9.6,
+    // MySQL 5.6. The SUBSTR function is currently only enabled in the Oracle
+    // library.
+    assertSubstrReturns(t, "abc", 1, "abc");
+    assertSubstrReturns(t, "abc", 2, "bc");
+    assertSubstrReturns(t, "abc", 3, "c");
+    assertSubstrReturns(t, "abc", 4, "");
+    assertSubstrReturns(t, "abc", 5, "");
+
+    switch (library) {
+    case ORACLE:
+      assertSubstrReturns(t, "abc", 0, "abc");
+      assertSubstrReturns(t, "abc", 0, 5, "abc");
+      assertSubstrReturns(t, "abc", 0, 4, "abc");
+      assertSubstrReturns(t, "abc", 0, 3, "abc");
+      assertSubstrReturns(t, "abc", 0, 2, "ab");
+      break;
+    case POSTGRESQL:
+      assertSubstrReturns(t, "abc", 0, "abc");
+      assertSubstrReturns(t, "abc", 0, 5, "abc");
+      assertSubstrReturns(t, "abc", 0, 4, "abc");
+      assertSubstrReturns(t, "abc", 0, 3, "ab");
+      assertSubstrReturns(t, "abc", 0, 2, "a");
+      break;
+    case MYSQL:
+      assertSubstrReturns(t, "abc", 0, "");
+      assertSubstrReturns(t, "abc", 0, 5, "");
+      assertSubstrReturns(t, "abc", 0, 4, "");
+      assertSubstrReturns(t, "abc", 0, 3, "");
+      assertSubstrReturns(t, "abc", 0, 2, "");
+      break;
+    }
+    assertSubstrReturns(t, "abc", 1, 2, "ab");
+    assertSubstrReturns(t, "abc", 1, 3, "abc");
+    assertSubstrReturns(t, "abc", 4, 3, "");
+    assertSubstrReturns(t, "abc", 4, 4, "");
+
+    assertSubstrReturns(t, "abc", 1, 0, "");
+    assertSubstrReturns(t, "abc", 1, -1, "");
+    assertSubstrReturns(t, "abc", 4, -1, "");
+    assertSubstrReturns(t, "abc", -2, "bc");
+    assertSubstrReturns(t, "abc", -1, "c");
+
+    assertSubstrReturns(t, "abc", -3, 3, "abc");
+    assertSubstrReturns(t, "abc", -3, 8, "abc");
+    assertSubstrReturns(t, "abc", -2, 3, "bc");
+    assertSubstrReturns(t, "abc", -1, 4, "c");
+    assertSubstrReturns(t, "abc", -4, 3, "");
+    assertSubstrReturns(t, "abc", -5, 1, "");
+    assertSubstrReturns(t, "abc", -500, 1, "");
+  }
+
+  void assertSubstrReturns(SqlTester t, String s, int start, String expected) {
+    assertSubstrReturns(t, s, start, null, expected);
+  }
+
+  void assertSubstrReturns(SqlTester t, String s, int start,
+      @Nullable Integer end, String expected) {
+    final String type = "VARCHAR(" + s.length() + ") NOT NULL";
+    t.checkString("substr(CAST('" + s + "' AS varchar(" + s.length() + ")), "
+        + start + (end == null ? "" : ", " + end) + ")", expected, type);
+  }
+
   @Test void testTrimFunc() {
     tester.setFor(SqlStdOperatorTable.TRIM);
 

Reply via email to