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

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


The following commit(s) were added to refs/heads/master by this push:
     new 9302f186d6 Fix null literal handling for null intolerant functions in 
multi-stage query engine (#13255)
9302f186d6 is described below

commit 9302f186d61aac317a800daeddc221ab136a0ffc
Author: Yash Mayya <[email protected]>
AuthorDate: Thu May 30 14:58:45 2024 +0530

    Fix null literal handling for null intolerant functions in multi-stage 
query engine (#13255)
    
    * Fix null literal handling for null intolerant functions in multi-stage 
query engine
    
    * Use data provider for null literal selection integration tests
---
 .../function/scalar/ArithmeticFunctions.java       |   2 +
 .../common/function/scalar/ArrayFunctions.java     |   2 +
 .../function/scalar/ComparisonFunctions.java       |   3 +-
 .../common/function/scalar/DateTimeFunctions.java  |   2 +
 .../common/function/scalar/LogicalFunctions.java   |   8 +-
 .../function/scalar/TrigonometricFunctions.java    |   3 +-
 .../tests/NullHandlingIntegrationTest.java         | 166 +++++++++------------
 .../src/test/resources/test_null_handling.schema   |  15 +-
 8 files changed, 96 insertions(+), 105 deletions(-)

diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/ArithmeticFunctions.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/ArithmeticFunctions.java
index 1a851aa404..b8d0cc2404 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/ArithmeticFunctions.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/ArithmeticFunctions.java
@@ -20,12 +20,14 @@ package org.apache.pinot.common.function.scalar;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
+import org.apache.calcite.linq4j.function.Strict;
 import org.apache.pinot.spi.annotations.ScalarFunction;
 
 
 /**
  * Arithmetic scalar functions.
  */
+@Strict
 public class ArithmeticFunctions {
   private ArithmeticFunctions() {
   }
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/ArrayFunctions.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/ArrayFunctions.java
index 53e6bc76c2..07fc94cebd 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/ArrayFunctions.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/ArrayFunctions.java
@@ -25,6 +25,7 @@ import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet;
 import it.unimi.dsi.fastutil.objects.ObjectSet;
 import java.math.BigDecimal;
 import java.util.Arrays;
+import org.apache.calcite.linq4j.function.SemiStrict;
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.pinot.spi.annotations.ScalarFunction;
 import org.apache.pinot.spi.utils.CommonConstants.NullValuePlaceHolder;
@@ -33,6 +34,7 @@ import 
org.apache.pinot.spi.utils.CommonConstants.NullValuePlaceHolder;
 /**
  * Inbuilt array scalar functions. See {@link ArrayUtils} for details.
  */
+@SemiStrict
 public class ArrayFunctions {
   private ArrayFunctions() {
   }
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/ComparisonFunctions.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/ComparisonFunctions.java
index e12cb7d8de..ca7a94eb0c 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/ComparisonFunctions.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/ComparisonFunctions.java
@@ -18,9 +18,10 @@
  */
 package org.apache.pinot.common.function.scalar;
 
+import org.apache.calcite.linq4j.function.Strict;
 import org.apache.pinot.spi.annotations.ScalarFunction;
 
-
+@Strict
 public class ComparisonFunctions {
 
   private static final double DOUBLE_COMPARISON_TOLERANCE = 1e-7d;
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/DateTimeFunctions.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/DateTimeFunctions.java
index 40467db59e..adb6c9bda6 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/DateTimeFunctions.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/DateTimeFunctions.java
@@ -21,6 +21,7 @@ package org.apache.pinot.common.function.scalar;
 import java.sql.Timestamp;
 import java.time.Duration;
 import java.util.concurrent.TimeUnit;
+import org.apache.calcite.linq4j.function.Strict;
 import org.apache.pinot.common.function.DateTimePatternHandler;
 import org.apache.pinot.common.function.DateTimeUtils;
 import org.apache.pinot.common.function.TimeZoneKey;
@@ -69,6 +70,7 @@ import org.joda.time.chrono.ISOChronology;
  *     }]
  *   </code>
  */
+@Strict
 public class DateTimeFunctions {
   private DateTimeFunctions() {
   }
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/LogicalFunctions.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/LogicalFunctions.java
index 863f56f783..648c8439f5 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/LogicalFunctions.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/LogicalFunctions.java
@@ -18,11 +18,13 @@
  */
 package org.apache.pinot.common.function.scalar;
 
+import org.apache.calcite.linq4j.function.Strict;
 import org.apache.pinot.spi.annotations.ScalarFunction;
 
-
-// Logical transformation on boolean values.
-// Currently, only not is supported.
+/**
+ * Logical transformation on boolean values. Currently, only not is supported.
+ */
+@Strict
 public class LogicalFunctions {
   private LogicalFunctions() {
   }
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/TrigonometricFunctions.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/TrigonometricFunctions.java
index c27265e39a..c3b327965f 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/TrigonometricFunctions.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/TrigonometricFunctions.java
@@ -18,9 +18,10 @@
  */
 package org.apache.pinot.common.function.scalar;
 
+import org.apache.calcite.linq4j.function.Strict;
 import org.apache.pinot.spi.annotations.ScalarFunction;
 
-
+@Strict
 public class TrigonometricFunctions {
   private TrigonometricFunctions() {
   }
diff --git 
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/NullHandlingIntegrationTest.java
 
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/NullHandlingIntegrationTest.java
index 5e57db53c8..7a838052f9 100644
--- 
a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/NullHandlingIntegrationTest.java
+++ 
b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/NullHandlingIntegrationTest.java
@@ -27,6 +27,7 @@ import 
org.apache.pinot.core.common.datatable.DataTableBuilderFactory;
 import org.apache.pinot.util.TestUtils;
 import org.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
 import static org.testng.AssertJUnit.assertEquals;
@@ -129,11 +130,6 @@ public class NullHandlingIntegrationTest extends 
BaseClusterIntegrationTestSet {
     return null;
   }
 
-  @Override
-  protected boolean getNullHandlingEnabled() {
-    return true;
-  }
-
   @Override
   protected long getCountStarResult() {
     return 100;
@@ -151,7 +147,6 @@ public class NullHandlingIntegrationTest extends 
BaseClusterIntegrationTestSet {
   public void testCountWithNullDescription(boolean useMultiStageQueryEngine)
       throws Exception {
     setUseMultiStageQueryEngine(useMultiStageQueryEngine);
-    notSupportedInV2();
     String query = "SELECT COUNT(*) FROM " + getTableName() + " WHERE 
description IS NOT NULL";
     testQuery(query);
   }
@@ -160,7 +155,6 @@ public class NullHandlingIntegrationTest extends 
BaseClusterIntegrationTestSet {
   public void testCountWithNullDescriptionAndSalary(boolean 
useMultiStageQueryEngine)
       throws Exception {
     setUseMultiStageQueryEngine(useMultiStageQueryEngine);
-    notSupportedInV2();
     String query = "SELECT COUNT(*) FROM " + getTableName() + " WHERE 
description IS NOT NULL AND salary IS NOT NULL";
     testQuery(query);
   }
@@ -215,101 +209,49 @@ public class NullHandlingIntegrationTest extends 
BaseClusterIntegrationTestSet {
     
DataTableBuilderFactory.setDataTableVersion(DataTableBuilderFactory.DEFAULT_VERSION);
   }
 
-  @Test(dataProvider = "useBothQueryEngines")
-  public void testNullLiteralSelectionOnlyBroker(boolean 
useMultiStageQueryEngine)
+  @Test(dataProvider = "nullLiteralQueries")
+  public void testNullLiteralSelectionOnlyBroker(String sqlQuery, Object 
expectedResult)
       throws Exception {
-    setUseMultiStageQueryEngine(useMultiStageQueryEngine);
-    notSupportedInV2();
-    // Null literal only
-    String sqlQuery = "SELECT null FROM mytable 
OPTION(enableNullHandling=true)";
+    // V2 handles such queries in the servers where all rows in the table are 
matched and hence the number of rows
+    // returned in the result set is equal to the total number of rows in the 
table (instead of a single row like in
+    // V1 where it is handled in the broker). The V2 way is more standard 
though and matches behavior in other
+    // databases like Postgres.
+    setUseMultiStageQueryEngine(false);
+
     JsonNode response = postQuery(sqlQuery);
     JsonNode rows = response.get("resultTable").get("rows");
     assertTrue(response.get("exceptions").isEmpty());
-    assertEquals(rows.size(), 1);
-    assertEquals(rows.get(0).get(0).asText(), "null");
-
-    // Null related functions
-    sqlQuery = "SELECT isNull(null) FROM " + getTableName() + "  OPTION 
(enableNullHandling=true);";
-    response = postQuery(sqlQuery);
-    rows = response.get("resultTable").get("rows");
-    assertTrue(response.get("exceptions").isEmpty());
-    assertEquals(rows.size(), 1);
-    assertEquals(rows.get(0).get(0).asBoolean(), true);
-
-    sqlQuery = "SELECT isNotNull(null) FROM " + getTableName() + "  OPTION 
(enableNullHandling=true);";
-    response = postQuery(sqlQuery);
-    rows = response.get("resultTable").get("rows");
-    assertTrue(response.get("exceptions").isEmpty());
-    assertEquals(rows.size(), 1);
-    assertEquals(rows.get(0).get(0).asBoolean(), false);
-
-
-    sqlQuery = "SELECT coalesce(null, 1) FROM " + getTableName() + "  OPTION 
(enableNullHandling=true);";
-    response = postQuery(sqlQuery);
-    rows = response.get("resultTable").get("rows");
-    assertTrue(response.get("exceptions").isEmpty());
-    assertEquals(rows.size(), 1);
-    assertEquals(rows.get(0).get(0).asInt(), 1);
-
-    sqlQuery = "SELECT coalesce(null, null) FROM " + getTableName() + "  
OPTION (enableNullHandling=true);";
-    response = postQuery(sqlQuery);
-    rows = response.get("resultTable").get("rows");
-    assertTrue(response.get("exceptions").isEmpty());
-    assertEquals(rows.size(), 1);
-    assertEquals(rows.get(0).get(0).asText(), "null");
-
-    sqlQuery = "SELECT isDistinctFrom(null, null) FROM " + getTableName() + "  
OPTION (enableNullHandling=true);";
-    response = postQuery(sqlQuery);
-    rows = response.get("resultTable").get("rows");
-    assertTrue(response.get("exceptions").isEmpty());
-    assertEquals(rows.size(), 1);
-    assertEquals(rows.get(0).get(0).asBoolean(), false);
-
-    sqlQuery = "SELECT isNotDistinctFrom(null, null) FROM " + getTableName() + 
"  OPTION (enableNullHandling=true);";
-    response = postQuery(sqlQuery);
-    rows = response.get("resultTable").get("rows");
-    assertTrue(response.get("exceptions").isEmpty());
-    assertEquals(rows.size(), 1);
-    assertEquals(rows.get(0).get(0).asBoolean(), true);
-
-
-    sqlQuery = "SELECT isDistinctFrom(null, 1) FROM " + getTableName() + "  
OPTION (enableNullHandling=true);";
-    response = postQuery(sqlQuery);
-    rows = response.get("resultTable").get("rows");
-    assertTrue(response.get("exceptions").isEmpty());
-    assertEquals(rows.size(), 1);
-    assertEquals(rows.get(0).get(0).asBoolean(), true);
-
-    sqlQuery = "SELECT isNotDistinctFrom(null, 1) FROM " + getTableName() + "  
OPTION (enableNullHandling=true);";
-    response = postQuery(sqlQuery);
-    rows = response.get("resultTable").get("rows");
-    assertTrue(response.get("exceptions").isEmpty());
-    assertEquals(rows.size(), 1);
-    assertEquals(rows.get(0).get(0).asBoolean(), false);
-
-    sqlQuery = "SELECT case when true then null end FROM " + getTableName() + 
"  OPTION (enableNullHandling=true);";
-    response = postQuery(sqlQuery);
-    rows = response.get("resultTable").get("rows");
-    assertTrue(response.get("exceptions").isEmpty());
-    assertEquals(rows.size(), 1);
-    assertEquals(rows.get(0).get(0).asText(), "null");
-
-
-    sqlQuery = "SELECT case when false then 1 end FROM " + getTableName() + "  
OPTION (enableNullHandling=true);";
-    response = postQuery(sqlQuery);
-    rows = response.get("resultTable").get("rows");
-    assertTrue(response.get("exceptions").isEmpty());
-    assertEquals(rows.size(), 1);
-    assertEquals(rows.get(0).get(0).asText(), "null");
+    assertEquals(1, rows.size());
+
+    if (expectedResult instanceof String) {
+      assertEquals(expectedResult, rows.get(0).get(0).asText());
+    } else if (expectedResult instanceof Integer) {
+      assertEquals(expectedResult, rows.get(0).get(0).asInt());
+    } else if (expectedResult instanceof Boolean) {
+      assertEquals(expectedResult, rows.get(0).get(0).asBoolean());
+    } else {
+      throw new IllegalArgumentException("Unexpected type for expectedResult: 
" + expectedResult.getClass());
+    }
+  }
 
+  @Test(dataProvider = "nullLiteralQueries")
+  public void testNullLiteralSelectionInV2(String sqlQuery, Object 
expectedResult) throws Exception {
+    setUseMultiStageQueryEngine(true);
 
-    // Null intolerant functions
-    sqlQuery = "SELECT add(null, 1) FROM " + getTableName() + "  OPTION 
(enableNullHandling=true);";
-    response = postQuery(sqlQuery);
-    rows = response.get("resultTable").get("rows");
+    JsonNode response = postQuery(sqlQuery);
+    JsonNode rows = response.get("resultTable").get("rows");
     assertTrue(response.get("exceptions").isEmpty());
-    assertEquals(rows.size(), 1);
-    assertEquals(rows.get(0).get(0).asText(), "null");
+    assertEquals(getCountStarResult(), rows.size());
+
+    if (expectedResult instanceof String) {
+      assertEquals(expectedResult, rows.get(0).get(0).asText());
+    } else if (expectedResult instanceof Integer) {
+      assertEquals(expectedResult, rows.get(0).get(0).asInt());
+    } else if (expectedResult instanceof Boolean) {
+      assertEquals(expectedResult, rows.get(0).get(0).asBoolean());
+    } else {
+      throw new IllegalArgumentException("Unexpected type for expectedResult: 
" + expectedResult.getClass());
+    }
   }
 
   @Test
@@ -378,4 +320,38 @@ public class NullHandlingIntegrationTest extends 
BaseClusterIntegrationTestSet {
 
     
assertEquals(response.get("resultTable").get("rows").get(0).get(0).asInt(), 1);
   }
+
+  @DataProvider(name = "nullLiteralQueries")
+  public Object[][] nullLiteralQueries() {
+    // Query string, expected value
+    return new Object[][]{
+        // Null literal only
+        {String.format("SELECT null FROM %s OPTION(enableNullHandling=true)", 
getTableName()), "null"},
+        // Null related functions
+        {String.format("SELECT isNull(null) FROM %s OPTION 
(enableNullHandling=true)", getTableName()), true},
+        {String.format("SELECT isNotNull(null) FROM %s OPTION 
(enableNullHandling=true)", getTableName()), false},
+        {String.format("SELECT coalesce(null, 1) FROM %s OPTION 
(enableNullHandling=true)", getTableName()), 1},
+        {String.format("SELECT coalesce(null, null) FROM %s OPTION 
(enableNullHandling=true)", getTableName()), "null"},
+        {String.format("SELECT isDistinctFrom(null, null) FROM %s OPTION 
(enableNullHandling=true)", getTableName()),
+            false},
+        {String.format("SELECT isNotDistinctFrom(null, null) FROM %s OPTION 
(enableNullHandling=true)", getTableName()),
+            true},
+        {String.format("SELECT isDistinctFrom(null, 1) FROM %s OPTION 
(enableNullHandling=true)", getTableName()),
+            true},
+        {String.format("SELECT isNotDistinctFrom(null, 1) FROM %s OPTION 
(enableNullHandling=true)", getTableName()),
+            false},
+        {String.format("SELECT case when true then null end FROM %s OPTION 
(enableNullHandling=true)", getTableName()),
+            "null"},
+        {String.format("SELECT case when false then 1 end FROM %s OPTION 
(enableNullHandling=true)", getTableName()),
+            "null"},
+        // Null intolerant functions
+        {String.format("SELECT add(null, 1) FROM %s OPTION 
(enableNullHandling=true)", getTableName()), "null"},
+        {String.format("SELECT greater_than(null, 1) FROM %s OPTION 
(enableNullHandling=true)", getTableName()),
+            "null"},
+        {String.format("SELECT to_epoch_seconds(null) FROM %s OPTION 
(enableNullHandling=true)", getTableName()),
+            "null"},
+        {String.format("SELECT not(null) FROM %s OPTION 
(enableNullHandling=true)", getTableName()), "null"},
+        {String.format("SELECT tan(null) FROM %s OPTION 
(enableNullHandling=true)", getTableName()), "null"}
+    };
+  }
 }
diff --git 
a/pinot-integration-tests/src/test/resources/test_null_handling.schema 
b/pinot-integration-tests/src/test/resources/test_null_handling.schema
index 1e9c0ec10b..0105f95d88 100644
--- a/pinot-integration-tests/src/test/resources/test_null_handling.schema
+++ b/pinot-integration-tests/src/test/resources/test_null_handling.schema
@@ -3,22 +3,26 @@
     {
       "dataType": "INT",
       "singleValueField": true,
-      "name": "clientId"
+      "name": "clientId",
+      "notNull": "true"
     },
     {
       "dataType": "STRING",
       "singleValueField": true,
-      "name": "city"
+      "name": "city",
+      "notNull": "true"
     },
     {
       "dataType": "STRING",
       "singleValueField": true,
-      "name": "description"
+      "name": "description",
+      "notNull": "false"
     },
     {
       "dataType": "INT",
       "singleValueField": true,
-      "name": "salary"
+      "name": "salary",
+      "notNull": "false"
     }
   ],
   "timeFieldSpec": {
@@ -28,5 +32,6 @@
       "name": "DaysSinceEpoch"
     }
   },
-  "schemaName": "mytable"
+  "schemaName": "mytable",
+  "enableColumnBasedNullHandling": true
 }


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

Reply via email to