This is an automated email from the ASF dual-hosted git repository.
jackie 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 98969168f4e Adding logical functions for Apache Pinot (#17189)
98969168f4e is described below
commit 98969168f4e0768a71e32bc6fac4469d8461a34e
Author: Akanksha kedia <[email protected]>
AuthorDate: Mon Dec 8 06:01:30 2025 +0530
Adding logical functions for Apache Pinot (#17189)
---
.../common/function/scalar/LogicalFunctions.java | 136 ++++++-
.../function/scalar/LogicalFunctionsTest.java | 445 +++++++++++++++++++++
.../pinot/core/function/FunctionRegistryTest.java | 4 +-
3 files changed, 578 insertions(+), 7 deletions(-)
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 af6bbd95490..cd1d7fa2454 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,20 +18,146 @@
*/
package org.apache.pinot.common.function.scalar;
+import javax.annotation.Nullable;
import org.apache.pinot.spi.annotations.ScalarFunction;
+
/**
- * Logical transformation on boolean values. Currently, only not is supported.
+ * Inbuilt logical transform functions with Trino-compatible NULL handling
+ *
+ * <p>These functions implement logical operators (AND, OR, NOT) with proper
three-valued logic
+ * support, following the SQL standard and Trino's behavior for NULL handling.
+ *
+ * <p>Truth tables for NULL handling:
+ * <ul>
+ * <li>AND: Returns NULL if one side is NULL and the other is not FALSE.
+ * Returns FALSE if at least one side is FALSE.</li>
+ * <li>OR: Returns NULL if one side is NULL and the other is not TRUE.
+ * Returns TRUE if at least one side is TRUE.</li>
+ * <li>NOT: Returns NULL if the input is NULL.</li>
+ * </ul>
+ *
+ * <p>Examples:
+ * <pre>
+ * and(true, true) → true
+ * and(true, false) → false
+ * and(true, null) → null
+ * and(false, null) → false
+ *
+ * or(true, false) → true
+ * or(false, false) → false
+ * or(false, null) → null
+ * or(true, null) → true
+ *
+ * not(true) → false
+ * not(false) → true
+ * not(null) → null
+ * </pre>
*/
public class LogicalFunctions {
+
private LogicalFunctions() {
}
/**
- * Returns logical negate of value
+ * Logical AND operator with Trino-compatible NULL handling.
+ *
+ * <p>Truth table:
+ * <pre>
+ * a | b | result
+ * ------|-------|-------
+ * TRUE | TRUE | TRUE
+ * TRUE | FALSE | FALSE
+ * TRUE | NULL | NULL
+ * FALSE | TRUE | FALSE
+ * FALSE | FALSE | FALSE
+ * FALSE | NULL | FALSE
+ * NULL | TRUE | NULL
+ * NULL | FALSE | FALSE
+ * NULL | NULL | NULL
+ * </pre>
+ *
+ * @param a First boolean value (nullable)
+ * @param b Second boolean value (nullable)
+ * @return The logical AND result, or NULL if result cannot be determined
+ */
+ @Nullable
+ @ScalarFunction(nullableParameters = true)
+ public static Boolean and(@Nullable Boolean a, @Nullable Boolean b) {
+ // If either value is FALSE, the result is FALSE
+ if (Boolean.FALSE.equals(a) || Boolean.FALSE.equals(b)) {
+ return false;
+ }
+
+ // If both values are TRUE, the result is TRUE
+ if (Boolean.TRUE.equals(a) && Boolean.TRUE.equals(b)) {
+ return true;
+ }
+
+ // Otherwise, at least one value is NULL and neither is FALSE, so result
is NULL
+ return null;
+ }
+
+ /**
+ * Logical OR operator with Trino-compatible NULL handling.
+ *
+ * <p>Truth table:
+ * <pre>
+ * a | b | result
+ * ------|-------|-------
+ * TRUE | TRUE | TRUE
+ * TRUE | FALSE | TRUE
+ * TRUE | NULL | TRUE
+ * FALSE | TRUE | TRUE
+ * FALSE | FALSE | FALSE
+ * FALSE | NULL | NULL
+ * NULL | TRUE | TRUE
+ * NULL | FALSE | NULL
+ * NULL | NULL | NULL
+ * </pre>
+ *
+ * @param a First boolean value (nullable)
+ * @param b Second boolean value (nullable)
+ * @return The logical OR result, or NULL if the result cannot be determined
+ */
+ @Nullable
+ @ScalarFunction(nullableParameters = true)
+ public static Boolean or(@Nullable Boolean a, @Nullable Boolean b) {
+ // If either value is TRUE, the result is TRUE
+ if (Boolean.TRUE.equals(a) || Boolean.TRUE.equals(b)) {
+ return true;
+ }
+
+ // If both values are FALSE, the result is FALSE
+ if (Boolean.FALSE.equals(a) && Boolean.FALSE.equals(b)) {
+ return false;
+ }
+
+ // Otherwise, at least one value is NULL and neither is TRUE, so result is
NULL
+ return null;
+ }
+
+ /**
+ * Logical NOT operator with Trino-compatible NULL handling.
+ *
+ * <p>Truth table:
+ * <pre>
+ * a | result
+ * ------|-------
+ * TRUE | FALSE
+ * FALSE | TRUE
+ * NULL | NULL
+ * </pre>
+ *
+ * @param a Boolean value to negate (nullable)
+ * @return The logical negation of the input, or NULL if the input is NULL
*/
- @ScalarFunction
- public static boolean not(boolean value) {
- return !value;
+ @Nullable
+ @ScalarFunction(nullableParameters = true)
+ public static Boolean not(@Nullable Boolean a) {
+ if (a == null) {
+ return null;
+ }
+ return !a;
}
}
diff --git
a/pinot-common/src/test/java/org/apache/pinot/common/function/scalar/LogicalFunctionsTest.java
b/pinot-common/src/test/java/org/apache/pinot/common/function/scalar/LogicalFunctionsTest.java
new file mode 100644
index 00000000000..8e307340c90
--- /dev/null
+++
b/pinot-common/src/test/java/org/apache/pinot/common/function/scalar/LogicalFunctionsTest.java
@@ -0,0 +1,445 @@
+/**
+ * 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.pinot.common.function.scalar;
+
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+
+/**
+ * Tests for Logical functions with Trino-compatible NULL handling
+ */
+public class LogicalFunctionsTest {
+
+ // ==================== Tests for and ====================
+
+ @Test
+ public void testAndTrueTrue() {
+ Boolean result = LogicalFunctions.and(true, true);
+ assertTrue(result);
+ }
+
+ @Test
+ public void testAndTrueFalse() {
+ Boolean result = LogicalFunctions.and(true, false);
+ assertFalse(result);
+ }
+
+ @Test
+ public void testAndFalseTrue() {
+ Boolean result = LogicalFunctions.and(false, true);
+ assertFalse(result);
+ }
+
+ @Test
+ public void testAndFalseFalse() {
+ Boolean result = LogicalFunctions.and(false, false);
+ assertFalse(result);
+ }
+
+ @Test
+ public void testAndTrueNull() {
+ Boolean result = LogicalFunctions.and(true, null);
+ assertNull(result, "AND with TRUE and NULL should return NULL");
+ }
+
+ @Test
+ public void testAndNullTrue() {
+ Boolean result = LogicalFunctions.and(null, true);
+ assertNull(result, "AND with NULL and TRUE should return NULL");
+ }
+
+ @Test
+ public void testAndFalseNull() {
+ Boolean result = LogicalFunctions.and(false, null);
+ assertFalse(result, "AND with FALSE and NULL should return FALSE");
+ }
+
+ @Test
+ public void testAndNullFalse() {
+ Boolean result = LogicalFunctions.and(null, false);
+ assertFalse(result, "AND with NULL and FALSE should return FALSE");
+ }
+
+ @Test
+ public void testAndNullNull() {
+ Boolean result = LogicalFunctions.and(null, null);
+ assertNull(result, "AND with NULL and NULL should return NULL");
+ }
+
+ // ==================== Tests for or ====================
+
+ @Test
+ public void testOrTrueTrue() {
+ Boolean result = LogicalFunctions.or(true, true);
+ assertTrue(result);
+ }
+
+ @Test
+ public void testOrTrueFalse() {
+ Boolean result = LogicalFunctions.or(true, false);
+ assertTrue(result);
+ }
+
+ @Test
+ public void testOrFalseTrue() {
+ Boolean result = LogicalFunctions.or(false, true);
+ assertTrue(result);
+ }
+
+ @Test
+ public void testOrFalseFalse() {
+ Boolean result = LogicalFunctions.or(false, false);
+ assertFalse(result);
+ }
+
+ @Test
+ public void testOrTrueNull() {
+ Boolean result = LogicalFunctions.or(true, null);
+ assertTrue(result, "OR with TRUE and NULL should return TRUE");
+ }
+
+ @Test
+ public void testOrNullTrue() {
+ Boolean result = LogicalFunctions.or(null, true);
+ assertTrue(result, "OR with NULL and TRUE should return TRUE");
+ }
+
+ @Test
+ public void testOrFalseNull() {
+ Boolean result = LogicalFunctions.or(false, null);
+ assertNull(result, "OR with FALSE and NULL should return NULL");
+ }
+
+ @Test
+ public void testOrNullFalse() {
+ Boolean result = LogicalFunctions.or(null, false);
+ assertNull(result, "OR with NULL and FALSE should return NULL");
+ }
+
+ @Test
+ public void testOrNullNull() {
+ Boolean result = LogicalFunctions.or(null, null);
+ assertNull(result, "OR with NULL and NULL should return NULL");
+ }
+
+ // ==================== Tests for not ====================
+
+ @Test
+ public void testNotTrue() {
+ Boolean result = LogicalFunctions.not(true);
+ assertFalse(result);
+ }
+
+ @Test
+ public void testNotFalse() {
+ Boolean result = LogicalFunctions.not(false);
+ assertTrue(result);
+ }
+
+ @Test
+ public void testNotNull() {
+ Boolean result = LogicalFunctions.not(null);
+ assertNull(result, "NOT with NULL should return NULL");
+ }
+
+ // ==================== Complex Combination Tests ====================
+
+ @Test
+ public void testComplexAndOrCombinations() {
+ // (TRUE AND FALSE) OR TRUE = FALSE OR TRUE = TRUE
+ Boolean andResult = LogicalFunctions.and(true, false);
+ Boolean finalResult = LogicalFunctions.or(andResult, true);
+ assertTrue(finalResult);
+
+ // (TRUE OR FALSE) AND FALSE = TRUE AND FALSE = FALSE
+ Boolean orResult = LogicalFunctions.or(true, false);
+ finalResult = LogicalFunctions.and(orResult, false);
+ assertFalse(finalResult);
+
+ // (NULL AND TRUE) OR FALSE = NULL OR FALSE = NULL
+ andResult = LogicalFunctions.and(null, true);
+ finalResult = LogicalFunctions.or(andResult, false);
+ assertNull(finalResult);
+
+ // (NULL OR TRUE) AND TRUE = TRUE AND TRUE = TRUE
+ orResult = LogicalFunctions.or(null, true);
+ finalResult = LogicalFunctions.and(orResult, true);
+ assertTrue(finalResult);
+ }
+
+ @Test
+ public void testComplexNotCombinations() {
+ // NOT(TRUE AND FALSE) = NOT(FALSE) = TRUE
+ Boolean andResult = LogicalFunctions.and(true, false);
+ Boolean notResult = LogicalFunctions.not(andResult);
+ assertTrue(notResult);
+
+ // NOT(TRUE OR FALSE) = NOT(TRUE) = FALSE
+ Boolean orResult = LogicalFunctions.or(true, false);
+ notResult = LogicalFunctions.not(orResult);
+ assertFalse(notResult);
+
+ // NOT(NULL AND TRUE) = NOT(NULL) = NULL
+ andResult = LogicalFunctions.and(null, true);
+ notResult = LogicalFunctions.not(andResult);
+ assertNull(notResult);
+
+ // NOT(NULL OR FALSE) = NOT(NULL) = NULL
+ orResult = LogicalFunctions.or(null, false);
+ notResult = LogicalFunctions.not(orResult);
+ assertNull(notResult);
+ }
+
+ @Test
+ public void testDeMorgansLaws() {
+ // De Morgan's Law: NOT(A AND B) = NOT(A) OR NOT(B)
+ // Test with TRUE and FALSE
+ Boolean notAndResult = LogicalFunctions.not(LogicalFunctions.and(true,
false));
+ Boolean notAOrNotB = LogicalFunctions.or(
+ LogicalFunctions.not(true),
+ LogicalFunctions.not(false)
+ );
+ assertEquals(notAndResult, notAOrNotB);
+
+ // De Morgan's Law: NOT(A OR B) = NOT(A) AND NOT(B)
+ // Test with TRUE and FALSE
+ Boolean notOrResult = LogicalFunctions.not(LogicalFunctions.or(true,
false));
+ Boolean notAAndNotB = LogicalFunctions.and(
+ LogicalFunctions.not(true),
+ LogicalFunctions.not(false)
+ );
+ assertEquals(notOrResult, notAAndNotB);
+ }
+
+ // ==================== Edge Cases and Truth Table Verification
====================
+
+ @Test
+ public void testAndTruthTableCompleteness() {
+ // Verify all 9 combinations in the AND truth table
+ assertTrue(LogicalFunctions.and(true, true));
+ assertFalse(LogicalFunctions.and(true, false));
+ assertNull(LogicalFunctions.and(true, null));
+
+ assertFalse(LogicalFunctions.and(false, true));
+ assertFalse(LogicalFunctions.and(false, false));
+ assertFalse(LogicalFunctions.and(false, null));
+
+ assertNull(LogicalFunctions.and(null, true));
+ assertFalse(LogicalFunctions.and(null, false));
+ assertNull(LogicalFunctions.and(null, null));
+ }
+
+ @Test
+ public void testOrTruthTableCompleteness() {
+ // Verify all 9 combinations in the OR truth table
+ assertTrue(LogicalFunctions.or(true, true));
+ assertTrue(LogicalFunctions.or(true, false));
+ assertTrue(LogicalFunctions.or(true, null));
+
+ assertTrue(LogicalFunctions.or(false, true));
+ assertFalse(LogicalFunctions.or(false, false));
+ assertNull(LogicalFunctions.or(false, null));
+
+ assertTrue(LogicalFunctions.or(null, true));
+ assertNull(LogicalFunctions.or(null, false));
+ assertNull(LogicalFunctions.or(null, null));
+ }
+
+ @Test
+ public void testNotTruthTableCompleteness() {
+ // Verify all 3 combinations in the NOT truth table
+ assertFalse(LogicalFunctions.not(true));
+ assertTrue(LogicalFunctions.not(false));
+ assertNull(LogicalFunctions.not(null));
+ }
+
+ // ==================== Commutative Property Tests ====================
+
+ @Test
+ public void testAndCommutativeProperty() {
+ // AND should be commutative: A AND B = B AND A
+ assertEquals(
+ LogicalFunctions.and(true, false),
+ LogicalFunctions.and(false, true)
+ );
+
+ assertEquals(
+ LogicalFunctions.and(true, null),
+ LogicalFunctions.and(null, true)
+ );
+
+ assertEquals(
+ LogicalFunctions.and(false, null),
+ LogicalFunctions.and(null, false)
+ );
+ }
+
+ @Test
+ public void testOrCommutativeProperty() {
+ // OR should be commutative: A OR B = B OR A
+ assertEquals(
+ LogicalFunctions.or(true, false),
+ LogicalFunctions.or(false, true)
+ );
+
+ assertEquals(
+ LogicalFunctions.or(true, null),
+ LogicalFunctions.or(null, true)
+ );
+
+ assertEquals(
+ LogicalFunctions.or(false, null),
+ LogicalFunctions.or(null, false)
+ );
+ }
+
+ // ==================== Associative Property Tests ====================
+
+ @Test
+ public void testAndAssociativeProperty() {
+ // AND should be associative: (A AND B) AND C = A AND (B AND C)
+ Boolean left = LogicalFunctions.and(
+ LogicalFunctions.and(true, false),
+ true
+ );
+ Boolean right = LogicalFunctions.and(
+ true,
+ LogicalFunctions.and(false, true)
+ );
+ assertEquals(left, right);
+
+ // Test with NULL
+ left = LogicalFunctions.and(
+ LogicalFunctions.and(true, null),
+ false
+ );
+ right = LogicalFunctions.and(
+ true,
+ LogicalFunctions.and(null, false)
+ );
+ assertEquals(left, right);
+ }
+
+ @Test
+ public void testOrAssociativeProperty() {
+ // OR should be associative: (A OR B) OR C = A OR (B OR C)
+ Boolean left = LogicalFunctions.or(
+ LogicalFunctions.or(false, false),
+ true
+ );
+ Boolean right = LogicalFunctions.or(
+ false,
+ LogicalFunctions.or(false, true)
+ );
+ assertEquals(left, right);
+
+ // Test with NULL
+ left = LogicalFunctions.or(
+ LogicalFunctions.or(false, null),
+ true
+ );
+ right = LogicalFunctions.or(
+ false,
+ LogicalFunctions.or(null, true)
+ );
+ assertEquals(left, right);
+ }
+
+ // ==================== Identity and Absorption Tests ====================
+
+ @Test
+ public void testAndIdentityElement() {
+ // TRUE is the identity element for AND: A AND TRUE = A
+ assertEquals(LogicalFunctions.and(true, true), true);
+ assertEquals(LogicalFunctions.and(false, true), false);
+ assertEquals(LogicalFunctions.and(null, true), null);
+ }
+
+ @Test
+ public void testOrIdentityElement() {
+ // FALSE is the identity element for OR: A OR FALSE = A
+ assertEquals(LogicalFunctions.or(true, false), true);
+ assertEquals(LogicalFunctions.or(false, false), false);
+ assertEquals(LogicalFunctions.or(null, false), null);
+ }
+
+ @Test
+ public void testAndAbsorptionElement() {
+ // FALSE is the absorption element for AND: A AND FALSE = FALSE
+ assertFalse(LogicalFunctions.and(true, false));
+ assertFalse(LogicalFunctions.and(false, false));
+ assertFalse(LogicalFunctions.and(null, false));
+ }
+
+ @Test
+ public void testOrAbsorptionElement() {
+ // TRUE is the absorption element for OR: A OR TRUE = TRUE
+ assertTrue(LogicalFunctions.or(true, true));
+ assertTrue(LogicalFunctions.or(false, true));
+ assertTrue(LogicalFunctions.or(null, true));
+ }
+
+ // ==================== Double Negation Tests ====================
+
+ @Test
+ public void testDoubleNegation() {
+ // NOT(NOT(A)) = A
+ assertEquals(
+ LogicalFunctions.not(LogicalFunctions.not(true)),
+ true
+ );
+ assertEquals(
+ LogicalFunctions.not(LogicalFunctions.not(false)),
+ false
+ );
+ assertNull(LogicalFunctions.not(LogicalFunctions.not(null)));
+ }
+
+ // ==================== Trino Compatibility Examples ====================
+
+ @Test
+ public void testTrinoExampleFromDocumentation() {
+ // Examples from Trino documentation:
+ // CAST(null AS boolean) AND true → null
+ assertNull(LogicalFunctions.and(null, true));
+
+ // CAST(null AS boolean) AND false → false
+ assertFalse(LogicalFunctions.and(null, false));
+
+ // CAST(null AS boolean) AND CAST(null AS boolean) → null
+ assertNull(LogicalFunctions.and(null, null));
+
+ // CAST(null AS boolean) OR CAST(null AS boolean) → null
+ assertNull(LogicalFunctions.or(null, null));
+
+ // CAST(null AS boolean) OR false → null
+ assertNull(LogicalFunctions.or(null, false));
+
+ // CAST(null AS boolean) OR true → true
+ assertTrue(LogicalFunctions.or(null, true));
+
+ // NOT CAST(null AS boolean) → null
+ assertNull(LogicalFunctions.not(null));
+ }
+}
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/function/FunctionRegistryTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/function/FunctionRegistryTest.java
index 78600325be4..82c6524ccc8 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/function/FunctionRegistryTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/function/FunctionRegistryTest.java
@@ -42,7 +42,7 @@ public class FunctionRegistryTest {
// TODO: Support these functions
TransformFunctionType.IN, TransformFunctionType.NOT_IN,
TransformFunctionType.IS_TRUE,
TransformFunctionType.IS_NOT_TRUE, TransformFunctionType.IS_FALSE,
TransformFunctionType.IS_NOT_FALSE,
- TransformFunctionType.AND, TransformFunctionType.OR,
TransformFunctionType.JSON_EXTRACT_SCALAR,
+ TransformFunctionType.JSON_EXTRACT_SCALAR,
TransformFunctionType.JSON_EXTRACT_KEY,
TransformFunctionType.TIME_CONVERT,
TransformFunctionType.DATE_TIME_CONVERT_WINDOW_HOP,
TransformFunctionType.ARRAY_LENGTH,
TransformFunctionType.ARRAY_AVERAGE, TransformFunctionType.ARRAY_MIN,
TransformFunctionType.ARRAY_MAX,
@@ -54,7 +54,7 @@ public class FunctionRegistryTest {
// Special filter functions without implementation
FilterKind.TEXT_MATCH, FilterKind.TEXT_CONTAINS, FilterKind.JSON_MATCH,
FilterKind.VECTOR_SIMILARITY,
// TODO: Support these functions
- FilterKind.AND, FilterKind.OR, FilterKind.RANGE, FilterKind.IN,
FilterKind.NOT_IN);
+ FilterKind.RANGE, FilterKind.IN, FilterKind.NOT_IN);
@Test
public void testTransformAndFilterFunctionsRegistered() {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]