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 c2057c32d5 [core][spark] Push down object table filters (#9383)
c2057c32d5 is described below
commit c2057c32d545cbf86feadb9116ef8205517d0a22
Author: Wenchao Wu <[email protected]>
AuthorDate: Wed Aug 26 10:55:39 2026 +0800
[core][spark] Push down object table filters (#9383)
---
.../paimon/table/object/ObjectTableImpl.java | 6 +-
.../paimon/table/object/ObjectTableTest.java | 96 ++++++++++++++++++++++
.../apache/paimon/spark/read/ObjectTableScan.scala | 9 +-
.../paimon/spark/table/PaimonObjectTableTest.scala | 55 +++++++++----
4 files changed, 148 insertions(+), 18 deletions(-)
diff --git
a/paimon-core/src/main/java/org/apache/paimon/table/object/ObjectTableImpl.java
b/paimon-core/src/main/java/org/apache/paimon/table/object/ObjectTableImpl.java
index 83f3c53536..f8548b3167 100644
---
a/paimon-core/src/main/java/org/apache/paimon/table/object/ObjectTableImpl.java
+++
b/paimon-core/src/main/java/org/apache/paimon/table/object/ObjectTableImpl.java
@@ -209,10 +209,11 @@ public class ObjectTableImpl implements ReadonlyTable,
ObjectTable {
private static class ObjectRead implements InnerTableRead {
private @Nullable RowType readType;
+ private @Nullable Predicate predicate;
@Override
public InnerTableRead withFilter(Predicate predicate) {
- // TODO
+ this.predicate = predicate;
return this;
}
@@ -261,6 +262,9 @@ public class ObjectTableImpl implements ReadonlyTable,
ObjectTable {
}
}
};
+ if (predicate != null) {
+ iterator = Iterators.filter(iterator, predicate::test);
+ }
if (readType != null) {
iterator =
Iterators.transform(
diff --git
a/paimon-core/src/test/java/org/apache/paimon/table/object/ObjectTableTest.java
b/paimon-core/src/test/java/org/apache/paimon/table/object/ObjectTableTest.java
new file mode 100644
index 0000000000..f9d5e33b17
--- /dev/null
+++
b/paimon-core/src/test/java/org/apache/paimon/table/object/ObjectTableTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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.paimon.table.object;
+
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.fs.FileIO;
+import org.apache.paimon.fs.Path;
+import org.apache.paimon.fs.local.LocalFileIO;
+import org.apache.paimon.predicate.Predicate;
+import org.apache.paimon.predicate.PredicateBuilder;
+import org.apache.paimon.reader.RecordReader;
+import org.apache.paimon.table.source.ReadBuilder;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests for {@link ObjectTableImpl}. */
+public class ObjectTableTest {
+
+ @TempDir java.nio.file.Path tempPath;
+ private ObjectTable table;
+
+ @BeforeEach
+ public void beforeEach() throws Exception {
+ FileIO fileIO = LocalFileIO.create();
+ Path objectPath = new Path(tempPath.toUri());
+ fileIO.writeFile(new Path(objectPath, "drop.txt"), "drop", false);
+ fileIO.writeFile(new Path(objectPath, "dir/keep.txt"), "keep", false);
+
+ table =
+ ObjectTable.builder()
+ .identifier(Identifier.create("default", "objects"))
+ .fileIO(fileIO)
+ .location(objectPath.toString())
+ .build();
+ }
+
+ @Test
+ public void testReadWithoutFilter() throws Exception {
+ assertThat(readPaths(table.newReadBuilder()))
+ .containsExactlyInAnyOrder("drop.txt", "dir/keep.txt");
+ }
+
+ @Test
+ public void testReadWithFilterAndProjection() throws Exception {
+ Predicate predicate =
+ new PredicateBuilder(ObjectTable.SCHEMA)
+ .equal(1, BinaryString.fromString("keep.txt"));
+ ReadBuilder readBuilder =
+
table.newReadBuilder().withFilter(predicate).withProjection(new int[] {0});
+
+ assertThat(readPaths(readBuilder)).containsExactly("dir/keep.txt");
+ }
+
+ @Test
+ public void testReadWithNoMatches() throws Exception {
+ Predicate predicate =
+ new PredicateBuilder(ObjectTable.SCHEMA)
+ .equal(1, BinaryString.fromString("missing.txt"));
+
+
assertThat(readPaths(table.newReadBuilder().withFilter(predicate))).isEmpty();
+ }
+
+ private List<String> readPaths(ReadBuilder readBuilder) throws Exception {
+ List<String> paths = new ArrayList<>();
+ try (RecordReader<InternalRow> reader =
+
readBuilder.newRead().createReader(readBuilder.newScan().plan())) {
+ reader.forEachRemaining(row ->
paths.add(row.getString(0).toString()));
+ }
+ return paths;
+ }
+}
diff --git
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/read/ObjectTableScan.scala
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/read/ObjectTableScan.scala
index 4542b4d2a4..ff2f3ca8c7 100644
---
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/read/ObjectTableScan.scala
+++
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/read/ObjectTableScan.scala
@@ -31,14 +31,17 @@ import scala.collection.JavaConverters._
class ObjectTableScanBuilder(val table: ObjectTable) extends
PaimonBaseScanBuilder {
override def build(): ObjectTableScan =
- ObjectTableScan(table, requiredSchema)
+ ObjectTableScan(table, requiredSchema, pushedDataFilters)
}
/** Scan implementation for [[ObjectTable]] */
-case class ObjectTableScan(table: ObjectTable, requiredSchema: StructType)
extends BaseScan {
+case class ObjectTableScan(
+ table: ObjectTable,
+ requiredSchema: StructType,
+ pushedDataFilters: Seq[Predicate])
+ extends BaseScan {
override val pushedPartitionFilters: Seq[PartitionPredicate] = Nil
- override val pushedDataFilters: Seq[Predicate] = Nil
override val pushedLimit: Option[Int] = None
protected def getInputSplits: Array[Split] = {
diff --git
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/table/PaimonObjectTableTest.scala
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/table/PaimonObjectTableTest.scala
index 3c3ecee016..be6d6b9815 100644
---
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/table/PaimonObjectTableTest.scala
+++
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/table/PaimonObjectTableTest.scala
@@ -21,6 +21,7 @@ package org.apache.paimon.spark.table
import org.apache.paimon.catalog.Identifier
import org.apache.paimon.fs.Path
import org.apache.paimon.spark.PaimonSparkTestWithRestCatalogBase
+import org.apache.paimon.spark.read.ObjectTableScan
import org.apache.paimon.table.`object`.ObjectTable
import org.apache.spark.sql.Row
@@ -35,7 +36,45 @@ class PaimonObjectTableTest extends
PaimonSparkTestWithRestCatalogBase {
}
test("ObjectTable: read file metadata") {
- val tableName = "object_table_test"
+ withObjectTable("object_table_metadata_test") {
+ tableName =>
+ checkAnswer(
+ sql(s"SELECT path, name FROM $tableName ORDER BY path"),
+ Seq(
+ Row("file1.txt", "file1.txt"),
+ Row("file2.txt", "file2.txt"),
+ Row("subdir/file3.txt", "file3.txt"))
+ )
+
+ // Verify schema has expected columns
+ checkAnswer(
+ sql(s"SELECT COUNT(*) FROM $tableName"),
+ Seq(Row(3))
+ )
+ }
+ }
+
+ test("ObjectTable: push down filters") {
+ withObjectTable("object_table_filter_pushdown_test") {
+ tableName =>
+ val filteredQuery =
+ s"SELECT path FROM $tableName WHERE name = 'file2.txt' AND length >
0"
+ checkAnswer(sql(filteredQuery), Seq(Row("file2.txt")))
+
assert(getScan(filteredQuery).asInstanceOf[ObjectTableScan].pushedDataFilters.size
== 2)
+ }
+ }
+
+ test("ObjectTable: retain unsupported filters for post-scan evaluation") {
+ withObjectTable("object_table_post_scan_filter_test") {
+ tableName =>
+ val filteredQuery =
+ s"SELECT path FROM $tableName WHERE reverse(name) = 'txt.2elif'"
+ checkAnswer(sql(filteredQuery), Seq(Row("file2.txt")))
+
assert(getScan(filteredQuery).asInstanceOf[ObjectTableScan].pushedDataFilters.isEmpty)
+ }
+ }
+
+ private def withObjectTable(tableName: String)(testCode: String => Unit):
Unit = {
withTable(tableName) {
sql(
s"CREATE TABLE $tableName TBLPROPERTIES (" +
@@ -54,19 +93,7 @@ class PaimonObjectTableTest extends
PaimonSparkTestWithRestCatalogBase {
fileIO.mkdirs(new Path(basePath, "subdir"))
fileIO.writeFile(new Path(basePath, "subdir/file3.txt"), "content3",
false)
- checkAnswer(
- sql(s"SELECT path, name FROM $tableName ORDER BY path"),
- Seq(
- Row("file1.txt", "file1.txt"),
- Row("file2.txt", "file2.txt"),
- Row("subdir/file3.txt", "file3.txt"))
- )
-
- // Verify schema has expected columns
- checkAnswer(
- sql(s"SELECT COUNT(*) FROM $tableName"),
- Seq(Row(3))
- )
+ testCode(tableName)
}
}
}