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

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


The following commit(s) were added to refs/heads/master by this push:
     new 79530d9bb4 [flink] Support negated predicates in PredicateConverter 
(#9427)
79530d9bb4 is described below

commit 79530d9bb4f7938fe3d750f499a425f53c23acd3
Author: Wenchao Wu <[email protected]>
AuthorDate: Sun Sep 20 13:13:08 2026 +0800

    [flink] Support negated predicates in PredicateConverter (#9427)
---
 .../apache/paimon/flink/PredicateConverter.java    | 292 ++++++++++---
 .../paimon/flink/PredicateConverterTest.java       | 454 ++++++++++++++++++++-
 .../apache/paimon/flink/ReadWriteTableITCase.java  |  92 +++++
 .../paimon/flink/source/FlinkTableSourceTest.java  | 235 +++++++++++
 4 files changed, 1016 insertions(+), 57 deletions(-)

diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/PredicateConverter.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/PredicateConverter.java
index db3754f86d..d14b1ccce1 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/PredicateConverter.java
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/PredicateConverter.java
@@ -36,13 +36,13 @@ import org.apache.flink.table.functions.FunctionDefinition;
 import org.apache.flink.table.types.DataType;
 import org.apache.flink.table.types.logical.LogicalType;
 import org.apache.flink.table.types.logical.LogicalTypeFamily;
+import org.apache.flink.table.types.logical.LogicalTypeRoot;
 import org.apache.flink.table.types.logical.RowType;
 
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Deque;
 import java.util.List;
-import java.util.Objects;
 import java.util.Optional;
 import java.util.function.BiFunction;
 import java.util.regex.Matcher;
@@ -74,74 +74,136 @@ public class PredicateConverter implements 
ExpressionVisitor<Predicate> {
 
     @Override
     public Predicate visit(CallExpression call) {
+        return visit(call, false);
+    }
+
+    private Predicate visit(CallExpression call, boolean negated) {
         FunctionDefinition func = call.getFunctionDefinition();
         List<Expression> children = call.getChildren();
 
         if (func == BuiltInFunctionDefinitions.AND) {
-            return PredicateBuilder.and(flattenAndConvert(children, func));
+            requireAtLeastArity(children, 2);
+            List<Predicate> predicates = flattenAndConvert(children, func, 
negated);
+            return negated ? PredicateBuilder.or(predicates) : 
PredicateBuilder.and(predicates);
         } else if (func == BuiltInFunctionDefinitions.OR) {
-            return PredicateBuilder.or(flattenAndConvert(children, func));
+            requireAtLeastArity(children, 2);
+            List<Predicate> predicates = flattenAndConvert(children, func, 
negated);
+            return negated ? PredicateBuilder.and(predicates) : 
PredicateBuilder.or(predicates);
+        } else if (func == BuiltInFunctionDefinitions.NOT) {
+            requireArity(children, 1);
+            return visit(children.get(0), !negated);
         } else if (func == BuiltInFunctionDefinitions.EQUALS) {
-            return visitBiFunction(children, builder::equal, builder::equal);
+            return visitComparison(
+                    children,
+                    negated,
+                    builder::notEqual,
+                    builder::notEqual,
+                    builder::equal,
+                    builder::equal);
         } else if (func == BuiltInFunctionDefinitions.NOT_EQUALS) {
-            return visitBiFunction(children, builder::notEqual, 
builder::notEqual);
+            return visitComparison(
+                    children,
+                    negated,
+                    builder::equal,
+                    builder::equal,
+                    builder::notEqual,
+                    builder::notEqual);
         } else if (func == BuiltInFunctionDefinitions.GREATER_THAN) {
-            return visitBiFunction(children, builder::greaterThan, 
builder::lessThan);
+            return visitComparison(
+                    children,
+                    negated,
+                    builder::lessOrEqual,
+                    builder::greaterOrEqual,
+                    builder::greaterThan,
+                    builder::lessThan);
         } else if (func == BuiltInFunctionDefinitions.GREATER_THAN_OR_EQUAL) {
-            return visitBiFunction(children, builder::greaterOrEqual, 
builder::lessOrEqual);
+            return visitComparison(
+                    children,
+                    negated,
+                    builder::lessThan,
+                    builder::greaterThan,
+                    builder::greaterOrEqual,
+                    builder::lessOrEqual);
         } else if (func == BuiltInFunctionDefinitions.LESS_THAN) {
-            return visitBiFunction(children, builder::lessThan, 
builder::greaterThan);
+            return visitComparison(
+                    children,
+                    negated,
+                    builder::greaterOrEqual,
+                    builder::lessOrEqual,
+                    builder::lessThan,
+                    builder::greaterThan);
         } else if (func == BuiltInFunctionDefinitions.LESS_THAN_OR_EQUAL) {
-            return visitBiFunction(children, builder::lessOrEqual, 
builder::greaterOrEqual);
+            return visitComparison(
+                    children,
+                    negated,
+                    builder::greaterThan,
+                    builder::lessThan,
+                    builder::lessOrEqual,
+                    builder::greaterOrEqual);
         } else if (func == BuiltInFunctionDefinitions.IN) {
-            FieldReferenceExpression fieldRefExpr =
-                    
extractFieldReference(children.get(0)).orElseThrow(UnsupportedExpression::new);
+            requireAtLeastArity(children, 2);
+            ResolvedField field = resolveField(children.get(0));
             List<Object> literals = new ArrayList<>();
             for (int i = 1; i < children.size(); i++) {
-                literals.add(extractLiteral(fieldRefExpr.getOutputDataType(), 
children.get(i)));
+                
literals.add(extractLiteral(field.expression.getOutputDataType(), 
children.get(i)));
+            }
+            if (negated) {
+                // SQL WHERE: v NOT IN (..., NULL, ...) is never true 
regardless of
+                // type, so this runs before the float residual. Passing NULL 
to
+                // notIn is also unsafe for BSI/range-bitmap file-index 
evaluation.
+                if (literals.contains(null)) {
+                    return PredicateBuilder.alwaysFalse();
+                }
+                rejectNegatedFloatingPoint(field.expression);
+                return builder.notIn(field.index, literals);
             }
-            return builder.in(builder.indexOf(fieldRefExpr.getName()), 
literals);
+            return builder.in(field.index, literals);
         } else if (func == BuiltInFunctionDefinitions.IS_NULL) {
-            return extractFieldReference(children.get(0))
-                    .map(FieldReferenceExpression::getName)
-                    .map(builder::indexOf)
-                    .map(builder::isNull)
-                    .orElseThrow(UnsupportedExpression::new);
+            requireArity(children, 1);
+            ResolvedField field = resolveField(children.get(0));
+            return negated ? builder.isNotNull(field.index) : 
builder.isNull(field.index);
         } else if (func == BuiltInFunctionDefinitions.IS_NOT_NULL) {
-            return extractFieldReference(children.get(0))
-                    .map(FieldReferenceExpression::getName)
-                    .map(builder::indexOf)
-                    .map(builder::isNotNull)
-                    .orElseThrow(UnsupportedExpression::new);
+            requireArity(children, 1);
+            ResolvedField field = resolveField(children.get(0));
+            return negated ? builder.isNull(field.index) : 
builder.isNotNull(field.index);
         } else if (func == BuiltInFunctionDefinitions.BETWEEN) {
-            FieldReferenceExpression fieldRefExpr =
-                    
extractFieldReference(children.get(0)).orElseThrow(UnsupportedExpression::new);
-            DataType fieldType = fieldRefExpr.getOutputDataType();
-            return builder.between(
-                    builder.indexOf(fieldRefExpr.getName()),
-                    extractLiteral(fieldType, children.get(1)),
-                    extractLiteral(fieldType, children.get(2)));
+            requireArity(children, 3);
+            ResolvedField field = resolveField(children.get(0));
+            DataType fieldType = field.expression.getOutputDataType();
+            Object lower = extractLiteral(fieldType, children.get(1));
+            Object upper = extractLiteral(fieldType, children.get(2));
+            Predicate between = builder.between(field.index, lower, upper);
+            if (negated) {
+                rejectNegatedFloatingPoint(field.expression);
+                // LeafTernaryFunction.test returns false if any literal is 
null, but
+                // 12 NOT BETWEEN 15 AND NULL is TRUE (TRUE OR UNKNOWN). Keep 
residual
+                // so Flink can evaluate the three-valued cases.
+                if (lower == null || upper == null) {
+                    throw new UnsupportedExpression();
+                }
+                return negate(between);
+            }
+            return between;
         } else if (func == BuiltInFunctionDefinitions.LIKE) {
-            FieldReferenceExpression fieldRefExpr =
-                    
extractFieldReference(children.get(0)).orElseThrow(UnsupportedExpression::new);
-            if (fieldRefExpr
+            if (children.size() != 2 && children.size() != 3) {
+                throw new UnsupportedExpression();
+            }
+            ResolvedField field = resolveField(children.get(0));
+            if (field.expression
                     .getOutputDataType()
                     .getLogicalType()
                     .getTypeRoot()
                     .getFamilies()
                     .contains(LogicalTypeFamily.CHARACTER_STRING)) {
                 String sqlPattern =
-                        Objects.requireNonNull(
-                                        extractLiteral(
-                                                
fieldRefExpr.getOutputDataType(), children.get(1)))
+                        
extractNonNullLiteral(field.expression.getOutputDataType(), children.get(1))
                                 .toString();
                 String escape =
                         children.size() <= 2
                                 ? null
-                                : Objects.requireNonNull(
-                                                extractLiteral(
-                                                        
fieldRefExpr.getOutputDataType(),
-                                                        children.get(2)))
+                                : extractNonNullLiteral(
+                                                
field.expression.getOutputDataType(),
+                                                children.get(2))
                                         .toString();
                 String escapedSqlPattern = sqlPattern;
                 boolean allowQuick = false;
@@ -185,27 +247,57 @@ public class PredicateConverter implements 
ExpressionVisitor<Predicate> {
                 if (allowQuick) {
                     Matcher beginMatcher = 
BEGIN_PATTERN.matcher(escapedSqlPattern);
                     if (beginMatcher.matches()) {
+                        if (negated) {
+                            // StartsWith has no negated predicate, so NOT 
LIKE must remain a
+                            // residual filter evaluated by Flink.
+                            throw new UnsupportedExpression();
+                        }
                         return builder.startsWith(
-                                builder.indexOf(fieldRefExpr.getName()),
-                                
BinaryString.fromString(beginMatcher.group(1)));
+                                field.index, 
BinaryString.fromString(beginMatcher.group(1)));
                     }
                 }
             }
         } else if (func == BuiltInFunctionDefinitions.IS_TRUE) {
-            FieldReferenceExpression fieldRefExpr =
-                    
extractFieldReference(children.get(0)).orElseThrow(UnsupportedExpression::new);
-            return builder.equal(builder.indexOf(fieldRefExpr.getName()), 
Boolean.TRUE);
+            requireArity(children, 1);
+            return booleanTest(resolveField(children.get(0)), true, negated);
         } else if (func == BuiltInFunctionDefinitions.IS_FALSE) {
-            FieldReferenceExpression fieldRefExpr =
-                    
extractFieldReference(children.get(0)).orElseThrow(UnsupportedExpression::new);
-            return builder.equal(builder.indexOf(fieldRefExpr.getName()), 
Boolean.FALSE);
+            requireArity(children, 1);
+            return booleanTest(resolveField(children.get(0)), false, negated);
+        } else if (func == BuiltInFunctionDefinitions.IS_NOT_TRUE) {
+            requireArity(children, 1);
+            return booleanTest(resolveField(children.get(0)), true, !negated);
+        } else if (func == BuiltInFunctionDefinitions.IS_NOT_FALSE) {
+            requireArity(children, 1);
+            return booleanTest(resolveField(children.get(0)), false, !negated);
         }
 
-        // TODO is_xxx, between_xxx, similar, in, not_in, not?
+        throw new UnsupportedExpression();
+    }
 
+    private Predicate visit(Expression expression, boolean negated) {
+        if (expression instanceof CallExpression) {
+            return visit((CallExpression) expression, negated);
+        }
         throw new UnsupportedExpression();
     }
 
+    private Predicate booleanTest(ResolvedField field, boolean expected, 
boolean complement) {
+        if (field.expression.getOutputDataType().getLogicalType().getTypeRoot()
+                != LogicalTypeRoot.BOOLEAN) {
+            throw new UnsupportedExpression();
+        }
+        Predicate equals = builder.equal(field.index, expected);
+        if (!complement) {
+            return equals;
+        }
+        return PredicateBuilder.or(
+                builder.isNull(field.index), builder.notEqual(field.index, 
expected));
+    }
+
+    private Predicate negate(Predicate predicate) {
+        return predicate.negate().orElseThrow(UnsupportedExpression::new);
+    }
+
     /**
      * Iteratively flattens a nested AND/OR expression tree into a flat list 
of child predicates,
      * avoiding stack overflow caused by recursive {@code accept} calls on 
deeply nested trees (e.g.
@@ -213,10 +305,11 @@ public class PredicateConverter implements 
ExpressionVisitor<Predicate> {
      *
      * @param children the children of the top-level AND/OR {@link 
CallExpression}
      * @param targetFunc the function definition to flatten ({@code AND} or 
{@code OR})
+     * @param negated whether to negate every flattened child and combine them 
using De Morgan's law
      * @return a flat list of converted child predicates in original order
      */
     private List<Predicate> flattenAndConvert(
-            List<Expression> children, FunctionDefinition targetFunc) {
+            List<Expression> children, FunctionDefinition targetFunc, boolean 
negated) {
         List<Predicate> result = new ArrayList<>();
         Deque<Expression> stack = new ArrayDeque<>();
         for (int i = children.size() - 1; i >= 0; i--) {
@@ -228,40 +321,112 @@ public class PredicateConverter implements 
ExpressionVisitor<Predicate> {
                 CallExpression ce = (CallExpression) expr;
                 if (ce.getFunctionDefinition() == targetFunc) {
                     List<Expression> ceChildren = ce.getChildren();
+                    requireAtLeastArity(ceChildren, 2);
                     for (int i = ceChildren.size() - 1; i >= 0; i--) {
                         stack.push(ceChildren.get(i));
                     }
                 } else {
-                    result.add(ce.accept(this));
+                    result.add(visit(ce, negated));
                 }
             } else {
-                result.add(expr.accept(this));
+                result.add(visit(expr, negated));
             }
         }
         return result;
     }
 
+    private Predicate visitComparison(
+            List<Expression> children,
+            boolean negated,
+            BiFunction<Integer, Object, Predicate> negatedVisit1,
+            BiFunction<Integer, Object, Predicate> negatedVisit2,
+            BiFunction<Integer, Object, Predicate> visit1,
+            BiFunction<Integer, Object, Predicate> visit2) {
+        // Flink FLOAT/DOUBLE comparisons use Java operators; Paimon uses
+        // Float/Double.compareTo. Negated equality, inequality, IN and BETWEEN
+        // are therefore not equivalent (NaN identity and signed zeros). Simple
+        // SQL such as NOT (d > 1.0) is often simplified to d <= 1.0 before
+        // pushdown; keep this guard for unsimplified NOT that still reaches
+        // applyFilters (for example De Morgan over AND/OR).
+        if (negated && isFloatingPointComparison(children)) {
+            throw new UnsupportedExpression();
+        }
+        return negated
+                ? visitBiFunction(children, negatedVisit1, negatedVisit2)
+                : visitBiFunction(children, visit1, visit2);
+    }
+
+    private void rejectNegatedFloatingPoint(FieldReferenceExpression field) {
+        if (isFloatingPointField(field)) {
+            throw new UnsupportedExpression();
+        }
+    }
+
+    private boolean isFloatingPointComparison(List<Expression> children) {
+        for (Expression child : children) {
+            Optional<FieldReferenceExpression> field = 
extractFieldReference(child);
+            if (field.isPresent() && isFloatingPointField(field.get())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean isFloatingPointField(FieldReferenceExpression field) {
+        LogicalTypeRoot root = 
field.getOutputDataType().getLogicalType().getTypeRoot();
+        return root == LogicalTypeRoot.FLOAT || root == LogicalTypeRoot.DOUBLE;
+    }
+
     private Predicate visitBiFunction(
             List<Expression> children,
             BiFunction<Integer, Object, Predicate> visit1,
             BiFunction<Integer, Object, Predicate> visit2) {
+        requireArity(children, 2);
         Optional<FieldReferenceExpression> fieldRefExpr = 
extractFieldReference(children.get(0));
         if (fieldRefExpr.isPresent()) {
+            int fieldIndex = resolveFieldIndex(fieldRefExpr.get());
             Object literal =
                     extractLiteral(fieldRefExpr.get().getOutputDataType(), 
children.get(1));
-            return visit1.apply(builder.indexOf(fieldRefExpr.get().getName()), 
literal);
+            return visit1.apply(fieldIndex, literal);
         } else {
             fieldRefExpr = extractFieldReference(children.get(1));
             if (fieldRefExpr.isPresent()) {
+                int fieldIndex = resolveFieldIndex(fieldRefExpr.get());
                 Object literal =
                         extractLiteral(fieldRefExpr.get().getOutputDataType(), 
children.get(0));
-                return 
visit2.apply(builder.indexOf(fieldRefExpr.get().getName()), literal);
+                return visit2.apply(fieldIndex, literal);
             }
         }
 
         throw new UnsupportedExpression();
     }
 
+    private ResolvedField resolveField(Expression expression) {
+        FieldReferenceExpression field =
+                
extractFieldReference(expression).orElseThrow(UnsupportedExpression::new);
+        return new ResolvedField(field, resolveFieldIndex(field));
+    }
+
+    private int resolveFieldIndex(FieldReferenceExpression field) {
+        int index = builder.indexOf(field.getName());
+        if (index < 0) {
+            throw new UnsupportedExpression();
+        }
+        return index;
+    }
+
+    private void requireArity(List<Expression> children, int expected) {
+        if (children.size() != expected) {
+            throw new UnsupportedExpression();
+        }
+    }
+
+    private void requireAtLeastArity(List<Expression> children, int minimum) {
+        if (children.size() < minimum) {
+            throw new UnsupportedExpression();
+        }
+    }
+
     private Optional<FieldReferenceExpression> 
extractFieldReference(Expression expression) {
         if (expression instanceof FieldReferenceExpression) {
             return Optional.of((FieldReferenceExpression) expression);
@@ -304,6 +469,14 @@ public class PredicateConverter implements 
ExpressionVisitor<Predicate> {
         throw new UnsupportedExpression();
     }
 
+    private Object extractNonNullLiteral(DataType expectedType, Expression 
expression) {
+        Object literal = extractLiteral(expectedType, expression);
+        if (literal == null) {
+            throw new UnsupportedExpression();
+        }
+        return literal;
+    }
+
     private boolean supportsPredicate(LogicalType type) {
         switch (type.getTypeRoot()) {
             case CHAR:
@@ -331,6 +504,17 @@ public class PredicateConverter implements 
ExpressionVisitor<Predicate> {
         }
     }
 
+    private static class ResolvedField {
+
+        private final FieldReferenceExpression expression;
+        private final int index;
+
+        private ResolvedField(FieldReferenceExpression expression, int index) {
+            this.expression = expression;
+            this.index = index;
+        }
+    }
+
     @Override
     public Predicate visit(ValueLiteralExpression valueLiteralExpression) {
         throw new UnsupportedExpression();
diff --git 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/PredicateConverterTest.java
 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/PredicateConverterTest.java
index e05773991f..b472fdafd8 100644
--- 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/PredicateConverterTest.java
+++ 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/PredicateConverterTest.java
@@ -22,6 +22,9 @@ import org.apache.paimon.data.BinaryString;
 import org.apache.paimon.data.GenericRow;
 import org.apache.paimon.format.SimpleColStats;
 import org.apache.paimon.predicate.CompoundPredicate;
+import org.apache.paimon.predicate.In;
+import org.apache.paimon.predicate.LeafPredicate;
+import org.apache.paimon.predicate.NotBetween;
 import org.apache.paimon.predicate.Or;
 import org.apache.paimon.predicate.Predicate;
 import org.apache.paimon.predicate.PredicateBuilder;
@@ -37,7 +40,9 @@ import org.apache.flink.table.functions.FunctionDefinition;
 import org.apache.flink.table.types.DataType;
 import org.apache.flink.table.types.logical.BigIntType;
 import org.apache.flink.table.types.logical.DoubleType;
+import org.apache.flink.table.types.logical.FloatType;
 import org.apache.flink.table.types.logical.IntType;
+import org.apache.flink.table.types.logical.LogicalType;
 import org.apache.flink.table.types.logical.RowType;
 import org.apache.flink.table.types.logical.VarCharType;
 import org.apache.flink.types.Row;
@@ -46,6 +51,7 @@ import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
@@ -79,8 +85,7 @@ public class PredicateConverterTest {
     @ParameterizedTest
     public void testVisitAndAutoTypeInference(ResolvedExpression expression, 
Predicate expected) {
         if (expression instanceof CallExpression) {
-            assertThat(CONVERTER.visit((CallExpression) expression).toString())
-                    .isEqualTo(expected.toString());
+            assertThat(CONVERTER.visit((CallExpression) 
expression)).isEqualTo(expected);
         } else {
             assertThatThrownBy(() -> CONVERTER.visit(expression))
                     
.isInstanceOf(PredicateConverter.UnsupportedExpression.class);
@@ -258,7 +263,7 @@ public class PredicateConverterTest {
                                 BuiltInFunctionDefinitions.BETWEEN,
                                 Arrays.asList(longRefExpr, intLitExpr, 
intLitExpr2),
                                 DataTypes.BOOLEAN()),
-                        BUILDER.between(0, 10, 20)),
+                        BUILDER.between(0, 10L, 20L)),
                 Arguments.of(
                         CallExpression.permanent(
                                 BuiltInFunctionDefinitions.IS_TRUE,
@@ -314,6 +319,449 @@ public class PredicateConverterTest {
         assertThat(nullUpperBound.test(GenericRow.of(15L))).isFalse();
     }
 
+    @Test
+    public void testInAndNotInRowSemantics() {
+        PredicateConverter converter = new PredicateConverter(RowType.of(new 
BigIntType()));
+        PredicateBuilder builder = predicateBuilder(RowType.of(new 
BigIntType()));
+        CallExpression in =
+                call(
+                        BuiltInFunctionDefinitions.IN,
+                        field(0, DataTypes.BIGINT()),
+                        literal(1, DataTypes.INT()),
+                        literal(null, DataTypes.BIGINT()),
+                        literal(3, DataTypes.INT()));
+
+        Predicate inPredicate = in.accept(converter);
+        Predicate notInPredicate = call(BuiltInFunctionDefinitions.NOT, 
in).accept(converter);
+
+        assertThat(inPredicate).isEqualTo(builder.in(0, Arrays.asList(1L, 
null, 3L)));
+        assertThat(notInPredicate).isEqualTo(PredicateBuilder.alwaysFalse());
+        assertThat(inPredicate.test(GenericRow.of(1L))).isTrue();
+        assertThat(inPredicate.test(GenericRow.of(2L))).isFalse();
+        assertThat(inPredicate.test(GenericRow.of(3L))).isTrue();
+        assertThat(inPredicate.test(GenericRow.of((Object) null))).isFalse();
+        for (Object value : Arrays.asList(null, 1L, 2L, 3L, 4L)) {
+            assertThat(notInPredicate.test(GenericRow.of(value))).isFalse();
+        }
+    }
+
+    @Test
+    public void testLargeInAndNotIn() {
+        PredicateConverter converter = new PredicateConverter(RowType.of(new 
BigIntType()));
+        PredicateBuilder builder = predicateBuilder(RowType.of(new 
BigIntType()));
+        List<ResolvedExpression> children = new ArrayList<>();
+        List<Object> expectedLiterals = new ArrayList<>();
+        children.add(field(0, DataTypes.BIGINT()));
+        for (int i = 0; i < 21; i++) {
+            children.add(literal(i, DataTypes.INT()));
+            expectedLiterals.add((long) i);
+        }
+        CallExpression in =
+                new CallExpression(
+                        false, null, BuiltInFunctionDefinitions.IN, children, 
DataTypes.BOOLEAN());
+
+        Predicate inPredicate = in.accept(converter);
+        Predicate notInPredicate = call(BuiltInFunctionDefinitions.NOT, 
in).accept(converter);
+
+        assertThat(inPredicate).isEqualTo(builder.in(0, expectedLiterals));
+        assertThat(inPredicate).isInstanceOf(LeafPredicate.class);
+        assertThat(((LeafPredicate) 
inPredicate).function()).isEqualTo(In.INSTANCE);
+        assertThat(((LeafPredicate) inPredicate).literals())
+                .containsExactlyElementsOf(expectedLiterals);
+        assertThat(notInPredicate).isEqualTo(builder.notIn(0, 
expectedLiterals));
+        assertThat(inPredicate.test(GenericRow.of(20L))).isTrue();
+        assertThat(inPredicate.test(GenericRow.of(21L))).isFalse();
+        assertThat(inPredicate.test(GenericRow.of((Object) null))).isFalse();
+        assertThat(notInPredicate.test(GenericRow.of(20L))).isFalse();
+        assertThat(notInPredicate.test(GenericRow.of(21L))).isTrue();
+        assertThat(notInPredicate.test(GenericRow.of((Object) 
null))).isFalse();
+    }
+
+    @Test
+    public void testNotBetweenStructure() {
+        PredicateConverter converter = new PredicateConverter(RowType.of(new 
BigIntType()));
+        PredicateBuilder builder = predicateBuilder(RowType.of(new 
BigIntType()));
+        CallExpression between =
+                call(
+                        BuiltInFunctionDefinitions.BETWEEN,
+                        field(0, DataTypes.BIGINT()),
+                        literal(10, DataTypes.INT()),
+                        literal(20, DataTypes.INT()));
+
+        Predicate predicate = call(BuiltInFunctionDefinitions.NOT, 
between).accept(converter);
+
+        assertThat(predicate).isEqualTo(builder.between(0, 10L, 
20L).negate().get());
+        assertThat(predicate).isInstanceOf(LeafPredicate.class);
+        assertThat(((LeafPredicate) 
predicate).function()).isEqualTo(NotBetween.INSTANCE);
+        assertThat(((LeafPredicate) 
predicate).literals()).containsExactly(10L, 20L);
+    }
+
+    @Test
+    public void testNotBetweenWithNullBoundsRemainUnsupported() {
+        RowType rowType = RowType.of(new BigIntType());
+        PredicateConverter converter = new PredicateConverter(rowType);
+        FieldReferenceExpression field = field(0, DataTypes.BIGINT());
+        CallExpression nullUpper =
+                call(
+                        BuiltInFunctionDefinitions.NOT,
+                        call(
+                                BuiltInFunctionDefinitions.BETWEEN,
+                                field,
+                                literal(15, DataTypes.INT()),
+                                literal(null, DataTypes.BIGINT())));
+        CallExpression nullLower =
+                call(
+                        BuiltInFunctionDefinitions.NOT,
+                        call(
+                                BuiltInFunctionDefinitions.BETWEEN,
+                                field,
+                                literal(null, DataTypes.BIGINT()),
+                                literal(10, DataTypes.INT())));
+
+        assertThatThrownBy(() -> nullUpper.accept(converter))
+                .isInstanceOf(PredicateConverter.UnsupportedExpression.class);
+        assertThatThrownBy(() -> nullLower.accept(converter))
+                .isInstanceOf(PredicateConverter.UnsupportedExpression.class);
+        assertThat(PredicateConverter.convert(rowType, nullUpper)).isEmpty();
+        assertThat(PredicateConverter.convert(rowType, nullLower)).isEmpty();
+    }
+
+    @MethodSource("provideNegatedComparisons")
+    @ParameterizedTest
+    public void testNegatedComparisons(
+            FunctionDefinition function, boolean literalOnLeft, Predicate 
expected) {
+        PredicateConverter converter = new PredicateConverter(RowType.of(new 
BigIntType()));
+        ResolvedExpression field = field(0, DataTypes.BIGINT());
+        ResolvedExpression literal = literal(1, DataTypes.INT());
+        CallExpression comparison =
+                literalOnLeft ? call(function, literal, field) : 
call(function, field, literal);
+
+        assertThat(call(BuiltInFunctionDefinitions.NOT, 
comparison).accept(converter))
+                .isEqualTo(expected);
+    }
+
+    public static Stream<Arguments> provideNegatedComparisons() {
+        PredicateBuilder builder = predicateBuilder(RowType.of(new 
BigIntType()));
+        return Stream.of(
+                Arguments.of(BuiltInFunctionDefinitions.EQUALS, false, 
builder.notEqual(0, 1L)),
+                Arguments.of(BuiltInFunctionDefinitions.NOT_EQUALS, false, 
builder.equal(0, 1L)),
+                Arguments.of(
+                        BuiltInFunctionDefinitions.GREATER_THAN, false, 
builder.lessOrEqual(0, 1L)),
+                Arguments.of(
+                        BuiltInFunctionDefinitions.GREATER_THAN_OR_EQUAL,
+                        false,
+                        builder.lessThan(0, 1L)),
+                Arguments.of(
+                        BuiltInFunctionDefinitions.LESS_THAN, false, 
builder.greaterOrEqual(0, 1L)),
+                Arguments.of(
+                        BuiltInFunctionDefinitions.LESS_THAN_OR_EQUAL,
+                        false,
+                        builder.greaterThan(0, 1L)),
+                Arguments.of(BuiltInFunctionDefinitions.EQUALS, true, 
builder.notEqual(0, 1L)),
+                Arguments.of(BuiltInFunctionDefinitions.NOT_EQUALS, true, 
builder.equal(0, 1L)),
+                Arguments.of(
+                        BuiltInFunctionDefinitions.GREATER_THAN,
+                        true,
+                        builder.greaterOrEqual(0, 1L)),
+                Arguments.of(
+                        BuiltInFunctionDefinitions.GREATER_THAN_OR_EQUAL,
+                        true,
+                        builder.greaterThan(0, 1L)),
+                Arguments.of(
+                        BuiltInFunctionDefinitions.LESS_THAN, true, 
builder.lessOrEqual(0, 1L)),
+                Arguments.of(
+                        BuiltInFunctionDefinitions.LESS_THAN_OR_EQUAL,
+                        true,
+                        builder.lessThan(0, 1L)));
+    }
+
+    @MethodSource("provideNegatedFloatingPointComparisons")
+    @ParameterizedTest
+    public void testNegatedFloatingPointComparisonsRemainUnsupported(
+            LogicalType fieldType,
+            DataType fieldDataType,
+            ResolvedExpression literal,
+            Object greater,
+            Object lesser,
+            Object nan) {
+        RowType rowType = RowType.of(fieldType);
+        PredicateConverter converter = new PredicateConverter(rowType);
+        ResolvedExpression field = field(0, fieldDataType);
+        for (FunctionDefinition function :
+                Arrays.asList(
+                        BuiltInFunctionDefinitions.EQUALS,
+                        BuiltInFunctionDefinitions.NOT_EQUALS,
+                        BuiltInFunctionDefinitions.GREATER_THAN,
+                        BuiltInFunctionDefinitions.GREATER_THAN_OR_EQUAL,
+                        BuiltInFunctionDefinitions.LESS_THAN,
+                        BuiltInFunctionDefinitions.LESS_THAN_OR_EQUAL)) {
+            CallExpression negated =
+                    call(BuiltInFunctionDefinitions.NOT, call(function, field, 
literal));
+            CallExpression negatedLiteralOnLeft =
+                    call(BuiltInFunctionDefinitions.NOT, call(function, 
literal, field));
+            assertThatThrownBy(() -> negated.accept(converter))
+                    
.isInstanceOf(PredicateConverter.UnsupportedExpression.class);
+            assertThatThrownBy(() -> negatedLiteralOnLeft.accept(converter))
+                    
.isInstanceOf(PredicateConverter.UnsupportedExpression.class);
+            assertThat(PredicateConverter.convert(rowType, negated)).isEmpty();
+            assertThat(PredicateConverter.convert(rowType, 
negatedLiteralOnLeft)).isEmpty();
+        }
+
+        ResolvedExpression nanLiteral = literal(nan, fieldDataType);
+        CallExpression notIn =
+                call(
+                        BuiltInFunctionDefinitions.NOT,
+                        call(BuiltInFunctionDefinitions.IN, field, 
nanLiteral));
+        CallExpression notBetween =
+                call(
+                        BuiltInFunctionDefinitions.NOT,
+                        call(BuiltInFunctionDefinitions.BETWEEN, field, 
literal, nanLiteral));
+        assertThatThrownBy(() -> notIn.accept(converter))
+                .isInstanceOf(PredicateConverter.UnsupportedExpression.class);
+        assertThatThrownBy(() -> notBetween.accept(converter))
+                .isInstanceOf(PredicateConverter.UnsupportedExpression.class);
+        assertThat(PredicateConverter.convert(rowType, notIn)).isEmpty();
+        assertThat(PredicateConverter.convert(rowType, notBetween)).isEmpty();
+
+        // Non-negated comparisons stay supported. Paimon orders NaN via 
compareTo,
+        // so GreaterThan(NaN, 1.0) is true, unlike Flink's Java operators.
+        Predicate greaterThan =
+                call(BuiltInFunctionDefinitions.GREATER_THAN, field, 
literal).accept(converter);
+        assertThat(greaterThan.test(GenericRow.of(greater))).isTrue();
+        assertThat(greaterThan.test(GenericRow.of(lesser))).isFalse();
+        assertThat(greaterThan.test(GenericRow.of(nan))).isTrue();
+        Predicate equal =
+                call(BuiltInFunctionDefinitions.EQUALS, field, 
nanLiteral).accept(converter);
+        assertThat(equal.test(GenericRow.of(nan))).isTrue();
+        Predicate in = call(BuiltInFunctionDefinitions.IN, field, 
nanLiteral).accept(converter);
+        assertThat(in.test(GenericRow.of(nan))).isTrue();
+        Predicate between =
+                call(BuiltInFunctionDefinitions.BETWEEN, field, literal, 
nanLiteral)
+                        .accept(converter);
+        assertThat(between.test(GenericRow.of(greater))).isTrue();
+    }
+
+    public static Stream<Arguments> provideNegatedFloatingPointComparisons() {
+        return Stream.of(
+                Arguments.of(
+                        new DoubleType(),
+                        DataTypes.DOUBLE(),
+                        literal(1.0d, DataTypes.DOUBLE()),
+                        2.0d,
+                        0.5d,
+                        Double.NaN),
+                Arguments.of(
+                        new FloatType(),
+                        DataTypes.FLOAT(),
+                        literal(1.0f, DataTypes.FLOAT()),
+                        2.0f,
+                        0.5f,
+                        Float.NaN));
+    }
+
+    @Test
+    public void 
testNegatedFloatingPointComparisonInCompoundRemainsUnsupported() {
+        // Covers unsimplified NOT(AND(float_cmp, ...)) reaching the converter,
+        // which simple SQL NOT (d > 1.0) does not (Flink rewrites that to d 
<= 1.0).
+        RowType rowType = RowType.of(new DoubleType(), new BigIntType());
+        PredicateConverter converter = new PredicateConverter(rowType);
+        CallExpression negated =
+                call(
+                        BuiltInFunctionDefinitions.NOT,
+                        call(
+                                BuiltInFunctionDefinitions.AND,
+                                call(
+                                        
BuiltInFunctionDefinitions.GREATER_THAN,
+                                        field(0, DataTypes.DOUBLE()),
+                                        literal(1.0d, DataTypes.DOUBLE())),
+                                call(
+                                        BuiltInFunctionDefinitions.EQUALS,
+                                        field(1, DataTypes.BIGINT()),
+                                        literal(10L, DataTypes.BIGINT()))));
+
+        assertThatThrownBy(() -> negated.accept(converter))
+                .isInstanceOf(PredicateConverter.UnsupportedExpression.class);
+        assertThat(PredicateConverter.convert(rowType, negated)).isEmpty();
+    }
+
+    @Test
+    public void testNegatedFloatingPointSignedZerosRemainUnsupported() {
+        RowType rowType = RowType.of(new DoubleType());
+        PredicateConverter converter = new PredicateConverter(rowType);
+        ResolvedExpression field = field(0, DataTypes.DOUBLE());
+        ResolvedExpression zero = literal(0.0d, DataTypes.DOUBLE());
+        // Java: -0.0 == 0.0, so NOT (d <> 0.0) keeps -0.0. Double.compare 
does not.
+        CallExpression negatedNotEquals =
+                call(
+                        BuiltInFunctionDefinitions.NOT,
+                        call(BuiltInFunctionDefinitions.NOT_EQUALS, field, 
zero));
+        CallExpression negatedEquals =
+                call(
+                        BuiltInFunctionDefinitions.NOT,
+                        call(BuiltInFunctionDefinitions.EQUALS, field, zero));
+
+        assertThatThrownBy(() -> negatedNotEquals.accept(converter))
+                .isInstanceOf(PredicateConverter.UnsupportedExpression.class);
+        assertThatThrownBy(() -> negatedEquals.accept(converter))
+                .isInstanceOf(PredicateConverter.UnsupportedExpression.class);
+        assertThat(PredicateConverter.convert(rowType, 
negatedNotEquals)).isEmpty();
+        assertThat(PredicateConverter.convert(rowType, 
negatedEquals)).isEmpty();
+
+        // Non-negated equal stays on the pre-existing compareTo path.
+        Predicate equal = call(BuiltInFunctionDefinitions.EQUALS, field, 
zero).accept(converter);
+        assertThat(equal.test(GenericRow.of(0.0d))).isTrue();
+        assertThat(equal.test(GenericRow.of(-0.0d))).isFalse();
+    }
+
+    @Test
+    public void testGenericNotComparisonAndDoubleNot() {
+        PredicateBuilder builder = predicateBuilder(RowType.of(new 
BigIntType()));
+        PredicateConverter converter = new PredicateConverter(RowType.of(new 
BigIntType()));
+        CallExpression equal =
+                call(
+                        BuiltInFunctionDefinitions.EQUALS,
+                        field(0, DataTypes.BIGINT()),
+                        literal(10, DataTypes.INT()));
+        CallExpression equalNull =
+                call(
+                        BuiltInFunctionDefinitions.EQUALS,
+                        field(0, DataTypes.BIGINT()),
+                        literal(null, DataTypes.BIGINT()));
+
+        Predicate notEqual = call(BuiltInFunctionDefinitions.NOT, 
equal).accept(converter);
+        Predicate notEqualNull = call(BuiltInFunctionDefinitions.NOT, 
equalNull).accept(converter);
+        Predicate doubleNot =
+                call(BuiltInFunctionDefinitions.NOT, 
call(BuiltInFunctionDefinitions.NOT, equal))
+                        .accept(converter);
+
+        assertThat(notEqual).isEqualTo(builder.notEqual(0, 10L));
+        assertThat(notEqualNull).isEqualTo(builder.notEqual(0, null));
+        assertThat(doubleNot).isEqualTo(builder.equal(0, 10L));
+        assertThat(notEqual.test(GenericRow.of(9L))).isTrue();
+        assertThat(notEqual.test(GenericRow.of(10L))).isFalse();
+        assertThat(notEqual.test(GenericRow.of((Object) null))).isFalse();
+        assertThat(notEqualNull.test(GenericRow.of(10L))).isFalse();
+        assertThat(notEqualNull.test(GenericRow.of((Object) null))).isFalse();
+    }
+
+    @Test
+    public void testGenericNotAndOr() {
+        PredicateBuilder builder = predicateBuilder(RowType.of(new 
BigIntType()));
+        PredicateConverter converter = new PredicateConverter(RowType.of(new 
BigIntType()));
+        CallExpression equal10 =
+                call(
+                        BuiltInFunctionDefinitions.EQUALS,
+                        field(0, DataTypes.BIGINT()),
+                        literal(10L, DataTypes.BIGINT()));
+        CallExpression equal20 =
+                call(
+                        BuiltInFunctionDefinitions.EQUALS,
+                        field(0, DataTypes.BIGINT()),
+                        literal(20L, DataTypes.BIGINT()));
+
+        Predicate notAnd =
+                call(
+                                BuiltInFunctionDefinitions.NOT,
+                                call(BuiltInFunctionDefinitions.AND, equal10, 
equal20))
+                        .accept(converter);
+        Predicate notOr =
+                call(
+                                BuiltInFunctionDefinitions.NOT,
+                                call(BuiltInFunctionDefinitions.OR, equal10, 
equal20))
+                        .accept(converter);
+
+        assertThat(notAnd)
+                .isEqualTo(PredicateBuilder.or(builder.notEqual(0, 10L), 
builder.notEqual(0, 20L)));
+        assertThat(notOr)
+                .isEqualTo(
+                        PredicateBuilder.and(builder.notEqual(0, 10L), 
builder.notEqual(0, 20L)));
+        assertThat(notAnd.test(GenericRow.of(10L))).isTrue();
+        assertThat(notAnd.test(GenericRow.of((Object) null))).isFalse();
+        assertThat(notOr.test(GenericRow.of(10L))).isFalse();
+        assertThat(notOr.test(GenericRow.of(15L))).isTrue();
+        assertThat(notOr.test(GenericRow.of((Object) null))).isFalse();
+    }
+
+    @Test
+    public void testBooleanTruthPredicatesAndNot() {
+        PredicateBuilder builder =
+                
predicateBuilder(RowType.of(DataTypes.BOOLEAN().getLogicalType()));
+        PredicateConverter converter =
+                new 
PredicateConverter(RowType.of(DataTypes.BOOLEAN().getLogicalType()));
+        ResolvedExpression boolField = field(0, DataTypes.BOOLEAN());
+        CallExpression isTrue = call(BuiltInFunctionDefinitions.IS_TRUE, 
boolField);
+        CallExpression isFalse = call(BuiltInFunctionDefinitions.IS_FALSE, 
boolField);
+        CallExpression isNotTrue = 
call(BuiltInFunctionDefinitions.IS_NOT_TRUE, boolField);
+        CallExpression isNotFalse = 
call(BuiltInFunctionDefinitions.IS_NOT_FALSE, boolField);
+
+        Predicate truePredicate = isTrue.accept(converter);
+        Predicate falsePredicate = isFalse.accept(converter);
+        Predicate notTruePredicate = isNotTrue.accept(converter);
+        Predicate notFalsePredicate = isNotFalse.accept(converter);
+
+        assertThat(truePredicate).isEqualTo(builder.equal(0, true));
+        assertThat(falsePredicate).isEqualTo(builder.equal(0, false));
+        assertBooleanResults(truePredicate, true, false, false);
+        assertBooleanResults(falsePredicate, false, true, false);
+        assertBooleanResults(notTruePredicate, false, true, true);
+        assertBooleanResults(notFalsePredicate, true, false, true);
+        assertBooleanResults(
+                call(BuiltInFunctionDefinitions.NOT, 
isTrue).accept(converter), false, true, true);
+        assertBooleanResults(
+                call(BuiltInFunctionDefinitions.NOT, 
isFalse).accept(converter), true, false, true);
+        assertBooleanResults(
+                call(BuiltInFunctionDefinitions.NOT, 
isNotTrue).accept(converter),
+                true,
+                false,
+                false);
+        assertBooleanResults(
+                call(BuiltInFunctionDefinitions.NOT, 
isNotFalse).accept(converter),
+                false,
+                true,
+                false);
+    }
+
+    @Test
+    public void testUnsupportedNotLike() {
+        RowType rowType = RowType.of(new VarCharType());
+        PredicateConverter converter = new PredicateConverter(RowType.of(new 
VarCharType()));
+        CallExpression unsupportedLike =
+                call(
+                        BuiltInFunctionDefinitions.LIKE,
+                        field(0, STRING()),
+                        literal("%middle%", STRING()));
+
+        assertThatThrownBy(
+                        () ->
+                                call(BuiltInFunctionDefinitions.NOT, 
unsupportedLike)
+                                        .accept(converter))
+                .isInstanceOf(PredicateConverter.UnsupportedExpression.class);
+
+        CallExpression prefixLike =
+                call(
+                        BuiltInFunctionDefinitions.LIKE,
+                        field(0, STRING()),
+                        literal("prefix%", STRING()));
+        CallExpression notPrefixLike = call(BuiltInFunctionDefinitions.NOT, 
prefixLike);
+        assertThatThrownBy(() -> notPrefixLike.accept(converter))
+                .isInstanceOf(PredicateConverter.UnsupportedExpression.class);
+        assertThat(PredicateConverter.convert(rowType, 
notPrefixLike)).isEmpty();
+    }
+
+    private static void assertBooleanResults(
+            Predicate predicate,
+            boolean expectedForTrue,
+            boolean expectedForFalse,
+            boolean expectedForNull) {
+        
assertThat(predicate.test(GenericRow.of(true))).isEqualTo(expectedForTrue);
+        
assertThat(predicate.test(GenericRow.of(false))).isEqualTo(expectedForFalse);
+        assertThat(predicate.test(GenericRow.of((Object) 
null))).isEqualTo(expectedForNull);
+    }
+
+    private static PredicateBuilder predicateBuilder(RowType rowType) {
+        return new PredicateBuilder(LogicalTypeConversion.toDataType(rowType));
+    }
+
     @MethodSource("provideLikeExpressions")
     @ParameterizedTest
     public void testStartsWith(
diff --git 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/ReadWriteTableITCase.java
 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/ReadWriteTableITCase.java
index e0b4172448..0b1d05ea60 100644
--- 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/ReadWriteTableITCase.java
+++ 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/ReadWriteTableITCase.java
@@ -1127,6 +1127,98 @@ public class ReadWriteTableITCase extends 
AbstractTestBase {
                 expected);
     }
 
+    @Test
+    public void testNullablePredicateThreeValuedLogic() throws Exception {
+        String table =
+                createTable(
+                        Arrays.asList("id INT", "v INT", "flag BOOLEAN"),
+                        Collections.emptyList(),
+                        Collections.singletonList("id"),
+                        Collections.emptyList());
+
+        insertInto(
+                table,
+                "(1, CAST(NULL AS INT), CAST(NULL AS BOOLEAN))",
+                "(2, 1, TRUE)",
+                "(3, 2, FALSE)",
+                "(4, 3, CAST(NULL AS BOOLEAN))",
+                "(5, 4, TRUE)");
+
+        testBatchRead(
+                buildQuery(table, "id", "WHERE v BETWEEN 1 AND 3"),
+                Arrays.asList(changelogRow("+I", 2), changelogRow("+I", 3), 
changelogRow("+I", 4)));
+        testBatchRead(
+                buildQuery(table, "id", "WHERE v NOT BETWEEN 1 AND 3"),
+                Collections.singletonList(changelogRow("+I", 5)));
+        testBatchRead(
+                buildQuery(table, "id", "WHERE v IN (1, 3)"),
+                Arrays.asList(changelogRow("+I", 2), changelogRow("+I", 4)));
+        testBatchRead(
+                buildQuery(table, "id", "WHERE v NOT IN (1, 3)"),
+                Arrays.asList(changelogRow("+I", 3), changelogRow("+I", 5)));
+        testBatchRead(
+                buildQuery(table, "id", "WHERE v IN (1, NULL, 3)"),
+                Arrays.asList(changelogRow("+I", 2), changelogRow("+I", 4)));
+        testBatchRead(
+                buildQuery(table, "id", "WHERE v NOT IN (1, NULL, 3)"), 
Collections.emptyList());
+        testBatchRead(
+                buildQuery(table, "id", "WHERE flag IS TRUE"),
+                Arrays.asList(changelogRow("+I", 2), changelogRow("+I", 5)));
+        testBatchRead(
+                buildQuery(table, "id", "WHERE flag IS NOT TRUE"),
+                Arrays.asList(changelogRow("+I", 1), changelogRow("+I", 3), 
changelogRow("+I", 4)));
+        testBatchRead(
+                buildQuery(table, "id", "WHERE NOT (flag IS TRUE)"),
+                Arrays.asList(changelogRow("+I", 1), changelogRow("+I", 3), 
changelogRow("+I", 4)));
+    }
+
+    @Test
+    public void testNotBetweenWithNullBoundsThreeValuedLogic() throws 
Exception {
+        String table =
+                createTable(
+                        Arrays.asList("id INT", "v INT"),
+                        Collections.emptyList(),
+                        Collections.singletonList("id"),
+                        Collections.emptyList());
+
+        insertInto(table, "(1, 12)", "(2, 16)", "(3, 8)", "(4, CAST(NULL AS 
INT))");
+
+        // 12 NOT BETWEEN 15 AND NULL = 12 < 15 OR 12 > NULL = TRUE
+        // 16 NOT BETWEEN 15 AND NULL = UNKNOWN (dropped by WHERE)
+        testBatchRead(
+                buildQuery(table, "id", "WHERE v NOT BETWEEN 15 AND NULL"),
+                Arrays.asList(changelogRow("+I", 1), changelogRow("+I", 3)));
+
+        // 12 NOT BETWEEN NULL AND 10 = 12 < NULL OR 12 > 10 = TRUE
+        // 8 NOT BETWEEN NULL AND 10 = UNKNOWN
+        testBatchRead(
+                buildQuery(table, "id", "WHERE v NOT BETWEEN NULL AND 10"),
+                Arrays.asList(changelogRow("+I", 1), changelogRow("+I", 2)));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"file-index.bsi.columns", 
"file-index.range-bitmap.columns"})
+    public void testNotInNullWithFileIndex(String indexOption) throws 
Exception {
+        Map<String, String> options = new HashMap<>();
+        options.put(indexOption, "v");
+        options.put("file-index.in-manifest-threshold", "1B");
+        String table =
+                createTable(
+                        Arrays.asList("id INT", "v INT"),
+                        Collections.emptyList(),
+                        Collections.singletonList("id"),
+                        Collections.emptyList(),
+                        options);
+
+        insertInto(table, "(1, CAST(NULL AS INT))", "(2, 1)", "(3, 2)", "(4, 
3)", "(5, 4)");
+
+        testBatchRead(
+                buildQuery(table, "id", "WHERE v NOT IN (1, NULL, 3)"), 
Collections.emptyList());
+        testBatchRead(
+                buildQuery(table, "id", "WHERE v NOT IN (1, 3)"),
+                Arrays.asList(changelogRow("+I", 3), changelogRow("+I", 5)));
+    }
+
     @Test
     public void testUnsupportedPredicate() throws Exception {
         String table =
diff --git 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FlinkTableSourceTest.java
 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FlinkTableSourceTest.java
index 74183a40e2..b8e236ae60 100644
--- 
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FlinkTableSourceTest.java
+++ 
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/source/FlinkTableSourceTest.java
@@ -23,6 +23,7 @@ import org.apache.paimon.fs.Path;
 import org.apache.paimon.fs.local.LocalFileIO;
 import org.apache.paimon.predicate.CompoundPredicate;
 import org.apache.paimon.predicate.Or;
+import org.apache.paimon.predicate.PredicateBuilder;
 import org.apache.paimon.schema.FileSystemSchemaManager;
 import org.apache.paimon.schema.Schema;
 import org.apache.paimon.schema.TableSchema;
@@ -141,6 +142,119 @@ public class FlinkTableSourceTest extends TableTestBase {
                 .isEqualTo(ImmutableList.of(filters.get(1)));
     }
 
+    @Test
+    public void testApplyNotFilters() throws Exception {
+        FileIO fileIO = LocalFileIO.create();
+        Path tablePath = new Path(String.format("%s/%s.db/%s", warehouse, 
database, "T"));
+        Schema schema =
+                Schema.newBuilder()
+                        .column("col1", DataTypes.INT())
+                        .column("col2", DataTypes.INT())
+                        .column("p1", DataTypes.INT())
+                        .column("p2", DataTypes.STRING())
+                        .partitionKeys("p1", "p2")
+                        .build();
+        TableSchema tableSchema =
+                new FileSystemSchemaManager(fileIO, 
tablePath).createTable(schema);
+        Table table = FileStoreTableFactory.create(LocalFileIO.create(), 
tablePath, tableSchema);
+        PredicateBuilder builder = new PredicateBuilder(table.rowType());
+
+        ResolvedExpression supportedPartitionFilter = not(p1Equal1());
+        FlinkTableSource tableSource = dataTableSource(table);
+        Assertions.assertThat(
+                        tableSource
+                                
.applyFilters(ImmutableList.of(supportedPartitionFilter))
+                                .getRemainingFilters())
+                .isEmpty();
+        
Assertions.assertThat(tableSource.predicate).isEqualTo(builder.notEqual(2, 1));
+
+        ResolvedExpression supportedNonPartitionFilter = not(col1Equal1());
+        tableSource = dataTableSource(table);
+        Assertions.assertThat(
+                        tableSource
+                                
.applyFilters(ImmutableList.of(supportedNonPartitionFilter))
+                                .getRemainingFilters())
+                .containsExactly(supportedNonPartitionFilter);
+        
Assertions.assertThat(tableSource.predicate).isEqualTo(builder.notEqual(0, 1));
+
+        ResolvedExpression unsupportedLike = not(p2Like("%a"));
+        tableSource = dataTableSource(table);
+        Assertions.assertThat(
+                        tableSource
+                                
.applyFilters(ImmutableList.of(unsupportedLike))
+                                .getRemainingFilters())
+                .containsExactly(unsupportedLike);
+        Assertions.assertThat(tableSource.predicate).isNull();
+
+        ResolvedExpression unsupportedPrefixLike = not(p2Like("prefix%"));
+        tableSource = dataTableSource(table);
+        Assertions.assertThat(
+                        tableSource
+                                
.applyFilters(ImmutableList.of(unsupportedPrefixLike))
+                                .getRemainingFilters())
+                .containsExactly(unsupportedPrefixLike);
+        Assertions.assertThat(tableSource.predicate).isNull();
+
+        ResolvedExpression unsupportedSimilar = not(p2Similar("a.*"));
+        tableSource = dataTableSource(table);
+        Assertions.assertThat(
+                        tableSource
+                                
.applyFilters(ImmutableList.of(unsupportedSimilar))
+                                .getRemainingFilters())
+                .containsExactly(unsupportedSimilar);
+        Assertions.assertThat(tableSource.predicate).isNull();
+    }
+
+    @Test
+    public void testApplyNegatedFloatingPointComparison() throws Exception {
+        // Simple SQL such as NOT (d = NaN), d NOT IN (NaN) and NOT (d <> 0.0)
+        // is normalized by Flink to <>(d, NaN) or =(d, 0.0) before 
applyFilters,
+        // so those queries never hit this residual. CAST(-0.0 AS DOUBLE) is 
also
+        // +0.0 in Flink. Coverage is the unsimplified AST below.
+        FileIO fileIO = LocalFileIO.create();
+        Path tablePath = new Path(String.format("%s/%s.db/%s", warehouse, 
database, "T"));
+        Schema schema = Schema.newBuilder().column("d", 
DataTypes.DOUBLE()).build();
+        TableSchema tableSchema =
+                new FileSystemSchemaManager(fileIO, 
tablePath).createTable(schema);
+        Table table = FileStoreTableFactory.create(LocalFileIO.create(), 
tablePath, tableSchema);
+
+        assertResidual(table, not(doubleGreaterThan1()));
+        assertResidual(table, not(doubleEqual(1.0d)));
+        assertResidual(table, not(doubleNotEqual(0.0d)));
+        assertResidual(table, not(doubleIn(Double.NaN)));
+        assertResidual(table, not(doubleBetween(1.0d, Double.NaN)));
+    }
+
+    @Test
+    public void testApplyNotBetweenWithNullBounds() throws Exception {
+        FileIO fileIO = LocalFileIO.create();
+        Path tablePath = new Path(String.format("%s/%s.db/%s", warehouse, 
database, "T"));
+        Schema schema = Schema.newBuilder().column("col1", 
DataTypes.INT()).build();
+        TableSchema tableSchema =
+                new FileSystemSchemaManager(fileIO, 
tablePath).createTable(schema);
+        Table table = FileStoreTableFactory.create(LocalFileIO.create(), 
tablePath, tableSchema);
+
+        assertResidual(table, not(col1Between(15, null)));
+        assertResidual(table, not(col1Between(null, 10)));
+    }
+
+    @Test
+    public void testApplyNotInWithNull() throws Exception {
+        FileIO fileIO = LocalFileIO.create();
+        Path tablePath = new Path(String.format("%s/%s.db/%s", warehouse, 
database, "T"));
+        Schema schema = Schema.newBuilder().column("col1", 
DataTypes.INT()).build();
+        TableSchema tableSchema =
+                new FileSystemSchemaManager(fileIO, 
tablePath).createTable(schema);
+        Table table = FileStoreTableFactory.create(LocalFileIO.create(), 
tablePath, tableSchema);
+
+        ResolvedExpression notInNull = not(col1InWithNull());
+        FlinkTableSource tableSource = dataTableSource(table);
+        Assertions.assertThat(
+                        
tableSource.applyFilters(ImmutableList.of(notInNull)).getRemainingFilters())
+                .containsExactly(notInNull);
+        
Assertions.assertThat(tableSource.predicate).isEqualTo(PredicateBuilder.alwaysFalse());
+    }
+
     // ==================== Nested OR Tree Tests ====================
     //
     // These tests construct OR trees in various shapes — mimicking what 
Flink's
@@ -335,6 +449,97 @@ public class FlinkTableSourceTest extends TableTestBase {
                 org.apache.flink.table.api.DataTypes.BOOLEAN());
     }
 
+    private ResolvedExpression col1InWithNull() {
+        return CallExpression.anonymous(
+                BuiltInFunctionDefinitions.IN,
+                ImmutableList.of(
+                        new FieldReferenceExpression(
+                                "col1", 
org.apache.flink.table.api.DataTypes.INT(), 0, 0),
+                        new ValueLiteralExpression(
+                                1, 
org.apache.flink.table.api.DataTypes.INT().notNull()),
+                        new ValueLiteralExpression(
+                                null, 
org.apache.flink.table.api.DataTypes.INT()),
+                        new ValueLiteralExpression(
+                                3, 
org.apache.flink.table.api.DataTypes.INT().notNull())),
+                org.apache.flink.table.api.DataTypes.BOOLEAN());
+    }
+
+    private ResolvedExpression doubleGreaterThan1() {
+        return CallExpression.anonymous(
+                BuiltInFunctionDefinitions.GREATER_THAN,
+                ImmutableList.of(
+                        new FieldReferenceExpression(
+                                "d", 
org.apache.flink.table.api.DataTypes.DOUBLE(), 0, 0),
+                        new ValueLiteralExpression(
+                                1.0d, 
org.apache.flink.table.api.DataTypes.DOUBLE().notNull())),
+                org.apache.flink.table.api.DataTypes.BOOLEAN());
+    }
+
+    private ResolvedExpression doubleEqual(double value) {
+        return CallExpression.anonymous(
+                BuiltInFunctionDefinitions.EQUALS,
+                ImmutableList.of(
+                        new FieldReferenceExpression(
+                                "d", 
org.apache.flink.table.api.DataTypes.DOUBLE(), 0, 0),
+                        new ValueLiteralExpression(
+                                value, 
org.apache.flink.table.api.DataTypes.DOUBLE().notNull())),
+                org.apache.flink.table.api.DataTypes.BOOLEAN());
+    }
+
+    private ResolvedExpression doubleNotEqual(double value) {
+        return CallExpression.anonymous(
+                BuiltInFunctionDefinitions.NOT_EQUALS,
+                ImmutableList.of(
+                        new FieldReferenceExpression(
+                                "d", 
org.apache.flink.table.api.DataTypes.DOUBLE(), 0, 0),
+                        new ValueLiteralExpression(
+                                value, 
org.apache.flink.table.api.DataTypes.DOUBLE().notNull())),
+                org.apache.flink.table.api.DataTypes.BOOLEAN());
+    }
+
+    private ResolvedExpression doubleIn(double value) {
+        return CallExpression.anonymous(
+                BuiltInFunctionDefinitions.IN,
+                ImmutableList.of(
+                        new FieldReferenceExpression(
+                                "d", 
org.apache.flink.table.api.DataTypes.DOUBLE(), 0, 0),
+                        new ValueLiteralExpression(
+                                value, 
org.apache.flink.table.api.DataTypes.DOUBLE().notNull())),
+                org.apache.flink.table.api.DataTypes.BOOLEAN());
+    }
+
+    private ResolvedExpression doubleBetween(double lower, double upper) {
+        return CallExpression.anonymous(
+                BuiltInFunctionDefinitions.BETWEEN,
+                ImmutableList.of(
+                        new FieldReferenceExpression(
+                                "d", 
org.apache.flink.table.api.DataTypes.DOUBLE(), 0, 0),
+                        new ValueLiteralExpression(
+                                lower, 
org.apache.flink.table.api.DataTypes.DOUBLE().notNull()),
+                        new ValueLiteralExpression(
+                                upper, 
org.apache.flink.table.api.DataTypes.DOUBLE().notNull())),
+                org.apache.flink.table.api.DataTypes.BOOLEAN());
+    }
+
+    private ResolvedExpression col1Between(Integer lower, Integer upper) {
+        return CallExpression.anonymous(
+                BuiltInFunctionDefinitions.BETWEEN,
+                ImmutableList.of(
+                        new FieldReferenceExpression(
+                                "col1", 
org.apache.flink.table.api.DataTypes.INT(), 0, 0),
+                        intLiteral(lower),
+                        intLiteral(upper)),
+                org.apache.flink.table.api.DataTypes.BOOLEAN());
+    }
+
+    private ValueLiteralExpression intLiteral(Integer value) {
+        if (value == null) {
+            return new ValueLiteralExpression(null, 
org.apache.flink.table.api.DataTypes.INT());
+        }
+        return new ValueLiteralExpression(
+                value, org.apache.flink.table.api.DataTypes.INT().notNull());
+    }
+
     private ResolvedExpression p1Equal1() {
         return CallExpression.anonymous(
                 BuiltInFunctionDefinitions.EQUALS,
@@ -357,6 +562,17 @@ public class FlinkTableSourceTest extends TableTestBase {
                 org.apache.flink.table.api.DataTypes.BOOLEAN());
     }
 
+    private ResolvedExpression p2Similar(String literal) {
+        return CallExpression.anonymous(
+                BuiltInFunctionDefinitions.SIMILAR,
+                ImmutableList.of(
+                        new FieldReferenceExpression(
+                                "p2", 
org.apache.flink.table.api.DataTypes.STRING(), 0, 3),
+                        new ValueLiteralExpression(
+                                literal, 
org.apache.flink.table.api.DataTypes.STRING().notNull())),
+                org.apache.flink.table.api.DataTypes.BOOLEAN());
+    }
+
     // where rand(42) > 0.1
     private ResolvedExpression rand() {
         return CallExpression.anonymous(
@@ -406,4 +622,23 @@ public class FlinkTableSourceTest extends TableTestBase {
                 ImmutableList.of(e1, e2),
                 org.apache.flink.table.api.DataTypes.BOOLEAN());
     }
+
+    private ResolvedExpression not(ResolvedExpression expression) {
+        return CallExpression.anonymous(
+                BuiltInFunctionDefinitions.NOT,
+                ImmutableList.of(expression),
+                org.apache.flink.table.api.DataTypes.BOOLEAN());
+    }
+
+    private void assertResidual(Table table, ResolvedExpression filter) {
+        FlinkTableSource tableSource = dataTableSource(table);
+        Assertions.assertThat(
+                        
tableSource.applyFilters(ImmutableList.of(filter)).getRemainingFilters())
+                .containsExactly(filter);
+        Assertions.assertThat(tableSource.predicate).isNull();
+    }
+
+    private DataTableSource dataTableSource(Table table) {
+        return new DataTableSource(ObjectIdentifier.of("catalog1", "db1", 
"T"), table, false, null);
+    }
 }

Reply via email to