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 9c9447fc969 [opt](infer predicate) Infer IN predicate from OR when 
slot is shared by sibling conjuncts (#69921)
9c9447fc969 is described below

commit 9c9447fc96922cc51d5a8f9260ab0f3a9704fb88
Author: Lijia Liu <[email protected]>
AuthorDate: Fri Nov 6 00:05:03 2026 +0800

    [opt](infer predicate) Infer IN predicate from OR when slot is shared by 
sibling conjuncts (#69921)
    
    OrToIn extract mode dropped "slot = c" / "slot IN (...)" candidates
    whose slot is also referenced by sibling conjuncts, e.g. an IN inferred
    from a kept inner OR. So "spm IN (...)" could not be inferred from
    nested multi-column ORs, and parquet lazy materialization decoded large
    JSON columns on nearly all rows.
    
    Skip the independence check for plain-slot candidates in extract mode
    only. The inferred IN is implied by the OR and the OR is kept, so
    results are unchanged. Add FE config
    enable_or_to_in_shared_slot_inference (default true, mutable) as a
    rollback switch covering only this change.
    
    ---------
    
    Co-authored-by: 刘立家 <[email protected]>
    Co-authored-by: liutang123 <[email protected]>
    Co-authored-by: Claude Code <[email protected]>
---
 .../nereids/rules/expression/rules/OrToIn.java     | 151 ++++++++++++++---
 .../doris/nereids/rules/rewrite/OrToInTest.java    | 180 ++++++++++++++++++++-
 .../or_to_in/or_to_in_complex_type.out             |  13 ++
 .../tpcds_sf100/noStatsRfPrune/query41.out         |   2 +-
 .../tpcds_sf100/no_stats_shape/query41.out         |   2 +-
 .../shape_check/tpcds_sf100/rf_prune/query41.out   |   2 +-
 .../data/shape_check/tpcds_sf100/shape/query41.out |   2 +-
 .../shape_check/tpcds_sf1000/dphyper/query41.out   |   2 +-
 .../data/shape_check/tpcds_sf1000/hint/query41.out |   2 +-
 .../shape_check/tpcds_sf1000/shape/query41.out     |   2 +-
 .../tpcds_sf1000_constraints/shape/query41.out     |   2 +-
 .../tpcds_sf1000_nopkfk/shape/query41.out          |   2 +-
 .../shape_check/tpcds_sf10t_orc/shape/query41.out  |   2 +-
 .../or_to_in/or_to_in_complex_type.groovy          |  98 +++++++++++
 14 files changed, 428 insertions(+), 34 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/OrToIn.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/OrToIn.java
index f356834fc26..adc55d58d5c 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/OrToIn.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/OrToIn.java
@@ -18,6 +18,7 @@
 package org.apache.doris.nereids.rules.expression.rules;
 
 import org.apache.doris.common.Pair;
+import org.apache.doris.nereids.analyzer.Unbound;
 import org.apache.doris.nereids.rules.expression.ExpressionRewrite;
 import org.apache.doris.nereids.rules.expression.ExpressionRewriteContext;
 import org.apache.doris.nereids.rules.expression.ExpressionRewriteRule;
@@ -29,6 +30,7 @@ import org.apache.doris.nereids.trees.expressions.InPredicate;
 import org.apache.doris.nereids.trees.expressions.Or;
 import org.apache.doris.nereids.trees.expressions.Slot;
 import org.apache.doris.nereids.trees.expressions.literal.Literal;
+import org.apache.doris.nereids.types.DataType;
 import org.apache.doris.nereids.util.ExpressionUtils;
 
 import com.google.common.collect.ImmutableList;
@@ -36,6 +38,7 @@ import com.google.common.collect.ImmutableList.Builder;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
@@ -59,6 +62,11 @@ public class OrToIn {
      * case 2: from (a=1) or (a=2), "a in (1,2)" is inferred, the inferred 
expr is equivalent to the original expr
      * - replaceMode/extractMode: output a in (1, 2)
      *
+     * case 3 (extractMode only): the candidate slot is also referenced by 
sibling conjuncts of the same disjunct,
+     * e.g. "(a=1 and b=1) or (a in (2, 3) and ((a=2 and c=1) or (a=3 and 
c=2)))".
+     * - replaceMode: output origin expr
+     * - extractMode: output a in (1, 2, 3) and (origin expr)
+     *
      * extractMode only used for filter, the inferred In-predicate could be 
