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]

Reply via email to