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

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


The following commit(s) were added to refs/heads/master by this push:
     new 34c55a0bde SQL: SUBSTRING support for non-literals. (#14480)
34c55a0bde is described below

commit 34c55a0bde701e18fbe064bfe574caedb752ff4f
Author: Gian Merlino <[email protected]>
AuthorDate: Wed Jun 28 13:43:05 2023 -0700

    SQL: SUBSTRING support for non-literals. (#14480)
    
    * SQL: SUBSTRING support for non-literals.
    
    * Fix AssertionError test.
    
    * Fix header.
---
 .../builtin/SubstringOperatorConversion.java       | 56 ++++++++++---
 .../sql/calcite/expression/ExpressionsTest.java    | 92 ++++++++++++++++++++++
 .../calcite/util/CalciteTestInjectorBuilder.java   |  4 +-
 .../druid/sql/calcite/util/CalciteTests.java       |  1 -
 .../sql/calcite/util/QueryFrameworkUtils.java      |  2 +-
 .../AssertionErrorOperatorConversion.java          | 66 ++++++++++++++++
 .../testoperator/CalciteTestOperatorModule.java    | 36 +++++++++
 .../org/apache/druid/sql/http/SqlResourceTest.java | 16 ++--
 8 files changed, 246 insertions(+), 27 deletions(-)

diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/SubstringOperatorConversion.java
 
b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/SubstringOperatorConversion.java
index ae470922c1..41f97b9322 100644
--- 
a/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/SubstringOperatorConversion.java
+++ 
b/sql/src/main/java/org/apache/druid/sql/calcite/expression/builtin/SubstringOperatorConversion.java
@@ -22,6 +22,7 @@ package org.apache.druid.sql.calcite.expression.builtin;
 import org.apache.calcite.rex.RexCall;
 import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexUtil;
 import org.apache.calcite.sql.SqlFunction;
 import org.apache.calcite.sql.SqlFunctionCategory;
 import org.apache.calcite.sql.SqlOperator;
@@ -36,6 +37,8 @@ import 
org.apache.druid.sql.calcite.expression.OperatorConversions;
 import org.apache.druid.sql.calcite.expression.SqlOperatorConversion;
 import org.apache.druid.sql.calcite.planner.PlannerContext;
 
+import java.util.List;
+
 public class SubstringOperatorConversion implements SqlOperatorConversion
 {
   private static final SqlFunction SQL_FUNCTION = OperatorConversions
@@ -63,29 +66,56 @@ public class SubstringOperatorConversion implements 
SqlOperatorConversion
     // SQL is 1-indexed, Druid is 0-indexed.
 
     final RexCall call = (RexCall) rexNode;
-    final DruidExpression input = Expressions.toDruidExpression(
-        plannerContext,
-        rowSignature,
-        call.getOperands().get(0)
-    );
+    final List<RexNode> operands = call.getOperands();
+    final RexNode inputNode = operands.get(0);
+    final DruidExpression input = 
Expressions.toDruidExpression(plannerContext, rowSignature, inputNode);
     if (input == null) {
       return null;
     }
-    final int index = RexLiteral.intValue(call.getOperands().get(1)) - 1;
-    final int length;
-    if (call.getOperands().size() > 2) {
-      length = RexLiteral.intValue(call.getOperands().get(2));
+
+    final RexNode indexNode = operands.get(1);
+    final Integer adjustedIndexLiteral = RexUtil.isLiteral(indexNode, true) ? 
RexLiteral.intValue(indexNode) - 1 : null;
+    final String adjustedIndexExpr;
+    if (adjustedIndexLiteral != null) {
+      adjustedIndexExpr = String.valueOf(adjustedIndexLiteral);
+    } else {
+      final DruidExpression indexExpr = 
Expressions.toDruidExpression(plannerContext, rowSignature, indexNode);
+      if (indexExpr == null) {
+        return null;
+      }
+      adjustedIndexExpr = StringUtils.format("(%s - 1)", 
indexExpr.getExpression());
+    }
+    if (adjustedIndexExpr == null) {
+      return null;
+    }
+
+    final RexNode lengthNode = operands.size() > 2 ? operands.get(2) : null;
+    final Integer lengthLiteral =
+        lengthNode != null && RexUtil.isLiteral(lengthNode, true) ? 
RexLiteral.intValue(lengthNode) : null;
+    final String lengthExpr;
+    if (lengthNode != null) {
+      final DruidExpression lengthExpression = 
Expressions.toDruidExpression(plannerContext, rowSignature, lengthNode);
+      if (lengthExpression == null) {
+        return null;
+      }
+      lengthExpr = lengthExpression.getExpression();
     } else {
-      length = -1;
+      lengthExpr = "-1";
     }
 
     return input.map(
-        simpleExtraction -> simpleExtraction.cascade(new 
SubstringDimExtractionFn(index, length < 0 ? null : length)),
+        simpleExtraction -> {
+          if (adjustedIndexLiteral != null && (lengthNode == null || 
lengthLiteral != null)) {
+            return simpleExtraction.cascade(new 
SubstringDimExtractionFn(adjustedIndexLiteral, lengthLiteral));
+          } else {
+            return null;
+          }
+        },
         expression -> StringUtils.format(
             "substring(%s, %s, %s)",
             expression,
-            DruidExpression.longLiteral(index),
-            DruidExpression.longLiteral(length)
+            adjustedIndexExpr,
+            lengthExpr
         )
     );
   }
diff --git 
a/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
 
b/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
index 9b269ecde3..3fbd517e1c 100644
--- 
a/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
+++ 
b/sql/src/test/java/org/apache/druid/sql/calcite/expression/ExpressionsTest.java
@@ -34,6 +34,7 @@ import org.apache.druid.java.util.common.DateTimes;
 import org.apache.druid.math.expr.ExpressionValidationException;
 import org.apache.druid.query.expression.TestExprMacroTable;
 import org.apache.druid.query.extraction.RegexDimExtractionFn;
+import org.apache.druid.query.extraction.SubstringDimExtractionFn;
 import org.apache.druid.query.filter.RegexDimFilter;
 import org.apache.druid.query.filter.SearchQueryDimFilter;
 import org.apache.druid.query.search.ContainsSearchQuerySpec;
@@ -55,6 +56,7 @@ import 
org.apache.druid.sql.calcite.expression.builtin.RightOperatorConversion;
 import org.apache.druid.sql.calcite.expression.builtin.RoundOperatorConversion;
 import 
org.apache.druid.sql.calcite.expression.builtin.StringFormatOperatorConversion;
 import 
org.apache.druid.sql.calcite.expression.builtin.StrposOperatorConversion;
+import 
org.apache.druid.sql.calcite.expression.builtin.SubstringOperatorConversion;
 import 
org.apache.druid.sql.calcite.expression.builtin.TimeCeilOperatorConversion;
 import 
org.apache.druid.sql.calcite.expression.builtin.TimeExtractOperatorConversion;
 import 
org.apache.druid.sql.calcite.expression.builtin.TimeFloorOperatorConversion;
@@ -167,6 +169,96 @@ public class ExpressionsTest extends ExpressionTestBase
     );
   }
 
+  @Test
+  public void testSubstring()
+  {
+    testHelper.testExpressionString(
+        new SubstringOperatorConversion().calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeInputRef("s"),
+            testHelper.makeLiteral(1),
+            testHelper.makeLiteral(2)
+        ),
+        makeExpression(
+            SimpleExtraction.of("s", new SubstringDimExtractionFn(0, 2)),
+            "substring(\"s\", 0, 2)"
+        ),
+        "fo"
+    );
+
+    testHelper.testExpressionString(
+        new SubstringOperatorConversion().calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeInputRef("s"),
+            testHelper.makeLiteral(2),
+            testHelper.makeLiteral(1)
+        ),
+        makeExpression(
+            SimpleExtraction.of("s", new SubstringDimExtractionFn(1, 1)),
+            "substring(\"s\", 1, 1)"
+        ),
+        "o"
+    );
+
+    testHelper.testExpressionString(
+        new SubstringOperatorConversion().calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeInputRef("s"),
+            testHelper.makeLiteral(1)
+        ),
+        makeExpression(
+            SimpleExtraction.of("s", new SubstringDimExtractionFn(0, null)),
+            "substring(\"s\", 0, -1)"
+        ),
+        "foo"
+    );
+
+    testHelper.testExpressionString(
+        new SubstringOperatorConversion().calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeInputRef("s"),
+            testHelper.makeLiteral(2)
+        ),
+        makeExpression(
+            SimpleExtraction.of("s", new SubstringDimExtractionFn(1, null)),
+            "substring(\"s\", 1, -1)"
+        ),
+        "oo"
+    );
+
+    testHelper.testExpressionString(
+        new SubstringOperatorConversion().calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeInputRef("s"),
+            testHelper.makeLiteral(1),
+            testHelper.makeInputRef("p") // p is 3
+        ),
+        makeExpression("substring(\"s\", 0, \"p\")"),
+        "foo"
+    );
+
+    testHelper.testExpressionString(
+        new SubstringOperatorConversion().calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeInputRef("spacey"),
+            testHelper.makeInputRef("p") // p is 3
+        ),
+        makeExpression("substring(\"spacey\", (\"p\" - 1), -1)"),
+        "hey there  "
+    );
+
+    testHelper.testExpressionString(
+        new SubstringOperatorConversion().calciteOperator(),
+        ImmutableList.of(
+            testHelper.makeInputRef("spacey"),
+            testHelper.makeInputRef("p"), // p is 3
+            testHelper.makeInputRef("p") // p is 3
+        ),
+        makeExpression("substring(\"spacey\", (\"p\" - 1), \"p\")"),
+        "hey"
+    );
+  }
+
   @Test
   public void testRegexpExtract()
   {
diff --git 
a/sql/src/test/java/org/apache/druid/sql/calcite/util/CalciteTestInjectorBuilder.java
 
b/sql/src/test/java/org/apache/druid/sql/calcite/util/CalciteTestInjectorBuilder.java
index 4dc825cac6..345b8b3ad9 100644
--- 
a/sql/src/test/java/org/apache/druid/sql/calcite/util/CalciteTestInjectorBuilder.java
+++ 
b/sql/src/test/java/org/apache/druid/sql/calcite/util/CalciteTestInjectorBuilder.java
@@ -26,6 +26,7 @@ import org.apache.druid.initialization.CoreInjectorBuilder;
 import org.apache.druid.math.expr.ExprMacroTable;
 import org.apache.druid.query.expression.TestExprMacroTable;
 import org.apache.druid.sql.calcite.aggregation.SqlAggregationModule;
+import 
org.apache.druid.sql.calcite.util.testoperator.CalciteTestOperatorModule;
 
 /**
  * Create the injector used for {@link CalciteTests#INJECTOR}, but in a way
@@ -41,7 +42,8 @@ public class CalciteTestInjectorBuilder extends 
CoreInjectorBuilder
     add(
         new SegmentWranglerModule(),
         new LookylooModule(),
-        new SqlAggregationModule()
+        new SqlAggregationModule(),
+        new CalciteTestOperatorModule()
     );
   }
 
diff --git 
a/sql/src/test/java/org/apache/druid/sql/calcite/util/CalciteTests.java 
b/sql/src/test/java/org/apache/druid/sql/calcite/util/CalciteTests.java
index 0dfec3759e..905c28c5fa 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/util/CalciteTests.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/util/CalciteTests.java
@@ -327,7 +327,6 @@ public class CalciteTests
   public static SystemSchema createMockSystemSchema(
       final DruidSchema druidSchema,
       final SpecificSegmentsQuerySegmentWalker walker,
-      final PlannerConfig plannerConfig,
       final AuthorizerMapper authorizerMapper
   )
   {
diff --git 
a/sql/src/test/java/org/apache/druid/sql/calcite/util/QueryFrameworkUtils.java 
b/sql/src/test/java/org/apache/druid/sql/calcite/util/QueryFrameworkUtils.java
index ed4459ba38..63235a76c7 100644
--- 
a/sql/src/test/java/org/apache/druid/sql/calcite/util/QueryFrameworkUtils.java
+++ 
b/sql/src/test/java/org/apache/druid/sql/calcite/util/QueryFrameworkUtils.java
@@ -146,7 +146,7 @@ public class QueryFrameworkUtils
         druidSchemaManager
     );
     SystemSchema systemSchema =
-        CalciteTests.createMockSystemSchema(druidSchema, walker, 
plannerConfig, authorizerMapper);
+        CalciteTests.createMockSystemSchema(druidSchema, walker, 
authorizerMapper);
 
     LookupSchema lookupSchema = createMockLookupSchema(injector);
     ViewSchema viewSchema = viewManager != null ? new ViewSchema(viewManager) 
: null;
diff --git 
a/sql/src/test/java/org/apache/druid/sql/calcite/util/testoperator/AssertionErrorOperatorConversion.java
 
b/sql/src/test/java/org/apache/druid/sql/calcite/util/testoperator/AssertionErrorOperatorConversion.java
new file mode 100644
index 0000000000..6d4aa44e5e
--- /dev/null
+++ 
b/sql/src/test/java/org/apache/druid/sql/calcite/util/testoperator/AssertionErrorOperatorConversion.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.sql.calcite.util.testoperator;
+
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.druid.segment.column.RowSignature;
+import org.apache.druid.sql.calcite.expression.DruidExpression;
+import org.apache.druid.sql.calcite.expression.OperatorConversions;
+import org.apache.druid.sql.calcite.expression.SqlOperatorConversion;
+import org.apache.druid.sql.calcite.planner.PlannerContext;
+import org.apache.druid.sql.http.SqlResourceTest;
+
+import javax.annotation.Nullable;
+
+/**
+ * There are various points where Calcite feels it is acceptable to throw an 
AssertionError when it receives bad
+ * input. This is unfortunate as a java.lang.Error is very clearly documented 
to be something that nobody should
+ * try to catch. But, we can editorialize all we want, we still have to deal 
with it. So, this operator triggers
+ * the AssertionError behavior by using RexLiteral.intValue with bad input (a 
RexNode that is not a literal).
+ *
+ * The test {@link 
SqlResourceTest#testAssertionErrorThrowsErrorWithFilterResponse()} verifies 
that our exception
+ * handling deals with this meaningfully.
+ */
+public class AssertionErrorOperatorConversion implements SqlOperatorConversion
+{
+  private static final SqlOperator OPERATOR =
+      OperatorConversions.operatorBuilder("assertion_error")
+                         .operandTypes()
+                         .returnTypeNonNull(SqlTypeName.BIGINT)
+                         .build();
+
+  @Override
+  public SqlOperator calciteOperator()
+  {
+    return OPERATOR;
+  }
+
+  @Nullable
+  @Override
+  public DruidExpression toDruidExpression(PlannerContext plannerContext, 
RowSignature rowSignature, RexNode rexNode)
+  {
+    // Throws AssertionError. See class-level javadoc for rationale about why 
we're doing this.
+    RexLiteral.intValue(rexNode);
+    return null;
+  }
+}
diff --git 
a/sql/src/test/java/org/apache/druid/sql/calcite/util/testoperator/CalciteTestOperatorModule.java
 
b/sql/src/test/java/org/apache/druid/sql/calcite/util/testoperator/CalciteTestOperatorModule.java
new file mode 100644
index 0000000000..16dfac5e3d
--- /dev/null
+++ 
b/sql/src/test/java/org/apache/druid/sql/calcite/util/testoperator/CalciteTestOperatorModule.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.sql.calcite.util.testoperator;
+
+import com.google.inject.Binder;
+import com.google.inject.Module;
+import org.apache.druid.sql.guice.SqlBindings;
+
+/**
+ * Adds operators that are only used in tests -- not production.
+ */
+public class CalciteTestOperatorModule implements Module
+{
+  @Override
+  public void configure(Binder binder)
+  {
+    SqlBindings.addOperatorConversion(binder, 
AssertionErrorOperatorConversion.class);
+  }
+}
diff --git a/sql/src/test/java/org/apache/druid/sql/http/SqlResourceTest.java 
b/sql/src/test/java/org/apache/druid/sql/http/SqlResourceTest.java
index d92b1cbcd4..2c7e002a5f 100644
--- a/sql/src/test/java/org/apache/druid/sql/http/SqlResourceTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/http/SqlResourceTest.java
@@ -1589,28 +1589,22 @@ public class SqlResourceTest extends CalciteTestBase
   }
 
   /**
-   * There are various points where Calcite feels it is acceptable to throw an 
AssertionError when it receives bad
-   * input.  This is unfortunate as a java.lang.Error is very clearly 
documented to be something that nobody should
-   * try to catch.  But, we can editorialize all we want, we still have to 
deal with it.  So, this test reproduces
-   * the AssertionError behavior by using the substr() command.  At the time 
that this test was written, the
-   * SQL substr assumes a literal for the second argument.  The code ends up 
calling `RexLiteral.intValue` on the
-   * argument, which ends up calling a method that fails with an 
AssertionError, so this should generate the
-   * bad behavior for us.  This test is validating that our exception handling 
deals with this meaningfully.
+   * See class-level javadoc for {@link 
org.apache.druid.sql.calcite.util.testoperator.AssertionErrorOperatorConversion}
+   * for rationale as to why this test exists.
+   *
    * If this test starts failing, it could be indicative of us not handling 
the AssertionErrors well anymore,
    * OR it could be indicative of this specific code path not throwing an 
AssertionError anymore.  If we run
    * into the latter case, we should seek out a new code path that generates 
the error from Calcite.  In the best
    * world, this test starts failing because Calcite has moved all of its 
execptions away from AssertionErrors
    * and we can no longer reproduce the behavior through Calcite, in that 
world, we should remove our own handling
    * and this test at the same time.
-   *
-   * @throws Exception
    */
   @Test
   public void testAssertionErrorThrowsErrorWithFilterResponse() throws 
Exception
   {
     ErrorResponse exception = postSyncForException(
         new SqlQuery(
-            "SELECT *, substr(dim2, strpos(dim2, 'hi')+2, 2) FROM foo LIMIT 2",
+            "SELECT assertion_error() FROM foo LIMIT 2",
             ResultFormat.OBJECT,
             false,
             false,
@@ -1625,7 +1619,7 @@ public class SqlResourceTest extends CalciteTestBase
         exception.getUnderlyingException(),
         DruidExceptionMatcher
             .invalidSqlInput()
-            .expectMessageIs("Calcite assertion violated: [not a literal: 
+(STRPOS($2, 'hi'), 2)]")
+            .expectMessageIs("Calcite assertion violated: [not a literal: 
assertion_error()]")
     );
     Assert.assertTrue(lifecycleManager.getAll("id").isEmpty());
   }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to