pushed down.
      */
     public enum Mode {
@@ -241,12 +249,128 @@ public class OrToIn {
         return true;
     }
 
+    /**
+     * Whether conjuncts.get(idx) may contribute an "X = literal" / "X IN 
(literals)" candidate.
+     *
+     * Replace mode may substitute the inferred In-predicate for the original 
OR, so it keeps the
+     * historical requirement that the candidate does not share input slots 
with sibling conjuncts.
+     *
+     * Extract mode never drops the original OR, it only appends "X IN (union 
of literals)". When X is a
+     * plain slot (deterministic, evaluated once per row), every disjunct 
implies its own top-level
+     * "X = c" / "X IN (...)", so the whole OR implies "X IN (union)" no 
matter whether X is also
+     * referenced by sibling conjuncts. Under three-valued logic "X IN (union) 
AND or" is equivalent to
+     * "or": or=TRUE implies X IN (union)=TRUE, and X IN (union)=FALSE implies 
every disjunct is FALSE.
+     * A typical shape where the independence check blocks a useful pushdown:
+     *   (spm='a' AND ...) OR (... AND spm IN ('c','d') AND ((spm='c' AND ...) 
OR (spm='d' AND ...)))
+     * Here "spm IN ('c','d')" was inferred from the inner OR, which is kept 
and still references spm.
+     */
+    private boolean isCandidateConjunct(int idx, List<Expression> conjuncts) {
+        if (mode == Mode.extractMode && 
isSlotCompareWithLiterals(conjuncts.get(idx))) {
+            return true;
+        }
+        return independentConjunct(idx, conjuncts);
+    }
+
+    private static boolean isSlotCompareWithLiterals(Expression conjunct) {
+        if (conjunct instanceof EqualTo) {
+            EqualTo eq = (EqualTo) conjunct;
+            return (eq.left() instanceof Slot && eq.right() instanceof Literal
+                            && isInferenceSafeScalarType(eq.left())
+                            && isInferenceSafeScalarType(eq.right()))
+                    || (eq.right() instanceof Slot && eq.left() instanceof 
Literal
+                            && isInferenceSafeScalarType(eq.right())
+                            && isInferenceSafeScalarType(eq.left()));
+        }
+        if (conjunct instanceof InPredicate) {
+            InPredicate inPredicate = (InPredicate) conjunct;
+            if (!(inPredicate.getCompareExpr() instanceof Slot)
+                    || 
!isInferenceSafeScalarType(inPredicate.getCompareExpr())) {
+                return false;
+            }
+            for (Expression option : inPredicate.getOptions()) {
+                if (!(option instanceof Literal) || 
!isInferenceSafeScalarType(option)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Whether the type of expr admits the relaxation.
+     *
+     * Both sides are inspected when possible: the slot decides how "=" is 
evaluated and the literal
+     * decides what IN puts into its hash set, so a type is only safe when 
neither side vetoes it.
+     */
+    private static boolean isInferenceSafeScalarType(Expression expr) {
+        if (expr instanceof Unbound) {
+            // UnboundSlot throws on getDataType(). Planning always binds 
slots before this rule
+            // runs, so an unknown type only shows up in unit tests; do not 
let it veto.
+            return true;
+        }
+        return isInferenceSafeScalarType(expr.getDataType());
+    }
+
+    /**
+     * Whether "X = c" and "X IN (c1, c2, ...)" agree for this type, which the 
three-valued
+     * argument above depends on.
+     *
+     * Only plain scalar types are accepted. Complex types (ARRAY/STRUCT/MAP) 
are excluded because
+     * their IN is executed by FunctionCollectionIn, which drops the null map 
of the left column
+     * instead of propagating it. "a IN ([1], [2])" then returns a non-null 
FALSE for a NULL row
+     * where "a = [1]" returns NULL, so "a IN (...) AND or" is no longer 
equivalent to "or" and a
+     * NULL-sensitive parent (IS NULL / NOT / CASE / ...) changes the query 
result. Equality on a
+     * nullable ARRAY is also legal in Doris, so the shape is reachable in 
practice.
+     *
+     * Floating point is excluded as a precaution: "X = c" and the hash-based 
IN may disagree on
+     * -0.0 and NaN, which would break the "or=TRUE implies X IN (union)=TRUE" 
direction.
+     *
+     * Rejecting a type is never a loss: the conjunct simply falls back to 
independentConjunct()
+     * and gets exactly the pre-change behavior.
+     */
+    private static boolean isInferenceSafeScalarType(DataType type) {
+        return type.isIntegralType() // TINYINT / SMALLINT / INT / BIGINT / 
LARGEINT
+                || type.isDecimalLikeType() // DECIMALV2 / DECIMALV3
+                || type.isDateLikeType() // DATE / DATETIME / DATEV2 / 
DATETIMEV2 / TIMESTAMP_NS / TIMESTAMPTZ
+                || type.isTimeType() // TIMEV2
+                || type.isStringLikeType() // CHAR / VARCHAR / STRING
+                || type.isBooleanType();
+    }
+
+    /**
+     * Register "compareExpr -> literals" as a candidate of the current 
disjunct.
+     *
+     * A second candidate for the same compareExpr (e.g. "A=1 and A in (1, 
2)") is a pattern for the
+     * SimplifyRange rule, so OrToIn gives up on that expression. Historically 
that was done by clearing
+     * every candidate of the disjunct and stopping, which was harmless 
because the independence check
+     * never let two conjuncts on the same slot get here. The shared-slot 
relaxation makes it reachable,
+     * and clearing everything would also throw away unrelated slots (e.g. "b 
= 10" in
+     * "b = 10 and a = 1 and a in (1, 2)"), losing an inference that existed 
before the relaxation.
+     * So only the conflicting compareExpr is dropped, and it is remembered in 
{@code conflicted} so that
+     * a third conjunct on it cannot bring it back. Dropping a candidate only 
weakens the appended
+     * In-predicate, never its correctness.
+     */
+    private static void addCandidate(Map<Expression, Set<Literal>> candidates, 
Set<Expression> conflicted,
+            Expression compareExpr, Set<Literal> literals) {
+        if (conflicted.contains(compareExpr)) {
+            return;
+        }
+        if (!candidates.containsKey(compareExpr)) {
+            candidates.put(compareExpr, literals);
+            return;
+        }
+        candidates.remove(compareExpr);
+        conflicted.add(compareExpr);
+    }
+
     private Map<Expression, Set<Literal>> getCandidates(Expression disjunct) {
         List<Expression> conjuncts = 
ExpressionUtils.extractConjunction(disjunct);
         Map<Expression, Set<Literal>> candidates = new LinkedHashMap<>();
+        Set<Expression> conflicted = new HashSet<>();
         // collect candidates from the first disjunction
         for (int idx = 0; idx < conjuncts.size(); idx++) {
-            if (!independentConjunct(idx, conjuncts)) {
+            if (!isCandidateConjunct(idx, conjuncts)) {
                 continue;
             }
             // find pattern: A=1 / A in (1, 2, 3 ...)
@@ -264,18 +388,9 @@ public class OrToIn {
                     literal = (Literal) eq.left();
                 }
                 if (compareExpr != null) {
-                    Set<Literal> literals = candidates.get(compareExpr);
-                    if (literals == null) {
-                        literals = Sets.newHashSet();
-                        literals.add(literal);
-                        candidates.put(compareExpr, literals);
-                    } else {
-                        // pattern like (A=1 and A=2) should be processed by 
SimplifyRange rule
-                        // OrToIn rule does apply to this expression
-                        candidates.clear();
-                        break;
-
-                    }
+                    Set<Literal> literals = Sets.newHashSet();
+                    literals.add(literal);
+                    addCandidate(candidates, conflicted, compareExpr, 
literals);
                 }
             } else if (conjunct instanceof InPredicate) {
                 InPredicate inPredicate = (InPredicate) conjunct;
@@ -291,15 +406,7 @@ public class OrToIn {
                 }
 
                 if (allLiteralOpts) {
-                    Set<Literal> alreadyMappedLiterals = 
candidates.get(inPredicate.getCompareExpr());
-                    if (alreadyMappedLiterals == null) {
-                        candidates.put(inPredicate.getCompareExpr(), 
literalOptions);
-                    } else {
-                        // pattern like (A=1 and A in (1, 2)) should be 
processed by SimplifyRange rule
-                        // OrToIn rule does apply to this expression
-                        candidates.clear();
-                        break;
-                    }
+                    addCandidate(candidates, conflicted, 
inPredicate.getCompareExpr(), literalOptions);
                 }
             }
         }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/OrToInTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/OrToInTest.java
index 796e0d3ac27..c7a1050be7a 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/OrToInTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/OrToInTest.java
@@ -19,11 +19,33 @@ package org.apache.doris.nereids.rules.rewrite;
 
 import org.apache.doris.nereids.rules.expression.ExpressionRewriteTestHelper;
 import org.apache.doris.nereids.rules.expression.rules.OrToIn;
+import org.apache.doris.nereids.trees.expressions.And;
+import org.apache.doris.nereids.trees.expressions.EqualTo;
 import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.InPredicate;
+import org.apache.doris.nereids.trees.expressions.IsNull;
+import org.apache.doris.nereids.trees.expressions.Not;
+import org.apache.doris.nereids.trees.expressions.Or;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.literal.ArrayLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.DoubleLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
+import org.apache.doris.nereids.types.ArrayType;
+import org.apache.doris.nereids.types.DoubleType;
+import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.nereids.util.ExpressionUtils;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
 class OrToInTest extends ExpressionRewriteTestHelper {
 
     @Test
@@ -167,11 +189,14 @@ class OrToInTest extends ExpressionRewriteTestHelper {
 
     @Test
     void test14() {
-        // no rewrite, because of "f(a)"
+        // "a = 1" shares slot a with "f(a) = 2". The OR still implies "a in 
(1, 3)", so extract mode
+        // appends it and keeps the original OR; replace mode must not touch 
the OR.
         String expr = "(a=1 and f(a)=2) or a=3";
         Expression expression = PARSER.parseExpression(expr);
         Expression rewritten = 
OrToIn.EXTRACT_MODE_INSTANCE.rewriteTree(expression, context);
-        Assertions.assertEquals("OR[AND[(a = 1),(f(a) = 2)],(a = 3)]", 
rewritten.toSql());
+        Assertions.assertEquals("AND[a IN (1, 3),OR[AND[(a = 1),(f(a) = 2)],(a 
= 3)]]", rewritten.toSql());
+        Expression replaced = 
OrToIn.REPLACE_MODE_INSTANCE.rewriteTree(expression, context);
+        Assertions.assertEquals("OR[AND[(a = 1),(f(a) = 2)],(a = 3)]", 
replaced.toSql());
     }
 
     @Test
@@ -199,4 +224,155 @@ class OrToInTest extends ExpressionRewriteTestHelper {
         Expression rewritten = 
OrToIn.EXTRACT_MODE_INSTANCE.rewriteTree(expression, context);
         System.out.println(rewritten);
     }
+
+    @Test
+    void testInferFromDisjunctWithDependentConjuncts() {
+        // Same shape as the homepage-rank query on dwd_traffic_events_di:
+        //   (spm=S1 and ep=P1 and ev=E1) or (spm=S2 and ep=P2 and ev=E2)
+        //   or (f(j)=.. and g(j) in (..) and ((spm=S3 and ep=P1 and ev=E1) or 
(spm=S4 and ep=P2 and ev=E2)))
+        // The inner OR is rewritten first and yields "spm in (3, 4)" etc. 
next to the kept inner OR, so these
+        // In-predicates share slots with their sibling. They must still take 
part in the outer inference.
+        String expr = "(spm=1 and ep=1 and ev=1) or (spm=2 and ep=2 and ev=2)"
+                + " or (f(j)=1 and g(j) in (1, 2) and ((spm=3 and ep=1 and 
ev=1) or (spm=4 and ep=2 and ev=2)))";
+        Expression expression = PARSER.parseExpression(expr);
+        // InferInPredicateFromOr calls rewriteTree(Expression) on filter 
predicates
+        Expression rewritten = 
OrToIn.EXTRACT_MODE_INSTANCE.rewriteTree(expression);
+        List<Expression> conjuncts = 
ExpressionUtils.extractConjunction(rewritten);
+        assertHasInPredicate(conjuncts, "spm", "1", "2", "3", "4");
+        assertHasInPredicate(conjuncts, "ep", "1", "2");
+        assertHasInPredicate(conjuncts, "ev", "1", "2");
+        // exactly 3 inferred In-predicates + the original OR. f(j) / g(j) are 
not plain slots and
+        // share j with each other, so they are never inferred to the top 
level.
+        Assertions.assertEquals(4, conjuncts.size(), conjuncts.toString());
+        Assertions.assertEquals(1, 
conjuncts.stream().filter(Or.class::isInstance).count());
+
+        assertFixedPoint(rewritten);
+
+        // replace mode must keep the original OR because the inferred 
In-predicates are not equivalent to it
+        Expression replaced = 
OrToIn.REPLACE_MODE_INSTANCE.rewriteTree(expression);
+        Assertions.assertTrue(replaced instanceof Or, replaced.toSql());
+    }
+
+    @Test
+    void testNonSlotCompareExprStillRequiresIndependence() {
+        // f(a) is not a plain slot, keep the conservative independence check 
even in extract mode
+        String expr = "(f(a)=1 and a>0) or f(a)=2";
+        Expression expression = PARSER.parseExpression(expr);
+        Expression rewritten = 
OrToIn.EXTRACT_MODE_INSTANCE.rewriteTree(expression);
+        Assertions.assertEquals("OR[AND[(f(a) = 1),(a > 0)],(f(a) = 2)]", 
rewritten.toSql());
+    }
+
+    @Test
+    void testSlotSharedWithOtherPredicate() {
+        // "a > 0" and "a is not null" share slot a with "a = 1" / "a in (2, 
3)"
+        String expr = "(a=1 and a>0) or (a in (2, 3) and a is not null)";
+        Expression expression = PARSER.parseExpression(expr);
+        Expression rewritten = 
OrToIn.EXTRACT_MODE_INSTANCE.rewriteTree(expression);
+        List<Expression> conjuncts = 
ExpressionUtils.extractConjunction(rewritten);
+        assertHasInPredicate(conjuncts, "a", "1", "2", "3");
+        Assertions.assertEquals(2, conjuncts.size(), conjuncts.toString());
+        Assertions.assertEquals(1, 
conjuncts.stream().filter(Or.class::isInstance).count());
+        assertFixedPoint(rewritten);
+    }
+
+    @Test
+    void testDuplicateSlotCandidateOnlyDropsConflictingSlot() {
+        // The inner OR is rewritten first and appends "a in (1, 2)" next to 
it, so the first outer disjunct
+        // holds "a = 1" and "a in (1, 2)" on the same slot. That slot is a 
pattern for SimplifyRange and is
+        // dropped, but "b = 10" must still be inferred.
+        String expr = "(b=10 and a=1 and ((a=1 and c=100) or (a=2 and c=200))) 
or (b=20 and a=3)";
+        // InferInPredicateFromOr calls rewriteTree(Expression) on filter 
predicates
+        Expression rewritten = 
OrToIn.EXTRACT_MODE_INSTANCE.rewriteTree(PARSER.parseExpression(expr));
+        List<Expression> conjuncts = 
ExpressionUtils.extractConjunction(rewritten);
+        assertHasInPredicate(conjuncts, "b", "10", "20");
+        Assertions.assertEquals(1, 
conjuncts.stream().filter(Or.class::isInstance).count(), conjuncts.toString());
+        // the conflicting slot is dropped, not inferred from a half-collected 
candidate
+        Assertions.assertFalse(conjuncts.stream().anyMatch(c -> c instanceof 
InPredicate
+                && ((InPredicate) c).getCompareExpr().toSql().equals("a")), 
conjuncts.toString());
+        assertFixedPoint(rewritten);
+    }
+
+    @Test
+    void testConflictingSlotIsNotRevivedByLaterConjunct() {
+        // Three conjuncts on slot a: after "a = 1" and "a in (1, 2)" 
conflict, the later "a in (1, 3)"
+        // must not register a again. Each single conjunct is implied by the 
disjunct, so re-registering
+        // would still be sound, but the outcome would then depend on the 
order the conjuncts are written
+        // in. Slot b is unrelated and is still inferred.
+        String expr = "(b=10 and a=1 and a in (1, 2) and a in (1, 3)) or (b=20 
and a=3)";
+        Expression rewritten = 
OrToIn.EXTRACT_MODE_INSTANCE.rewriteTree(PARSER.parseExpression(expr));
+        List<Expression> conjuncts = 
ExpressionUtils.extractConjunction(rewritten);
+        assertHasInPredicate(conjuncts, "b", "10", "20");
+        Assertions.assertFalse(conjuncts.stream().anyMatch(c -> c instanceof 
InPredicate
+                && ((InPredicate) c).getCompareExpr().toSql().equals("a")), 
conjuncts.toString());
+        Assertions.assertEquals(1, 
conjuncts.stream().filter(Or.class::isInstance).count());
+        assertFixedPoint(rewritten);
+    }
+
+    @Test
+    void testDuplicateSlotCandidateWithoutUnrelatedSlotLeavesOrAlone() {
+        // Two conjuncts on slot a and no other candidate slot in the first 
disjunct: no candidate survives,
+        // so the OR is left alone (same end result as the old "clear 
everything and stop" branch).
+        String expr = "(a=1 and a in (1, 2)) or (a=3 and b=1)";
+        Expression rewritten = 
OrToIn.EXTRACT_MODE_INSTANCE.rewriteTree(PARSER.parseExpression(expr));
+        Assertions.assertTrue(rewritten instanceof Or, rewritten.toSql());
+    }
+
+    @Test
+    void testArrayCompareKeepsIndependenceCheck() {
+        // "a = [1]" shares slot a with "size(a) > 0", so only the relaxed 
extract path could infer
+        // "a IN ([1], [2])". ARRAY is not inference-safe 
(FunctionCollectionIn drops the null map of
+        // the left column), so the OR must be left alone.
+        SlotReference a = new SlotReference("a", 
ArrayType.of(IntegerType.INSTANCE), true);
+        Expression eq1 = new EqualTo(a, new ArrayLiteral(ImmutableList.of(new 
IntegerLiteral(1))));
+        Expression eq2 = new EqualTo(a, new ArrayLiteral(ImmutableList.of(new 
IntegerLiteral(2))));
+        Expression or = new Or(new And(eq1, new Not(new IsNull(a))), eq2);
+        Expression rewritten = OrToIn.EXTRACT_MODE_INSTANCE.rewriteTree(or);
+        Assertions.assertTrue(rewritten instanceof Or, rewritten.toSql());
+        Assertions.assertEquals(0, 
ExpressionUtils.extractConjunction(rewritten).stream()
+                .filter(InPredicate.class::isInstance).count(), 
rewritten.toSql());
+    }
+
+    @Test
+    void testFloatCompareKeepsIndependenceCheck() {
+        // "X = c" and the hash-based IN may disagree on -0.0 / NaN, so 
floating point is not relaxed.
+        SlotReference d = new SlotReference("d", DoubleType.INSTANCE, true);
+        Expression or = new Or(
+                new And(new EqualTo(d, new DoubleLiteral(1.0)), new Not(new 
IsNull(d))),
+                new EqualTo(d, new DoubleLiteral(2.0)));
+        Expression rewritten = OrToIn.EXTRACT_MODE_INSTANCE.rewriteTree(or);
+        Assertions.assertTrue(rewritten instanceof Or, rewritten.toSql());
+    }
+
+    @Test
+    void testScalarTypesStillRelaxed() {
+        // 'S' -> STRING, 'I' -> INT, 'C' -> DATEV2, 'M' -> DECIMALV3, 'B' -> 
BOOLEAN.
+        // These keep the relaxation, so the target "spm IN (...)" inference 
is unaffected.
+        for (String expr : Lists.newArrayList("(Sa='x' and Sa>'a') or Sa='y'",
+                "(Ia=1 and Ia>0) or Ia=2", "(Ma=1 and Ma>0) or Ma=2")) {
+            Expression rewritten = 
OrToIn.EXTRACT_MODE_INSTANCE.rewriteTree(PARSER.parseExpression(expr));
+            Assertions.assertFalse(rewritten instanceof Or, expr + " -> " + 
rewritten.toSql());
+        }
+    }
+
+    // InferInPredicateFromOr stores the extracted conjuncts as a set and is 
re-applied by a top-down
+    // rewrite job until the filter no longer changes, so a second application 
must keep the same set.
+    private static void assertFixedPoint(Expression firstRewritten) {
+        Set<Expression> firstPass = new 
LinkedHashSet<>(ExpressionUtils.extractConjunction(firstRewritten));
+        Expression secondInput = 
ExpressionUtils.and(Lists.newArrayList(firstPass));
+        Set<Expression> secondPass = new 
LinkedHashSet<>(ExpressionUtils.extractConjunction(
+                OrToIn.EXTRACT_MODE_INSTANCE.rewriteTree(secondInput)));
+        Assertions.assertEquals(firstPass, secondPass);
+    }
+
+    private static void assertHasInPredicate(List<Expression> conjuncts, 
String slotName, String... values) {
+        Set<String> expectedValues = ImmutableSet.copyOf(values);
+        boolean found = conjuncts.stream()
+                .filter(InPredicate.class::isInstance)
+                .map(InPredicate.class::cast)
+                .anyMatch(in -> in.getCompareExpr().toSql().equals(slotName)
+                        && in.getOptions().size() == expectedValues.size()
+                        && 
in.getOptions().stream().map(Expression::toSql).collect(Collectors.toSet())
+                                .equals(expectedValues));
+        Assertions.assertTrue(found, "expect " + slotName + " IN " + 
expectedValues + " in " + conjuncts);
+    }
 }
