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 296b2e8ff8 [python] Make write_pandas respect write_cols and add 
to_duckdb parallelism (#8636)
296b2e8ff8 is described below

commit 296b2e8ff8f94d9397412733f32996ebcdcc8fca
Author: chaoyang <[email protected]>
AuthorDate: Thu Jul 16 22:11:28 2026 +0800

    [python] Make write_pandas respect write_cols and add to_duckdb parallelism 
(#8636)
---
 paimon-python/pypaimon/read/table_read.py          |  9 ++-
 paimon-python/pypaimon/tests/reader_base_test.py   | 38 +++++++++++++
 .../pypaimon/tests/write/table_write_test.py       | 65 ++++++++++++++++++++++
 paimon-python/pypaimon/write/table_write.py        |  9 ++-
 4 files changed, 118 insertions(+), 3 deletions(-)

diff --git a/paimon-python/pypaimon/read/table_read.py 
b/paimon-python/pypaimon/read/table_read.py
index 89ca539b73..6e641b5096 100644
--- a/paimon-python/pypaimon/read/table_read.py
+++ b/paimon-python/pypaimon/read/table_read.py
@@ -538,11 +538,16 @@ class TableRead:
         return arrow_table.to_pandas()
 
     def to_duckdb(self, splits: List[Split], table_name: str,
-                  connection: Optional["DuckDBPyConnection"] = None) -> 
"DuckDBPyConnection":
+                  connection: Optional["DuckDBPyConnection"] = None,
+                  parallelism: Optional[int] = None) -> "DuckDBPyConnection":
+        """Materialize ``splits`` into an in-memory table registered with 
DuckDB.
+
+        See :meth:`to_arrow` for the semantics of ``parallelism``.
+        """
         import duckdb
 
         con = connection or duckdb.connect(database=":memory:")
-        con.register(table_name, self.to_arrow(splits))
+        con.register(table_name, self.to_arrow(splits, 
parallelism=parallelism))
         return con
 
     def to_ray(
diff --git a/paimon-python/pypaimon/tests/reader_base_test.py 
b/paimon-python/pypaimon/tests/reader_base_test.py
index 12875cabaf..455b7765b8 100644
--- a/paimon-python/pypaimon/tests/reader_base_test.py
+++ b/paimon-python/pypaimon/tests/reader_base_test.py
@@ -364,6 +364,44 @@ class ReaderBasicTest(unittest.TestCase):
         expect = pd.DataFrame(self.raw_data)
         pd.testing.assert_frame_equal(actual.reset_index(drop=True), 
expect.reset_index(drop=True))
 
+    def test_reader_duckDB_parallelism(self):
+        # A dedicated partitioned table with rows across multiple partitions so
+        # scan planning yields >= 2 splits, exercising the real parallel 
fan-out
+        # path (``_should_run_parallel`` requires parallelism >= 2 AND
+        # splits >= 2).
+        schema = Schema.from_pyarrow_schema(self.pa_schema, 
partition_keys=['dt'])
+        self.catalog.create_table('default.test_duckdb_parallel', schema, 
False)
+        table = self.catalog.get_table('default.test_duckdb_parallel')
+        write_builder = table.new_batch_write_builder()
+        table_write = write_builder.new_write()
+        table_commit = write_builder.new_commit()
+        table_write.write_arrow(pa.Table.from_pydict({
+            'user_id': [1, 2, 3, 4, 5, 6],
+            'item_id': [1001, 1002, 1003, 1004, 1005, 1006],
+            'behavior': ['a', 'b', 'c', 'd', 'e', 'f'],
+            'dt': ['p1', 'p1', 'p2', 'p2', 'p3', 'p3'],
+        }, schema=self.pa_schema))
+        table_commit.commit(table_write.prepare_commit())
+        table_write.close()
+        table_commit.close()
+
+        read_builder = table.new_read_builder()
+        splits = read_builder.new_scan().plan().splits()
+        self.assertGreaterEqual(len(splits), 2)
+
+        serial = read_builder.new_read().to_duckdb(
+            splits, 'duckdb_serial', parallelism=1)
+        parallel = read_builder.new_read().to_duckdb(
+            splits, 'duckdb_parallel', parallelism=4)
+
+        serial_df = serial.query(
+            "SELECT * FROM duckdb_serial ORDER BY item_id").fetchdf()
+        parallel_df = parallel.query(
+            "SELECT * FROM duckdb_parallel ORDER BY item_id").fetchdf()
+        pd.testing.assert_frame_equal(
+            serial_df.reset_index(drop=True),
+            parallel_df.reset_index(drop=True))
+
     def test_mixed_add_and_delete_entries_compute_stats(self):
         """Test record_count calculation with mixed ADD/DELETE entries in same 
partition."""
         pa_schema = pa.schema([
diff --git a/paimon-python/pypaimon/tests/write/table_write_test.py 
b/paimon-python/pypaimon/tests/write/table_write_test.py
index f6eb97c5ee..ee2f75bec4 100644
--- a/paimon-python/pypaimon/tests/write/table_write_test.py
+++ b/paimon-python/pypaimon/tests/write/table_write_test.py
@@ -587,6 +587,71 @@ class TableWriteTest(unittest.TestCase):
         self.assertTrue(str(e.exception).startswith(
             "Input schema isn't consistent with table schema and write cols."))
 
+    def test_write_pandas_respects_write_cols(self):
+        import pandas as pd
+
+        pa_schema = pa.schema([
+            ('id', pa.int32()),
+            ('name', pa.string()),
+            ('score', pa.int64()),
+        ])
+        schema = Schema.from_pyarrow_schema(pa_schema)
+        self.catalog.create_table(
+            'default.test_write_pandas_write_cols', schema, False)
+        table = self.catalog.get_table(
+            'default.test_write_pandas_write_cols')
+
+        write_builder = table.new_batch_write_builder()
+        table_write = write_builder.new_write().with_write_type(['id', 'name'])
+        table_commit = write_builder.new_commit()
+        # DataFrame only carries the written subset; missing ``score`` is
+        # padded with null on read.
+        table_write.write_pandas(pd.DataFrame({
+            'id': [1, 2],
+            'name': ['a', 'b'],
+        }))
+        table_commit.commit(table_write.prepare_commit())
+        table_write.close()
+        table_commit.close()
+
+        expected = pa.Table.from_pydict({
+            'id': [1, 2],
+            'name': ['a', 'b'],
+            'score': [None, None],
+        }, schema=pa_schema)
+        actual = self._read_sorted(table, 'id')
+        self.assertEqual(expected, actual)
+
+    def test_write_pandas_full_columns_unchanged(self):
+        import pandas as pd
+
+        pa_schema = pa.schema([
+            ('id', pa.int32()),
+            ('name', pa.string()),
+        ])
+        schema = Schema.from_pyarrow_schema(pa_schema)
+        self.catalog.create_table(
+            'default.test_write_pandas_full', schema, False)
+        table = self.catalog.get_table('default.test_write_pandas_full')
+
+        write_builder = table.new_batch_write_builder()
+        table_write = write_builder.new_write()
+        table_commit = write_builder.new_commit()
+        table_write.write_pandas(pd.DataFrame({
+            'id': [1, 2],
+            'name': ['a', 'b'],
+        }))
+        table_commit.commit(table_write.prepare_commit())
+        table_write.close()
+        table_commit.close()
+
+        expected = pa.Table.from_pydict({
+            'id': [1, 2],
+            'name': ['a', 'b'],
+        }, schema=pa_schema)
+        actual = self._read_sorted(table, 'id')
+        self.assertEqual(expected, actual)
+
     def test_validate_schema_allows_binary_family_for_write_cols(self):
         pa_schema = pa.schema([
             ('id', pa.int32()),
diff --git a/paimon-python/pypaimon/write/table_write.py 
b/paimon-python/pypaimon/write/table_write.py
index 629383409f..ad88295b64 100644
--- a/paimon-python/pypaimon/write/table_write.py
+++ b/paimon-python/pypaimon/write/table_write.py
@@ -75,7 +75,14 @@ class TableWrite:
         self.file_store_write.write_row(partition, bucket, row, values_by_name)
 
     def write_pandas(self, dataframe):
-        pa_schema = 
PyarrowFieldParser.from_paimon_schema(self.table.table_schema.fields)
+        write_cols = self.file_store_write.write_cols
+        if write_cols is not None:
+            # Column-subset write (append-only ``with_write_type``): build the
+            # RecordBatch against the subset schema so the input only needs the
+            # written columns, mirroring the ``write_arrow`` path.
+            pa_schema = self._write_cols_pyarrow_schema(write_cols)
+        else:
+            pa_schema = self.table_pyarrow_schema
         record_batch = pa.RecordBatch.from_pandas(dataframe, schema=pa_schema)
         return self.write_arrow_batch(record_batch)
 

Reply via email to