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 aa965b313 [flink][spark] Fix partition pruning for non-string
partition keys (#3322)
aa965b313 is described below
commit aa965b313b6e66478ea70708183ba6f7059de9ce
Author: Anton Borisov <[email protected]>
AuthorDate: Tue May 26 12:29:26 2026 +0100
[flink][spark] Fix partition pruning for non-string partition keys (#3322)
---
.../org/apache/fluss/predicate/LeafPredicate.java | 3 +-
.../org/apache/fluss/utils/PartitionUtils.java | 27 ++++++++
.../fluss/flink/source/FlinkTableSource.java | 4 +-
.../source/enumerator/FlinkSourceEnumerator.java | 25 ++++----
.../flink/utils/StringifyPredicateVisitor.java | 72 ----------------------
.../fluss/flink/source/FlinkTableSourceITCase.java | 4 +-
.../fluss/rpc/util/PredicateMessageUtils.java | 10 ++-
.../fluss/rpc/util/PredicateMessageUtilsTest.java | 26 +++++---
.../org/apache/fluss/spark/read/FlussBatch.scala | 10 ++-
.../apache/fluss/spark/row/FlussAsSparkRow.scala | 8 +--
.../spark/utils/SparkPartitionPredicate.scala | 60 +++---------------
.../spark/utils/SparkPredicateConverter.scala | 4 +-
.../apache/fluss/spark/SparkLogTableReadTest.scala | 36 +++++++++++
.../spark/utils/SparkPredicateConverterTest.scala | 2 +-
14 files changed, 124 insertions(+), 167 deletions(-)
diff --git
a/fluss-common/src/main/java/org/apache/fluss/predicate/LeafPredicate.java
b/fluss-common/src/main/java/org/apache/fluss/predicate/LeafPredicate.java
index c008bd80d..8918efbf7 100644
--- a/fluss-common/src/main/java/org/apache/fluss/predicate/LeafPredicate.java
+++ b/fluss-common/src/main/java/org/apache/fluss/predicate/LeafPredicate.java
@@ -175,7 +175,7 @@ public class LeafPredicate implements Predicate {
return internalRow.getTimestampNtz(pos,
timestampType.getPrecision());
case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
LocalZonedTimestampType lzTs = (LocalZonedTimestampType)
fieldType;
- return internalRow.getTimestampNtz(pos, lzTs.getPrecision());
+ return internalRow.getTimestampLtz(pos, lzTs.getPrecision());
case FLOAT:
return internalRow.getFloat(pos);
case DOUBLE:
@@ -188,6 +188,7 @@ public class LeafPredicate implements Predicate {
return internalRow.getDecimal(
pos, decimalType.getPrecision(),
decimalType.getScale());
case BINARY:
+ case BYTES:
return internalRow.getBytes(pos);
default:
throw new UnsupportedOperationException("Unsupported type: " +
fieldType);
diff --git
a/fluss-common/src/main/java/org/apache/fluss/utils/PartitionUtils.java
b/fluss-common/src/main/java/org/apache/fluss/utils/PartitionUtils.java
index 6474d2ce9..c8f67955a 100644
--- a/fluss-common/src/main/java/org/apache/fluss/utils/PartitionUtils.java
+++ b/fluss-common/src/main/java/org/apache/fluss/utils/PartitionUtils.java
@@ -22,11 +22,14 @@ import org.apache.fluss.config.AutoPartitionTimeUnit;
import org.apache.fluss.exception.InvalidPartitionException;
import org.apache.fluss.metadata.PartitionSpec;
import org.apache.fluss.metadata.ResolvedPartitionSpec;
+import org.apache.fluss.metadata.TableInfo;
import org.apache.fluss.metadata.TablePath;
import org.apache.fluss.row.BinaryString;
+import org.apache.fluss.row.GenericRow;
import org.apache.fluss.row.TimestampLtz;
import org.apache.fluss.row.TimestampNtz;
import org.apache.fluss.types.DataTypeRoot;
+import org.apache.fluss.types.RowType;
import java.time.Instant;
import java.time.ZonedDateTime;
@@ -345,4 +348,28 @@ public class PartitionUtils {
}
return stringPartitionKey;
}
+
+ /** Projects {@code tableInfo}'s row type down to its partition key
columns, in key order. */
+ public static RowType partitionRowType(TableInfo tableInfo) {
+ RowType schema = tableInfo.getRowType();
+ List<String> fieldNames = schema.getFieldNames();
+ int[] indexes =
+
tableInfo.getPartitionKeys().stream().mapToInt(fieldNames::indexOf).toArray();
+ return schema.project(indexes);
+ }
+
+ /**
+ * Builds a row of typed partition values by parsing each string with
{@link
+ * #parseValueOfType(String, DataTypeRoot)} for the column at that ordinal
in {@code
+ * partitionRowType}.
+ */
+ public static GenericRow toPartitionRow(
+ List<String> partitionValues, RowType partitionRowType) {
+ GenericRow row = new GenericRow(partitionValues.size());
+ for (int i = 0; i < partitionValues.size(); i++) {
+ DataTypeRoot type = partitionRowType.getTypeAt(i).getTypeRoot();
+ row.setField(i, parseValueOfType(partitionValues.get(i), type));
+ }
+ return row;
+ }
}
diff --git
a/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/source/FlinkTableSource.java
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/source/FlinkTableSource.java
index b0bba81a7..24a8d00d5 100644
---
a/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/source/FlinkTableSource.java
+++
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/source/FlinkTableSource.java
@@ -103,7 +103,6 @@ import static
org.apache.fluss.flink.utils.LakeSourceUtils.createLakeSource;
import static
org.apache.fluss.flink.utils.PredicateConverter.convertToFlussPredicate;
import static
org.apache.fluss.flink.utils.PushdownUtils.ValueConversion.FLINK_INTERNAL_VALUE;
import static org.apache.fluss.flink.utils.PushdownUtils.extractFieldEquals;
-import static
org.apache.fluss.flink.utils.StringifyPredicateVisitor.stringifyPartitionPredicate;
import static org.apache.fluss.utils.Preconditions.checkNotNull;
/** Flink table source to scan Fluss data. */
@@ -626,8 +625,7 @@ public class FlinkTableSource
} else {
acceptedFilters.add(filter);
}
- // Convert literals in the predicate to partition string
- converted.add(stringifyPartitionPredicate(p));
+ converted.add(p);
} else {
remainingFilters.add(filter);
}
diff --git
a/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/source/enumerator/FlinkSourceEnumerator.java
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/source/enumerator/FlinkSourceEnumerator.java
index 0b7e98b39..f58c4adfa 100644
---
a/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/source/enumerator/FlinkSourceEnumerator.java
+++
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/source/enumerator/FlinkSourceEnumerator.java
@@ -50,11 +50,10 @@ import org.apache.fluss.metadata.TableBucket;
import org.apache.fluss.metadata.TableInfo;
import org.apache.fluss.metadata.TablePath;
import org.apache.fluss.predicate.Predicate;
-import org.apache.fluss.row.BinaryString;
-import org.apache.fluss.row.GenericRow;
-import org.apache.fluss.row.InternalRow;
import org.apache.fluss.shaded.guava32.com.google.common.collect.Lists;
+import org.apache.fluss.types.RowType;
import org.apache.fluss.utils.ExceptionUtils;
+import org.apache.fluss.utils.PartitionUtils;
import org.apache.flink.annotation.Internal;
import org.apache.flink.annotation.VisibleForTesting;
@@ -561,9 +560,17 @@ public class FlinkSourceEnumerator
return partitionInfos;
} else {
int originalSize = partitionInfos.size();
+ RowType partitionRowType =
PartitionUtils.partitionRowType(tableInfo);
List<PartitionInfo> filteredPartitionInfos =
partitionInfos.stream()
- .filter(partition ->
partitionFilters.test(toInternalRow(partition)))
+ .filter(
+ partition ->
+ partitionFilters.test(
+
PartitionUtils.toPartitionRow(
+ partition
+
.getResolvedPartitionSpec()
+
.getPartitionValues(),
+ partitionRowType)))
.collect(Collectors.toList());
int filteredSize = filteredPartitionInfos.size();
@@ -586,16 +593,6 @@ public class FlinkSourceEnumerator
}
}
- private static InternalRow toInternalRow(PartitionInfo partitionInfo) {
- List<String> partitionValues =
- partitionInfo.getResolvedPartitionSpec().getPartitionValues();
- GenericRow genericRow = new GenericRow(partitionValues.size());
- for (int i = 0; i < partitionValues.size(); i++) {
- genericRow.setField(i,
BinaryString.fromString(partitionValues.get(i)));
- }
- return genericRow;
- }
-
/** Init the splits for Fluss. */
private void checkPartitionChanges(Set<PartitionInfo> partitionInfos,
Throwable t) {
if (closed) {
diff --git
a/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/utils/StringifyPredicateVisitor.java
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/utils/StringifyPredicateVisitor.java
deleted file mode 100644
index 1d0f39457..000000000
---
a/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/utils/StringifyPredicateVisitor.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * 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.flink.utils;
-
-import org.apache.fluss.predicate.CompoundPredicate;
-import org.apache.fluss.predicate.LeafPredicate;
-import org.apache.fluss.predicate.Predicate;
-import org.apache.fluss.predicate.PredicateVisitor;
-import org.apache.fluss.row.BinaryString;
-import org.apache.fluss.types.DataTypeRoot;
-import org.apache.fluss.types.DataTypes;
-import org.apache.fluss.utils.PartitionUtils;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A {@link PredicateVisitor} that converts all literals in {@link
LeafPredicate} to string type in
- * the string format of {@link PartitionUtils#convertValueOfType(Object,
DataTypeRoot)}. This is
- * necessary because partition metadata is stored as string.
- */
-public class StringifyPredicateVisitor implements PredicateVisitor<Predicate> {
-
- public static Predicate stringifyPartitionPredicate(Predicate predicate) {
- StringifyPredicateVisitor visitor = new StringifyPredicateVisitor();
- return predicate.visit(visitor);
- }
-
- @Override
- public Predicate visit(LeafPredicate predicate) {
- List<Object> convertedLiterals = new ArrayList<>();
- for (Object literal : predicate.literals()) {
- if (literal != null) {
- String stringValue =
- PartitionUtils.convertValueOfType(literal,
predicate.type().getTypeRoot());
- convertedLiterals.add(BinaryString.fromString(stringValue));
- } else {
- convertedLiterals.add(null);
- }
- }
- return new LeafPredicate(
- predicate.function(),
- DataTypes.STRING(),
- predicate.index(),
- predicate.fieldName(),
- convertedLiterals);
- }
-
- @Override
- public Predicate visit(CompoundPredicate predicate) {
- List<Predicate> newChildren = new ArrayList<>();
- for (Predicate child : predicate.children()) {
- newChildren.add(child.visit(this));
- }
- return new CompoundPredicate(predicate.function(), newChildren);
- }
-}
diff --git
a/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/source/FlinkTableSourceITCase.java
b/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/source/FlinkTableSourceITCase.java
index 6cebccb9b..564a3e29b 100644
---
a/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/source/FlinkTableSourceITCase.java
+++
b/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/source/FlinkTableSourceITCase.java
@@ -1228,7 +1228,7 @@ abstract class FlinkTableSourceITCase extends
AbstractTestBase {
String query =
"select * from all_type_partitioned_table where "
- + "p_bool is false and p_int not in (12) and p_bigint
in (99999) and p_bytes=CAST('Hi' AS VARBINARY) "
+ + "p_bool is false and p_int < 11 and p_bigint in
(99999) and p_bytes=CAST('Hi' AS VARBINARY) "
+ "and p_string='hello' and p_float=CAST(12.5 AS
FLOAT) and p_double=7.88 and p_date=DATE '2025-10-12' "
+ "and p_time=TIME '12:55:00' and p_ts_ntz=TIMESTAMP
'2025-10-12 12:55:00.001' "
+ "and p_ts_ltz=TO_TIMESTAMP_LTZ(4001, 3)";
@@ -1236,7 +1236,7 @@ abstract class FlinkTableSourceITCase extends
AbstractTestBase {
assertThat(plan)
.contains(
"TableSourceScan(table=[[testcatalog, defaultdb,
all_type_partitioned_table, "
- +
"filter=[and(and(and(and(and(and(and(and(and(and(<>(p_int, 12), "
+ +
"filter=[and(and(and(and(and(and(and(and(and(and(<(p_int, 11), "
+ "=(p_bigint, 99999:BIGINT)), =(p_bytes,
X'4869':VARBINARY(2147483647))), "
+ "=(p_string,
_UTF-16LE'hello':VARCHAR(2147483647) CHARACTER SET \"UTF-16LE\")), "
+ "=(p_float, 1.25E1:FLOAT)), =(p_double,
7.88E0:DOUBLE)), =(p_date, 2025-10-12)), "
diff --git
a/fluss-rpc/src/main/java/org/apache/fluss/rpc/util/PredicateMessageUtils.java
b/fluss-rpc/src/main/java/org/apache/fluss/rpc/util/PredicateMessageUtils.java
index 3eb138e6f..f492e3957 100644
---
a/fluss-rpc/src/main/java/org/apache/fluss/rpc/util/PredicateMessageUtils.java
+++
b/fluss-rpc/src/main/java/org/apache/fluss/rpc/util/PredicateMessageUtils.java
@@ -51,8 +51,6 @@ import org.apache.fluss.types.DataTypeRoot;
import org.apache.fluss.types.DecimalType;
import org.apache.fluss.types.RowType;
-import java.time.LocalDate;
-import java.time.LocalTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -145,9 +143,9 @@ public class PredicateMessageUtils {
decimalType.getScale());
}
case DATE:
- return LocalDate.ofEpochDay(pbLiteral.getBigintValue());
+ return (int) pbLiteral.getBigintValue();
case TIME_WITHOUT_TIME_ZONE:
- return LocalTime.ofNanoOfDay(pbLiteral.getIntValue() *
1_000_000L);
+ return pbLiteral.getIntValue();
case TIMESTAMP_WITHOUT_TIME_ZONE:
return TimestampNtz.fromMillis(
pbLiteral.getTimestampMillisValue(),
@@ -282,10 +280,10 @@ public class PredicateMessageUtils {
pbLiteral.setIntValue((Integer) literal);
break;
case DATE:
- pbLiteral.setBigintValue(((LocalDate) literal).toEpochDay());
+ pbLiteral.setBigintValue(((Integer) literal).longValue());
break;
case TIME_WITHOUT_TIME_ZONE:
- pbLiteral.setIntValue((int) (((LocalTime)
literal).toNanoOfDay() / 1_000_000L));
+ pbLiteral.setIntValue((Integer) literal);
break;
case TIMESTAMP_WITHOUT_TIME_ZONE:
pbLiteral.setTimestampMillisValue(((TimestampNtz)
literal).getMillisecond());
diff --git
a/fluss-rpc/src/test/java/org/apache/fluss/rpc/util/PredicateMessageUtilsTest.java
b/fluss-rpc/src/test/java/org/apache/fluss/rpc/util/PredicateMessageUtilsTest.java
index 8e1e66f76..76b926d97 100644
---
a/fluss-rpc/src/test/java/org/apache/fluss/rpc/util/PredicateMessageUtilsTest.java
+++
b/fluss-rpc/src/test/java/org/apache/fluss/rpc/util/PredicateMessageUtilsTest.java
@@ -56,7 +56,6 @@ import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.math.BigDecimal;
-import java.time.LocalDate;
import java.time.LocalTime;
import java.util.Arrays;
import java.util.Collections;
@@ -94,6 +93,20 @@ public class PredicateMessageUtilsTest {
return buildRowType(Arrays.asList(predicates));
}
+ @Test
+ public void testLeafPredicateTimeIntegerLiteral() {
+ int timeMillis = 45_000_000;
+ DataType type = new TimeType(false, 3);
+ LeafPredicate predicate =
+ new LeafPredicate(
+ Equal.INSTANCE, type, 0, "f_time",
Collections.singletonList(timeMillis));
+ RowType rowType = buildRowType(predicate);
+ PbPredicate pb = PredicateMessageUtils.toPbPredicate(predicate,
rowType);
+ Predicate result = PredicateMessageUtils.toPredicate(pb, rowType);
+ LeafPredicate lp = (LeafPredicate) result;
+ assertThat(lp.literals().get(0)).isEqualTo(timeMillis);
+ }
+
@Test
public void testLeafPredicateIntEqual() {
DataType type = new IntType(false);
@@ -317,22 +330,17 @@ public class PredicateMessageUtilsTest {
DataType dateType = new DateType(false);
LeafPredicate datePredicate =
new LeafPredicate(
- Equal.INSTANCE,
- dateType,
- 8,
- "f_date",
- Collections.singletonList(
- LocalDate.ofEpochDay(19000L))); // days since
epoch
+ Equal.INSTANCE, dateType, 8, "f_date",
Collections.singletonList(19000));
// time
DataType timeType = new TimeType(false, 3);
- LocalTime time = LocalTime.of(12, 30);
+ int timeMillis = (int) (LocalTime.of(12, 30).toNanoOfDay() /
1_000_000L);
LeafPredicate timePredicate =
new LeafPredicate(
Equal.INSTANCE,
timeType,
9,
"f_time",
- Collections.singletonList(time)); // millis of day
+ Collections.singletonList(timeMillis));
// timestamp
DataType tsType = new TimestampType(false, 3);
TimestampNtz ts = TimestampNtz.fromMillis(1680000000000L, 3);
diff --git
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussBatch.scala
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussBatch.scala
index 87f2fdad0..03e473a82 100644
---
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussBatch.scala
+++
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/read/FlussBatch.scala
@@ -153,7 +153,10 @@ class FlussAppendBatch(
if (tableInfo.isPartitioned) {
val matching =
- SparkPartitionPredicate.filterPartitions(partitionInfos.asScala.toSeq,
partitionPredicate)
+ SparkPartitionPredicate.filterPartitions(
+ tableInfo,
+ partitionInfos.asScala.toSeq,
+ partitionPredicate)
matching
.map {
partitionInfo =>
@@ -232,7 +235,10 @@ class FlussUpsertBatch(
override def planInputPartitions(): Array[InputPartition] = {
if (tableInfo.isPartitioned) {
val matching =
- SparkPartitionPredicate.filterPartitions(partitionInfos.asScala.toSeq,
partitionPredicate)
+ SparkPartitionPredicate.filterPartitions(
+ tableInfo,
+ partitionInfos.asScala.toSeq,
+ partitionPredicate)
matching.flatMap {
partitionInfo =>
val partitionName = partitionInfo.getPartitionName
diff --git
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/row/FlussAsSparkRow.scala
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/row/FlussAsSparkRow.scala
index 474426470..5b356e033 100644
---
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/row/FlussAsSparkRow.scala
+++
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/row/FlussAsSparkRow.scala
@@ -18,7 +18,7 @@
package org.apache.fluss.spark.row
import org.apache.fluss.row.{InternalRow => FlussInternalRow}
-import org.apache.fluss.types.{ArrayType => FlussArrayType, BinaryType =>
FlussBinaryType, LocalZonedTimestampType, MapType => FlussMapType, RowType,
TimestampType}
+import org.apache.fluss.types.{ArrayType => FlussArrayType, BinaryType =>
FlussBinaryType, BytesType => FlussBytesType, LocalZonedTimestampType, MapType
=> FlussMapType, RowType, TimestampType}
import org.apache.fluss.utils.InternalRowUtils
import org.apache.spark.sql.catalyst.{InternalRow => SparkInteralRow}
@@ -84,9 +84,9 @@ class FlussAsSparkRow(rowType: RowType) extends
SparkInteralRow {
DataConverter.toSparkUTF8String(row.getString(ordinal))
}
- override def getBinary(ordinal: Int): Array[Byte] = {
- val binaryType = rowType.getTypeAt(ordinal).asInstanceOf[FlussBinaryType]
- row.getBinary(ordinal, binaryType.getLength)
+ override def getBinary(ordinal: Int): Array[Byte] =
rowType.getTypeAt(ordinal) match {
+ case b: FlussBinaryType => row.getBinary(ordinal, b.getLength)
+ case _: FlussBytesType => row.getBytes(ordinal)
}
override def getInterval(ordinal: Int): CalendarInterval =
diff --git
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/utils/SparkPartitionPredicate.scala
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/utils/SparkPartitionPredicate.scala
index cf300892a..353334c6e 100644
---
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/utils/SparkPartitionPredicate.scala
+++
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/utils/SparkPartitionPredicate.scala
@@ -18,9 +18,7 @@
package org.apache.fluss.spark.utils
import org.apache.fluss.metadata.{PartitionInfo, TableInfo}
-import org.apache.fluss.predicate.{CompoundPredicate, LeafPredicate,
PartitionPredicateVisitor, Predicate => FlussPredicate, PredicateBuilder,
PredicateVisitor}
-import org.apache.fluss.row.{BinaryString, GenericRow}
-import org.apache.fluss.types.{DataTypes, RowType}
+import org.apache.fluss.predicate.{PartitionPredicateVisitor, Predicate =>
FlussPredicate, PredicateBuilder}
import org.apache.fluss.utils.PartitionUtils
import org.apache.spark.sql.connector.expressions.filter.Predicate
@@ -34,7 +32,7 @@ object SparkPartitionPredicate {
val partitionKeys = tableInfo.getPartitionKeys
if (partitionKeys.isEmpty) return None
- val rowType = partitionRowType(tableInfo)
+ val rowType = PartitionUtils.partitionRowType(tableInfo)
val onlyPartitionKeys = new PartitionPredicateVisitor(partitionKeys)
val converted = predicates.flatMap {
@@ -42,7 +40,6 @@ object SparkPartitionPredicate {
SparkPredicateConverter
.convert(rowType, sparkPredicate)
.filter(_.visit(onlyPartitionKeys))
- .map(stringifyLiterals)
}
converted match {
@@ -53,54 +50,17 @@ object SparkPartitionPredicate {
}
def filterPartitions(
+ tableInfo: TableInfo,
partitionInfos: Seq[PartitionInfo],
partitionPredicate: Option[FlussPredicate]): Seq[PartitionInfo] =
partitionPredicate match {
case None => partitionInfos
- case Some(predicate) => partitionInfos.filter(p =>
predicate.test(toPartitionRow(p)))
+ case Some(predicate) =>
+ val rowType = PartitionUtils.partitionRowType(tableInfo)
+ partitionInfos.filter {
+ p =>
+ predicate.test(
+
PartitionUtils.toPartitionRow(p.getResolvedPartitionSpec.getPartitionValues,
rowType))
+ }
}
-
- private def partitionRowType(tableInfo: TableInfo): RowType = {
- val schemaRowType = tableInfo.getRowType
- val fieldNames = schemaRowType.getFieldNames
- val partitionFieldIndexes =
tableInfo.getPartitionKeys.asScala.map(fieldNames.indexOf).toArray
- schemaRowType.project(partitionFieldIndexes)
- }
-
- private def toPartitionRow(partitionInfo: PartitionInfo): GenericRow = {
- val values = partitionInfo.getResolvedPartitionSpec.getPartitionValues
- val row = new GenericRow(values.size)
- var i = 0
- while (i < values.size) {
- row.setField(i, BinaryString.fromString(values.get(i)))
- i += 1
- }
- row
- }
-
- // Partition values are stored as strings; literals must be coerced before
evaluation.
- private val stringifier: PredicateVisitor[FlussPredicate] = new
PredicateVisitor[FlussPredicate] {
- override def visit(leaf: LeafPredicate): FlussPredicate = {
- val converted: Seq[Object] = leaf.literals.asScala.toSeq.map {
- case null => null
- case literal =>
- BinaryString.fromString(
- PartitionUtils.convertValueOfType(literal,
leaf.`type`.getTypeRoot))
- }
- new LeafPredicate(
- leaf.function,
- DataTypes.STRING,
- leaf.index,
- leaf.fieldName,
- converted.asJava)
- }
-
- override def visit(compound: CompoundPredicate): FlussPredicate = {
- val children = compound.children.asScala.map(_.visit(this)).asJava
- new CompoundPredicate(compound.function, children)
- }
- }
-
- private def stringifyLiterals(predicate: FlussPredicate): FlussPredicate =
- predicate.visit(stringifier)
}
diff --git
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/utils/SparkPredicateConverter.scala
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/utils/SparkPredicateConverter.scala
index 4ea5fd75f..ff103bb18 100644
---
a/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/utils/SparkPredicateConverter.scala
+++
b/fluss-spark/fluss-spark-common/src/main/scala/org/apache/fluss/spark/utils/SparkPredicateConverter.scala
@@ -27,7 +27,6 @@ import org.apache.spark.sql.types.{Decimal => SparkDecimal}
import org.apache.spark.unsafe.types.UTF8String
import java.lang.{Boolean => JBoolean, Byte => JByte, Double => JDouble, Float
=> JFloat, Long => JLong, Short => JShort}
-import java.time.LocalDate
import scala.jdk.CollectionConverters._
@@ -277,9 +276,8 @@ object SparkPredicateConverter {
}
case DataTypeRoot.DATE =>
- // RPC serialization (PredicateMessageUtils) expects LocalDate.
value match {
- case d: Integer => LocalDate.ofEpochDay(d.longValue())
+ case d: Integer => d
case _ => throw new UnsupportedExpression
}
diff --git
a/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/SparkLogTableReadTest.scala
b/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/SparkLogTableReadTest.scala
index 42b0aa62d..3276decb7 100644
---
a/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/SparkLogTableReadTest.scala
+++
b/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/SparkLogTableReadTest.scala
@@ -498,6 +498,42 @@ class SparkLogTableReadTest extends FlussSparkTestBase {
}
}
+ test("Spark Read: partition pushdown — all supported types") {
+ withTable("t") {
+ sql(s"""
+ |CREATE TABLE $DEFAULT_DATABASE.t (
+ | id INT,
+ | p_bool BOOLEAN, p_int_eq INT, p_int_range INT,
+ | p_bigint BIGINT, p_float FLOAT, p_double DOUBLE,
+ | p_string STRING, p_binary BINARY,
+ | p_date DATE, p_ts TIMESTAMP, p_ts_ntz TIMESTAMP_NTZ)
+ |PARTITIONED BY (
+ | p_bool, p_int_eq, p_int_range, p_bigint, p_float, p_double,
+ | p_string, p_binary, p_date, p_ts, p_ts_ntz)""".stripMargin)
+ sql(s"""INSERT INTO $DEFAULT_DATABASE.t VALUES
+ |(1, false, 10, 10, 99999L, CAST(12.5 AS FLOAT), 7.88, 'hello',
+ | CAST('Hi' AS BINARY), DATE '2026-01-01',
+ | TIMESTAMP '2026-01-01 12:00:00', TIMESTAMP_NTZ '2026-01-01
12:00:00'),
+ |(2, true, 11, 2, 99998L, CAST(13.5 AS FLOAT), 8.88, 'world',
+ | CAST('Bye' AS BINARY), DATE '2026-01-02',
+ | TIMESTAMP '2026-01-02 12:00:00', TIMESTAMP_NTZ '2026-01-02
12:00:00')
+ |""".stripMargin)
+
+ val query = sql(s"""
+ |SELECT id FROM $DEFAULT_DATABASE.t WHERE
+ | p_bool = false AND p_int_eq = 10 AND p_int_range >
2
+ | AND p_bigint = 99999L
+ | AND p_float = CAST(12.5 AS FLOAT) AND p_double =
7.88
+ | AND p_string = 'hello' AND p_binary = CAST('Hi' AS
BINARY)
+ | AND p_date = DATE '2026-01-01'
+ | AND p_ts = TIMESTAMP '2026-01-01 12:00:00'
+ | AND p_ts_ntz = TIMESTAMP_NTZ '2026-01-01 12:00:00'
+ |""".stripMargin)
+ checkAnswer(query, Row(1) :: Nil)
+ assert(partitionPredicate(query).isDefined)
+ }
+ }
+
test("Spark Read: scan description surfaces partition filter when pushed") {
withPartitionedTable {
val withPart = sql(s"SELECT * FROM $DEFAULT_DATABASE.t WHERE dt =
'2026-01-01'")
diff --git
a/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/utils/SparkPredicateConverterTest.scala
b/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/utils/SparkPredicateConverterTest.scala
index 07fca2081..154a2f938 100644
---
a/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/utils/SparkPredicateConverterTest.scala
+++
b/fluss-spark/fluss-spark-ut/src/test/scala/org/apache/fluss/spark/utils/SparkPredicateConverterTest.scala
@@ -195,7 +195,7 @@ class SparkPredicateConverterTest extends AnyFunSuite {
test("Date literal from epoch days") {
val days = Integer.valueOf(LocalDate.of(2025, 1, 15).toEpochDay.toInt)
val predicate = convert(pred("=", ref("dt"), lit(days, DateType)))
- val expected = new PredicateBuilder(rowType).equal(6, LocalDate.of(2025,
1, 15))
+ val expected = new PredicateBuilder(rowType).equal(6, days)
assertThat(predicate).isEqualTo(expected)
}