diff --git 
a/regression-test/data/nereids_rules_p0/or_to_in/or_to_in_complex_type.out 
b/regression-test/data/nereids_rules_p0/or_to_in/or_to_in_complex_type.out
new file mode 100644
index 00000000000..25dedd3c3e6
--- /dev/null
+++ b/regression-test/data/nereids_rules_p0/or_to_in/or_to_in_complex_type.out
@@ -0,0 +1,13 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !array_shared_slot_is_null --
+1
+
+-- !scalar_shared_slot_is_null --
+4
+
+-- !scalar_shared_slot_where --
+1
+
+-- !array_shared_slot_where --
+2
+
diff --git 
a/regression-test/data/shape_check/tpcds_sf100/noStatsRfPrune/query41.out 
b/regression-test/data/shape_check/tpcds_sf100/noStatsRfPrune/query41.out
index 12dad98df5b..bfcf8199cff 100644
--- a/regression-test/data/shape_check/tpcds_sf100/noStatsRfPrune/query41.out
+++ b/regression-test/data/shape_check/tpcds_sf100/noStatsRfPrune/query41.out
@@ -18,6 +18,6 @@ PhysicalResultSink
 ------------------------PhysicalDistribute[DistributionSpecHash]
 --------------------------hashAgg[LOCAL]
 ----------------------------PhysicalProject
