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

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


The following commit(s) were added to refs/heads/master by this push:
     new a84a4654cbfc feat: [HUDI-9553] Add equals and hashCode override to 
Predicate inner classes (#13481)
a84a4654cbfc is described below

commit a84a4654cbfc6fd294afcc085a2c7a2160d1ee55
Author: voonhous <[email protected]>
AuthorDate: Fri Jul 3 22:38:53 2026 +0800

    feat: [HUDI-9553] Add equals and hashCode override to Predicate inner 
classes (#13481)
    
    * [HUDI-9553] Add equals and hashCode override to Predicate inner classes
    
    * [HUDI-9553] Use Lombok @EqualsAndHashCode for Predicate inner classes
    
    Replace the hand-written equals/hashCode in Predicates with Lombok
    @EqualsAndHashCode, per review feedback on #13481.
    
    - Add @EqualsAndHashCode to BinaryExpression so its subclasses (And, Or,
      StringStartsWith, StringContains, BinaryComparison) get equality over the
      shared left/operator/right fields via callSuper = true.
    - Annotate the self-contained predicates (In, IsNull, IsNotNull, Not,
      StringStartsWithAny) with @EqualsAndHashCode.
    - Keep TrueExpression/FalseExpression manual: as field-less singletons,
      Lombok would emit a constant hashCode and collide the two classes.
    - Fix import ordering in PredicatesTest.
    
    * [HUDI-9553] Add @EqualsAndHashCode to Literal and NameReference leaf 
operands
    
    Predicate @EqualsAndHashCode compared operands by identity, because the leaf
    expression types Literal and NameReference did not override equals/hashCode.
    Independently built predicate trees with value-equal operands - the case the
    Trino Expression Index comparison relies on - were therefore unequal.
    
    Add @EqualsAndHashCode to both leaf types, plus a PredicatesTest case that
    builds distinct-but-equal operands so equality is exercised by value rather
    than by identity.
---
 .../apache/hudi/expression/BinaryExpression.java   |   2 +
 .../java/org/apache/hudi/expression/Literal.java   |   2 +
 .../org/apache/hudi/expression/NameReference.java  |   2 +
 .../org/apache/hudi/expression/Predicates.java     |  39 ++-
 .../org/apache/hudi/expression/PredicatesTest.java | 265 +++++++++++++++++++++
 5 files changed, 309 insertions(+), 1 deletion(-)

diff --git 
a/hudi-common/src/main/java/org/apache/hudi/expression/BinaryExpression.java 
b/hudi-common/src/main/java/org/apache/hudi/expression/BinaryExpression.java
index d38c531a2359..05dfa8e3ef01 100644
--- a/hudi-common/src/main/java/org/apache/hudi/expression/BinaryExpression.java
+++ b/hudi-common/src/main/java/org/apache/hudi/expression/BinaryExpression.java
@@ -20,6 +20,7 @@ package org.apache.hudi.expression;
 
 import lombok.AccessLevel;
 import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
 import lombok.Getter;
 
 import java.util.Arrays;
@@ -30,6 +31,7 @@ import java.util.List;
  */
 @AllArgsConstructor(access = AccessLevel.PACKAGE)
 @Getter
+@EqualsAndHashCode
 public abstract class BinaryExpression implements Expression {
 
   private final Expression left;
diff --git a/hudi-common/src/main/java/org/apache/hudi/expression/Literal.java 
b/hudi-common/src/main/java/org/apache/hudi/expression/Literal.java
index c364a5595d67..179ef5645646 100644
--- a/hudi-common/src/main/java/org/apache/hudi/expression/Literal.java
+++ b/hudi-common/src/main/java/org/apache/hudi/expression/Literal.java
@@ -22,6 +22,7 @@ import org.apache.hudi.internal.schema.Type;
 import org.apache.hudi.internal.schema.Types;
 
 import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
 import lombok.Getter;
 
 import javax.xml.bind.DatatypeConverter;
@@ -32,6 +33,7 @@ import java.util.UUID;
 
 @AllArgsConstructor
 @Getter
+@EqualsAndHashCode
 public class Literal<T> extends LeafExpression {
 
   public static <V> Literal from(V value) {
diff --git 
a/hudi-common/src/main/java/org/apache/hudi/expression/NameReference.java 
b/hudi-common/src/main/java/org/apache/hudi/expression/NameReference.java
index 19ce43ddfa82..e34d477a0967 100644
--- a/hudi-common/src/main/java/org/apache/hudi/expression/NameReference.java
+++ b/hudi-common/src/main/java/org/apache/hudi/expression/NameReference.java
@@ -21,10 +21,12 @@ package org.apache.hudi.expression;
 import org.apache.hudi.internal.schema.Type;
 
 import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
 import lombok.Getter;
 
 @AllArgsConstructor
 @Getter
+@EqualsAndHashCode
 public class NameReference extends LeafExpression {
 
   private final String name;
diff --git 
a/hudi-common/src/main/java/org/apache/hudi/expression/Predicates.java 
b/hudi-common/src/main/java/org/apache/hudi/expression/Predicates.java
index e5dd07e2bfe6..368f71aaa492 100644
--- a/hudi-common/src/main/java/org/apache/hudi/expression/Predicates.java
+++ b/hudi-common/src/main/java/org/apache/hudi/expression/Predicates.java
@@ -20,6 +20,7 @@ package org.apache.hudi.expression;
 
 import org.apache.hudi.internal.schema.Type;
 
+import lombok.EqualsAndHashCode;
 import lombok.Getter;
 
 import java.util.ArrayList;
@@ -122,6 +123,19 @@ public class Predicates {
     public String toString() {
       return "TRUE";
     }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      return o != null && getClass() == o.getClass();
+    }
+
+    @Override
+    public int hashCode() {
+      return getClass().hashCode();
+    }
   }
 
   public static class FalseExpression extends LeafExpression implements 
Predicate {
@@ -151,8 +165,22 @@ public class Predicates {
     public String toString() {
       return "FALSE";
     }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      return o != null && getClass() == o.getClass();
+    }
+
+    @Override
+    public int hashCode() {
+      return getClass().hashCode();
+    }
   }
 
+  @EqualsAndHashCode(callSuper = true)
   public static class And extends BinaryExpression implements Predicate {
 
     public And(Expression left, Expression right) {
@@ -188,6 +216,7 @@ public class Predicates {
     }
   }
 
+  @EqualsAndHashCode(callSuper = true)
   public static class Or extends BinaryExpression implements Predicate {
 
     public Or(Expression left, Expression right) {
@@ -225,6 +254,7 @@ public class Predicates {
     }
   }
 
+  @EqualsAndHashCode(callSuper = true)
   public static class StringStartsWith extends BinaryExpression implements 
Predicate {
 
     StringStartsWith(Expression left, Expression right) {
@@ -242,6 +272,7 @@ public class Predicates {
     }
   }
 
+  @EqualsAndHashCode(callSuper = true)
   public static class StringContains extends BinaryExpression implements 
Predicate {
 
     StringContains(Expression left, Expression right) {
@@ -259,6 +290,7 @@ public class Predicates {
     }
   }
 
+  @EqualsAndHashCode
   public static class In implements Predicate {
 
     protected final Expression value;
@@ -301,6 +333,7 @@ public class Predicates {
     }
   }
 
+  @EqualsAndHashCode
   public static class IsNull implements Predicate {
 
     protected final Expression child;
@@ -330,6 +363,7 @@ public class Predicates {
     }
   }
 
+  @EqualsAndHashCode
   public static class IsNotNull implements Predicate {
 
     protected final Expression child;
@@ -359,6 +393,7 @@ public class Predicates {
     }
   }
 
+  @EqualsAndHashCode
   public static class Not implements Predicate {
 
     Expression child;
@@ -374,7 +409,7 @@ public class Predicates {
 
     @Override
     public Boolean eval(StructLike data) {
-      return ! (Boolean) child.eval(data);
+      return !(Boolean) child.eval(data);
     }
 
     @Override
@@ -388,6 +423,7 @@ public class Predicates {
     }
   }
 
+  @EqualsAndHashCode(callSuper = true)
   public static class BinaryComparison extends BinaryExpression implements 
Predicate {
 
     public BinaryComparison(Expression left, Operator operator, Expression 
right) {
@@ -417,6 +453,7 @@ public class Predicates {
     }
   }
 
+  @EqualsAndHashCode
   public static class StringStartsWithAny implements Predicate {
 
     @Getter
diff --git 
a/hudi-common/src/test/java/org/apache/hudi/expression/PredicatesTest.java 
b/hudi-common/src/test/java/org/apache/hudi/expression/PredicatesTest.java
new file mode 100644
index 000000000000..b990e0c24754
--- /dev/null
+++ b/hudi-common/src/test/java/org/apache/hudi/expression/PredicatesTest.java
@@ -0,0 +1,265 @@
+/*
+ * 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.hudi.expression;
+
+import org.apache.hudi.expression.Predicates.And;
+import org.apache.hudi.expression.Predicates.BinaryComparison;
+import org.apache.hudi.expression.Predicates.In;
+import org.apache.hudi.expression.Predicates.IsNotNull;
+import org.apache.hudi.expression.Predicates.IsNull;
+import org.apache.hudi.expression.Predicates.Not;
+import org.apache.hudi.expression.Predicates.Or;
+import org.apache.hudi.expression.Predicates.StringContains;
+import org.apache.hudi.expression.Predicates.StringStartsWith;
+import org.apache.hudi.expression.Predicates.StringStartsWithAny;
+import org.apache.hudi.internal.schema.Types;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.apache.hudi.expression.Predicates.alwaysFalse;
+import static org.apache.hudi.expression.Predicates.alwaysTrue;
+import static org.apache.hudi.expression.Predicates.and;
+import static org.apache.hudi.expression.Predicates.contains;
+import static org.apache.hudi.expression.Predicates.eq;
+import static org.apache.hudi.expression.Predicates.gt;
+import static org.apache.hudi.expression.Predicates.gteq;
+import static org.apache.hudi.expression.Predicates.in;
+import static org.apache.hudi.expression.Predicates.isNotNull;
+import static org.apache.hudi.expression.Predicates.isNull;
+import static org.apache.hudi.expression.Predicates.lteq;
+import static org.apache.hudi.expression.Predicates.not;
+import static org.apache.hudi.expression.Predicates.or;
+import static org.apache.hudi.expression.Predicates.startsWith;
+import static org.apache.hudi.expression.Predicates.startsWithAny;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+class PredicatesTest {
+
+  // Define some common literal expressions for use in tests
+  private final Expression a = new Literal<>("a", Types.StringType.get());
+  private final Expression b = new Literal<>(10, Types.IntType.get());
+  private final Expression c = new Literal<>("c", Types.StringType.get());
+
+  @Test
+  void testSingletonPredicates() {
+    // Test alwaysTrue
+    assertEquals(alwaysTrue(), alwaysTrue(), "alwaysTrue() should be equal to 
itself");
+    assertNotEquals(alwaysTrue(), alwaysFalse(), "alwaysTrue() should not be 
equal to alwaysFalse()");
+    assertNotEquals(alwaysTrue(), null, "Predicate should not be equal to 
null");
+    assertNotEquals(alwaysTrue(), new Object(), "Predicate should not be equal 
to a different type");
+    assertEquals(alwaysTrue().hashCode(), alwaysTrue().hashCode(), "Hash code 
for alwaysTrue() should be consistent");
+
+    // Test alwaysFalse
+    assertEquals(alwaysFalse(), alwaysFalse(), "alwaysFalse() should be equal 
to itself");
+    assertEquals(alwaysFalse().hashCode(), alwaysFalse().hashCode(), "Hash 
code for alwaysFalse() should be consistent");
+
+    // Cross-check hash codes
+    assertNotEquals(alwaysTrue().hashCode(), alwaysFalse().hashCode(), "Hash 
codes of singletons should differ");
+  }
+
+  @Test
+  void testAnd() {
+    And p1 = and(a, b);
+    And p2 = and(a, b);
+    And p3 = and(b, a);
+    And p4 = and(a, c);
+
+    // Standard checks
+    assertEquals(p1, p1);
+    assertNotEquals(null, p1);
+    assertNotEquals(new Object(), p1);
+
+    // Equality
+    assertEquals(p1, p2, "And predicates with the same children should be 
equal");
+    assertEquals(p1.hashCode(), p2.hashCode(), "Hash codes for equal And 
predicates should be the same");
+
+    // Inequality
+    assertNotEquals(p1, p3, "And predicates with swapped children should not 
be equal");
+    assertNotEquals(p1, p4, "And predicates with different children should not 
be equal");
+  }
+
+  @Test
+  void testOr() {
+    Or p1 = or(a, b);
+    Or p2 = or(a, b);
+    Or p3 = or(b, a);
+    Or p4 = or(a, c);
+
+    assertEquals(p1, p2, "Or predicates with the same children should be 
equal");
+    assertEquals(p1.hashCode(), p2.hashCode(), "Hash codes for equal Or 
predicates should be the same");
+
+    assertNotEquals(p1, p3, "Or predicates with swapped children should not be 
equal");
+    assertNotEquals(p1, p4, "Or predicates with different children should not 
be equal");
+  }
+
+  @Test
+  void testNot() {
+    Not p1 = not(a);
+    Not p2 = not(a);
+    Not p3 = not(b);
+
+    assertEquals(p1, p2, "Not predicates with the same child should be equal");
+    assertEquals(p1.hashCode(), p2.hashCode(), "Hash codes for equal Not 
predicates should be the same");
+    assertNotEquals(p1, p3, "Not predicates with different children should not 
be equal");
+  }
+
+  @Test
+  void testIsNull() {
+    IsNull p1 = isNull(a);
+    IsNull p2 = isNull(a);
+    IsNull p3 = isNull(b);
+
+    assertEquals(p1, p2, "IsNull predicates with the same child should be 
equal");
+    assertEquals(p1.hashCode(), p2.hashCode(), "Hash codes for equal IsNull 
predicates should be the same");
+    assertNotEquals(p1, p3, "IsNull predicates with different children should 
not be equal");
+  }
+
+  @Test
+  void testIsNotNull() {
+    IsNotNull p1 = isNotNull(a);
+    IsNotNull p2 = isNotNull(a);
+    IsNotNull p3 = isNotNull(b);
+
+    assertEquals(p1, p2, "IsNotNull predicates with the same child should be 
equal");
+    assertEquals(p1.hashCode(), p2.hashCode(), "Hash codes for equal IsNotNull 
predicates should be the same");
+    assertNotEquals(p1, p3, "IsNotNull predicates with different children 
should not be equal");
+  }
+
+  @Test
+  void testBinaryComparison() {
+    BinaryComparison p1 = gteq(a, b); // a >= b
+    BinaryComparison p2 = gteq(a, b); // a >= b (should be equal)
+    BinaryComparison p3 = gt(a, b);   // a > b (not equal, different operator)
+    BinaryComparison p4 = gteq(b, a); // b >= a (not equal, different children)
+    BinaryComparison p5 = lteq(a, b); // a <= b (not equal, different operator)
+
+    assertEquals(p1, p2, "BinaryComparisons with same children and operator 
should be equal");
+    assertEquals(p1.hashCode(), p2.hashCode(), "Hash codes for equal 
BinaryComparisons should be the same");
+
+    assertNotEquals(p1, p3, "BinaryComparisons with different operators should 
not be equal");
+    assertNotEquals(p1, p4, "BinaryComparisons with different children should 
not be equal");
+    assertNotEquals(p1, p5, "BinaryComparisons with different operators should 
not be equal");
+
+    // Test another operator
+    BinaryComparison eq1 = eq(a, b);
+    BinaryComparison eq2 = eq(a, b);
+    assertEquals(eq1, eq2);
+    assertEquals(eq1.hashCode(), eq2.hashCode());
+    assertNotEquals(p1, eq1);
+  }
+
+  @Test
+  void testStringStartsWith() {
+    StringStartsWith p1 = startsWith(a, b);
+    StringStartsWith p2 = startsWith(a, b);
+    StringStartsWith p3 = startsWith(b, a);
+    StringStartsWith p4 = startsWith(a, c);
+
+    assertEquals(p1, p2);
+    assertEquals(p1.hashCode(), p2.hashCode());
+    assertNotEquals(p1, p3);
+    assertNotEquals(p1, p4);
+  }
+
+  @Test
+  void testStringContains() {
+    StringContains p1 = contains(a, b);
+    StringContains p2 = contains(a, b);
+    StringContains p3 = contains(b, a);
+    StringContains p4 = contains(a, c);
+
+    assertEquals(p1, p2);
+    assertEquals(p1.hashCode(), p2.hashCode());
+    assertNotEquals(p1, p3);
+    assertNotEquals(p1, p4);
+  }
+
+  @Test
+  void testIn() {
+    List<Expression> list1 = Arrays.asList(b, c);
+    List<Expression> list2 = Arrays.asList(b, c);
+    List<Expression> list3 = Arrays.asList(c, b); // different order
+    List<Expression> list4 = Collections.singletonList(b); // different content
+
+    In p1 = in(a, list1);
+    In p2 = in(a, list2); // equal
+    In p3 = in(b, list1); // different value expression
+    In p4 = in(a, list3); // different list order
+    In p5 = in(a, list4); // different list content
+
+    assertEquals(p1, p2, "In predicates with same value and list should be 
equal");
+    assertEquals(p1.hashCode(), p2.hashCode(), "Hash codes for equal In 
predicates should be the same");
+
+    assertNotEquals(p1, p3, "In predicates with different value expressions 
should not be equal");
+    assertNotEquals(p1, p4, "In predicates with different list order should 
not be equal");
+    assertNotEquals(p1, p5, "In predicates with different list content should 
not be equal");
+  }
+
+  @Test
+  void testStringStartsWithAny() {
+    List<Expression> list1 = Arrays.asList(b, c);
+    List<Expression> list2 = Arrays.asList(b, c);
+    List<Expression> list3 = Arrays.asList(c, b); // different order
+    List<Expression> list4 = Collections.singletonList(b); // different content
+
+    StringStartsWithAny p1 = startsWithAny(a, list1);
+    StringStartsWithAny p2 = startsWithAny(a, list2); // equal
+    StringStartsWithAny p3 = startsWithAny(b, list1); // different left value
+    StringStartsWithAny p4 = startsWithAny(a, list3); // different list order
+    StringStartsWithAny p5 = startsWithAny(a, list4); // different list content
+
+    assertEquals(p1, p2, "StringStartsWithAny predicates with same children 
should be equal");
+    assertEquals(p1.hashCode(), p2.hashCode(), "Hash codes for equal 
StringStartsWithAny predicates should be the same");
+
+    assertNotEquals(p1, p3, "StringStartsWithAny with different left 
expressions should not be equal");
+    assertNotEquals(p1, p4, "StringStartsWithAny with different list order 
should not be equal");
+    assertNotEquals(p1, p5, "StringStartsWithAny with different list content 
should not be equal");
+  }
+
+  @Test
+  void testEqualsWithDistinctButEqualOperands() {
+    // Operands are built independently (distinct instances) but are 
value-equal. This is the
+    // independently-built-tree case the Trino Expression Index comparison 
relies on: it only holds
+    // if the leaf operands (Literal, NameReference) compare by value rather 
than by identity.
+    Expression lit1 = new Literal<>("a", Types.StringType.get());
+    Expression lit2 = new Literal<>("a", Types.StringType.get());
+    Expression col1 = new NameReference("col");
+    Expression col2 = new NameReference("col");
+
+    // Leaf predicate over distinct-but-equal literals.
+    assertEquals(isNull(lit1), isNull(lit2), "IsNull over value-equal literals 
should be equal");
+    assertEquals(isNull(lit1).hashCode(), isNull(lit2).hashCode());
+
+    // Nested tree built from distinct-but-equal Literal and NameReference 
operands.
+    assertEquals(and(eq(col1, lit1), isNotNull(col1)), and(eq(col2, lit2), 
isNotNull(col2)),
+        "Independently built predicate trees with value-equal operands should 
be equal");
+    assertEquals(and(eq(col1, lit1), isNotNull(col1)).hashCode(),
+        and(eq(col2, lit2), isNotNull(col2)).hashCode());
+
+    // Value still distinguishes: a different literal value or column name is 
not equal.
+    assertNotEquals(isNull(new Literal<>("a", Types.StringType.get())),
+        isNull(new Literal<>("b", Types.StringType.get())));
+    assertNotEquals(isNull(new NameReference("col")), isNull(new 
NameReference("other")));
+  }
+}
\ No newline at end of file

Reply via email to