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

luoyuxia pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fluss.git


The following commit(s) were added to refs/heads/main by this push:
     new 70a71b99c [lake/hudi] Support predicate pushdown (#3501)
70a71b99c is described below

commit 70a71b99cbf66b4b4d843af80df09422ba22832f
Author: fhan <[email protected]>
AuthorDate: Sun Jun 21 08:43:51 2026 +0800

    [lake/hudi] Support predicate pushdown (#3501)
---
 .../fluss/lake/hudi/source/HudiLakeSource.java     |  46 +++-
 .../FlussToHudiExpressionPredicateConverter.java   | 281 +++++++++++++++++++++
 .../fluss/lake/hudi/source/HudiLakeSourceTest.java |  23 +-
 ...lussToHudiExpressionPredicateConverterTest.java | 163 ++++++++++++
 4 files changed, 491 insertions(+), 22 deletions(-)

diff --git 
a/fluss-lake/fluss-lake-hudi/src/main/java/org/apache/fluss/lake/hudi/source/HudiLakeSource.java
 
b/fluss-lake/fluss-lake-hudi/src/main/java/org/apache/fluss/lake/hudi/source/HudiLakeSource.java
index 87f9ee0de..7b1636862 100644
--- 
a/fluss-lake/fluss-lake-hudi/src/main/java/org/apache/fluss/lake/hudi/source/HudiLakeSource.java
+++ 
b/fluss-lake/fluss-lake-hudi/src/main/java/org/apache/fluss/lake/hudi/source/HudiLakeSource.java
@@ -18,6 +18,8 @@
 package org.apache.fluss.lake.hudi.source;
 
 import org.apache.fluss.config.Configuration;
+import 
org.apache.fluss.lake.hudi.utils.FlussToHudiExpressionPredicateConverter;
+import org.apache.fluss.lake.hudi.utils.HudiTableInfo;
 import org.apache.fluss.lake.serializer.SimpleVersionedSerializer;
 import org.apache.fluss.lake.source.LakeSource;
 import org.apache.fluss.lake.source.Planner;
@@ -25,12 +27,17 @@ import org.apache.fluss.lake.source.RecordReader;
 import org.apache.fluss.metadata.TablePath;
 import org.apache.fluss.predicate.Predicate;
 
+import org.apache.hudi.org.apache.avro.Schema;
+import org.apache.hudi.source.ExpressionPredicates;
+import org.apache.hudi.util.StreamerUtil;
+
 import javax.annotation.Nullable;
 
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 
 /** Hudi implementation of {@link LakeSource}. */
 public class HudiLakeSource implements LakeSource<HudiSplit> {
@@ -40,6 +47,7 @@ public class HudiLakeSource implements LakeSource<HudiSplit> {
     private final Configuration hudiConfig;
     private final TablePath tablePath;
     private @Nullable int[][] project;
+    private List<ExpressionPredicates.Predicate> predicates = 
Collections.emptyList();
 
     public HudiLakeSource(Configuration hudiConfig, TablePath tablePath) {
         this.hudiConfig = hudiConfig;
@@ -58,7 +66,32 @@ public class HudiLakeSource implements LakeSource<HudiSplit> 
{
 
     @Override
     public FilterPushDownResult withFilters(List<Predicate> predicates) {
-        return FilterPushDownResult.of(Collections.emptyList(), new 
ArrayList<>(predicates));
+        if (predicates.isEmpty()) {
+            this.predicates = Collections.emptyList();
+            return FilterPushDownResult.of(Collections.emptyList(), 
Collections.emptyList());
+        }
+
+        List<Predicate> remainingPredicates = new ArrayList<>();
+        List<Predicate> acceptedPredicates = new ArrayList<>();
+        List<ExpressionPredicates.Predicate> convertedPredicates = new 
ArrayList<>();
+
+        Schema hudiSchema = getHudiSchema();
+        for (Predicate predicate : predicates) {
+            Optional<ExpressionPredicates.Predicate> convertedPredicate =
+                    
FlussToHudiExpressionPredicateConverter.convert(hudiSchema, predicate);
+            if (convertedPredicate.isPresent()) {
+                acceptedPredicates.add(predicate);
+                convertedPredicates.add(convertedPredicate.get());
+            } else {
+                remainingPredicates.add(predicate);
+            }
+        }
+
+        this.predicates =
+                convertedPredicates.isEmpty()
+                        ? Collections.emptyList()
+                        : Collections.unmodifiableList(new 
ArrayList<>(convertedPredicates));
+        return FilterPushDownResult.of(acceptedPredicates, 
remainingPredicates);
     }
 
     @Override
@@ -69,7 +102,8 @@ public class HudiLakeSource implements LakeSource<HudiSplit> 
{
     @Override
     public RecordReader createRecordReader(ReaderContext<HudiSplit> context) 
throws IOException {
         try {
-            return new HudiRecordReader(hudiConfig, tablePath, 
context.lakeSplit(), project);
+            return new HudiRecordReader(
+                    hudiConfig, tablePath, context.lakeSplit(), project, 
predicates);
         } catch (Exception e) {
             throw new IOException("Fail to create Hudi record reader for " + 
tablePath + ".", e);
         }
@@ -79,4 +113,12 @@ public class HudiLakeSource implements 
LakeSource<HudiSplit> {
     public SimpleVersionedSerializer<HudiSplit> getSplitSerializer() {
         return new HudiSplitSerializer();
     }
+
+    private Schema getHudiSchema() {
+        try (HudiTableInfo hudiTableInfo = HudiTableInfo.create(tablePath, 
hudiConfig)) {
+            return 
StreamerUtil.getTableAvroSchema(hudiTableInfo.getMetaClient(), true);
+        } catch (Exception e) {
+            throw new RuntimeException("Fail to get Hudi schema for " + 
tablePath + ".", e);
+        }
+    }
 }
diff --git 
a/fluss-lake/fluss-lake-hudi/src/main/java/org/apache/fluss/lake/hudi/utils/FlussToHudiExpressionPredicateConverter.java
 
b/fluss-lake/fluss-lake-hudi/src/main/java/org/apache/fluss/lake/hudi/utils/FlussToHudiExpressionPredicateConverter.java
new file mode 100644
index 000000000..b4f26739d
--- /dev/null
+++ 
b/fluss-lake/fluss-lake-hudi/src/main/java/org/apache/fluss/lake/hudi/utils/FlussToHudiExpressionPredicateConverter.java
@@ -0,0 +1,281 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.fluss.lake.hudi.utils;
+
+import org.apache.fluss.predicate.And;
+import org.apache.fluss.predicate.CompoundPredicate;
+import org.apache.fluss.predicate.FieldRef;
+import org.apache.fluss.predicate.FunctionVisitor;
+import org.apache.fluss.predicate.LeafPredicate;
+import org.apache.fluss.predicate.Or;
+import org.apache.fluss.predicate.Predicate;
+import org.apache.fluss.predicate.PredicateVisitor;
+import org.apache.fluss.row.BinaryString;
+import org.apache.fluss.row.TimestampLtz;
+import org.apache.fluss.row.TimestampNtz;
+
+import org.apache.flink.table.expressions.FieldReferenceExpression;
+import org.apache.flink.table.expressions.ValueLiteralExpression;
+import org.apache.flink.table.types.DataType;
+import org.apache.flink.table.types.logical.LogicalTypeRoot;
+import org.apache.hudi.org.apache.avro.Schema;
+import org.apache.hudi.source.ExpressionPredicates;
+import org.apache.hudi.util.AvroSchemaConverter;
+
+import java.time.ZoneOffset;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/** Converts Fluss predicates to Hudi expression predicates for Hudi reader 
pushdown. */
+public class FlussToHudiExpressionPredicateConverter
+        implements PredicateVisitor<ExpressionPredicates.Predicate> {
+
+    private static final String HUDI_METADATA_COLUMN_PREFIX = "_hoodie_";
+
+    private final Schema hudiSchema;
+    private final int metadataFieldCount;
+    private final LeafFunctionConverter converter = new 
LeafFunctionConverter();
+
+    private FlussToHudiExpressionPredicateConverter(Schema hudiSchema) {
+        this.hudiSchema = hudiSchema;
+        this.metadataFieldCount = metadataFieldCount(hudiSchema);
+    }
+
+    public static Optional<ExpressionPredicates.Predicate> convert(
+            Schema hudiSchema, Predicate flussPredicate) {
+        try {
+            return Optional.of(
+                    flussPredicate.visit(new 
FlussToHudiExpressionPredicateConverter(hudiSchema)));
+        } catch (UnsupportedOperationException e) {
+            return Optional.empty();
+        }
+    }
+
+    @Override
+    public ExpressionPredicates.Predicate visit(LeafPredicate predicate) {
+        return predicate.visit(converter);
+    }
+
+    @Override
+    public ExpressionPredicates.Predicate visit(CompoundPredicate predicate) {
+        List<ExpressionPredicates.Predicate> children =
+                predicate.children().stream().map(p -> 
p.visit(this)).collect(Collectors.toList());
+
+        CompoundPredicate.Function function = predicate.function();
+        if (function instanceof And) {
+            return reducePredicates(children, true);
+        } else if (function instanceof Or) {
+            return reducePredicates(children, false);
+        }
+
+        throw new UnsupportedOperationException(
+                "Unsupported Fluss compound predicate function: " + function);
+    }
+
+    private ExpressionPredicates.Predicate reducePredicates(
+            List<ExpressionPredicates.Predicate> children, boolean and) {
+        if (children.isEmpty()) {
+            throw new UnsupportedOperationException("Empty compound 
predicate.");
+        }
+
+        ExpressionPredicates.Predicate result = children.get(0);
+        for (int i = 1; i < children.size(); i++) {
+            result =
+                    and
+                            ? ExpressionPredicates.And.getInstance()
+                                    .bindPredicates(result, children.get(i))
+                            : ExpressionPredicates.Or.getInstance()
+                                    .bindPredicates(result, children.get(i));
+        }
+        return result;
+    }
+
+    private class LeafFunctionConverter implements 
FunctionVisitor<ExpressionPredicates.Predicate> {
+
+        @Override
+        public ExpressionPredicates.Predicate visitIsNotNull(FieldRef 
fieldRef) {
+            throw new UnsupportedOperationException("Hudi does not support 
isNotNull pushdown.");
+        }
+
+        @Override
+        public ExpressionPredicates.Predicate visitIsNull(FieldRef fieldRef) {
+            throw new UnsupportedOperationException("Hudi does not support 
isNull pushdown.");
+        }
+
+        @Override
+        public ExpressionPredicates.Predicate visitStartsWith(FieldRef 
fieldRef, Object literal) {
+            throw new UnsupportedOperationException("Hudi does not support 
startsWith pushdown.");
+        }
+
+        @Override
+        public ExpressionPredicates.Predicate visitEndsWith(FieldRef fieldRef, 
Object literal) {
+            throw new UnsupportedOperationException("Hudi does not support 
endsWith pushdown.");
+        }
+
+        @Override
+        public ExpressionPredicates.Predicate visitContains(FieldRef fieldRef, 
Object literal) {
+            throw new UnsupportedOperationException("Hudi does not support 
contains pushdown.");
+        }
+
+        @Override
+        public ExpressionPredicates.Predicate visitLessThan(FieldRef fieldRef, 
Object literal) {
+            return bind(ExpressionPredicates.LessThan.getInstance(), fieldRef, 
literal);
+        }
+
+        @Override
+        public ExpressionPredicates.Predicate visitGreaterOrEqual(
+                FieldRef fieldRef, Object literal) {
+            return bind(ExpressionPredicates.GreaterThanOrEqual.getInstance(), 
fieldRef, literal);
+        }
+
+        @Override
+        public ExpressionPredicates.Predicate visitNotEqual(FieldRef fieldRef, 
Object literal) {
+            return bind(ExpressionPredicates.NotEquals.getInstance(), 
fieldRef, literal);
+        }
+
+        @Override
+        public ExpressionPredicates.Predicate visitLessOrEqual(FieldRef 
fieldRef, Object literal) {
+            return bind(ExpressionPredicates.LessThanOrEqual.getInstance(), 
fieldRef, literal);
+        }
+
+        @Override
+        public ExpressionPredicates.Predicate visitEqual(FieldRef fieldRef, 
Object literal) {
+            return bind(ExpressionPredicates.Equals.getInstance(), fieldRef, 
literal);
+        }
+
+        @Override
+        public ExpressionPredicates.Predicate visitGreaterThan(FieldRef 
fieldRef, Object literal) {
+            return bind(ExpressionPredicates.GreaterThan.getInstance(), 
fieldRef, literal);
+        }
+
+        @Override
+        public ExpressionPredicates.Predicate visitIn(FieldRef fieldRef, 
List<Object> literals) {
+            Schema.Field field = getFieldByFlussIndex(fieldRef.index());
+            DataType dataType = supportedHudiDataType(field);
+            List<ValueLiteralExpression> literalExpressions =
+                    literals.stream()
+                            .map(literal -> createValueLiteral(literal, 
dataType))
+                            .collect(Collectors.toList());
+            return ExpressionPredicates.In.getInstance()
+                    .bindValueLiterals(literalExpressions)
+                    .bindFieldReference(createFieldReference(field, dataType));
+        }
+
+        @Override
+        public ExpressionPredicates.Predicate visitNotIn(FieldRef fieldRef, 
List<Object> literals) {
+            ExpressionPredicates.Predicate inPredicate = visitIn(fieldRef, 
literals);
+            return 
ExpressionPredicates.Not.getInstance().bindPredicate(inPredicate);
+        }
+
+        @Override
+        public ExpressionPredicates.Predicate visitAnd(
+                List<ExpressionPredicates.Predicate> children) {
+            throw new UnsupportedOperationException("Unsupported visitAnd 
method.");
+        }
+
+        @Override
+        public ExpressionPredicates.Predicate visitOr(
+                List<ExpressionPredicates.Predicate> children) {
+            throw new UnsupportedOperationException("Unsupported visitOr 
method.");
+        }
+
+        private ExpressionPredicates.Predicate bind(
+                ExpressionPredicates.ColumnPredicate predicate, FieldRef 
fieldRef, Object literal) {
+            Schema.Field field = getFieldByFlussIndex(fieldRef.index());
+            DataType dataType = supportedHudiDataType(field);
+            return predicate
+                    .bindValueLiteral(createValueLiteral(literal, dataType))
+                    .bindFieldReference(createFieldReference(field, dataType));
+        }
+
+        private FieldReferenceExpression createFieldReference(
+                Schema.Field field, DataType dataType) {
+            return new FieldReferenceExpression(field.name(), dataType, 0, 
field.pos());
+        }
+
+        private ValueLiteralExpression createValueLiteral(Object literal, 
DataType dataType) {
+            if (literal == null) {
+                throw new UnsupportedOperationException(
+                        "Hudi does not support null literal pushdown.");
+            }
+            return new ValueLiteralExpression(convertValue(literal), 
dataType.notNull());
+        }
+
+        private Schema.Field getFieldByFlussIndex(int index) {
+            int hudiFieldIndex = metadataFieldCount + index;
+            if (index < 0 || hudiFieldIndex >= hudiSchema.getFields().size()) {
+                throw new IllegalArgumentException("Field index out of bounds: 
" + index);
+            }
+            return hudiSchema.getFields().get(hudiFieldIndex);
+        }
+
+        private DataType supportedHudiDataType(Schema.Field field) {
+            DataType dataType = 
AvroSchemaConverter.convertToDataType(field.schema());
+            LogicalTypeRoot typeRoot = dataType.getLogicalType().getTypeRoot();
+            switch (typeRoot) {
+                case BOOLEAN:
+                case TINYINT:
+                case SMALLINT:
+                case INTEGER:
+                case DATE:
+                case TIME_WITHOUT_TIME_ZONE:
+                case BIGINT:
+                case TIMESTAMP_WITHOUT_TIME_ZONE:
+                case FLOAT:
+                case DOUBLE:
+                case BINARY:
+                case VARBINARY:
+                case CHAR:
+                case VARCHAR:
+                    return dataType;
+                default:
+                    throw new UnsupportedOperationException(
+                            "Hudi does not support predicate pushdown for 
field "
+                                    + field.name()
+                                    + " with type "
+                                    + typeRoot
+                                    + ".");
+            }
+        }
+    }
+
+    private static int metadataFieldCount(Schema schema) {
+        int metadataFieldCount = 0;
+        for (Schema.Field field : schema.getFields()) {
+            if (!field.name().startsWith(HUDI_METADATA_COLUMN_PREFIX)) {
+                break;
+            }
+            metadataFieldCount++;
+        }
+        return metadataFieldCount;
+    }
+
+    private static Object convertValue(Object value) {
+        if (value instanceof BinaryString) {
+            return value.toString();
+        }
+        if (value instanceof TimestampNtz) {
+            return ((TimestampNtz) value).toLocalDateTime();
+        }
+        if (value instanceof TimestampLtz) {
+            return ((TimestampLtz) 
value).toInstant().atZone(ZoneOffset.UTC).toLocalDateTime();
+        }
+        return value;
+    }
+}
diff --git 
a/fluss-lake/fluss-lake-hudi/src/test/java/org/apache/fluss/lake/hudi/source/HudiLakeSourceTest.java
 
b/fluss-lake/fluss-lake-hudi/src/test/java/org/apache/fluss/lake/hudi/source/HudiLakeSourceTest.java
index e8446ee85..5d8bbdf29 100644
--- 
a/fluss-lake/fluss-lake-hudi/src/test/java/org/apache/fluss/lake/hudi/source/HudiLakeSourceTest.java
+++ 
b/fluss-lake/fluss-lake-hudi/src/test/java/org/apache/fluss/lake/hudi/source/HudiLakeSourceTest.java
@@ -20,14 +20,9 @@ package org.apache.fluss.lake.hudi.source;
 import org.apache.fluss.config.Configuration;
 import org.apache.fluss.lake.source.LakeSource;
 import org.apache.fluss.metadata.TablePath;
-import org.apache.fluss.predicate.Predicate;
-import org.apache.fluss.predicate.PredicateBuilder;
-import org.apache.fluss.types.IntType;
-import org.apache.fluss.types.RowType;
 
 import org.junit.jupiter.api.Test;
 
-import java.io.IOException;
 import java.util.Collections;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -53,16 +48,14 @@ class HudiLakeSourceTest {
     }
 
     @Test
-    void testFiltersRemainWhenPushDownIsNotSupportedYet() {
+    void testWithEmptyFiltersReturnsEmptyPushDownResult() {
         HudiLakeSource source =
                 new HudiLakeSource(new Configuration(), TablePath.of("db1", 
"table1"));
-        Predicate predicate = new PredicateBuilder(RowType.of(new 
IntType())).equal(0, 1);
 
-        LakeSource.FilterPushDownResult result =
-                source.withFilters(Collections.singletonList(predicate));
+        LakeSource.FilterPushDownResult result = 
source.withFilters(Collections.emptyList());
 
         assertThat(result.acceptedPredicates()).isEmpty();
-        assertThat(result.remainingPredicates()).containsExactly(predicate);
+        assertThat(result.remainingPredicates()).isEmpty();
     }
 
     @Test
@@ -74,14 +67,4 @@ class HudiLakeSourceTest {
                 .isInstanceOf(UnsupportedOperationException.class)
                 .hasMessageContaining("limit");
     }
-
-    @Test
-    void testCreateRecordReaderWrapsReaderErrors() {
-        HudiLakeSource source =
-                new HudiLakeSource(new Configuration(), TablePath.of("db1", 
"table1"));
-
-        assertThatThrownBy(() -> source.createRecordReader(() -> null))
-                .isInstanceOf(IOException.class)
-                .hasMessageContaining("Fail to create Hudi record reader");
-    }
 }
diff --git 
a/fluss-lake/fluss-lake-hudi/src/test/java/org/apache/fluss/lake/hudi/utils/FlussToHudiExpressionPredicateConverterTest.java
 
b/fluss-lake/fluss-lake-hudi/src/test/java/org/apache/fluss/lake/hudi/utils/FlussToHudiExpressionPredicateConverterTest.java
new file mode 100644
index 000000000..08c7bb779
--- /dev/null
+++ 
b/fluss-lake/fluss-lake-hudi/src/test/java/org/apache/fluss/lake/hudi/utils/FlussToHudiExpressionPredicateConverterTest.java
@@ -0,0 +1,163 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.fluss.lake.hudi.utils;
+
+import org.apache.fluss.predicate.Predicate;
+import org.apache.fluss.predicate.PredicateBuilder;
+import org.apache.fluss.row.BinaryString;
+import org.apache.fluss.row.TimestampNtz;
+import org.apache.fluss.types.DataTypes;
+import org.apache.fluss.types.RowType;
+
+import org.apache.hudi.org.apache.avro.Schema;
+import org.apache.hudi.source.ExpressionPredicates;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Test for {@link FlussToHudiExpressionPredicateConverter}. */
+class FlussToHudiExpressionPredicateConverterTest {
+
+    private static final PredicateBuilder PREDICATE_BUILDER =
+            new PredicateBuilder(
+                    RowType.builder()
+                            .field("id", DataTypes.BIGINT())
+                            .field("name", DataTypes.STRING())
+                            .field("score", DataTypes.DOUBLE())
+                            .field("event_time", DataTypes.TIMESTAMP(3))
+                            .field("payload", DataTypes.BYTES())
+                            .field("amount", DataTypes.DECIMAL(10, 2))
+                            .build());
+
+    public static Stream<Arguments> supportedPredicates() {
+        return Stream.of(
+                Arguments.of(PREDICATE_BUILDER.equal(0, 12L), "id = 12"),
+                Arguments.of(
+                        PREDICATE_BUILDER.notEqual(1, 
BinaryString.fromString("test")),
+                        "name != test"),
+                Arguments.of(PREDICATE_BUILDER.lessThan(2, 10.5D), "score < 
10.5"),
+                Arguments.of(PREDICATE_BUILDER.lessOrEqual(0, 50L), "id <= 
50"),
+                Arguments.of(PREDICATE_BUILDER.greaterThan(2, 100.5D), "score 
> 100.5"),
+                Arguments.of(PREDICATE_BUILDER.greaterOrEqual(0, 100L), "id >= 
100"),
+                Arguments.of(
+                        PREDICATE_BUILDER.lessOrEqual(3, 
TimestampNtz.fromMillis(1000L)),
+                        "event_time <= 1000"),
+                Arguments.of(
+                        PREDICATE_BUILDER.in(0, longValues(21)),
+                        "id IN([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, "
+                                + "10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 
20])"),
+                Arguments.of(
+                        PREDICATE_BUILDER.notIn(0, longValues(21)),
+                        "NOT(id IN([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, "
+                                + "10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 
20]))"));
+    }
+
+    @ParameterizedTest
+    @MethodSource("supportedPredicates")
+    void testSupportedPredicates(Predicate flussPredicate, String 
expectedPredicate) {
+        Optional<ExpressionPredicates.Predicate> convertedPredicate =
+                FlussToHudiExpressionPredicateConverter.convert(hudiSchema(), 
flussPredicate);
+
+        assertThat(convertedPredicate).isPresent();
+        
assertThat(convertedPredicate.get().toString()).isEqualTo(expectedPredicate);
+    }
+
+    public static Stream<Arguments> unsupportedPredicates() {
+        return Stream.of(
+                Arguments.of(PREDICATE_BUILDER.isNull(1)),
+                Arguments.of(PREDICATE_BUILDER.isNotNull(1)),
+                Arguments.of(PREDICATE_BUILDER.startsWith(1, 
BinaryString.fromString("prefix"))),
+                Arguments.of(PREDICATE_BUILDER.endsWith(1, 
BinaryString.fromString("suffix"))),
+                Arguments.of(PREDICATE_BUILDER.contains(1, 
BinaryString.fromString("part"))),
+                Arguments.of(PREDICATE_BUILDER.equal(0, null)),
+                Arguments.of(PREDICATE_BUILDER.equal(5, 12.34D)));
+    }
+
+    @ParameterizedTest
+    @MethodSource("unsupportedPredicates")
+    void testUnsupportedPredicates(Predicate flussPredicate) {
+        
assertThat(FlussToHudiExpressionPredicateConverter.convert(hudiSchema(), 
flussPredicate))
+                .isEqualTo(Optional.empty());
+    }
+
+    @ParameterizedTest
+    @MethodSource("compoundPredicates")
+    void testCompoundPredicates(Predicate flussPredicate, String[] 
expectedReferences) {
+        Optional<ExpressionPredicates.Predicate> convertedPredicate =
+                FlussToHudiExpressionPredicateConverter.convert(hudiSchema(), 
flussPredicate);
+
+        assertThat(convertedPredicate).isPresent();
+        
assertThat(convertedPredicate.get().references()).containsExactly(expectedReferences);
+    }
+
+    public static Stream<Arguments> compoundPredicates() {
+        return Stream.of(
+                Arguments.of(
+                        PredicateBuilder.and(
+                                PREDICATE_BUILDER.equal(0, 1L),
+                                PREDICATE_BUILDER.equal(1, 
BinaryString.fromString("one")),
+                                PREDICATE_BUILDER.greaterThan(2, 10.0D)),
+                        new String[] {"id", "name", "score"}),
+                Arguments.of(
+                        PredicateBuilder.or(
+                                PREDICATE_BUILDER.equal(0, 1L),
+                                PREDICATE_BUILDER.greaterThan(2, 10.0D)),
+                        new String[] {"id", "score"}));
+    }
+
+    private static Schema hudiSchema() {
+        return new Schema.Parser()
+                .parse(
+                        "{"
+                                + "\"type\":\"record\","
+                                + "\"name\":\"hudi_record\","
+                                + "\"fields\":["
+                                + 
"{\"name\":\"_hoodie_commit_time\",\"type\":\"string\"},"
+                                + 
"{\"name\":\"_hoodie_commit_seqno\",\"type\":\"string\"},"
+                                + 
"{\"name\":\"_hoodie_record_key\",\"type\":\"string\"},"
+                                + 
"{\"name\":\"_hoodie_partition_path\",\"type\":\"string\"},"
+                                + 
"{\"name\":\"_hoodie_file_name\",\"type\":\"string\"},"
+                                + "{\"name\":\"id\",\"type\":\"long\"},"
+                                + "{\"name\":\"name\",\"type\":\"string\"},"
+                                + "{\"name\":\"score\",\"type\":\"double\"},"
+                                + "{\"name\":\"event_time\","
+                                + 
"\"type\":{\"type\":\"long\",\"logicalType\":\"timestamp-millis\"}},"
+                                + "{\"name\":\"payload\",\"type\":\"bytes\"},"
+                                + "{\"name\":\"amount\","
+                                + "\"type\":{\"type\":\"bytes\","
+                                + 
"\"logicalType\":\"decimal\",\"precision\":10,\"scale\":2}},"
+                                + "{\"name\":\"__bucket\",\"type\":\"int\"},"
+                                + "{\"name\":\"__offset\",\"type\":\"long\"},"
+                                + 
"{\"name\":\"__timestamp\",\"type\":\"long\"}"
+                                + "]"
+                                + "}");
+    }
+
+    private static java.util.List<Object> longValues(int size) {
+        return IntStream.range(0, size)
+                .mapToObj(i -> (Object) (long) i)
+                .collect(Collectors.toList());
+    }
+}

Reply via email to