-------------------------------filter(OR[AND[(item.i_category = 
'Men'),item.i_size IN ('N/A', 'economy', 'large', 'medium'),OR[AND[item.i_size 
IN ('economy', 'medium'),item.i_color IN ('dodger', 'indian', 'spring', 
'tan'),item.i_units IN ('Bunch', 'Carton', 'Tsp', 
'Unknown'),OR[AND[item.i_color IN ('dodger', 'tan'),item.i_units IN ('Bunch', 
'Tsp')],AND[item.i_color IN ('indian', 'spring'),item.i_units IN ('Carton', 
'Unknown')]]],AND[item.i_size IN ('N/A', 'large'),item.i_color IN ('blue', [...]
+------------------------------filter(OR[AND[(item.i_category = 
'Men'),item.i_size IN ('N/A', 'economy', 'large', 'medium'),item.i_color IN 
('blue', 'chartreuse', 'dodger', 'indian', 'peru', 'saddle', 'spring', 
'tan'),item.i_units IN ('Bunch', 'Carton', 'Each', 'Gram', 'Oz', 'Pallet', 
'Tsp', 'Unknown'),OR[AND[item.i_size IN ('economy', 'medium'),item.i_color IN 
('dodger', 'indian', 'spring', 'tan'),item.i_units IN ('Bunch', 'Carton', 
'Tsp', 'Unknown'),OR[AND[item.i_color IN ('dodger', 'ta [...]
 --------------------------------PhysicalOlapScan[item]
 
diff --git 
a/regression-test/data/shape_check/tpcds_sf100/no_stats_shape/query41.out 
b/regression-test/data/shape_check/tpcds_sf100/no_stats_shape/query41.out
index 12dad98df5b..bfcf8199cff 100644
--- a/regression-test/data/shape_check/tpcds_sf100/no_stats_shape/query41.out
+++ b/regression-test/data/shape_check/tpcds_sf100/no_stats_shape/query41.out
@@ -18,6 +18,6 @@ PhysicalResultSink
 ------------------------PhysicalDistribute[DistributionSpecHash]
 --------------------------hashAgg[LOCAL]
 ----------------------------PhysicalProject
-------------------------------filter(OR[AND[(item.i_category = 
'Men'),item.i_size IN ('N/A', 'economy', 'large', 'medium'),OR[AND[item.i_size 
IN ('economy', 'medium'),item.i_color IN ('dodger', 'indian', 'spring', 
'tan'),item.i_units IN ('Bunch', 'Carton', 'Tsp', 
'Unknown'),OR[AND[item.i_color IN ('dodger', 'tan'),item.i_units IN ('Bunch', 
'Tsp')],AND[item.i_color IN ('indian', 'spring'),item.i_units IN ('Carton', 
'Unknown')]]],AND[item.i_size IN ('N/A', 'large'),item.i_color IN ('blue', [...]
+------------------------------filter(OR[AND[(item.i_category = 
'Men'),item.i_size IN ('N/A', 'economy', 'large', 'medium'),item.i_color IN 
('blue', 'chartreuse', 'dodger', 'indian', 'peru', 'saddle', 'spring', 
'tan'),item.i_units IN ('Bunch', 'Carton', 'Each', 'Gram', 'Oz', 'Pallet', 
'Tsp', 'Unknown'),OR[AND[item.i_size IN ('economy', 'medium'),item.i_color IN 
('dodger', 'indian', 'spring', 'tan'),item.i_units IN ('Bunch', 'Carton', 
'Tsp', 'Unknown'),OR[AND[item.i_color IN ('dodger', 'ta [...]
 --------------------------------PhysicalOlapScan[item]
 
diff --git a/regression-test/data/shape_check/tpcds_sf100/rf_prune/query41.out 
b/regression-test/data/shape_check/tpcds_sf100/rf_prune/query41.out
index 12dad98df5b..bfcf8199cff 100644
--- a/regression-test/data/shape_check/tpcds_sf100/rf_prune/query41.out
+++ b/regression-test/data/shape_check/tpcds_sf100/rf_prune/query41.out
@@ -18,6 +18,6 @@ PhysicalResultSink
 ------------------------PhysicalDistribute[DistributionSpecHash]
 --------------------------hashAgg[LOCAL]
 ----------------------------PhysicalProject
-------------------------------filter(OR[AND[(item.i_category = 
'Men'),item.i_size IN ('N/A', 'economy', 'large', 'medium'),OR[AND[item.i_size 
IN ('economy', 'medium'),item.i_color IN ('dodger', 'indian', 'spring', 
'tan'),item.i_units IN ('Bunch', 'Carton', 'Tsp', 
'Unknown'),OR[AND[item.i_color IN ('dodger', 'tan'),item.i_units IN ('Bunch', 
'Tsp')],AND[item.i_color IN ('indian', 'spring'),item.i_units IN ('Carton', 
'Unknown')]]],AND[item.i_size IN ('N/A', 'large'),item.i_color IN ('blue', [...]
+------------------------------filter(OR[AND[(item.i_category = 
'Men'),item.i_size IN ('N/A', 'economy', 'large', 'medium'),item.i_color IN 
('blue', 'chartreuse', 'dodger', 'indian', 'peru', 'saddle', 'spring', 
'tan'),item.i_units IN ('Bunch', 'Carton', 'Each', 'Gram', 'Oz', 'Pallet', 
'Tsp', 'Unknown'),OR[AND[item.i_size IN ('economy', 'medium'),item.i_color IN 
('dodger', 'indian', 'spring', 'tan'),item.i_units IN ('Bunch', 'Carton', 
'Tsp', 'Unknown'),OR[AND[item.i_color IN ('dodger', 'ta [...]
 --------------------------------PhysicalOlapScan[item]
 
diff --git a/regression-test/data/shape_check/tpcds_sf100/shape/query41.out 
b/regression-test/data/shape_check/tpcds_sf100/shape/query41.out
index 12dad98df5b..bfcf8199cff 100644
--- a/regression-test/data/shape_check/tpcds_sf100/shape/query41.out
+++ b/regression-test/data/shape_check/tpcds_sf100/shape/query41.out
@@ -18,6 +18,6 @@ PhysicalResultSink
 ------------------------PhysicalDistribute[DistributionSpecHash]
 --------------------------hashAgg[LOCAL]
 ----------------------------PhysicalProject
-------------------------------filter(OR[AND[(item.i_category = 
'Men'),item.i_size IN ('N/A', 'economy', 'large', 'medium'),OR[AND[item.i_size 
IN ('economy', 'medium'),item.i_color IN ('dodger', 'indian', 'spring', 
'tan'),item.i_units IN ('Bunch', 'Carton', 'Tsp', 
'Unknown'),OR[AND[item.i_color IN ('dodger', 'tan'),item.i_units IN ('Bunch', 
'Tsp')],AND[item.i_color IN ('indian', 'spring'),item.i_units IN ('Carton', 
'Unknown')]]],AND[item.i_size IN ('N/A', 'large'),item.i_color IN ('blue', [...]
+------------------------------filter(OR[AND[(item.i_category = 
'Men'),item.i_size IN ('N/A', 'economy', 'large', 'medium'),item.i_color IN 
('blue', 'chartreuse', 'dodger', 'indian', 'peru', 'saddle', 'spring', 
'tan'),item.i_units IN ('Bunch', 'Carton', 'Each', 'Gram', 'Oz', 'Pallet', 
'Tsp', 'Unknown'),OR[AND[item.i_size IN ('economy', 'medium'),item.i_color IN 
('dodger', 'indian', 'spring', 'tan'),item.i_units IN ('Bunch', 'Carton', 
'Tsp', 'Unknown'),OR[AND[item.i_color IN ('dodger', 'ta [...]
 --------------------------------PhysicalOlapScan[item]
 
diff --git a/regression-test/data/shape_check/tpcds_sf1000/dphyper/query41.out 
b/regression-test/data/shape_check/tpcds_sf1000/dphyper/query41.out
index da8ef93bfa0..4619303a182 100644
--- a/regression-test/data/shape_check/tpcds_sf1000/dphyper/query41.out
+++ b/regression-test/data/shape_check/tpcds_sf1000/dphyper/query41.out
@@ -18,6 +18,6 @@ PhysicalResultSink
 ------------------------PhysicalDistribute[DistributionSpecHash]
 --------------------------hashAgg[LOCAL]
 ----------------------------PhysicalProject
-------------------------------filter(OR[AND[(item.i_category = 
'Men'),item.i_size IN ('N/A', 'economy', 'large', 'small'),OR[AND[item.i_size 
IN ('economy', 'small'),item.i_color IN ('firebrick', 'maroon', 'sienna', 
'smoke'),item.i_units IN ('Case', 'Cup', 'Each', 'Ounce'),OR[AND[item.i_color 
IN ('maroon', 'smoke'),item.i_units IN ('Case', 'Ounce')],AND[item.i_color IN 
('firebrick', 'sienna'),item.i_units IN ('Cup', 'Each')]]],AND[item.i_size IN 
('N/A', 'large'),item.i_color IN ('papaya', [...]
+------------------------------filter(OR[AND[(item.i_category = 
'Men'),item.i_size IN ('N/A', 'economy', 'large', 'small'),item.i_color IN 
('firebrick', 'maroon', 'papaya', 'peach', 'powder', 'sienna', 'sky', 
'smoke'),item.i_units IN ('Bundle', 'Carton', 'Case', 'Cup', 'Dozen', 'Each', 
'Lb', 'Ounce'),OR[AND[item.i_size IN ('economy', 'small'),item.i_color IN 
('firebrick', 'maroon', 'sienna', 'smoke'),item.i_units IN ('Case', 'Cup', 
'Each', 'Ounce'),OR[AND[item.i_color IN ('maroon', 'smoke [...]
 --------------------------------PhysicalOlapScan[item]
 
diff --git a/regression-test/data/shape_check/tpcds_sf1000/hint/query41.out 
b/regression-test/data/shape_check/tpcds_sf1000/hint/query41.out
index da8ef93bfa0..4619303a182 100644
--- a/regression-test/data/shape_check/tpcds_sf1000/hint/query41.out
+++ b/regression-test/data/shape_check/tpcds_sf1000/hint/query41.out
@@ -18,6 +18,6 @@ PhysicalResultSink
 ------------------------PhysicalDistribute[DistributionSpecHash]
 --------------------------hashAgg[LOCAL]
 ----------------------------PhysicalProject
-------------------------------filter(OR[AND[(item.i_category = 
'Men'),item.i_size IN ('N/A', 'economy', 'large', 'small'),OR[AND[item.i_size 
IN ('economy', 'small'),item.i_color IN ('firebrick', 'maroon', 'sienna', 
'smoke'),item.i_units IN ('Case', 'Cup', 'Each', 'Ounce'),OR[AND[item.i_color 
IN ('maroon', 'smoke'),item.i_units IN ('Case', 'Ounce')],AND[item.i_color IN 
('firebrick', 'sienna'),item.i_units IN ('Cup', 'Each')]]],AND[item.i_size IN 
('N/A', 'large'),item.i_color IN ('papaya', [...]
+------------------------------filter(OR[AND[(item.i_category = 
'Men'),item.i_size IN ('N/A', 'economy', 'large', 'small'),item.i_color IN 
('firebrick', 'maroon', 'papaya', 'peach', 'powder', 'sienna', 'sky', 
'smoke'),item.i_units IN ('Bundle', 'Carton', 'Case', 'Cup', 'Dozen', 'Each', 
'Lb', 'Ounce'),OR[AND[item.i_size IN ('economy', 'small'),item.i_color IN 
('firebrick', 'maroon', 'sienna', 'smoke'),item.i_units IN ('Case', 'Cup', 
'Each', 'Ounce'),OR[AND[item.i_color IN ('maroon', 'smoke [...]
 --------------------------------PhysicalOlapScan[item]
 
diff --git a/regression-test/data/shape_check/tpcds_sf1000/shape/query41.out 
b/regression-test/data/shape_check/tpcds_sf1000/shape/query41.out
index da8ef93bfa0..4619303a182 100644
--- a/regression-test/data/shape_check/tpcds_sf1000/shape/query41.out
+++ b/regression-test/data/shape_check/tpcds_sf1000/shape/query41.out
@@ -18,6 +18,6 @@ PhysicalResultSink
 ------------------------PhysicalDistribute[DistributionSpecHash]
 --------------------------hashAgg[LOCAL]
 ----------------------------PhysicalProject
-------------------------------filter(OR[AND[(item.i_category = 
'Men'),item.i_size IN ('N/A', 'economy', 'large', 'small'),OR[AND[item.i_size 
IN ('economy', 'small'),item.i_color IN ('firebrick', 'maroon', 'sienna', 
'smoke'),item.i_units IN ('Case', 'Cup', 'Each', 'Ounce'),OR[AND[item.i_color 
IN ('maroon', 'smoke'),item.i_units IN ('Case', 'Ounce')],AND[item.i_color IN 
('firebrick', 'sienna'),item.i_units IN ('Cup', 'Each')]]],AND[item.i_size IN 
('N/A', 'large'),item.i_color IN ('papaya', [...]
+------------------------------filter(OR[AND[(item.i_category = 
'Men'),item.i_size IN ('N/A', 'economy', 'large', 'small'),item.i_color IN 
('firebrick', 'maroon', 'papaya', 'peach', 'powder', 'sienna', 'sky', 
'smoke'),item.i_units IN ('Bundle', 'Carton', 'Case', 'Cup', 'Dozen', 'Each', 
'Lb', 'Ounce'),OR[AND[item.i_size IN ('economy', 'small'),item.i_color IN 
('firebrick', 'maroon', 'sienna', 'smoke'),item.i_units IN ('Case', 'Cup', 
'Each', 'Ounce'),OR[AND[item.i_color IN ('maroon', 'smoke [...]
 --------------------------------PhysicalOlapScan[item]
 
diff --git 
a/regression-test/data/shape_check/tpcds_sf1000_constraints/shape/query41.out 
b/regression-test/data/shape_check/tpcds_sf1000_constraints/shape/query41.out
index d033b973b6c..706dcf578ac 100644
--- 
a/regression-test/data/shape_check/tpcds_sf1000_constraints/shape/query41.out
+++ 
b/regression-test/data/shape_check/tpcds_sf1000_constraints/shape/query41.out
@@ -18,6 +18,6 @@ PhysicalResultSink
 ------------------------PhysicalDistribute[DistributionSpecHash]
 --------------------------hashAgg[LOCAL]
 ----------------------------PhysicalProject
-------------------------------filter(OR[AND[(item.i_category = 
'Men'),item.i_size IN ('N/A', 'economy', 'large', 'small'),OR[AND[item.i_size 
IN ('economy', 'small'),item.i_color IN ('firebrick', 'maroon', 'sienna', 
'smoke'),item.i_units IN ('Case', 'Cup', 'Each', 'Ounce'),OR[AND[item.i_color 
IN ('maroon', 'smoke'),item.i_units IN ('Case', 'Ounce')],AND[item.i_color IN 
('firebrick', 'sienna'),item.i_units IN ('Cup', 'Each')]]],AND[item.i_size IN 
('N/A', 'large'),item.i_color IN ('papaya', [...]
+------------------------------filter(OR[AND[(item.i_category = 
'Men'),item.i_size IN ('N/A', 'economy', 'large', 'small'),item.i_color IN 
('firebrick', 'maroon', 'papaya', 'peach', 'powder', 'sienna', 'sky', 
'smoke'),item.i_units IN ('Bundle', 'Carton', 'Case', 'Cup', 'Dozen', 'Each', 
'Lb', 'Ounce'),OR[AND[item.i_size IN ('economy', 'small'),item.i_color IN 
('firebrick', 'maroon', 'sienna', 'smoke'),item.i_units IN ('Case', 'Cup', 
'Each', 'Ounce'),OR[AND[item.i_color IN ('maroon', 'smoke [...]
 --------------------------------PhysicalOlapScan[item]
 
diff --git 
a/regression-test/data/shape_check/tpcds_sf1000_nopkfk/shape/query41.out 
b/regression-test/data/shape_check/tpcds_sf1000_nopkfk/shape/query41.out
index da8ef93bfa0..4619303a182 100644
--- a/regression-test/data/shape_check/tpcds_sf1000_nopkfk/shape/query41.out
+++ b/regression-test/data/shape_check/tpcds_sf1000_nopkfk/shape/query41.out
@@ -18,6 +18,6 @@ PhysicalResultSink
 ------------------------PhysicalDistribute[DistributionSpecHash]
 --------------------------hashAgg[LOCAL]
 ----------------------------PhysicalProject
-------------------------------filter(OR[AND[(item.i_category = 
'Men'),item.i_size IN ('N/A', 'economy', 'large', 'small'),OR[AND[item.i_size 
IN ('economy', 'small'),item.i_color IN ('firebrick', 'maroon', 'sienna', 
'smoke'),item.i_units IN ('Case', 'Cup', 'Each', 'Ounce'),OR[AND[item.i_color 
IN ('maroon', 'smoke'),item.i_units IN ('Case', 'Ounce')],AND[item.i_color IN 
('firebrick', 'sienna'),item.i_units IN ('Cup', 'Each')]]],AND[item.i_size IN 
('N/A', 'large'),item.i_color IN ('papaya', [...]
+------------------------------filter(OR[AND[(item.i_category = 
'Men'),item.i_size IN ('N/A', 'economy', 'large', 'small'),item.i_color IN 
('firebrick', 'maroon', 'papaya', 'peach', 'powder', 'sienna', 'sky', 
'smoke'),item.i_units IN ('Bundle', 'Carton', 'Case', 'Cup', 'Dozen', 'Each', 
'Lb', 'Ounce'),OR[AND[item.i_size IN ('economy', 'small'),item.i_color IN 
('firebrick', 'maroon', 'sienna', 'smoke'),item.i_units IN ('Case', 'Cup', 
'Each', 'Ounce'),OR[AND[item.i_color IN ('maroon', 'smoke [...]
 --------------------------------PhysicalOlapScan[item]
 
diff --git a/regression-test/data/shape_check/tpcds_sf10t_orc/shape/query41.out 
b/regression-test/data/shape_check/tpcds_sf10t_orc/shape/query41.out
index 9f24d6d0ca0..ee0504be27f 100644
--- a/regression-test/data/shape_check/tpcds_sf10t_orc/shape/query41.out
+++ b/regression-test/data/shape_check/tpcds_sf10t_orc/shape/query41.out
@@ -18,6 +18,6 @@ PhysicalResultSink
 ------------------------PhysicalDistribute[DistributionSpecHash]
 --------------------------hashAgg[LOCAL]
 ----------------------------PhysicalProject
-------------------------------filter(OR[AND[(item.i_category = 
'Men'),item.i_size IN ('N/A', 'large', 'medium', 'small'),OR[AND[item.i_size IN 
('large', 'medium'),item.i_color IN ('cornflower', 'cyan', 'firebrick', 
'maroon'),item.i_units IN ('Each', 'N/A', 'Oz', 'Pound'),OR[AND[item.i_color IN 
('cornflower', 'firebrick'),item.i_units IN ('Oz', 'Pound')],AND[item.i_color 
IN ('cyan', 'maroon'),item.i_units IN ('Each', 'N/A')]]],AND[item.i_size IN 
('N/A', 'small'),item.i_color IN ('lavender [...]
+------------------------------filter(OR[AND[(item.i_category = 
'Men'),item.i_size IN ('N/A', 'large', 'medium', 'small'),item.i_color IN 
('cornflower', 'cyan', 'firebrick', 'lavender', 'magenta', 'maroon', 'papaya', 
'slate'),item.i_units IN ('Bundle', 'Carton', 'Cup', 'Each', 'N/A', 'Oz', 
'Pallet', 'Pound'),OR[AND[item.i_size IN ('large', 'medium'),item.i_color IN 
('cornflower', 'cyan', 'firebrick', 'maroon'),item.i_units IN ('Each', 'N/A', 
'Oz', 'Pound'),OR[AND[item.i_color IN ('cornflo [...]
 --------------------------------PhysicalOlapScan[item]
 
diff --git 
a/regression-test/suites/nereids_rules_p0/or_to_in/or_to_in_complex_type.groovy 
b/regression-test/suites/nereids_rules_p0/or_to_in/or_to_in_complex_type.groovy
new file mode 100644
index 00000000000..ba5520cd08e
--- /dev/null
+++ 
b/regression-test/suites/nereids_rules_p0/or_to_in/or_to_in_complex_type.groovy
@@ -0,0 +1,98 @@
+
+// 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.
+
+suite("or_to_in_complex_type") {
+    sql "SET enable_nereids_planner=true"
+    sql "SET enable_fallback_to_original_planner=false"
+    sql "drop table if exists or_to_in_array_null"
+
+    sql """
+        CREATE TABLE or_to_in_array_null (
+            id INT,
+            a ARRAY<INT> NULL,
+            k INT
+        ) DUPLICATE KEY(id)
+        DISTRIBUTED BY HASH(id) BUCKETS 1
+        PROPERTIES ("replication_num" = "1")
+    """
+
+    sql """
+        INSERT INTO or_to_in_array_null VALUES
+            (1, NULL, 0),
+            (2, [1], 0),
+            (3, [3], 0)
+    """
+
+    // OrToIn in extract mode relaxes the independence check for "slot = 
literal" candidates so
+    // that a top-level "slot IN (...)" can still be inferred when the slot is 
shared with a
+    // sibling conjunct. That relaxation is only sound while "=" and IN agree 
on NULL.
+    //
+    // For ARRAY, IN is executed by FunctionCollectionIn, which unwraps the 
left nullable column
+    // without merging its null map: for a = NULL it returns a non-null FALSE 
where "a = [1]"
+    // returns NULL. So "a IN ([1], [2]) AND (<original OR>)" is no longer 
equivalent to the
+    // original OR, and a NULL-sensitive parent changes the result.
+    //
+    // Correct answer: only id=1, whose OR evaluates to NULL.
+    // With the array candidate admitted, the added IN yields FALSE for id=1, 
so
+    // "FALSE AND NULL" makes IS NULL false and the row disappears.
+    qt_array_shared_slot_is_null """
+        SELECT id FROM or_to_in_array_null
+        WHERE ((a = [1] AND size(a) > k) OR a = [2]) IS NULL
+        ORDER BY id
+    """
+
+    // The same relaxation must still fire for scalar types, otherwise the 
optimization loses
+    // its value for the STRING/INT predicates it was written for.
+    // id=4 is the NULL row: the inferred "s IN ('x', 'y')" keeps NULL, so the 
OR stays NULL.
+    sql "drop table if exists or_to_in_scalar_null"
+    sql """
+        CREATE TABLE or_to_in_scalar_null (
+            id INT,
+            s VARCHAR(16) NULL,
+            k INT
+        ) DUPLICATE KEY(id)
+        DISTRIBUTED BY HASH(id) BUCKETS 1
+        PROPERTIES ("replication_num" = "1")
+    """
+    sql """
+        INSERT INTO or_to_in_scalar_null VALUES
+            (1, 'x', 0),
+            (2, 'other', 0),
+            (3, 'z', 0),
+            (4, NULL, 0)
+    """
+    qt_scalar_shared_slot_is_null """
+        SELECT id FROM or_to_in_scalar_null
+        WHERE ((s = 'x' AND length(s) > k) OR s = 'y') IS NULL
+        ORDER BY id
+    """
+
+    // The inferred IN is only appended, never substituted, so the plain WHERE 
form must return
+    // exactly the rows the OR selects.
+    qt_scalar_shared_slot_where """
+        SELECT id FROM or_to_in_scalar_null
+        WHERE (s = 'x' AND length(s) > k) OR s = 'y'
+        ORDER BY id
+    """
+
+    qt_array_shared_slot_where """
+        SELECT id FROM or_to_in_array_null
+        WHERE (a = [1] AND size(a) > k) OR a = [2]
+        ORDER BY id
+    """
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to