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 bde536c134 [python] Exclude partition columns from value-stats pruning
(#8438)
bde536c134 is described below
commit bde536c134b612df3077d1e25a8b256226e41de3
Author: XiaoHongbo <[email protected]>
AuthorDate: Fri Jul 3 11:32:25 2026 +0800
[python] Exclude partition columns from value-stats pruning (#8438)
---
paimon-python/pypaimon/read/push_down_utils.py | 18 ++++++++++++++++
.../pypaimon/read/scanner/file_scanner.py | 4 ++++
paimon-python/pypaimon/tests/predicates_test.py | 11 ++++++++++
.../pypaimon/tests/reader_append_only_test.py | 24 ++++++++++++++++++++++
4 files changed, 57 insertions(+)
diff --git a/paimon-python/pypaimon/read/push_down_utils.py
b/paimon-python/pypaimon/read/push_down_utils.py
index 9f558f558a..ee564a4c79 100644
--- a/paimon-python/pypaimon/read/push_down_utils.py
+++ b/paimon-python/pypaimon/read/push_down_utils.py
@@ -161,3 +161,21 @@ def remove_row_id_filter(predicate: Predicate) ->
Optional[Predicate]:
return new_children[0]
return PredicateBuilder.or_predicates(new_children)
return predicate
+
+
+def exclude_predicate_with_fields(predicate: Optional[Predicate], fields:
Set[str]) -> Optional[Predicate]:
+ """Drop predicate parts referencing any of ``fields`` (mirrors Java
+ PredicateBuilder.excludePredicateWithFields)."""
+ if not predicate or not fields:
+ return predicate
+ if predicate.method == "and":
+ kept = []
+ for p in _split_and(predicate):
+ r = exclude_predicate_with_fields(p, fields)
+ if r is not None:
+ kept.append(r)
+ return PredicateBuilder.and_predicates(kept) if kept else None
+ # leaf or OR: drop the whole thing if it touches any field (OR isn't split
apart)
+ if _get_all_fields(predicate) & fields:
+ return None
+ return predicate
diff --git a/paimon-python/pypaimon/read/scanner/file_scanner.py
b/paimon-python/pypaimon/read/scanner/file_scanner.py
index d6c7ad6f36..46eb340b43 100755
--- a/paimon-python/pypaimon/read/scanner/file_scanner.py
+++ b/paimon-python/pypaimon/read/scanner/file_scanner.py
@@ -33,6 +33,7 @@ from pypaimon.manifest.simple_stats_evolutions import
SimpleStatsEvolutions
from pypaimon.schema.data_types import DataField
from pypaimon.read.plan import Plan
from pypaimon.read.push_down_utils import (_get_all_fields,
+ exclude_predicate_with_fields,
remove_row_id_filter,
trim_and_transform_predicate)
from pypaimon.read.scan_stats import ScanStats
@@ -221,6 +222,9 @@ class FileScanner:
self.manifest_scanner = manifest_scanner
self.predicate = predicate
self.predicate_for_stats = remove_row_id_filter(predicate) if
predicate else None
+ # Partition columns aren't in data files, so skip them for value-stats
pruning.
+ self.predicate_for_stats = exclude_predicate_with_fields(
+ self.predicate_for_stats, set(self.table.partition_keys))
self.limit = limit
self.snapshot_manager = table.snapshot_manager()
diff --git a/paimon-python/pypaimon/tests/predicates_test.py
b/paimon-python/pypaimon/tests/predicates_test.py
index 509efc79ca..9bcea3b39a 100644
--- a/paimon-python/pypaimon/tests/predicates_test.py
+++ b/paimon-python/pypaimon/tests/predicates_test.py
@@ -97,6 +97,17 @@ class PredicateTest(unittest.TestCase):
predicate_builder.equal('f2', 'a')
self.assertEqual(str(e.exception), "The field f2 is not in field list
['f0', 'f1'].")
+ def test_exclude_predicate_with_fields(self):
+ from pypaimon.read.push_down_utils import exclude_predicate_with_fields
+ pb =
self.catalog.get_table('default.test_append').new_read_builder().new_predicate_builder()
+ f0 = pb.is_null('f0')
+ f1 = pb.is_null('f1')
+
+ self.assertIsNone(exclude_predicate_with_fields(f0, {'f0'}))
+ self.assertIs(exclude_predicate_with_fields(f1, {'f0'}), f1)
+ self.assertIs(exclude_predicate_with_fields(pb.and_predicates([f0,
f1]), {'f0'}), f1)
+ self.assertIsNone(exclude_predicate_with_fields(pb.or_predicates([f0,
f1]), {'f0'}))
+
def test_append_with_duplicate(self):
pa_schema = pa.schema([
('f0', pa.int64()),
diff --git a/paimon-python/pypaimon/tests/reader_append_only_test.py
b/paimon-python/pypaimon/tests/reader_append_only_test.py
index 34509db74a..7e0c5a6748 100644
--- a/paimon-python/pypaimon/tests/reader_append_only_test.py
+++ b/paimon-python/pypaimon/tests/reader_append_only_test.py
@@ -101,6 +101,30 @@ class AoReaderTest(unittest.TestCase):
actual = self._read_test_table(read_builder).sort_by('user_id')
self.assertEqual(actual, self.expected)
+ def test_isnull_partition_filter(self):
+ schema = Schema.from_pyarrow_schema(self.pa_schema,
partition_keys=['dt'])
+ self.catalog.create_table('default.isnull_partition_filter', schema,
False)
+ table = self.catalog.get_table('default.isnull_partition_filter')
+ data = pa.Table.from_pydict({
+ 'user_id': [1, 2],
+ 'item_id': [1001, 1002],
+ 'behavior': ['a', 'b'],
+ 'dt': [None, 'p1'],
+ }, schema=self.pa_schema)
+ wb = table.new_batch_write_builder()
+ w, c = wb.new_write(), wb.new_commit()
+ w.write_arrow(data)
+ c.commit(w.prepare_commit())
+ w.close()
+ c.close()
+
+ pb = table.new_read_builder().new_predicate_builder()
+ rb = table.new_read_builder().with_filter(pb.is_null('dt'))
+ result = self._read_test_table(rb)
+
+ self.assertEqual(result.column('dt').to_pylist(), [None])
+ self.assertEqual(result.column('user_id').to_pylist(), [1])
+
def test_plan_snapshot_id_for_empty_and_non_empty_scan(self):
schema = Schema.from_pyarrow_schema(self.pa_schema,
partition_keys=['dt'])
self.catalog.create_table('default.test_plan_snapshot_id', schema,
False)