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 1e059d4bec [python][daft] Fix data evolution fallback routing (#8670)
1e059d4bec is described below

commit 1e059d4bec7f6df20d03962a13200d1bb91180c6
Author: QuakeWang <[email protected]>
AuthorDate: Thu Jul 16 14:28:35 2026 +0800

    [python][daft] Fix data evolution fallback routing (#8670)
    
    Daft treated every append-table split as directly readable Parquet and
    ignored `raw_convertible` for non-primary-key tables.
    Non-raw-convertible data-evolution splits can contain column fragments
    for the same row-id range, so reading each file independently returned
    physical rows instead of merged logical rows.
    
    Require `raw_convertible` for native Parquet routing, fall back to the
    pypaimon merge reader otherwise, and report the correct data-evolution
    fallback reason.
---
 paimon-python/pypaimon/daft/daft_datasource.py     |  6 ++-
 .../pypaimon/tests/daft/daft_explain_test.py       | 47 ++++++++++++++++++-
 .../pypaimon/tests/daft/daft_integration_test.py   | 52 ++++++++++++++++++++++
 3 files changed, 102 insertions(+), 3 deletions(-)

diff --git a/paimon-python/pypaimon/daft/daft_datasource.py 
b/paimon-python/pypaimon/daft/daft_datasource.py
index f80ae62e5c..ccb98c1140 100644
--- a/paimon-python/pypaimon/daft/daft_datasource.py
+++ b/paimon-python/pypaimon/daft/daft_datasource.py
@@ -652,7 +652,7 @@ class PaimonDataSource(DataSource):
         can_use_native_reader = (
             self._is_parquet
             and not self._has_blob_columns
-            and (not self._table.is_primary_key_table or raw_convertible)
+            and raw_convertible
             and not has_deletion_vectors
             and not has_auth
         )
@@ -667,8 +667,10 @@ class PaimonDataSource(DataSource):
             reason = "query auth active"
         elif has_deletion_vectors:
             reason = "deletion vectors present"
-        else:
+        elif self._table.is_primary_key_table:
             reason = "LSM merge required"
+        else:
+            reason = "data-evolution merge required"
         return _ReaderRouting(READER_MODE_PYPAIMON_FALLBACK, reason)
 
     @staticmethod
diff --git a/paimon-python/pypaimon/tests/daft/daft_explain_test.py 
b/paimon-python/pypaimon/tests/daft/daft_explain_test.py
index f0941175dc..6f3a54a62b 100644
--- a/paimon-python/pypaimon/tests/daft/daft_explain_test.py
+++ b/paimon-python/pypaimon/tests/daft/daft_explain_test.py
@@ -90,6 +90,7 @@ def _single_split_explain(
     raw_convertible: bool,
     has_deletion_vectors: bool,
     has_auth: bool = False,
+    data_evolution_enabled: bool = False,
 ) -> ExplainResult:
     split = ExplainSplitInfo(
         partition={},
@@ -109,7 +110,7 @@ def _single_split_explain(
         is_primary_key_table=False,
         bucket_mode="unaware",
         deletion_vectors_enabled=has_deletion_vectors,
-        data_evolution_enabled=False,
+        data_evolution_enabled=data_evolution_enabled,
         snapshot_id=1,
         schema_id=0,
         file_count=1,
@@ -334,6 +335,50 @@ def 
test_explain_scan_reports_pk_lsm_fallback(catalog_options):
     assert all(split.fallback_reason == "LSM merge required" for split in 
result.splits)
 
 
+def test_explain_scan_reports_data_evolution_fallback(catalog_options, 
monkeypatch):
+    pa_schema = pa.schema([
+        ("id", pa.int64()),
+        ("name", pa.string()),
+    ])
+    _, table = _create_table(
+        catalog_options,
+        "explain_data_evolution_fallback",
+        pa_schema,
+        options={
+            "bucket": "-1",
+            "file.format": "parquet",
+            "row-tracking.enabled": "true",
+            "data-evolution.enabled": "true",
+        },
+    )
+
+    class FakeReadBuilder:
+        def explain(self, verbose: bool = False) -> ExplainResult:
+            assert verbose is True
+            return _single_split_explain(
+                table_identifier="test_db.explain_data_evolution_fallback",
+                raw_convertible=False,
+                has_deletion_vectors=False,
+                data_evolution_enabled=True,
+            )
+
+    def fake_scan_read_builder(self, table, read_pushdowns):
+        return FakeReadBuilder()
+
+    monkeypatch.setattr(PaimonDataSource, "_scan_read_builder", 
fake_scan_read_builder)
+
+    result = _explain_table(table, catalog_options=catalog_options, 
verbose=True)
+
+    assert table.is_primary_key_table is False
+    assert result.pypaimon_fallback_split_count == 1
+    assert result.native_parquet_split_count == 0
+    assert result.fallback_reasons == {"data-evolution merge required": 1}
+    assert result.splits is not None
+    assert len(result.splits) == 1
+    assert result.splits[0].reader_mode == READER_MODE_PYPAIMON_FALLBACK
+    assert result.splits[0].fallback_reason == "data-evolution merge required"
+
+
 def test_explain_scan_reports_non_parquet_fallback(catalog_options):
     pa_schema = pa.schema([
         ("id", pa.int64()),
diff --git a/paimon-python/pypaimon/tests/daft/daft_integration_test.py 
b/paimon-python/pypaimon/tests/daft/daft_integration_test.py
index 4b1ff9fbe4..0a8e44466c 100644
--- a/paimon-python/pypaimon/tests/daft/daft_integration_test.py
+++ b/paimon-python/pypaimon/tests/daft/daft_integration_test.py
@@ -112,6 +112,58 @@ def test_read_paimon_basic(catalog_options):
     }
 
 
+def test_read_paimon_data_evolution_merges_column_fragments(catalog_options):
+    pa_schema = pa.schema([
+        ("id", pa.int32()),
+        ("name", pa.string()),
+        ("score", pa.float64()),
+    ])
+    identifier, table = _create_table(
+        catalog_options,
+        "read_data_evolution",
+        pa_schema,
+        options={
+            "row-tracking.enabled": "true",
+            "data-evolution.enabled": "true",
+            "file.format": "parquet",
+        },
+    )
+    write_builder = table.new_batch_write_builder()
+    id_name_write = write_builder.new_write().with_write_type(["id", "name"])
+    score_write = write_builder.new_write().with_write_type(["score"])
+    table_commit = write_builder.new_commit()
+    try:
+        id_name_write.write_arrow(pa.table({
+            "id": pa.array([1, 2, 3], pa.int32()),
+            "name": pa.array(["a", "b", "c"], pa.string()),
+        }))
+        score_write.write_arrow(pa.table({
+            "score": pa.array([1.1, 2.2, 3.3], pa.float64()),
+        }))
+        commit_messages = id_name_write.prepare_commit() + 
score_write.prepare_commit()
+        # Both files are column fragments for the same logical row range.
+        for message in commit_messages:
+            for data_file in message.new_files:
+                data_file.first_row_id = 0
+        table_commit.commit(commit_messages)
+    finally:
+        id_name_write.close()
+        score_write.close()
+        table_commit.close()
+
+    splits = table.new_read_builder().new_scan().plan().splits()
+    assert len(splits) == 1
+    assert splits[0].raw_convertible is False
+
+    result = read_paimon(identifier, catalog_options).to_pydict()
+
+    assert result == {
+        "id": [1, 2, 3],
+        "name": ["a", "b", "c"],
+        "score": [1.1, 2.2, 3.3],
+    }
+
+
 def test_read_paimon_projection(catalog_options):
     data = pa.table(
         {

Reply via email to