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