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 b96b7a6a28 [python] Complete object RowKind filtering and reuse Arrow 
event columns (#10462)
b96b7a6a28 is described below

commit b96b7a6a284bfd48cc052b258be5380e9cfe3430
Author: zhigang <[email protected]>
AuthorDate: Fri Oct 9 21:54:48 2026 +0800

    [python] Complete object RowKind filtering and reuse Arrow event columns 
(#10462)
---
 paimon-python/README.md                            |   2 +
 .../pypaimon/tests/write/object_row_kind_test.py   | 358 +++++++++++++++++++++
 .../tests/write/postpone_event_write_test.py       |  14 +-
 .../pypaimon/write/postpone_batch_table_write.py   |   4 +
 paimon-python/pypaimon/write/row_kind.py           |  65 ++--
 paimon-python/pypaimon/write/writer/data_writer.py |   8 +-
 .../pypaimon/write/writer/key_value_data_writer.py |  10 +-
 7 files changed, 433 insertions(+), 28 deletions(-)

diff --git a/paimon-python/README.md b/paimon-python/README.md
index f4dd3c7fe8..cc28e3b66d 100644
--- a/paimon-python/README.md
+++ b/paimon-python/README.md
@@ -168,6 +168,8 @@ path select the Python writer before native data is 
written. If the runtime or t
 unavailable, write uses Python. Once Rust starts writing a batch, errors
 propagate without retrying that batch through Python.
 
+For primary-key Row writes, `rowkind.field` takes precedence over the object's 
`RowKind`; otherwise the object event is preserved. Plain Arrow input defaults 
to INSERT. Both Python and native writers apply `ignore-delete` and 
`ignore-update-before` before routing object rows.
+
 Batch and stream `merge_into` also use Rust core for eligible data-evolution
 Parquet tables when `write.native.enabled=true`. Existing `WhenMatched` and
 `WhenNotMatched` clauses accept Arrow/pandas input, Paimon table sources, or
diff --git a/paimon-python/pypaimon/tests/write/object_row_kind_test.py 
b/paimon-python/pypaimon/tests/write/object_row_kind_test.py
new file mode 100644
index 0000000000..8158f5d5a9
--- /dev/null
+++ b/paimon-python/pypaimon/tests/write/object_row_kind_test.py
@@ -0,0 +1,358 @@
+# 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.
+
+"""Object events survive filtering, routing and Row-to-Arrow conversion."""
+
+from typing import Any, List
+
+import pyarrow as pa
+import pyarrow.parquet as pq
+import pytest
+
+from pypaimon import CatalogFactory, Schema
+from pypaimon.table.file_store_table import FileStoreTable
+from pypaimon.table.row.generic_row import GenericRow
+from pypaimon.table.row.projected_row import ProjectedRow
+from pypaimon.table.row.row_kind import RowKind
+from pypaimon.write.native_write import NativeTableWrite
+
+
[email protected](
+    params=[
+        pytest.param(False, marks=pytest.mark.python_write, id='python'),
+        pytest.param(True, marks=pytest.mark.native_plan, id='native'),
+    ]
+)
+def native(request):
+    return request.param
+
+
+def make_table(tmp_path, native, bucket='1', options=None, partition=False):
+    fields: List[pa.Field] = [
+        pa.field('id', pa.int32(), nullable=False),
+        pa.field('v', pa.int32()),
+    ]
+    if options and options.get('rowkind.field') is not None:
+        fields.append(pa.field('op', pa.string()))
+    if partition:
+        fields.append(pa.field('pt', pa.string(), nullable=False))
+    schema = pa.schema(fields)
+    settings = {
+        'bucket': bucket,
+        'file.format': 'parquet',
+        'write.native.enabled': str(native).lower(),
+        'commit.native.enabled': 'false',
+        'read.native.enabled': 'false',
+        'scan.native-plan.enabled': 'false',
+    }
+    settings.update(options or {})
+    catalog = CatalogFactory.create({'warehouse': str(tmp_path)})
+    catalog.create_database('default', True)
+    catalog.create_table(
+        'default.events',
+        Schema.from_pyarrow_schema(
+            schema,
+            primary_keys=['id', 'pt'] if partition else ['id'],
+            partition_keys=['pt'] if partition else [],
+            options=settings,
+        ),
+        False,
+    )
+    table = catalog.get_table('default.events')
+    assert isinstance(table, FileStoreTable)
+    return table, schema
+
+
+def new_builder(table, fixed=False):
+    return (
+        table.new_postpone_fixed_bucket_write_builder()
+        if fixed
+        else table.new_batch_write_builder()
+    )
+
+
+def assert_backend(writer, native):
+    assert isinstance(writer, NativeTableWrite) == native
+    if native:
+        assert writer._python_writer is None
+        assert writer._native_writer is not None
+        assert type(writer._native_writer).__module__ == 
'pypaimon_rust.datafusion'
+
+
+def physical_rows(table, messages):
+    return [
+        row
+        for message in messages
+        for file in message.new_files
+        for row in pq.ParquetFile(file.file_path).read().to_pylist()
+    ]
+
+
+def read_merged_rows(table):
+    builder = table.new_read_builder()
+    splits = builder.new_scan().plan().splits()
+    assert splits and all(not split.raw_convertible for split in splits)
+    assert sum(len(split.files) for split in splits) == 2
+    return builder.new_read().to_arrow(splits).to_pylist()
+
+
[email protected](
+    'bucket,options',
+    [
+        ('1', {}),
+        ('-1', {}),
+        ('-2', {}),
+        pytest.param('1', {'rowkind.field': None}, id='null-rowkind-field'),
+    ],
+)
+def test_object_delete_across_commits(tmp_path, native, bucket, options):
+    table, _ = make_table(
+        tmp_path, native, bucket, {'postpone.default-bucket-num': '1', 
**options}
+    )
+    if 'rowkind.field' in options:
+        # Creation retains explicit nulls; table.copy() would remove this key.
+        assert 'rowkind.field' in table.options.options.to_map()
+        assert table.options.options.to_map()['rowkind.field'] is None
+    for kind in (RowKind.INSERT, RowKind.DELETE):
+        builder = new_builder(table, fixed=bucket == '-2')
+        writer, commit = builder.new_write(), builder.new_commit()
+        try:
+            writer.write_row(GenericRow([1, 10], table.fields, kind))
+            assert_backend(writer, native)
+            messages = writer.prepare_commit()
+            assert [row['_VALUE_KIND'] for row in physical_rows(table, 
messages)] == [
+                kind.value
+            ]
+            commit.commit(messages)
+        finally:
+            writer.close()
+            commit.close()
+    # Two overlapping files select merge reading, independently of 
delete-count metadata.
+    assert read_merged_rows(table) == []
+
+
[email protected](
+    'options,expected',
+    [
+        ({}, [0, 1, 2, 3]),
+        ({'rowkind.field': 'op'}, [0, 1, 2, 3]),
+        ({'ignore-delete': 'true'}, [0, 2]),
+        ({'ignore-update-before': 'true'}, [0, 2, 3]),
+        ({'ignore-delete': 'true', 'ignore-update-before': 'true'}, [0, 2]),
+        ({'rowkind.field': 'op', 'ignore-delete': 'true'}, [0, 2]),
+    ],
+)
+def test_object_events_and_configured_precedence(tmp_path, native, options, 
expected):
+    # Ordinary postpone files preserve source events without merging equal 
keys.
+    table, _ = make_table(tmp_path, native, '-2', options)
+    writer = new_builder(table).new_write()
+    try:
+        for kind in RowKind:
+            values: List[Any] = [kind.value, 10]
+            object_kind = kind
+            if 'rowkind.field' in options:
+                values.append(kind.to_string())
+                object_kind = RowKind((kind.value + 1) % 4)
+            writer.write_row(GenericRow(values, table.fields, object_kind))
+        assert_backend(writer, native)
+        rows = physical_rows(table, writer.prepare_commit())
+        assert [(row['id'], row['_VALUE_KIND']) for row in rows] == [
+            (k, k) for k in expected
+        ]
+        assert all(row['v'] == 10 for row in rows)
+    finally:
+        writer.close()
+
+
[email protected](
+    'bucket,fixed,options',
+    [
+        ('1', False, {'ignore-delete': 'true'}),
+        ('-1', False, {'ignore-update-before': 'true'}),
+        ('-2', True, {'ignore-delete': 'true'}),
+    ],
+)
+def test_filter_precedes_invalid_routing(tmp_path, native, bucket, fixed, 
options):
+    # Python's automatic postpone planner must also filter before sizing 
inputs.
+    if native and fixed:
+        options = dict(options, **{'postpone.default-bucket-num': '1'})
+    table, _ = make_table(tmp_path, native, bucket, options, partition=True)
+    writer = new_builder(table, fixed).new_write()
+    try:
+        kinds = (
+            (RowKind.UPDATE_BEFORE, RowKind.DELETE)
+            if 'ignore-delete' in options
+            else (RowKind.UPDATE_BEFORE,)
+        )
+        for kind in kinds:
+            # These values cannot be hashed, converted to Arrow, or used as a 
partition.
+            writer.write_row(GenericRow([[], object(), []], table.fields, 
kind))
+        assert_backend(writer, native)
+        assert writer.prepare_commit() == []
+        assert not list(tmp_path.rglob('*.parquet'))
+    finally:
+        writer.close()
+
+
+def test_internal_row_events_and_arrow_default_insert(tmp_path, native):
+    table, schema = make_table(tmp_path, native)
+    builder = new_builder(table)
+    writer, commit = builder.new_write(), builder.new_commit()
+    try:
+        writer.write_arrow(pa.Table.from_pylist([{'id': 10, 'v': 10}], 
schema=schema))
+        for kind in RowKind:
+            writer.write_row(
+                ProjectedRow([0, 1]).replace_row(
+                    GenericRow([kind.value, 20], table.fields, kind)
+                )
+            )
+        writer.write_row(GenericRow([11, 30], table.fields))
+        writer.write_arrow(pa.Table.from_pylist([{'id': 12, 'v': 40}], 
schema=schema))
+        assert_backend(writer, native)
+        messages = writer.prepare_commit()
+        rows = sorted(physical_rows(table, messages), key=lambda row: 
row['id'])
+        assert [(row['id'], row['_VALUE_KIND']) for row in rows] == [
+            (0, 0),
+            (1, 1),
+            (2, 2),
+            (3, 3),
+            (10, 0),
+            (11, 0),
+            (12, 0),
+        ]
+        commit.commit(messages)
+    finally:
+        writer.close()
+        commit.close()
+
+
+def test_stream_object_events(tmp_path, native):
+    table, _ = make_table(tmp_path, native, '-2')
+    writer = table.new_stream_write_builder().new_write()
+    try:
+        for checkpoint, kind in enumerate(RowKind):
+            writer.write_row(GenericRow([checkpoint, 10], table.fields, kind))
+            assert_backend(writer, native)
+            rows = physical_rows(table, writer.prepare_commit(checkpoint))
+            assert [row['_VALUE_KIND'] for row in rows] == [kind.value]
+    finally:
+        writer.close()
+
+
+def test_arrow_filter_preserves_sliced_chunk_order_and_null_values(tmp_path, 
native):
+    table, schema = make_table(
+        tmp_path,
+        native,
+        '-2',
+        {
+            'rowkind.field': 'op',
+            'ignore-delete': 'true',
+            'ignore-update-before': 'true',
+        },
+    )
+    # The slice excludes two valid rows and spans chunks with repeated keys.
+    data = pa.table(
+        {
+            'id': [99, 7, 2, 7, 4, 2, 98],
+            'v': [99, 10, 20, None, 40, None, 98],
+            'op': ['+I', '+I', '-D', '+U', '-U', '+I', '+I'],
+        },
+        schema=schema,
+    )
+    data = pa.Table.from_batches(data.to_batches(max_chunksize=4)).slice(1, 5)
+    writer = new_builder(table).new_write()
+    try:
+        writer.write_arrow(data)
+        assert_backend(writer, native)
+        messages = writer.prepare_commit()
+        rows = physical_rows(table, messages)
+        assert [(row['id'], row['v'], row['_VALUE_KIND']) for row in rows] == [
+            (7, 10, 0),
+            (7, None, 2),
+            (2, None, 0),
+        ]
+        assert [row['_SEQUENCE_NUMBER'] for row in rows] == [0, 1, 2]
+        assert all(
+            file.delete_row_count == 0 for msg in messages for file in 
msg.new_files
+        )
+    finally:
+        writer.close()
+
+
+def test_empty_and_filtered_batches_do_not_affect_next_checkpoint(tmp_path, 
native):
+    table, schema = make_table(
+        tmp_path,
+        native,
+        '-2',
+        {'rowkind.field': 'op', 'ignore-delete': 'true'},
+    )
+    writer = table.new_stream_write_builder().new_write()
+    try:
+        writer.write_arrow_batch(pa.RecordBatch.from_pylist([], schema=schema))
+        writer.write_arrow_batch(
+            pa.RecordBatch.from_pylist(
+                [
+                    {'id': 1, 'v': 10, 'op': '-U'},
+                    {'id': 2, 'v': 20, 'op': '-D'},
+                ],
+                schema=schema,
+            )
+        )
+        assert_backend(writer, native)
+        assert writer.prepare_commit(0) == []
+        assert not list(tmp_path.rglob('*.parquet'))
+
+        writer.write_arrow_batch(
+            pa.RecordBatch.from_pylist(
+                [{'id': 3, 'v': None, 'op': '+I'}], schema=schema
+            )
+        )
+        rows = physical_rows(table, writer.prepare_commit(1))
+        assert [
+            (row['id'], row['v'], row['_VALUE_KIND'], row['_SEQUENCE_NUMBER'])
+            for row in rows
+        ] == [(3, None, 0, 0)]
+    finally:
+        writer.close()
+
+
[email protected](
+    'events,error',
+    [
+        (['+I', None], 'Unknown row kind string: None'),
+        (['-D', 'invalid'], 'Unknown row kind string: invalid'),
+        (['invalid', None], 'Unknown row kind string: invalid'),
+        ([None, 'invalid'], 'Unknown row kind string: None'),
+    ],
+)
+def test_invalid_arrow_events_reject_the_whole_batch(tmp_path, events, error):
+    table, schema = make_table(
+        tmp_path,
+        False,
+        '-2',
+        {'rowkind.field': 'op', 'ignore-delete': 'true'},
+    )
+    writer = new_builder(table).new_write()
+    try:
+        data = pa.record_batch(
+            {'id': [1, 2], 'v': [10, 20], 'op': events}, schema=schema
+        )
+        with pytest.raises(ValueError, match=error):
+            writer.write_arrow_batch(data)
+        assert writer.prepare_commit() == []
+        assert not list(tmp_path.rglob('*.parquet'))
+    finally:
+        writer.close()
diff --git a/paimon-python/pypaimon/tests/write/postpone_event_write_test.py 
b/paimon-python/pypaimon/tests/write/postpone_event_write_test.py
index 16285e2a11..7664ee8792 100644
--- a/paimon-python/pypaimon/tests/write/postpone_event_write_test.py
+++ b/paimon-python/pypaimon/tests/write/postpone_event_write_test.py
@@ -58,11 +58,15 @@ def 
test_postpone_preserves_unsorted_duplicate_rows(tmp_path, engine):
 
 
 @pytest.mark.parametrize('row_input', [False, True])
[email protected]('options,expected', [
-    ({}, [0, 1, 2, 3]),
-    ({'ignore-delete': 'true'}, [0, 2]),
-    ({'ignore-update-before': 'true'}, [0, 2, 3]),
-])
[email protected](
+    'options,expected',
+    [
+        ({}, [0, 1, 2, 3]),
+        ({'ignore-delete': 'true'}, [0, 2]),
+        ({'ignore-update-before': 'true'}, [0, 2, 3]),
+        ({'ignore-delete': 'true', 'ignore-update-before': 'true'}, [0, 2]),
+    ],
+)
 def test_python_postpone_row_kinds_are_filtered_before_writing(tmp_path, 
row_input, options, expected):
     from pypaimon.table.row.generic_row import GenericRow
     table, schema = make_table(tmp_path, {'rowkind.field': 'op', **options})
diff --git a/paimon-python/pypaimon/write/postpone_batch_table_write.py 
b/paimon-python/pypaimon/write/postpone_batch_table_write.py
index f00f15b544..f15f0bafb5 100644
--- a/paimon-python/pypaimon/write/postpone_batch_table_write.py
+++ b/paimon-python/pypaimon/write/postpone_batch_table_write.py
@@ -156,6 +156,10 @@ class PostponeFixedBucketBatchTableWrite(BatchTableWrite):
         )
         require_columns(values_by_name, column_names, "write_row")
         require_columns(values_by_name, self.table.partition_keys, "write_row")
+        from pypaimon.write.row_kind import skip_write_row
+
+        if skip_write_row(self.table, values_by_name, row.get_row_kind()):
+            return
         partition = tuple(
             values_by_name[key] for key in self.table.partition_keys)
         if self._bucket_plan.contains(partition):
diff --git a/paimon-python/pypaimon/write/row_kind.py 
b/paimon-python/pypaimon/write/row_kind.py
index 78b5bc5e92..df279aadf6 100644
--- a/paimon-python/pypaimon/write/row_kind.py
+++ b/paimon-python/pypaimon/write/row_kind.py
@@ -16,23 +16,39 @@
 
 """Configured row kinds and pre-routing filters, matching Java 
RowKindFilter."""
 
+from typing import List, Union
+
 import pyarrow as pa
+import pyarrow.compute as pc
 
+from pypaimon.common.options.core_options import CoreOptions
 from pypaimon.table.row.row_kind import RowKind
+from pypaimon.table.special_fields import SpecialFields
 
 
-def row_kinds(options, data):
-    name = options.options.to_map().get('rowkind.field')
-    if name is None:
-        if '_VALUE_KIND' in data.schema.names:
-            return data.column('_VALUE_KIND').to_pylist()
-        return [0] * data.num_rows
-    if name not in data.schema.names:
-        raise ValueError('Cannot find rowkind field %s in table schema' % name)
-    column = data.column(name)
+def row_kinds(
+    options: CoreOptions,
+    data: Union[pa.Table, pa.RecordBatch],
+) -> Union[pa.Array, pa.ChunkedArray]:
+    """Resolve events as an int8 Arrow column, reusing existing event data."""
+    row_kind_field_name = options.options.to_map().get('rowkind.field')
+    if row_kind_field_name is None:
+        if SpecialFields.VALUE_KIND.name in data.schema.names:
+            return data.column(SpecialFields.VALUE_KIND.name)
+        return pa.repeat(pa.scalar(RowKind.INSERT.value, type=pa.int8()), 
data.num_rows)
+    if row_kind_field_name not in data.schema.names:
+        raise ValueError(
+            'Cannot find rowkind field %s in table schema' % 
row_kind_field_name
+        )
+    column = data.column(row_kind_field_name)
     if not pa.types.is_string(column.type):
         raise ValueError('rowkind.field column must be a string')
-    return [RowKind.from_string(value).value for value in column.to_pylist()]
+    kinds: List[int] = []
+    for value in column.to_pylist():
+        if value is None:
+            raise ValueError('Unknown row kind string: None')
+        kinds.append(RowKind.from_string(value).value)
+    return pa.array(kinds, type=pa.int8())
 
 
 def filter_write_batch(table, data):
@@ -47,8 +63,12 @@ def filter_write_batch(table, data):
     ignore_before = str(raw.get('ignore-update-before', 'false')).lower() == 
'true'
     if not ignore_delete and not ignore_before:
         return data
-    keep = [not _is_filtered(kind, ignore_delete, ignore_before) for kind in 
kinds]
-    return data.filter(pa.array(keep, type=pa.bool_()))
+    ignored = [RowKind.UPDATE_BEFORE.value]
+    if ignore_delete:
+        ignored.append(RowKind.DELETE.value)
+    return data.filter(
+        pc.invert(pc.is_in(kinds, value_set=pa.array(ignored, type=pa.int8())))
+    )
 
 
 def with_row_kind(table, data, row):
@@ -57,10 +77,15 @@ def with_row_kind(table, data, row):
     A configured rowkind.field takes precedence, as in Java RowKindGenerator.
     Public Arrow writes still accept only the declared table/write schema.
     """
-    if table.is_primary_key_table and 
table.options.options.to_map().get('rowkind.field') is None:
-        return data.append_column(
-            pa.field('_VALUE_KIND', pa.int8(), nullable=False),
-            pa.array([row.get_row_kind().value] * data.num_rows, 
type=pa.int8()))
+    if (
+        table.is_primary_key_table
+        and table.options.options.to_map().get('rowkind.field') is None
+    ):
+        kind = row.get_row_kind()
+        data = data.append_column(
+            pa.field(SpecialFields.VALUE_KIND.name, pa.int8(), nullable=False),
+            pa.repeat(pa.scalar(kind.value, type=pa.int8()), data.num_rows),
+        )
     return data
 
 
@@ -69,8 +94,12 @@ def skip_write_row(table, values, row_kind=RowKind.INSERT):
         return False
     options = table.options
     raw = options.options.to_map()
-    name = raw.get('rowkind.field')
-    kind = row_kind.value if name is None else 
RowKind.from_string(values[name]).value
+    row_kind_field_name = raw.get('rowkind.field')
+    kind = (
+        row_kind.value
+        if row_kind_field_name is None
+        else RowKind.from_string(values[row_kind_field_name]).value
+    )
     return _is_filtered(kind, options.ignore_delete(),
                         str(raw.get('ignore-update-before', 'false')).lower() 
== 'true')
 
diff --git a/paimon-python/pypaimon/write/writer/data_writer.py 
b/paimon-python/pypaimon/write/writer/data_writer.py
index d6ded1e0f0..98cdd66d2b 100644
--- a/paimon-python/pypaimon/write/writer/data_writer.py
+++ b/paimon-python/pypaimon/write/writer/data_writer.py
@@ -30,6 +30,7 @@ from pypaimon.schema.data_types import PyarrowFieldParser
 from pypaimon.table.bucket_mode import BucketMode
 from pypaimon.table.row.generic_row import GenericRow
 from pypaimon.table.row.row_kind import RowKind
+from pypaimon.table.special_fields import SpecialFields
 from pypaimon.utils.file_store_path_factory import canonical_data_file_path
 from pypaimon.write.map_shared_shredding_writer import MapSharedShreddingWriter
 from pypaimon.write.writer.mosaic_writer_options import 
create_mosaic_writer_options
@@ -432,9 +433,12 @@ class DataWriter(ABC):
 
     def _count_delete_rows(self, data: pa.Table) -> int:
         # Count the final file contents, after merging and rolling the buffer.
-        if not self.table.is_primary_key_table or '_VALUE_KIND' not in 
data.schema.names:
+        if (
+            not self.table.is_primary_key_table
+            or SpecialFields.VALUE_KIND.name not in data.schema.names
+        ):
             return 0
-        kind_counts = pc.value_counts(data.column('_VALUE_KIND'))
+        kind_counts = 
pc.value_counts(data.column(SpecialFields.VALUE_KIND.name))
         return sum(entry['counts'].as_py() for entry in kind_counts
                    if entry['values'].as_py() in (RowKind.UPDATE_BEFORE.value, 
RowKind.DELETE.value))
 
diff --git a/paimon-python/pypaimon/write/writer/key_value_data_writer.py 
b/paimon-python/pypaimon/write/writer/key_value_data_writer.py
index b2e79d50d0..f513e148d6 100644
--- a/paimon-python/pypaimon/write/writer/key_value_data_writer.py
+++ b/paimon-python/pypaimon/write/writer/key_value_data_writer.py
@@ -26,6 +26,7 @@ from pypaimon.read.reader.deduplicate_merge_function import \
     DeduplicateMergeFunction
 from pypaimon.common.options.core_options import ChangelogProducer
 from pypaimon.table.row.key_value import KeyValue
+from pypaimon.table.special_fields import SpecialFields
 from pypaimon.write.writer.data_writer import DataWriter
 from pypaimon.write.writer.write_buffer import WriteBuffer
 
@@ -313,12 +314,15 @@ class KeyValueDataWriter(DataWriter):
         new_fields.append(pa.field('_SEQUENCE_NUMBER', pa.int64(), 
nullable=False))
 
         from pypaimon.write.row_kind import row_kinds
-        value_kind_column = pa.array(row_kinds(self.options, data), 
type=pa.int8())
+
+        value_kind_column = row_kinds(self.options, data)
         new_arrays.append(value_kind_column)
-        new_fields.append(pa.field('_VALUE_KIND', pa.int8(), nullable=False))
+        new_fields.append(
+            pa.field(SpecialFields.VALUE_KIND.name, pa.int8(), nullable=False)
+        )
 
         for i in range(data.num_columns):
-            if data.schema.field(i).name == '_VALUE_KIND':
+            if data.schema.field(i).name == SpecialFields.VALUE_KIND.name:
                 continue
             new_arrays.append(data.column(i))
             new_fields.append(data.schema.field(i))

Reply via email to