This is an automated email from the ASF dual-hosted git repository.
morrySnow pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new f9fea7d33c6 [fix](planner) align legacy literal compareLiteral with
Nereids ComparableLiteral semantics (#63481)
f9fea7d33c6 is described below
commit f9fea7d33c6603dd04ef7740821932587f5ef680
Author: Chenyang Sun <[email protected]>
AuthorDate: Tue May 26 23:06:38 2026 +0800
[fix](planner) align legacy literal compareLiteral with Nereids
ComparableLiteral semantics (#63481)
IPv4Literal/IPv6Literal.compareLiteral both used to return 0
unconditionally, making any two IP literals appear equal to
LiteralExpr.equals(). The downstream effect:
WHERE ip4 != '1.1.1.1' AND ip4 != '1.1.1.2'
collapsed to just the first conjunct during Set<Expr> dedup inside the
legacy planner's ScanNode.expressionToRanges / PartitionColumnFilter /
HashDistributionPruner paths. Same shape for NOT BETWEEN and NOT IN.
Multiple rows that should have been filtered out leaked through, and
EXPLAIN only showed the first predicate.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
---
fe/fe-catalog/pom.xml | 6 +
.../org/apache/doris/analysis/IPv4Literal.java | 31 +-
.../org/apache/doris/analysis/IPv6Literal.java | 45 +-
.../java/org/apache/doris/analysis/MapLiteral.java | 2 +-
.../org/apache/doris/analysis/StructLiteral.java | 2 +-
.../org/apache/doris/analysis/TimeV2Literal.java | 22 +-
.../org/apache/doris/analysis/ExprEqualsTest.java | 348 ++++++++++++++++
.../analysis/LiteralExprCompareLiteralTest.java | 455 +++++++++++++++++++++
.../doris/analysis/LiteralExprEqualsTest.java | 366 +++++++++++++++++
.../test_ipv4_ipv6_multi_not_equal.out | 36 ++
.../test_ipv4_ipv6_multi_not_equal.groovy | 107 +++++
11 files changed, 1415 insertions(+), 5 deletions(-)
diff --git a/fe/fe-catalog/pom.xml b/fe/fe-catalog/pom.xml
index 74ca1a60fb4..780000dc8d7 100644
--- a/fe/fe-catalog/pom.xml
+++ b/fe/fe-catalog/pom.xml
@@ -53,6 +53,12 @@ under the License.
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
</dependency>
+ <!--
https://mvnrepository.com/artifact/com.googlecode.java-ipv6/java-ipv6 -->
+ <dependency>
+ <groupId>com.googlecode.java-ipv6</groupId>
+ <artifactId>java-ipv6</artifactId>
+ <version>0.17</version>
+ </dependency>
<dependency>
<groupId>org.antlr</groupId>
<artifactId>antlr4-runtime</artifactId>
diff --git
a/fe/fe-catalog/src/main/java/org/apache/doris/analysis/IPv4Literal.java
b/fe/fe-catalog/src/main/java/org/apache/doris/analysis/IPv4Literal.java
index 3348c982733..57fd79867ed 100644
--- a/fe/fe-catalog/src/main/java/org/apache/doris/analysis/IPv4Literal.java
+++ b/fe/fe-catalog/src/main/java/org/apache/doris/analysis/IPv4Literal.java
@@ -108,7 +108,36 @@ public class IPv4Literal extends LiteralExpr {
@Override
public int compareLiteral(LiteralExpr expr) {
- return 0;
+ if (expr instanceof PlaceHolderExpr) {
+ return this.compareLiteral(((PlaceHolderExpr) expr).getLiteral());
+ }
+ if (expr instanceof NullLiteral) {
+ return 1;
+ }
+ if (expr == MaxLiteral.MAX_VALUE) {
+ return -1;
+ }
+ if (expr instanceof IPv4Literal) {
+ return Long.compare(this.value, ((IPv4Literal) expr).value);
+ }
+ throw new RuntimeException("Cannot compare two values with different
data types: "
+ + this + " (" + this.type + ") vs " + expr + " (" + expr.type
+ ")");
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof IPv4Literal)) {
+ return false;
+ }
+ return this.value == ((IPv4Literal) obj).value;
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * super.hashCode() + Long.hashCode(value);
}
@Override
diff --git
a/fe/fe-catalog/src/main/java/org/apache/doris/analysis/IPv6Literal.java
b/fe/fe-catalog/src/main/java/org/apache/doris/analysis/IPv6Literal.java
index 7a46f74368a..fb9a06b7ac8 100644
--- a/fe/fe-catalog/src/main/java/org/apache/doris/analysis/IPv6Literal.java
+++ b/fe/fe-catalog/src/main/java/org/apache/doris/analysis/IPv6Literal.java
@@ -21,6 +21,7 @@ import org.apache.doris.catalog.Type;
import org.apache.doris.common.AnalysisException;
import com.google.gson.annotations.SerializedName;
+import com.googlecode.ipv6.IPv6Address;
import java.util.regex.Pattern;
@@ -90,7 +91,49 @@ public class IPv6Literal extends LiteralExpr {
@Override
public int compareLiteral(LiteralExpr expr) {
- return 0;
+ if (expr instanceof PlaceHolderExpr) {
+ return this.compareLiteral(((PlaceHolderExpr) expr).getLiteral());
+ }
+ if (expr instanceof NullLiteral) {
+ return 1;
+ }
+ if (expr == MaxLiteral.MAX_VALUE) {
+ return -1;
+ }
+ if (expr instanceof IPv6Literal) {
+ return
parseAddress(this.value).compareTo(parseAddress(((IPv6Literal) expr).value));
+ }
+ throw new RuntimeException("Cannot compare two values with different
data types: "
+ + this + " (" + this.type + ") vs " + expr + " (" + expr.type
+ ")");
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof IPv6Literal)) {
+ return false;
+ }
+ return parseAddress(this.value).equals(parseAddress(((IPv6Literal)
obj).value));
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * super.hashCode() + parseAddress(this.value).hashCode();
+ }
+
+ // IPv6Address keeps the full 128-bit value for IPv4-mapped literals
+ // (e.g. ::ffff:0.0.0.1, ::ffff:0:1) and matches the canonicalization used
by
+ // the Nereids IPv6Literal, so dedup/range logic stays consistent across
both
+ // planners. InetAddress.getByName would otherwise collapse mapped forms
to a
+ // 4-byte Inet4Address and hash-collide with addresses like ::1.
+ private static IPv6Address parseAddress(String ipv6) {
+ try {
+ return IPv6Address.fromString(ipv6);
+ } catch (Exception e) {
+ throw new IllegalStateException("Invalid IPv6 literal: " + ipv6,
e);
+ }
}
@Override
diff --git
a/fe/fe-catalog/src/main/java/org/apache/doris/analysis/MapLiteral.java
b/fe/fe-catalog/src/main/java/org/apache/doris/analysis/MapLiteral.java
index 2ffa94a8df0..78ee4c80f44 100644
--- a/fe/fe-catalog/src/main/java/org/apache/doris/analysis/MapLiteral.java
+++ b/fe/fe-catalog/src/main/java/org/apache/doris/analysis/MapLiteral.java
@@ -95,7 +95,7 @@ public class MapLiteral extends LiteralExpr {
@Override
public int compareLiteral(LiteralExpr expr) {
- return 0;
+ throw new RuntimeException("Not support comparison between MAP
literals");
}
@Override
diff --git
a/fe/fe-catalog/src/main/java/org/apache/doris/analysis/StructLiteral.java
b/fe/fe-catalog/src/main/java/org/apache/doris/analysis/StructLiteral.java
index 8476f446417..7dd1cd9149b 100644
--- a/fe/fe-catalog/src/main/java/org/apache/doris/analysis/StructLiteral.java
+++ b/fe/fe-catalog/src/main/java/org/apache/doris/analysis/StructLiteral.java
@@ -88,7 +88,7 @@ public class StructLiteral extends LiteralExpr {
@Override
public int compareLiteral(LiteralExpr expr) {
- return 0;
+ throw new RuntimeException("Not support comparison between STRUCT
literals");
}
@Override
diff --git
a/fe/fe-catalog/src/main/java/org/apache/doris/analysis/TimeV2Literal.java
b/fe/fe-catalog/src/main/java/org/apache/doris/analysis/TimeV2Literal.java
index 6379fc3b6a1..96a4014bd59 100644
--- a/fe/fe-catalog/src/main/java/org/apache/doris/analysis/TimeV2Literal.java
+++ b/fe/fe-catalog/src/main/java/org/apache/doris/analysis/TimeV2Literal.java
@@ -85,7 +85,27 @@ public class TimeV2Literal extends LiteralExpr {
@Override
public int compareLiteral(LiteralExpr expr) {
- return 0;
+ throw new RuntimeException("Not support comparison between TIMEV2
literals");
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof TimeV2Literal)) {
+ return false;
+ }
+ return Double.compare(this.getValue(), ((TimeV2Literal)
obj).getValue()) == 0;
+ }
+
+ @Override
+ public int hashCode() {
+ // Must mirror equals(), which only compares getValue().
super.hashCode()
+ // mixes in the ScalarType, so TIMEV2(0) and TIMEV2(6) would otherwise
+ // produce different hashes for the same logical time value and break
the
+ // equals/hashCode contract — predicate dedup would bucket them apart.
+ return Double.hashCode(getValue());
}
@Override
diff --git
a/fe/fe-catalog/src/test/java/org/apache/doris/analysis/ExprEqualsTest.java
b/fe/fe-catalog/src/test/java/org/apache/doris/analysis/ExprEqualsTest.java
new file mode 100644
index 00000000000..e3d025e7981
--- /dev/null
+++ b/fe/fe-catalog/src/test/java/org/apache/doris/analysis/ExprEqualsTest.java
@@ -0,0 +1,348 @@
+// 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.doris.analysis;
+
+import org.apache.doris.catalog.Function.NullableMode;
+import org.apache.doris.catalog.Type;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+// Covers equals()/hashCode() for non-literal Expr subclasses that override
+// equals. Expr.equals already checks getClass() and recursive children
+// equality; subclasses additionally compare their distinguishing fields
+// (operator, isNot* flag, name, etc.). These tests pin both pieces.
+//
+// Skipped:
+// - BetweenPredicate / SearchPredicate: no usable public constructor for unit
+// tests (BetweenPredicate is rewritten away pre-execution, SearchPredicate
+// needs a parsed QsPlan).
+// - ColumnRefExpr / EncryptKeyRef / LambdaFunctionExpr /
TimestampArithmeticExpr:
+// no equals override (inherit Expr.equals as-is, already exercised via the
+// subclass tests below through children comparison).
+class ExprEqualsTest {
+
+ private static IntLiteral intLit(long v) {
+ return new IntLiteral(v);
+ }
+
+ @Nested
+ class ArithmeticExprEquals {
+ private ArithmeticExpr make(ArithmeticExpr.Operator op, long l, long
r) {
+ return new ArithmeticExpr(op, intLit(l), intLit(r),
+ Type.BIGINT, NullableMode.DEPEND_ON_ARGUMENT, true);
+ }
+
+ @Test
+ void sameOpAndChildren() {
+ Assertions.assertEquals(
+ make(ArithmeticExpr.Operator.ADD, 1, 2),
+ make(ArithmeticExpr.Operator.ADD, 1, 2));
+ }
+
+ @Test
+ void differentOp() {
+ Assertions.assertNotEquals(
+ make(ArithmeticExpr.Operator.ADD, 1, 2),
+ make(ArithmeticExpr.Operator.SUBTRACT, 1, 2));
+ }
+
+ @Test
+ void differentChildren() {
+ Assertions.assertNotEquals(
+ make(ArithmeticExpr.Operator.ADD, 1, 2),
+ make(ArithmeticExpr.Operator.ADD, 1, 3));
+ }
+
+ @Test
+ void hashCodeMatchesEquality() {
+ Assertions.assertEquals(
+ make(ArithmeticExpr.Operator.ADD, 1, 2).hashCode(),
+ make(ArithmeticExpr.Operator.ADD, 1, 2).hashCode());
+ }
+ }
+
+ @Nested
+ class BinaryPredicateEquals {
+ @Test
+ void sameOpAndChildren() {
+ Assertions.assertEquals(
+ new BinaryPredicate(BinaryPredicate.Operator.EQ,
intLit(1), intLit(2)),
+ new BinaryPredicate(BinaryPredicate.Operator.EQ,
intLit(1), intLit(2)));
+ }
+
+ @Test
+ void differentOp() {
+ Assertions.assertNotEquals(
+ new BinaryPredicate(BinaryPredicate.Operator.EQ,
intLit(1), intLit(2)),
+ new BinaryPredicate(BinaryPredicate.Operator.NE,
intLit(1), intLit(2)));
+ }
+
+ @Test
+ void differentChildren() {
+ Assertions.assertNotEquals(
+ new BinaryPredicate(BinaryPredicate.Operator.EQ,
intLit(1), intLit(2)),
+ new BinaryPredicate(BinaryPredicate.Operator.EQ,
intLit(1), intLit(3)));
+ }
+ }
+
+ @Nested
+ class CastExprEquals {
+ @Test
+ void sameTargetTypeAndChild() {
+ Assertions.assertEquals(
+ new CastExpr(Type.BIGINT, intLit(1), true),
+ new CastExpr(Type.BIGINT, intLit(1), true));
+ }
+
+ @Test
+ void differentChild() {
+ Assertions.assertNotEquals(
+ new CastExpr(Type.BIGINT, intLit(1), true),
+ new CastExpr(Type.BIGINT, intLit(2), true));
+ }
+ }
+
+ @Nested
+ class CompoundPredicateEquals {
+ private CompoundPredicate make(CompoundPredicate.Operator op) {
+ BinaryPredicate left = new BinaryPredicate(
+ BinaryPredicate.Operator.EQ, intLit(1), intLit(1));
+ BinaryPredicate right = new BinaryPredicate(
+ BinaryPredicate.Operator.EQ, intLit(2), intLit(2));
+ return new CompoundPredicate(op, left, right);
+ }
+
+ @Test
+ void sameOpAndChildren() {
+ Assertions.assertEquals(make(CompoundPredicate.Operator.AND),
+ make(CompoundPredicate.Operator.AND));
+ }
+
+ @Test
+ void differentOp() {
+ Assertions.assertNotEquals(make(CompoundPredicate.Operator.AND),
+ make(CompoundPredicate.Operator.OR));
+ }
+ }
+
+ @Nested
+ class FunctionCallExprEquals {
+ @Test
+ void sameNameAndArgs() {
+ Assertions.assertEquals(
+ new FunctionCallExpr("abs", ImmutableList.of(intLit(1)),
true),
+ new FunctionCallExpr("abs", ImmutableList.of(intLit(1)),
true));
+ }
+
+ @Test
+ void differentFunctionName() {
+ Assertions.assertNotEquals(
+ new FunctionCallExpr("abs", ImmutableList.of(intLit(1)),
true),
+ new FunctionCallExpr("ceil", ImmutableList.of(intLit(1)),
true));
+ }
+
+ @Test
+ void differentArgs() {
+ Assertions.assertNotEquals(
+ new FunctionCallExpr("abs", ImmutableList.of(intLit(1)),
true),
+ new FunctionCallExpr("abs", ImmutableList.of(intLit(2)),
true));
+ }
+ }
+
+ @Nested
+ class InformationFunctionEquals {
+ @Test
+ void sameFuncType() {
+ Assertions.assertEquals(new InformationFunction("CURRENT_USER"),
+ new InformationFunction("CURRENT_USER"));
+ }
+
+ @Test
+ void differentFuncType() {
+ Assertions.assertNotEquals(new InformationFunction("CURRENT_USER"),
+ new InformationFunction("DATABASE"));
+ }
+ }
+
+ @Nested
+ class InPredicateEquals {
+ @Test
+ void sameContent() {
+ Assertions.assertEquals(
+ new InPredicate(intLit(0),
+ ImmutableList.of(intLit(1), intLit(2)), false),
+ new InPredicate(intLit(0),
+ ImmutableList.of(intLit(1), intLit(2)), false));
+ }
+
+ @Test
+ void differentIsNotIn() {
+ Assertions.assertNotEquals(
+ new InPredicate(intLit(0),
+ ImmutableList.of(intLit(1), intLit(2)), false),
+ new InPredicate(intLit(0),
+ ImmutableList.of(intLit(1), intLit(2)), true));
+ }
+
+ @Test
+ void differentList() {
+ Assertions.assertNotEquals(
+ new InPredicate(intLit(0),
+ ImmutableList.of(intLit(1), intLit(2)), false),
+ new InPredicate(intLit(0),
+ ImmutableList.of(intLit(1), intLit(3)), false));
+ }
+ }
+
+ @Nested
+ class IsNullPredicateEquals {
+ @Test
+ void sameContent() {
+ Assertions.assertEquals(
+ new IsNullPredicate(intLit(1), false),
+ new IsNullPredicate(intLit(1), false));
+ }
+
+ @Test
+ void differentIsNotNull() {
+ Assertions.assertNotEquals(
+ new IsNullPredicate(intLit(1), false),
+ new IsNullPredicate(intLit(1), true));
+ }
+ }
+
+ @Nested
+ class LikePredicateEquals {
+ @Test
+ void sameOpAndChildren() {
+ Assertions.assertEquals(
+ new LikePredicate(LikePredicate.Operator.LIKE,
+ new StringLiteral("abc"), new StringLiteral("a%")),
+ new LikePredicate(LikePredicate.Operator.LIKE,
+ new StringLiteral("abc"), new
StringLiteral("a%")));
+ }
+
+ @Test
+ void differentOp() {
+ Assertions.assertNotEquals(
+ new LikePredicate(LikePredicate.Operator.LIKE,
+ new StringLiteral("abc"), new StringLiteral("a%")),
+ new LikePredicate(LikePredicate.Operator.REGEXP,
+ new StringLiteral("abc"), new
StringLiteral("a%")));
+ }
+
+ @Test
+ void differentPattern() {
+ Assertions.assertNotEquals(
+ new LikePredicate(LikePredicate.Operator.LIKE,
+ new StringLiteral("abc"), new StringLiteral("a%")),
+ new LikePredicate(LikePredicate.Operator.LIKE,
+ new StringLiteral("abc"), new
StringLiteral("b%")));
+ }
+ }
+
+ @Nested
+ class MatchPredicateEquals {
+ private MatchPredicate make(MatchPredicate.Operator op) {
+ return new MatchPredicate(op,
+ new StringLiteral("col"), new StringLiteral("term"),
+ Type.BOOLEAN, NullableMode.ALWAYS_NULLABLE, null, true);
+ }
+
+ @Test
+ void sameOp() {
+ Assertions.assertEquals(make(MatchPredicate.Operator.MATCH_ANY),
+ make(MatchPredicate.Operator.MATCH_ANY));
+ }
+
+ @Test
+ void differentOp() {
+ Assertions.assertNotEquals(make(MatchPredicate.Operator.MATCH_ANY),
+ make(MatchPredicate.Operator.MATCH_ALL));
+ }
+ }
+
+ @Nested
+ class SlotRefEquals {
+ @Test
+ void sameTypeAndNullableAndNoDesc() {
+ // Without a SlotDescriptor both refs hit the notCheckDescIdEquals
+ // path which compares table name + column name (both null here).
+ Assertions.assertEquals(
+ new SlotRef(Type.INT, true),
+ new SlotRef(Type.INT, true));
+ }
+
+ @Test
+ void notEqualToDifferentExprClass() {
+ Assertions.assertNotEquals(new SlotRef(Type.INT, true), intLit(1));
+ }
+ }
+
+ @Nested
+ class VariableExprEquals {
+ @Test
+ void sameNameAndScope() {
+ Assertions.assertEquals(new VariableExpr("autocommit"),
+ new VariableExpr("autocommit"));
+ }
+
+ @Test
+ void differentName() {
+ Assertions.assertNotEquals(new VariableExpr("autocommit"),
+ new VariableExpr("time_zone"));
+ }
+
+ @Test
+ void differentScope() {
+ Assertions.assertNotEquals(
+ new VariableExpr("autocommit", SetType.SESSION),
+ new VariableExpr("autocommit", SetType.GLOBAL));
+ }
+ }
+
+ @Nested
+ class CaseExprEquals {
+ private CaseExpr make(boolean withCaseExpr, boolean withElse) {
+ CaseWhenClause clause = new CaseWhenClause(intLit(1), intLit(10));
+ // CaseExpr's first child is the optional case-expr (nullable),
+ // followed by each when/then pair, optionally followed by an else.
+ if (withCaseExpr) {
+ CaseExpr ce = new CaseExpr(ImmutableList.of(clause),
+ withElse ? intLit(99) : null, true);
+ ce.getChildren().add(0, intLit(0));
+ return ce;
+ }
+ return new CaseExpr(ImmutableList.of(clause),
+ withElse ? intLit(99) : null, true);
+ }
+
+ @Test
+ void sameShape() {
+ Assertions.assertEquals(make(false, true), make(false, true));
+ }
+
+ @Test
+ void differentHasElse() {
+ Assertions.assertNotEquals(make(false, true), make(false, false));
+ }
+ }
+}
diff --git
a/fe/fe-catalog/src/test/java/org/apache/doris/analysis/LiteralExprCompareLiteralTest.java
b/fe/fe-catalog/src/test/java/org/apache/doris/analysis/LiteralExprCompareLiteralTest.java
new file mode 100644
index 00000000000..6bc4d1a8874
--- /dev/null
+++
b/fe/fe-catalog/src/test/java/org/apache/doris/analysis/LiteralExprCompareLiteralTest.java
@@ -0,0 +1,455 @@
+// 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.doris.analysis;
+
+import org.apache.doris.catalog.ArrayType;
+import org.apache.doris.catalog.ScalarType;
+import org.apache.doris.catalog.Type;
+import org.apache.doris.common.AnalysisException;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+// Covers compareLiteral() for every legacy LiteralExpr subclass that the
+// optimizer relies on for predicate dedup, partition pruning, and range
+// intersection. MapLiteral/StructLiteral/TimeV2Literal are intentionally
+// skipped: they all return 0 today, but SQL has no way to feed two of them
+// into the dedup paths so the bug is unreachable from queries.
+class LiteralExprCompareLiteralTest {
+
+ @Nested
+ class BoolLiteralCompare {
+ @Test
+ void sameValueIsZero() {
+ Assertions.assertEquals(0, new
BoolLiteral(true).compareLiteral(new BoolLiteral(true)));
+ Assertions.assertEquals(0, new
BoolLiteral(false).compareLiteral(new BoolLiteral(false)));
+ }
+
+ @Test
+ void falseLessThanTrue() {
+ Assertions.assertTrue(new BoolLiteral(false).compareLiteral(new
BoolLiteral(true)) < 0);
+ Assertions.assertTrue(new BoolLiteral(true).compareLiteral(new
BoolLiteral(false)) > 0);
+ }
+
+ @Test
+ void vsNullLiteralReturnsOne() {
+ Assertions.assertEquals(1, new
BoolLiteral(true).compareLiteral(new NullLiteral()));
+ }
+
+ @Test
+ void vsMaxLiteralReturnsMinusOne() {
+ Assertions.assertEquals(-1, new
BoolLiteral(true).compareLiteral(MaxLiteral.MAX_VALUE));
+ }
+
+ @Test
+ void vsPlaceHolderDelegatesToInner() {
+ BoolLiteral self = new BoolLiteral(true);
+ Assertions.assertEquals(0, self.compareLiteral(new
PlaceHolderExpr(new BoolLiteral(true))));
+ Assertions.assertTrue(self.compareLiteral(new PlaceHolderExpr(new
BoolLiteral(false))) > 0);
+ }
+ }
+
+ @Nested
+ class IntLiteralCompare {
+ @Test
+ void sameValueIsZero() {
+ Assertions.assertEquals(0, new IntLiteral(42).compareLiteral(new
IntLiteral(42)));
+ }
+
+ @Test
+ void orderingMatchesValue() {
+ Assertions.assertTrue(new IntLiteral(1).compareLiteral(new
IntLiteral(2)) < 0);
+ Assertions.assertTrue(new IntLiteral(2).compareLiteral(new
IntLiteral(1)) > 0);
+ }
+
+ @Test
+ void vsNullLiteralReturnsOne() {
+ Assertions.assertEquals(1, new IntLiteral(0).compareLiteral(new
NullLiteral()));
+ }
+
+ @Test
+ void vsMaxLiteralReturnsMinusOne() {
+ Assertions.assertEquals(-1, new
IntLiteral(Long.MAX_VALUE).compareLiteral(MaxLiteral.MAX_VALUE));
+ }
+
+ @Test
+ void vsPlaceHolderDelegatesToInner() {
+ Assertions.assertEquals(0, new IntLiteral(7).compareLiteral(new
PlaceHolderExpr(new IntLiteral(7))));
+ }
+ }
+
+ @Nested
+ class FloatLiteralCompare {
+ @Test
+ void sameValueIsZero() {
+ Assertions.assertEquals(0, new
FloatLiteral(1.5).compareLiteral(new FloatLiteral(1.5)));
+ }
+
+ @Test
+ void orderingMatchesValue() {
+ Assertions.assertTrue(new FloatLiteral(1.0).compareLiteral(new
FloatLiteral(2.0)) < 0);
+ Assertions.assertTrue(new FloatLiteral(2.0).compareLiteral(new
FloatLiteral(1.0)) > 0);
+ }
+
+ @Test
+ void vsNullLiteralReturnsOne() {
+ Assertions.assertEquals(1, new
FloatLiteral(0.0).compareLiteral(new NullLiteral()));
+ }
+
+ @Test
+ void vsMaxLiteralReturnsMinusOne() {
+ Assertions.assertEquals(-1, new
FloatLiteral(1.0).compareLiteral(MaxLiteral.MAX_VALUE));
+ }
+ }
+
+ @Nested
+ class DecimalLiteralCompare {
+ @Test
+ void sameValueIsZero() {
+ BigDecimal v = new BigDecimal("12.34");
+ DecimalLiteral a = new DecimalLiteral(v,
ScalarType.createDecimalV3Type(10, 2));
+ DecimalLiteral b = new DecimalLiteral(v,
ScalarType.createDecimalV3Type(10, 2));
+ Assertions.assertEquals(0, a.compareLiteral(b));
+ }
+
+ @Test
+ void orderingMatchesValue() {
+ DecimalLiteral small = new DecimalLiteral(new BigDecimal("1.00"),
ScalarType.createDecimalV3Type(10, 2));
+ DecimalLiteral big = new DecimalLiteral(new BigDecimal("9.99"),
ScalarType.createDecimalV3Type(10, 2));
+ Assertions.assertTrue(small.compareLiteral(big) < 0);
+ Assertions.assertTrue(big.compareLiteral(small) > 0);
+ }
+
+ @Test
+ void vsNullLiteralReturnsOne() {
+ DecimalLiteral d = new DecimalLiteral(BigDecimal.ZERO,
ScalarType.createDecimalV3Type(10, 2));
+ Assertions.assertEquals(1, d.compareLiteral(new NullLiteral()));
+ }
+
+ @Test
+ void vsMaxLiteralReturnsMinusOne() {
+ DecimalLiteral d = new DecimalLiteral(BigDecimal.ZERO,
ScalarType.createDecimalV3Type(10, 2));
+ Assertions.assertEquals(-1,
d.compareLiteral(MaxLiteral.MAX_VALUE));
+ }
+ }
+
+ @Nested
+ class LargeIntLiteralCompare {
+ @Test
+ void sameValueIsZero() {
+ BigInteger v = new BigInteger("12345678901234567890");
+ Assertions.assertEquals(0, new
LargeIntLiteral(v).compareLiteral(new LargeIntLiteral(v)));
+ }
+
+ @Test
+ void orderingMatchesValue() {
+ LargeIntLiteral small = new LargeIntLiteral(BigInteger.ONE);
+ LargeIntLiteral big = new LargeIntLiteral(new
BigInteger("12345678901234567890"));
+ Assertions.assertTrue(small.compareLiteral(big) < 0);
+ Assertions.assertTrue(big.compareLiteral(small) > 0);
+ }
+
+ @Test
+ void vsNullLiteralReturnsOne() {
+ Assertions.assertEquals(1, new
LargeIntLiteral(BigInteger.ZERO).compareLiteral(new NullLiteral()));
+ }
+
+ @Test
+ void vsMaxLiteralReturnsMinusOne() {
+ Assertions.assertEquals(-1, new
LargeIntLiteral(BigInteger.ZERO).compareLiteral(MaxLiteral.MAX_VALUE));
+ }
+ }
+
+ @Nested
+ class DateLiteralCompare {
+ @Test
+ void sameValueIsZero() {
+ Assertions.assertEquals(0,
+ new DateLiteral(2026, 5, 21).compareLiteral(new
DateLiteral(2026, 5, 21)));
+ }
+
+ @Test
+ void orderingMatchesValue() {
+ DateLiteral earlier = new DateLiteral(2026, 5, 21);
+ DateLiteral later = new DateLiteral(2026, 5, 22);
+ Assertions.assertTrue(earlier.compareLiteral(later) < 0);
+ Assertions.assertTrue(later.compareLiteral(earlier) > 0);
+ }
+
+ @Test
+ void vsNullLiteralReturnsOne() {
+ Assertions.assertEquals(1, new DateLiteral(2026, 5,
21).compareLiteral(new NullLiteral()));
+ }
+
+ @Test
+ void vsMaxLiteralReturnsMinusOne() {
+ Assertions.assertEquals(-1, new DateLiteral(2026, 5,
21).compareLiteral(MaxLiteral.MAX_VALUE));
+ }
+ }
+
+ @Nested
+ class StringLiteralCompare {
+ @Test
+ void sameValueIsZero() {
+ Assertions.assertEquals(0, new
StringLiteral("abc").compareLiteral(new StringLiteral("abc")));
+ }
+
+ @Test
+ void orderingMatchesValue() {
+ Assertions.assertTrue(new StringLiteral("abc").compareLiteral(new
StringLiteral("abd")) < 0);
+ Assertions.assertTrue(new StringLiteral("abd").compareLiteral(new
StringLiteral("abc")) > 0);
+ }
+
+ @Test
+ void vsNullLiteralReturnsOne() {
+ Assertions.assertEquals(1, new
StringLiteral("x").compareLiteral(new NullLiteral()));
+ }
+
+ @Test
+ void vsMaxLiteralReturnsMinusOne() {
+ Assertions.assertEquals(-1, new
StringLiteral("zzz").compareLiteral(MaxLiteral.MAX_VALUE));
+ }
+ }
+
+ @Nested
+ class IPv4LiteralCompare {
+ @Test
+ void sameValueIsZero() throws AnalysisException {
+ Assertions.assertEquals(0, new
IPv4Literal("1.1.1.1").compareLiteral(new IPv4Literal("1.1.1.1")));
+ }
+
+ @Test
+ void orderingMatchesValue() throws AnalysisException {
+ Assertions.assertTrue(new
IPv4Literal("1.1.1.1").compareLiteral(new IPv4Literal("1.1.1.2")) < 0);
+ Assertions.assertTrue(new
IPv4Literal("1.1.1.2").compareLiteral(new IPv4Literal("1.1.1.1")) > 0);
+ }
+
+ @Test
+ void vsNullLiteralReturnsOne() throws AnalysisException {
+ Assertions.assertEquals(1, new
IPv4Literal("1.1.1.1").compareLiteral(new NullLiteral()));
+ }
+
+ @Test
+ void vsMaxLiteralReturnsMinusOne() throws AnalysisException {
+ Assertions.assertEquals(-1, new
IPv4Literal("255.255.255.255").compareLiteral(MaxLiteral.MAX_VALUE));
+ }
+
+ @Test
+ void vsPlaceHolderDelegatesToInner() throws AnalysisException {
+ IPv4Literal self = new IPv4Literal("1.1.1.1");
+ Assertions.assertEquals(0, self.compareLiteral(new
PlaceHolderExpr(new IPv4Literal("1.1.1.1"))));
+ Assertions.assertTrue(self.compareLiteral(new PlaceHolderExpr(new
IPv4Literal("1.1.1.2"))) < 0);
+ }
+
+ @Test
+ void crossTypeThrows() throws AnalysisException {
+ Assertions.assertThrows(RuntimeException.class,
+ () -> new IPv4Literal("1.1.1.1").compareLiteral(new
IntLiteral(1)));
+ }
+ }
+
+ @Nested
+ class IPv6LiteralCompare {
+ @Test
+ void sameValueIsZero() throws AnalysisException {
+ Assertions.assertEquals(0, new
IPv6Literal("::1").compareLiteral(new IPv6Literal("::1")));
+ }
+
+ @Test
+ void canonicalizesEqualAddresses() throws AnalysisException {
+ // "::1" and "0:0:0:0:0:0:0:1" are the same address; with the fix
+ // they must compare equal even though the strings differ.
+ Assertions.assertEquals(0,
+ new IPv6Literal("::1").compareLiteral(new
IPv6Literal("0:0:0:0:0:0:0:1")));
+ }
+
+ @Test
+ void orderingMatchesValue() throws AnalysisException {
+ Assertions.assertTrue(new IPv6Literal("::1").compareLiteral(new
IPv6Literal("::2")) < 0);
+ Assertions.assertTrue(new IPv6Literal("::2").compareLiteral(new
IPv6Literal("::1")) > 0);
+ }
+
+ @Test
+ void vsNullLiteralReturnsOne() throws AnalysisException {
+ Assertions.assertEquals(1, new
IPv6Literal("::1").compareLiteral(new NullLiteral()));
+ }
+
+ @Test
+ void vsMaxLiteralReturnsMinusOne() throws AnalysisException {
+ Assertions.assertEquals(-1, new
IPv6Literal("ffff::ffff").compareLiteral(MaxLiteral.MAX_VALUE));
+ }
+
+ @Test
+ void crossTypeThrows() throws AnalysisException {
+ Assertions.assertThrows(RuntimeException.class,
+ () -> new IPv6Literal("::1").compareLiteral(new
StringLiteral("::1")));
+ }
+
+ @Test
+ void ipv4MappedNotEqualToLoopback() throws AnalysisException {
+ // ::ffff:0.0.0.1 must keep its full 128-bit value (the ::ffff:
+ // prefix is part of the address) and order strictly above ::1.
+ // Earlier the helper let Inet4Address collapse it to 4 bytes, so
+ // both literals compared as BigInteger(1) and dedup folded them
+ // into one range.
+ Assertions.assertTrue(
+ new IPv6Literal("::ffff:0.0.0.1").compareLiteral(new
IPv6Literal("::1")) > 0);
+ Assertions.assertNotEquals(0,
+ new IPv6Literal("::ffff:0.0.0.1").compareLiteral(new
IPv6Literal("::1")));
+ }
+ }
+
+ @Nested
+ class ArrayLiteralCompare {
+ private final Type intArray = new ArrayType(Type.INT);
+
+ @Test
+ void sameElementsIsZero() {
+ ArrayLiteral a = new ArrayLiteral(intArray, new IntLiteral(1), new
IntLiteral(2));
+ ArrayLiteral b = new ArrayLiteral(intArray, new IntLiteral(1), new
IntLiteral(2));
+ Assertions.assertEquals(0, a.compareLiteral(b));
+ }
+
+ @Test
+ void elementWiseDifference() {
+ ArrayLiteral a = new ArrayLiteral(intArray, new IntLiteral(1), new
IntLiteral(2));
+ ArrayLiteral b = new ArrayLiteral(intArray, new IntLiteral(1), new
IntLiteral(3));
+ Assertions.assertTrue(a.compareLiteral(b) < 0);
+ Assertions.assertTrue(b.compareLiteral(a) > 0);
+ }
+
+ @Test
+ void shorterIsLessWhenPrefixEqual() {
+ ArrayLiteral shorter = new ArrayLiteral(intArray, new
IntLiteral(1));
+ ArrayLiteral longer = new ArrayLiteral(intArray, new
IntLiteral(1), new IntLiteral(2));
+ Assertions.assertTrue(shorter.compareLiteral(longer) < 0);
+ Assertions.assertTrue(longer.compareLiteral(shorter) > 0);
+ }
+ }
+
+ @Nested
+ class VarBinaryLiteralCompare {
+ @Test
+ void sameBytesIsZero() throws AnalysisException {
+ VarBinaryLiteral a = new VarBinaryLiteral(new byte[]{1, 2, 3});
+ VarBinaryLiteral b = new VarBinaryLiteral(new byte[]{1, 2, 3});
+ Assertions.assertEquals(0, a.compareLiteral(b));
+ }
+
+ @Test
+ void unsignedByteOrdering() throws AnalysisException {
+ // 0xFF must be > 0x01 because compare uses unsigned bytes.
+ VarBinaryLiteral small = new VarBinaryLiteral(new byte[]{0x01});
+ VarBinaryLiteral big = new VarBinaryLiteral(new byte[]{(byte)
0xFF});
+ Assertions.assertTrue(small.compareLiteral(big) < 0);
+ Assertions.assertTrue(big.compareLiteral(small) > 0);
+ }
+ }
+
+ @Nested
+ class JsonLiteralCompare {
+ @Test
+ void alwaysThrows() throws AnalysisException {
+ JsonLiteral a = new JsonLiteral("{\"a\":1}");
+ JsonLiteral b = new JsonLiteral("{\"a\":2}");
+ Assertions.assertThrows(RuntimeException.class, () ->
a.compareLiteral(b));
+ }
+ }
+
+ @Nested
+ class MaxLiteralCompare {
+ @Test
+ void vsMaxIsZero() {
+ Assertions.assertEquals(0,
MaxLiteral.MAX_VALUE.compareLiteral(MaxLiteral.MAX_VALUE));
+ }
+
+ @Test
+ void vsAnythingElseIsOne() {
+ Assertions.assertEquals(1, MaxLiteral.MAX_VALUE.compareLiteral(new
IntLiteral(42)));
+ Assertions.assertEquals(1, MaxLiteral.MAX_VALUE.compareLiteral(new
NullLiteral()));
+ Assertions.assertEquals(1, MaxLiteral.MAX_VALUE.compareLiteral(new
StringLiteral("zzz")));
+ }
+ }
+
+ @Nested
+ class NullLiteralCompare {
+ @Test
+ void vsNullIsZero() {
+ Assertions.assertEquals(0, new NullLiteral().compareLiteral(new
NullLiteral()));
+ }
+
+ @Test
+ void vsValueLiteralIsMinusOne() {
+ Assertions.assertEquals(-1, new NullLiteral().compareLiteral(new
IntLiteral(42)));
+ Assertions.assertEquals(-1, new NullLiteral().compareLiteral(new
StringLiteral("x")));
+ }
+
+ @Test
+ void vsPlaceHolderDelegates() {
+ Assertions.assertEquals(0, new NullLiteral().compareLiteral(new
PlaceHolderExpr(new NullLiteral())));
+ }
+ }
+
+ @Nested
+ class PlaceHolderExprCompare {
+ @Test
+ void delegatesToWrappedLiteral() {
+ PlaceHolderExpr wrapsTen = new PlaceHolderExpr(new IntLiteral(10));
+ PlaceHolderExpr wrapsTwenty = new PlaceHolderExpr(new
IntLiteral(20));
+ Assertions.assertEquals(0, wrapsTen.compareLiteral(new
IntLiteral(10)));
+ Assertions.assertTrue(wrapsTen.compareLiteral(new IntLiteral(20))
< 0);
+ Assertions.assertTrue(wrapsTwenty.compareLiteral(new
IntLiteral(10)) > 0);
+ }
+ }
+
+ // The Nereids counterparts for MAP / STRUCT / TIMEV2 do NOT implement
+ // ComparableLiteral. In legacy these returned 0, silently making any two
+ // such literals compare-equal in dedup paths. They now throw so the bug
+ // surfaces loudly if the planner ever does feed them through
compareLiteral.
+ @Nested
+ class MapLiteralCompare {
+ @Test
+ void alwaysThrows() {
+ Assertions.assertThrows(RuntimeException.class,
+ () -> new MapLiteral().compareLiteral(new MapLiteral()));
+ }
+ }
+
+ @Nested
+ class StructLiteralCompare {
+ @Test
+ void alwaysThrows() {
+ Assertions.assertThrows(RuntimeException.class,
+ () -> new StructLiteral().compareLiteral(new
StructLiteral()));
+ }
+ }
+
+ @Nested
+ class TimeV2LiteralCompare {
+ @Test
+ void alwaysThrows() {
+ Assertions.assertThrows(RuntimeException.class,
+ () -> new TimeV2Literal(1, 0, 0, 0, 0, false)
+ .compareLiteral(new TimeV2Literal(2, 0, 0, 0, 0,
false)));
+ }
+ }
+}
diff --git
a/fe/fe-catalog/src/test/java/org/apache/doris/analysis/LiteralExprEqualsTest.java
b/fe/fe-catalog/src/test/java/org/apache/doris/analysis/LiteralExprEqualsTest.java
new file mode 100644
index 00000000000..4ca9473f052
--- /dev/null
+++
b/fe/fe-catalog/src/test/java/org/apache/doris/analysis/LiteralExprEqualsTest.java
@@ -0,0 +1,366 @@
+// 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.doris.analysis;
+
+import org.apache.doris.catalog.ArrayType;
+import org.apache.doris.catalog.ScalarType;
+import org.apache.doris.catalog.Type;
+import org.apache.doris.common.AnalysisException;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+// Covers equals()/hashCode() for every legacy LiteralExpr subclass that the
+// optimizer uses for predicate dedup. Most types inherit LiteralExpr.equals
+// (which itself calls compareLiteral); IPv4Literal / IPv6Literal /
+// ArrayLiteral / NullLiteral override equals directly.
+//
+// Skipped: MapLiteral, StructLiteral, TimeV2Literal (SQL has no way to feed
+// two of them into the dedup paths).
+class LiteralExprEqualsTest {
+
+ @Nested
+ class BoolLiteralEquals {
+ @Test
+ void sameValue() {
+ Assertions.assertEquals(new BoolLiteral(true), new
BoolLiteral(true));
+ Assertions.assertEquals(new BoolLiteral(false), new
BoolLiteral(false));
+ }
+
+ @Test
+ void differentValue() {
+ Assertions.assertNotEquals(new BoolLiteral(true), new
BoolLiteral(false));
+ }
+
+ @Test
+ void notEqualToNullObject() {
+ Assertions.assertNotEquals(null, new BoolLiteral(true));
+ }
+
+ @Test
+ void self() {
+ BoolLiteral b = new BoolLiteral(true);
+ Assertions.assertEquals(b, b);
+ }
+ }
+
+ @Nested
+ class IntLiteralEquals {
+ @Test
+ void sameValue() {
+ Assertions.assertEquals(new IntLiteral(42), new IntLiteral(42));
+ }
+
+ @Test
+ void differentValue() {
+ Assertions.assertNotEquals(new IntLiteral(1), new IntLiteral(2));
+ }
+
+ @Test
+ void hashCodeMatchesEquality() {
+ Assertions.assertEquals(new IntLiteral(42).hashCode(), new
IntLiteral(42).hashCode());
+ }
+ }
+
+ @Nested
+ class FloatLiteralEquals {
+ @Test
+ void sameValue() {
+ Assertions.assertEquals(new FloatLiteral(1.5), new
FloatLiteral(1.5));
+ }
+
+ @Test
+ void differentValue() {
+ Assertions.assertNotEquals(new FloatLiteral(1.0), new
FloatLiteral(2.0));
+ }
+ }
+
+ @Nested
+ class DecimalLiteralEquals {
+ @Test
+ void sameValue() {
+ BigDecimal v = new BigDecimal("12.34");
+ DecimalLiteral a = new DecimalLiteral(v,
ScalarType.createDecimalV3Type(10, 2));
+ DecimalLiteral b = new DecimalLiteral(v,
ScalarType.createDecimalV3Type(10, 2));
+ Assertions.assertEquals(a, b);
+ }
+
+ @Test
+ void differentValue() {
+ DecimalLiteral a = new DecimalLiteral(new BigDecimal("1.00"),
ScalarType.createDecimalV3Type(10, 2));
+ DecimalLiteral b = new DecimalLiteral(new BigDecimal("9.99"),
ScalarType.createDecimalV3Type(10, 2));
+ Assertions.assertNotEquals(a, b);
+ }
+ }
+
+ @Nested
+ class LargeIntLiteralEquals {
+ @Test
+ void sameValue() {
+ BigInteger v = new BigInteger("12345678901234567890");
+ Assertions.assertEquals(new LargeIntLiteral(v), new
LargeIntLiteral(v));
+ }
+
+ @Test
+ void differentValue() {
+ Assertions.assertNotEquals(
+ new LargeIntLiteral(BigInteger.ONE),
+ new LargeIntLiteral(new
BigInteger("12345678901234567890")));
+ }
+ }
+
+ @Nested
+ class DateLiteralEquals {
+ @Test
+ void sameValue() {
+ Assertions.assertEquals(new DateLiteral(2026, 5, 21), new
DateLiteral(2026, 5, 21));
+ }
+
+ @Test
+ void differentValue() {
+ Assertions.assertNotEquals(new DateLiteral(2026, 5, 21), new
DateLiteral(2026, 5, 22));
+ }
+ }
+
+ @Nested
+ class StringLiteralEquals {
+ @Test
+ void sameValue() {
+ Assertions.assertEquals(new StringLiteral("abc"), new
StringLiteral("abc"));
+ }
+
+ @Test
+ void differentValue() {
+ Assertions.assertNotEquals(new StringLiteral("abc"), new
StringLiteral("abd"));
+ }
+
+ @Test
+ void notEqualToNonStringLiteral() {
+ // LiteralExpr.equals short-circuits StringLiteral vs
non-StringLiteral to false.
+ Assertions.assertNotEquals(new StringLiteral("1"), new
IntLiteral(1));
+ }
+ }
+
+ @Nested
+ class IPv4LiteralEquals {
+ @Test
+ void sameValue() throws AnalysisException {
+ Assertions.assertEquals(new IPv4Literal("1.1.1.1"), new
IPv4Literal("1.1.1.1"));
+ }
+
+ @Test
+ void differentValue() throws AnalysisException {
+ Assertions.assertNotEquals(new IPv4Literal("1.1.1.1"), new
IPv4Literal("1.1.1.2"));
+ }
+
+ @Test
+ void notEqualToNonIPv4Literal() throws AnalysisException {
+ // Before the fix, equals would call compareLiteral which returned
0 for any
+ // peer, making this assertion fail. The fix's instanceof
short-circuit prevents
+ // that, and also keeps equals from throwing on cross-type.
+ Assertions.assertNotEquals(new IPv4Literal("1.1.1.1"), new
StringLiteral("1.1.1.1"));
+ Assertions.assertNotEquals(new IPv4Literal("0.0.0.0"), new
IntLiteral(0));
+ }
+
+ @Test
+ void hashCodeMatchesEquality() throws AnalysisException {
+ Assertions.assertEquals(new IPv4Literal("1.1.1.1").hashCode(), new
IPv4Literal("1.1.1.1").hashCode());
+ }
+ }
+
+ @Nested
+ class IPv6LiteralEquals {
+ @Test
+ void sameValue() throws AnalysisException {
+ Assertions.assertEquals(new IPv6Literal("::1"), new
IPv6Literal("::1"));
+ }
+
+ @Test
+ void canonicalizedFormsEqual() throws AnalysisException {
+ // "::1" and "0:0:0:0:0:0:0:1" must hash and compare-equal after
canonicalize.
+ IPv6Literal compact = new IPv6Literal("::1");
+ IPv6Literal expanded = new IPv6Literal("0:0:0:0:0:0:0:1");
+ Assertions.assertEquals(compact, expanded);
+ Assertions.assertEquals(compact.hashCode(), expanded.hashCode());
+ }
+
+ @Test
+ void differentValue() throws AnalysisException {
+ Assertions.assertNotEquals(new IPv6Literal("::1"), new
IPv6Literal("::2"));
+ }
+
+ @Test
+ void notEqualToNonIPv6Literal() throws AnalysisException {
+ Assertions.assertNotEquals(new IPv6Literal("::1"), new
StringLiteral("::1"));
+ }
+
+ @Test
+ void ipv4MappedNotEqualToLoopback() throws AnalysisException {
+ // ::ffff:0.0.0.1 is the IPv4-mapped IPv6 form of 0.0.0.1 and must
+ // preserve its full 128-bit value (the ::ffff: prefix), so it must
+ // NOT collide with ::1 in equals/hashCode. Earlier the helper
+ // collapsed Inet4Address results to 4 bytes and both literals
+ // ended up as BigInteger(1).
+ IPv6Literal mapped = new IPv6Literal("::ffff:0.0.0.1");
+ IPv6Literal loopback = new IPv6Literal("::1");
+ Assertions.assertNotEquals(mapped, loopback);
+ Assertions.assertNotEquals(mapped.hashCode(), loopback.hashCode());
+ }
+
+ @Test
+ void ipv4MappedDottedAndCompressedFormsEqual() throws
AnalysisException {
+ // The dotted-decimal mapped form (::ffff:0.0.0.1) and the
+ // colon-only compressed form (::ffff:0:1) are the same 128-bit
+ // address, so they must hash and compare-equal.
+ IPv6Literal dotted = new IPv6Literal("::ffff:0.0.0.1");
+ IPv6Literal compressed = new IPv6Literal("::ffff:0:1");
+ Assertions.assertEquals(dotted, compressed);
+ Assertions.assertEquals(dotted.hashCode(), compressed.hashCode());
+ }
+ }
+
+ @Nested
+ class ArrayLiteralEquals {
+ private final Type intArray = new ArrayType(Type.INT);
+
+ @Test
+ void sameElements() {
+ ArrayLiteral a = new ArrayLiteral(intArray, new IntLiteral(1), new
IntLiteral(2));
+ ArrayLiteral b = new ArrayLiteral(intArray, new IntLiteral(1), new
IntLiteral(2));
+ Assertions.assertEquals(a, b);
+ }
+
+ @Test
+ void differentElements() {
+ ArrayLiteral a = new ArrayLiteral(intArray, new IntLiteral(1), new
IntLiteral(2));
+ ArrayLiteral b = new ArrayLiteral(intArray, new IntLiteral(1), new
IntLiteral(3));
+ Assertions.assertNotEquals(a, b);
+ }
+
+ @Test
+ void differentLength() {
+ ArrayLiteral shorter = new ArrayLiteral(intArray, new
IntLiteral(1));
+ ArrayLiteral longer = new ArrayLiteral(intArray, new
IntLiteral(1), new IntLiteral(2));
+ Assertions.assertNotEquals(shorter, longer);
+ }
+
+ @Test
+ void notEqualToNonArray() {
+ ArrayLiteral a = new ArrayLiteral(intArray, new IntLiteral(1));
+ Assertions.assertNotEquals(a, new IntLiteral(1));
+ }
+ }
+
+ @Nested
+ class VarBinaryLiteralEquals {
+ @Test
+ void sameBytes() throws AnalysisException {
+ Assertions.assertEquals(new VarBinaryLiteral(new byte[]{1, 2, 3}),
+ new VarBinaryLiteral(new byte[]{1, 2, 3}));
+ }
+
+ @Test
+ void differentBytes() throws AnalysisException {
+ Assertions.assertNotEquals(new VarBinaryLiteral(new byte[]{1, 2,
3}),
+ new VarBinaryLiteral(new byte[]{1, 2, 4}));
+ }
+ }
+
+ @Nested
+ class MaxLiteralEquals {
+ @Test
+ void singletonEqualsItself() {
+ Assertions.assertEquals(MaxLiteral.MAX_VALUE,
MaxLiteral.MAX_VALUE);
+ }
+
+ @Test
+ void notEqualToValueLiteral() {
+ Assertions.assertNotEquals(MaxLiteral.MAX_VALUE, new
IntLiteral(Long.MAX_VALUE));
+ }
+ }
+
+ @Nested
+ class NullLiteralEquals {
+ @Test
+ void anyTwoNullsEqual() {
+ Assertions.assertEquals(new NullLiteral(), new NullLiteral());
+ }
+
+ @Test
+ void notEqualToValueLiteral() {
+ Assertions.assertNotEquals(new NullLiteral(), new IntLiteral(0));
+ }
+ }
+
+ @Nested
+ class PlaceHolderExprEquals {
+ @Test
+ void delegatesToWrappedForEquality() {
+ // PlaceHolderExpr inherits LiteralExpr.equals, which calls
compareLiteral,
+ // and PlaceHolderExpr.compareLiteral delegates to the wrapped
literal.
+ PlaceHolderExpr wrapsTen = new PlaceHolderExpr(new IntLiteral(10));
+ Assertions.assertTrue(wrapsTen.equals(new IntLiteral(10)));
+ Assertions.assertFalse(wrapsTen.equals(new IntLiteral(20)));
+ }
+ }
+
+ // TimeV2Literal.compareLiteral now throws, but equals/hashCode are
overridden
+ // so dedup paths that don't go through compareLiteral still work.
+ @Nested
+ class TimeV2LiteralEquals {
+ @Test
+ void sameValue() {
+ Assertions.assertEquals(new TimeV2Literal(1, 2, 3, 0, 0, false),
+ new TimeV2Literal(1, 2, 3, 0, 0, false));
+ }
+
+ @Test
+ void differentValue() {
+ Assertions.assertNotEquals(new TimeV2Literal(1, 2, 3, 0, 0, false),
+ new TimeV2Literal(4, 5, 6, 0, 0, false));
+ }
+
+ @Test
+ void negativeAndPositiveDiffer() {
+ Assertions.assertNotEquals(new TimeV2Literal(1, 0, 0, 0, 0, false),
+ new TimeV2Literal(1, 0, 0, 0, 0, true));
+ }
+
+ @Test
+ void hashCodeMatchesEquality() {
+ Assertions.assertEquals(new TimeV2Literal(1, 2, 3, 0, 0,
false).hashCode(),
+ new TimeV2Literal(1, 2, 3, 0, 0, false).hashCode());
+ }
+
+ @Test
+ void sameValueDifferentScaleEqualAndHashAlike() {
+ // equals() compares only getValue(); two TIMEV2 literals with the
same
+ // logical time but different declared scales (TIMEV2(0) vs
TIMEV2(6))
+ // must therefore also share a hashCode, otherwise predicate dedup
+ // buckets them apart and violates the Object.equals/hashCode
contract.
+ TimeV2Literal scale0 = new TimeV2Literal(1, 2, 3, 0, 0, false);
+ TimeV2Literal scale6 = new TimeV2Literal(1, 2, 3, 0, 6, false);
+ Assertions.assertEquals(scale0, scale6);
+ Assertions.assertEquals(scale0.hashCode(), scale6.hashCode());
+ }
+ }
+}
diff --git
a/regression-test/data/query_p0/sql_functions/ip_functions/test_ipv4_ipv6_multi_not_equal.out
b/regression-test/data/query_p0/sql_functions/ip_functions/test_ipv4_ipv6_multi_not_equal.out
new file mode 100644
index 00000000000..66a1761fc52
--- /dev/null
+++
b/regression-test/data/query_p0/sql_functions/ip_functions/test_ipv4_ipv6_multi_not_equal.out
@@ -0,0 +1,36 @@
+-- This file is automatically generated. You should know what you did if you
want to edit this
+-- !ipv4_multi_ne --
+3 1.1.1.3
+4 2.2.2.2
+5 3.3.3.3
+6 10.1.1.1
+7 10.1.1.5
+8 10.1.1.10
+9 192.168.1.1
+10 192.168.1.2
+
+-- !ipv4_not_between --
+4 2.2.2.2
+5 3.3.3.3
+9 192.168.1.1
+10 192.168.1.2
+
+-- !ipv6_multi_ne --
+3 ::3
+4 2001::1
+5 2001::2
+6 fe80::1
+7 fe80::5
+8 fe80::a
+9 2001:db8::1
+10 2001:db8::2
+
+-- !ipv4_not_in_plus_ne --
+4 2.2.2.2
+5 3.3.3.3
+6 10.1.1.1
+7 10.1.1.5
+8 10.1.1.10
+9 192.168.1.1
+10 192.168.1.2
+
diff --git
a/regression-test/suites/query_p0/sql_functions/ip_functions/test_ipv4_ipv6_multi_not_equal.groovy
b/regression-test/suites/query_p0/sql_functions/ip_functions/test_ipv4_ipv6_multi_not_equal.groovy
new file mode 100644
index 00000000000..2ff09bc42cd
--- /dev/null
+++
b/regression-test/suites/query_p0/sql_functions/ip_functions/test_ipv4_ipv6_multi_not_equal.groovy
@@ -0,0 +1,107 @@
+// 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.
+
+// Regression test for CIR-20160 / apache/doris#62672:
+// Multiple != / NOT BETWEEN / NOT IN conditions on the same IPv4/IPv6
+// column used to fold into the first conjunct because
+// IPv4Literal/IPv6Literal.compareLiteral() always returned 0,
+// making every IP literal compare-equal in legacy expr deduplication.
+suite("test_ipv4_ipv6_multi_not_equal") {
+ sql "DROP TABLE IF EXISTS test_ip_multi_not_equal"
+ sql """
+ CREATE TABLE test_ip_multi_not_equal (
+ id INT,
+ ip4 IPV4,
+ ip6 IPV6
+ ) DUPLICATE KEY(id)
+ DISTRIBUTED BY HASH(id) BUCKETS 1
+ PROPERTIES("replication_num" = "1")
+ """
+
+ sql """
+ INSERT INTO test_ip_multi_not_equal VALUES
+ (1, '1.1.1.1', '::1'),
+ (2, '1.1.1.2', '::2'),
+ (3, '1.1.1.3', '::3'),
+ (4, '2.2.2.2', '2001::1'),
+ (5, '3.3.3.3', '2001::2'),
+ (6, '10.1.1.1', 'fe80::1'),
+ (7, '10.1.1.5', 'fe80::5'),
+ (8, '10.1.1.10', 'fe80::a'),
+ (9, '192.168.1.1', '2001:db8::1'),
+ (10,'192.168.1.2', '2001:db8::2')
+ """
+
+ // -- Case 1: multiple != on IPv4 -------------------------------------
+ qt_ipv4_multi_ne """
+ SELECT id, ip4 FROM test_ip_multi_not_equal
+ WHERE ip4 != '1.1.1.1' AND ip4 != '1.1.1.2'
+ ORDER BY id
+ """
+ // EXPLAIN must retain BOTH literals; before the fix only "1.1.1.1"
+ // survived because the second != predicate was dropped as duplicate.
+ explain {
+ sql "SELECT id FROM test_ip_multi_not_equal WHERE ip4 != '1.1.1.1' AND
ip4 != '1.1.1.2'"
+ contains "1.1.1.1"
+ contains "1.1.1.2"
+ }
+
+ // -- Case 2: two NOT BETWEEN ranges on IPv4 --------------------------
+ qt_ipv4_not_between """
+ SELECT id, ip4 FROM test_ip_multi_not_equal
+ WHERE NOT (ip4 BETWEEN '1.1.1.1' AND '1.1.1.10')
+ AND NOT (ip4 BETWEEN '10.1.1.1' AND '10.1.1.10')
+ ORDER BY id
+ """
+ explain {
+ sql """
+ SELECT id FROM test_ip_multi_not_equal
+ WHERE NOT (ip4 BETWEEN '1.1.1.1' AND '1.1.1.10')
+ AND NOT (ip4 BETWEEN '10.1.1.1' AND '10.1.1.10')
+ """
+ contains "1.1.1.1"
+ contains "10.1.1.1"
+ }
+
+ // -- Case 3: multiple != on IPv6 -------------------------------------
+ qt_ipv6_multi_ne """
+ SELECT id, ip6 FROM test_ip_multi_not_equal
+ WHERE ip6 != '::1' AND ip6 != '::2'
+ ORDER BY id
+ """
+ explain {
+ sql "SELECT id FROM test_ip_multi_not_equal WHERE ip6 != '::1' AND ip6
!= '::2'"
+ contains "::1"
+ contains "::2"
+ }
+
+ // -- Case 4: NOT IN + != combined on IPv4 ----------------------------
+ qt_ipv4_not_in_plus_ne """
+ SELECT id, ip4 FROM test_ip_multi_not_equal
+ WHERE ip4 NOT IN ('1.1.1.1','1.1.1.2') AND ip4 != '1.1.1.3'
+ ORDER BY id
+ """
+ explain {
+ sql """
+ SELECT id FROM test_ip_multi_not_equal
+ WHERE ip4 NOT IN ('1.1.1.1','1.1.1.2') AND ip4 != '1.1.1.3'
+ """
+ contains "1.1.1.1"
+ contains "1.1.1.2"
+ contains "1.1.1.3"
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]