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 8e5b6c21d4 [Python] Align Native write validation and file metadata 
with Java (#10660)
8e5b6c21d4 is described below

commit 8e5b6c21d4d4e13a41c069ee15c513006718d8e7
Author: Jingsong Lee <[email protected]>
AuthorDate: Mon Oct 19 22:09:21 2026 +0800

    [Python] Align Native write validation and file metadata with Java (#10660)
---
 .../paimon/utils/InternalRowPartitionComputer.java |  17 +-
 .../utils/InternalRowPartitionComputerTest.java    |  73 +++++
 paimon-python/README.md                            |   4 +
 .../pypaimon/manifest/manifest_file_manager.py     |   9 +-
 .../pypaimon/manifest/schema/data_file_meta.py     |  17 +-
 paimon-python/pypaimon/snapshot/snapshot_commit.py |   3 +-
 .../pypaimon/table/source/native_vector_search.py  |   7 -
 .../pypaimon/tests/data_file_creation_time_test.py | 175 ++++++++++++
 .../tests/manifest/creation_time_precision_test.py |  74 ++++++
 paimon-python/pypaimon/tests/native_commit_test.py |  10 +
 .../tests/native_filtered_write_validation_test.py |  94 +++++++
 .../pypaimon/tests/native_partition_types_test.py  |  52 +++-
 .../pypaimon/tests/native_vector_context_test.py   |  78 ++++++
 .../pypaimon/tests/native_vector_search_test.py    |  35 ++-
 .../tests/pure_writer_creation_time_test.py        | 149 +++++++++++
 .../pypaimon/tests/reader_primary_key_test.py      |  15 +-
 .../tests/write/row_kind_nullability_test.py       | 295 +++++++++++++++++++++
 .../pypaimon/utils/file_store_path_factory.py      |   6 +-
 paimon-python/pypaimon/write/file_store_commit.py  |  15 +-
 paimon-python/pypaimon/write/native_commit.py      |  11 +-
 paimon-python/pypaimon/write/native_merge_into.py  |   7 -
 paimon-python/pypaimon/write/native_update.py      |   7 +-
 paimon-python/pypaimon/write/native_write.py       |  30 +--
 paimon-python/pypaimon/write/row_kind.py           |  27 +-
 paimon-python/pypaimon/write/table_write.py        |  52 +++-
 paimon-python/pypaimon/write/writer/blob_writer.py |   2 +-
 .../pypaimon/write/writer/composite_data_writer.py |   2 +-
 paimon-python/pypaimon/write/writer/data_writer.py |  10 +-
 .../pypaimon/write/writer/vector_writer.py         |   2 +-
 29 files changed, 1144 insertions(+), 134 deletions(-)

diff --git 
a/paimon-common/src/main/java/org/apache/paimon/utils/InternalRowPartitionComputer.java
 
b/paimon-common/src/main/java/org/apache/paimon/utils/InternalRowPartitionComputer.java
index 0145ee3f78..d82ef6b532 100644
--- 
a/paimon-common/src/main/java/org/apache/paimon/utils/InternalRowPartitionComputer.java
+++ 
b/paimon-common/src/main/java/org/apache/paimon/utils/InternalRowPartitionComputer.java
@@ -65,7 +65,12 @@ public class InternalRowPartitionComputer {
             int i = columnList.indexOf(partitionColumns[j]);
             DataType type = rowType.getTypeAt(i);
             partitionFieldGetters[j] = createNullCheckingFieldGetter(type, i);
-            partitionCastExecutors[j] = CastExecutors.resolve(type, 
VarCharType.STRING_TYPE);
+            // byte[].toString contains an allocation identity, which changes 
every time a
+            // BinaryRow field is read. Always use the UTF-8 cast for binary 
partition names.
+            partitionCastExecutors[j] =
+                    legacyPartitionName && !TypeCheckUtils.isBinaryString(type)
+                            ? value -> value
+                            : CastExecutors.resolve(type, 
VarCharType.STRING_TYPE);
         }
     }
 
@@ -76,13 +81,9 @@ public class InternalRowPartitionComputer {
             Object field = partitionFieldGetters[i].getFieldOrNull(in);
             String partitionValue = null;
             if (field != null) {
-                if (legacyPartitionName) {
-                    partitionValue = field.toString();
-                } else {
-                    Object casted = partitionCastExecutors[i].cast(field);
-                    if (casted != null) {
-                        partitionValue = casted.toString();
-                    }
+                Object casted = partitionCastExecutors[i].cast(field);
+                if (casted != null) {
+                    partitionValue = casted.toString();
                 }
             }
             if (StringUtils.isNullOrWhitespaceOnly(partitionValue)) {
diff --git 
a/paimon-common/src/test/java/org/apache/paimon/utils/InternalRowPartitionComputerTest.java
 
b/paimon-common/src/test/java/org/apache/paimon/utils/InternalRowPartitionComputerTest.java
index 6f92d72ed9..42019a4ff8 100644
--- 
a/paimon-common/src/test/java/org/apache/paimon/utils/InternalRowPartitionComputerTest.java
+++ 
b/paimon-common/src/test/java/org/apache/paimon/utils/InternalRowPartitionComputerTest.java
@@ -27,6 +27,8 @@ import org.apache.paimon.types.RowType;
 
 import org.junit.jupiter.api.Test;
 
+import java.nio.charset.StandardCharsets;
+
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.entry;
 
@@ -108,4 +110,75 @@ public class InternalRowPartitionComputerTest {
         assertThat(computer.generatePartValues(binaryRow))
                 .containsExactly(entry("dt", "20240731"), entry("id", "1"));
     }
+
+    @Test
+    public void testBinaryPartitionNamesUseStableUtf8Values() {
+        DataType[] binaryTypes = {DataTypes.BINARY(16), 
DataTypes.VARBINARY(16), DataTypes.BYTES()};
+        byte[][] values = {
+            null,
+            new byte[0],
+            " \t\u001c".getBytes(StandardCharsets.UTF_8),
+            "a/b=c".getBytes(StandardCharsets.UTF_8),
+            "汉字".getBytes(StandardCharsets.UTF_8),
+            "\u00a0".getBytes(StandardCharsets.UTF_8),
+            new byte[] {(byte) 0xed, (byte) 0xa0, (byte) 0x80}
+        };
+        String[] expected = {
+            "__DEFAULT_PARTITION__",
+            "__DEFAULT_PARTITION__",
+            "__DEFAULT_PARTITION__",
+            "a/b=c",
+            "汉字",
+            "\u00a0",
+            "\ufffd"
+        };
+        for (boolean legacy : new boolean[] {true, false}) {
+            for (DataType type : binaryTypes) {
+                RowType rowType =
+                        RowType.of(
+                                new DataType[] {DataTypes.INT(), type}, new 
String[] {"id", "p"});
+                InternalRowPartitionComputer computer =
+                        new InternalRowPartitionComputer(
+                                "__DEFAULT_PARTITION__", rowType, new String[] 
{"p"}, legacy);
+                for (int valueIndex = 0; valueIndex < values.length; 
valueIndex++) {
+                    BinaryRow row = new BinaryRow(2);
+                    BinaryRowWriter writer = new BinaryRowWriter(row);
+                    writer.writeInt(0, 123);
+                    byte[] value = values[valueIndex];
+                    if (value == null) {
+                        writer.setNullAt(1);
+                    } else {
+                        writer.writeBinary(1, value, 0, value.length);
+                    }
+                    writer.complete();
+                    // BinaryRow.getBinary creates a fresh byte[] on every 
call. Partition
+                    // names must depend on its contents, including after row 
serialization.
+                    assertThat(computer.generatePartValues(row))
+                            .containsExactly(entry("p", expected[valueIndex]));
+                    assertThat(computer.generatePartValues(row))
+                            .containsExactly(entry("p", expected[valueIndex]));
+                    assertThat(computer.generatePartValues(row.copy()))
+                            .containsExactly(entry("p", expected[valueIndex]));
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testBinaryPartitionKeepsOtherLegacyValues() {
+        RowType rowType =
+                RowType.of(
+                        new DataType[] {DataTypes.DATE(), DataTypes.BYTES()},
+                        new String[] {"dt", "p"});
+        InternalRowPartitionComputer computer =
+                new InternalRowPartitionComputer(
+                        "__DEFAULT_PARTITION__", rowType, new String[] {"p", 
"dt"}, true);
+        BinaryRow row = new BinaryRow(2);
+        BinaryRowWriter writer = new BinaryRowWriter(row);
+        writer.writeInt(0, 1);
+        writer.writeBinary(1, new byte[] {65}, 0, 1);
+        writer.complete();
+        assertThat(computer.generatePartValues(row))
+                .containsExactly(entry("p", "A"), entry("dt", "1"));
+    }
 }
diff --git a/paimon-python/README.md b/paimon-python/README.md
index 55f53762d3..5f0e4c81d5 100644
--- a/paimon-python/README.md
+++ b/paimon-python/README.md
@@ -551,6 +551,10 @@ Writer restore, overwrite and index construction use this 
entrypoint and delegat
 planning to Rust core when Native planning is enabled.
 Native planning remains optional and is disabled by default.
 
+Binary partition keys use UTF-8 text and normal partition escaping in both
+`partition.legacy-name` modes, matching Java. This makes repeated writes to the
+same binary value use a stable directory.
+
 # Coalesced BLOB reads
 
 FileIO merges nearby BLOB ranges before reading. Set
diff --git a/paimon-python/pypaimon/manifest/manifest_file_manager.py 
b/paimon-python/pypaimon/manifest/manifest_file_manager.py
index 27ae7b32d8..d52b9f50d1 100644
--- a/paimon-python/pypaimon/manifest/manifest_file_manager.py
+++ b/paimon-python/pypaimon/manifest/manifest_file_manager.py
@@ -29,7 +29,7 @@ try:
 except ImportError:  # pragma: no cover - supported fastavro versions provide 
these
     _read_bytes = _read_long = _read_record = _skip_record = None
 
-from datetime import datetime
+from datetime import datetime, timezone
 
 from pypaimon.filesystem.caching_file_io import (
     CachingInputStream, _open_input_stream_with_known_size)
@@ -265,15 +265,16 @@ class ManifestFileManager:
                 max_values=BinaryRow(value_dict['_MAX_VALUES'], fields),
                 null_counts=value_dict['_NULL_COUNTS'],
             )
-            # fastavro returns UTC-aware datetime for timestamp-millis, we 
need to convert properly
+            # Avro exposes the stored local TIMESTAMP fields as a UTC datetime.
+            # Keep integer millisecond precision, without applying the system 
zone.
             from pypaimon.data.timestamp import Timestamp
             creation_time_value = file_dict['_CREATION_TIME']
             creation_time_ts = None
             if creation_time_value is not None:
                 if isinstance(creation_time_value, datetime):
                     if creation_time_value.tzinfo:
-                        epoch_millis = int(creation_time_value.timestamp() * 
1000)
-                        creation_time_ts = 
Timestamp.from_epoch_millis(epoch_millis)
+                        creation_time_ts = Timestamp.from_local_date_time(
+                            
creation_time_value.astimezone(timezone.utc).replace(tzinfo=None))
                     else:
                         creation_time_ts = 
Timestamp.from_local_date_time(creation_time_value)
                 elif isinstance(creation_time_value, (int, float)):
diff --git a/paimon-python/pypaimon/manifest/schema/data_file_meta.py 
b/paimon-python/pypaimon/manifest/schema/data_file_meta.py
index 8e454bc84a..3a16086668 100644
--- a/paimon-python/pypaimon/manifest/schema/data_file_meta.py
+++ b/paimon-python/pypaimon/manifest/schema/data_file_meta.py
@@ -18,7 +18,6 @@
 from dataclasses import dataclass
 from datetime import datetime
 from typing import List, Optional
-import time
 
 from pypaimon.utils.range import Range
 from pypaimon.data.timestamp import Timestamp
@@ -75,12 +74,13 @@ class DataFileMeta:
     def creation_time_epoch_millis(self) -> Optional[int]:
         if self.creation_time is None:
             return None
-        local_dt = self.creation_time.to_local_date_time()
-        local_time_struct = local_dt.timetuple()
-        local_timestamp = time.mktime(local_time_struct)
-        utc_timestamp = time.mktime(time.gmtime(local_timestamp))
-        tz_offset_seconds = int(local_timestamp - utc_timestamp)
-        return int((local_timestamp - tz_offset_seconds) * 1000)
+        from pypaimon.casting.temporal_utils import _local_to_utc_seconds
+
+        # Java stores Timestamp.now() as local wall-clock fields, then resolves
+        # LocalDateTime.atZone(systemDefault()) when an epoch instant is 
needed.
+        # Preserve the millisecond remainder, including for pre-epoch values.
+        seconds, millis = divmod(self.creation_time.get_millisecond(), 1000)
+        return _local_to_utc_seconds(seconds) * 1000 + millis
 
     def creation_time_as_datetime(self) -> Optional[datetime]:
         if self.creation_time is None:
@@ -114,7 +114,8 @@ class DataFileMeta:
         write_cols_sequences: Optional[List[int]] = None,
     ) -> 'DataFileMeta':
         if creation_time is None:
-            creation_time = Timestamp.now()
+            # Java forAppend/forLevel0 factories persist TIMESTAMP(3).
+            creation_time = Timestamp.now().to_millis_timestamp()
 
         return cls(
             file_name=file_name,
diff --git a/paimon-python/pypaimon/snapshot/snapshot_commit.py 
b/paimon-python/pypaimon/snapshot/snapshot_commit.py
index dd9c90aead..7c11b2608c 100644
--- a/paimon-python/pypaimon/snapshot/snapshot_commit.py
+++ b/paimon-python/pypaimon/snapshot/snapshot_commit.py
@@ -63,7 +63,8 @@ class PartitionStatistics:
             record_count=record_count,
             file_count=file_count,
             file_size_in_bytes=file_size_in_bytes,
-            last_file_creation_time=last_file_creation_time or int(time.time() 
* 1000),
+            last_file_creation_time=(int(time.time() * 1000)
+                                     if last_file_creation_time is None else 
last_file_creation_time),
             total_buckets=total_buckets
         )
 
diff --git a/paimon-python/pypaimon/table/source/native_vector_search.py 
b/paimon-python/pypaimon/table/source/native_vector_search.py
index f700d4a9e5..01db9cbd2a 100644
--- a/paimon-python/pypaimon/table/source/native_vector_search.py
+++ b/paimon-python/pypaimon/table/source/native_vector_search.py
@@ -37,13 +37,6 @@ def _native_vector_builder(builder, batch=False):
     loader = getattr(getattr(table, 'catalog_environment', None), 
'catalog_loader', None)
     if loader is None or _catalog_metastore(loader) != 'rest':
         return None
-    context_fn = getattr(loader, 'context', None)
-    if not callable(context_fn):
-        return None
-    context = context_fn()
-    if any(getattr(context, attr, None) is not None for attr in (
-            'hadoop_conf', 'prefer_io_loader', 'fallback_io_loader')):
-        return None
     # Python's batch reader currently supports only global row-ID results.
     if batch and builder._is_primary_key_vector_search():
         return None
diff --git a/paimon-python/pypaimon/tests/data_file_creation_time_test.py 
b/paimon-python/pypaimon/tests/data_file_creation_time_test.py
new file mode 100644
index 0000000000..3c921a13bc
--- /dev/null
+++ b/paimon-python/pypaimon/tests/data_file_creation_time_test.py
@@ -0,0 +1,175 @@
+# 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.
+
+"""Data-file creation times follow Java local Timestamp/epoch semantics."""
+
+import json
+import os
+import subprocess
+import sys
+import time
+from datetime import datetime
+
+import pytest
+
+
+pytestmark = [pytest.mark.python_write, pytest.mark.python_commit]
+_CASES = {
+    'UTC': [('2024-01-02T12:34:56.789', '2024-01-02T12:34:56.789'),
+            ('1970-01-01T00:00:00.000', '1970-01-01T00:00:00.000'),
+            ('1969-12-31T23:59:59.999', '1969-12-31T23:59:59.999')],
+    'Asia/Shanghai': [('2024-01-02T12:34:56.789', '2024-01-02T04:34:56.789'),
+                      ('1970-01-01T07:59:59.999', '1969-12-31T23:59:59.999')],
+    'America/New_York': [('2024-01-02T12:34:56.789', 
'2024-01-02T17:34:56.789'),
+                         ('2024-03-10T02:30:00.123', 
'2024-03-10T07:30:00.123'),
+                         ('2024-11-03T01:30:00.456', 
'2024-11-03T05:30:00.456')],
+}
+_SCRIPT = r'''import json, sys, time
+from datetime import datetime
+from pypaimon.data.timestamp import Timestamp
+from pypaimon.manifest.schema.data_file_meta import DataFileMeta
+if hasattr(time, 'tzset'):
+    time.tzset()
+result = []
+for wall in json.loads(sys.argv[1]):
+    metadata = DataFileMeta('data.parquet', 1, 1, None, None, None, None, 0, 
0, 0, 0, [])
+    metadata.creation_time = 
Timestamp.from_local_date_time(datetime.strptime(wall, '%Y-%m-%dT%H:%M:%S.%f'))
+    result.append(metadata.creation_time_epoch_millis())
+metadata.creation_time = None
+assert metadata.creation_time_epoch_millis() is None
+print(json.dumps(result))
+'''
+
+
[email protected](not hasattr(time, 'tzset'), reason='process TZ cannot be 
changed on this platform')
[email protected]('zone', sorted(_CASES))
+def 
test_creation_time_epoch_millis_matches_java_zone_rules_and_precision(zone):
+    # Run separate processes so TZ changes cannot race other tests or cached
+    # native timezone detection. Goldens use LocalDateTime.atZone semantics:
+    # earlier instant in an overlap, forward through a spring gap.
+    environment = dict(os.environ, TZ=zone)
+    output = subprocess.check_output([sys.executable, '-c', _SCRIPT,
+                                      json.dumps([wall for wall, _ in 
_CASES[zone]])], env=environment)
+    assert json.loads(output) == _expected_epoch_millis(zone)
+
+
+def test_create_preserves_an_explicit_creation_timestamp():
+    from pypaimon.data.timestamp import Timestamp
+    from pypaimon.manifest.schema.data_file_meta import DataFileMeta
+
+    # Java's generic factory accepts supplied timestamps unchanged, while its
+    # normal writer factories create millisecond timestamps.
+    explicit = Timestamp(1001, 987654)
+    metadata = DataFileMeta.create('data.parquet', 1, 1, None, None, None, 
None, 0, 0, 0, 0, [],
+                                   creation_time=explicit)
+    assert metadata.creation_time == explicit
+
+
+_REST_SCRIPT = r'''import json, sys, time
+from contextlib import ExitStack
+from datetime import datetime
+from unittest.mock import Mock, patch
+import pyarrow as pa
+from pypaimon import CatalogFactory, Schema
+from pypaimon.api.api_response import ConfigResponse
+from pypaimon.api.auth import BearTokenAuthProvider
+from pypaimon.data.timestamp import Timestamp
+from pypaimon.tests.rest.rest_server import RESTCatalogServer
+if hasattr(time, 'tzset'):
+    time.tzset()
+server = RESTCatalogServer(sys.argv[1], BearTokenAuthProvider('test-token'),
+                           ConfigResponse(defaults={'prefix': 'native-test'}), 
'warehouse')
+server.start()
+try:
+    catalog = CatalogFactory.create({'metastore': 'rest', 'uri': 
server.get_url(), 'warehouse': 'warehouse',
+                                      'token.provider': 'bear', 'token': 
'test-token', 'data-token.enabled': 'false'})
+    catalog.create_database('default', True)
+    schema = pa.schema([('id', pa.int32()), ('pt', pa.string())])
+    catalog.create_table('default.t', Schema.from_pyarrow_schema(schema, 
partition_keys=['pt'], options={
+        'write.native.enabled': 'false', 'read.native.enabled': 'false', 
'scan.native-plan.enabled': 'false',
+        'commit.native.enabled': sys.argv[2]}), False)
+    table = catalog.get_table('default.t')
+    captured = []
+    original = server._table_commit_handle
+    def publish(method, data, *args, **kwargs):
+        captured.append(json.loads(data)['statistics'])
+        return original(method, data, *args, **kwargs)
+    for wall in json.loads(sys.argv[3]):
+        builder = table.new_batch_write_builder()
+        writer, earlier_writer, commit = builder.new_write(), 
builder.new_write(), builder.new_commit()
+        callback = Mock()
+        commit.add_commit_callback(callback)
+        timestamp = Timestamp.from_local_date_time(datetime.strptime(wall, 
'%Y-%m-%dT%H:%M:%S.%f'))
+        try:
+            writer.write_arrow(pa.Table.from_pylist([{'id': 1, 'pt': 'p'}], 
schema=schema))
+            messages = writer.prepare_commit()
+            for message in messages:
+                for file in message.new_files:
+                    file.creation_time = timestamp
+            earlier_writer.write_arrow(pa.Table.from_pylist([{'id': 2, 'pt': 
'p'}], schema=schema))
+            earlier_messages = earlier_writer.prepare_commit()
+            earlier = Timestamp(timestamp.get_millisecond() - 1)
+            for message in earlier_messages:
+                for file in message.new_files:
+                    file.creation_time = earlier
+            messages.extend(earlier_messages)
+            if sys.argv[4] == 'true':
+                messages.reverse()
+            with ExitStack() as stack:
+                stack.enter_context(patch.object(server, 
'_table_commit_handle', side_effect=publish))
+                if sys.argv[2] == 'true':
+                    stack.enter_context(patch.object(commit.file_store_commit, 
'_try_commit',
+                                                     
side_effect=AssertionError('native commit fell back')))
+                commit.commit(messages)
+            callback.call.assert_called_once()
+            entries = callback.call.call_args[0][0].commit_entries
+            assert sorted(entry.file.creation_time.get_millisecond() for entry 
in entries) == [
+                earlier.get_millisecond(), timestamp.get_millisecond()]
+            assert captured[-1][0]['recordCount'] == 2
+            assert captured[-1][0]['fileCount'] == 2
+        finally:
+            writer.close()
+            earlier_writer.close()
+            commit.close()
+    print('CREATION_TIME_RESULT=' + 
json.dumps([stats[0]['lastFileCreationTime'] for stats in captured]))
+finally:
+    server.shutdown()
+'''
+
+
+def _expected_epoch_millis(zone):
+    values = []
+    for _, instant in _CASES[zone]:
+        delta = datetime.strptime(instant, '%Y-%m-%dT%H:%M:%S.%f') - 
datetime(1970, 1, 1)
+        values.append((delta.days * 86400 + delta.seconds) * 1000 + 
delta.microseconds // 1000)
+    return values
+
+
[email protected](not hasattr(time, 'tzset'), reason='process TZ cannot be 
changed on this platform')
[email protected]('zone', sorted(_CASES))
[email protected]('native', [False, True], ids=['python', 'native'])
[email protected]('reverse', [False, True], ids=['newer-first', 
'older-first'])
+def test_rest_commit_publishes_creation_time_instants(tmp_path, zone, native, 
reverse):
+    from pypaimon.read.native_plan import native_runtime_available
+
+    if native and not native_runtime_available():
+        pytest.skip('pypaimon-rust not installed')
+    output = subprocess.check_output(
+        [sys.executable, '-c', _REST_SCRIPT, str(tmp_path), 
str(native).lower(),
+         json.dumps([wall for wall, _ in _CASES[zone]]), 
str(reverse).lower()], env=dict(os.environ, TZ=zone))
+    result = next(line for line in output.decode().splitlines() if 
line.startswith('CREATION_TIME_RESULT='))
+    assert json.loads(result.split('=', 1)[1]) == _expected_epoch_millis(zone)
diff --git 
a/paimon-python/pypaimon/tests/manifest/creation_time_precision_test.py 
b/paimon-python/pypaimon/tests/manifest/creation_time_precision_test.py
new file mode 100644
index 0000000000..1d0fa35583
--- /dev/null
+++ b/paimon-python/pypaimon/tests/manifest/creation_time_precision_test.py
@@ -0,0 +1,74 @@
+# 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.
+
+"""Avro creation timestamps retain Java's exact local millisecond fields."""
+
+from datetime import datetime, timedelta, timezone
+
+import fastavro
+import pyarrow as pa
+import pytest
+
+from pypaimon import CatalogFactory, Schema
+from pypaimon.data.timestamp import Timestamp
+from pypaimon.manifest.manifest_file_manager import ManifestFileManager
+from pypaimon.manifest.schema.data_file_meta import DataFileMeta
+from pypaimon.manifest.schema.manifest_entry import ManifestEntry
+from pypaimon.manifest.schema.simple_stats import SimpleStats
+from pypaimon.table.row.generic_row import GenericRow
+
+
+pytestmark = [pytest.mark.python_write, pytest.mark.python_commit]
+
+
[email protected]('prefix_filter', [False, True], ids=['generic', 
'filtered-block'])
+def test_creation_time_milliseconds_survive_real_avro_decoding(tmp_path, 
prefix_filter):
+    catalog = CatalogFactory.create({'warehouse': str(tmp_path)})
+    catalog.create_database('default', True)
+    catalog.create_table('default.t', 
Schema.from_pyarrow_schema(pa.schema([('id', pa.int32())])), False)
+    table = catalog.get_table('default.t')
+    manager = ManifestFileManager(table)
+    empty = GenericRow([], [])
+    stats = SimpleStats(empty, empty, [])
+    millis = [None, -1001, -1, 0, 1, 1001, 1005, 2017, 1_700_000_000_123]
+    entries = [ManifestEntry(
+        kind=0, partition=empty, bucket=0, total_buckets=1,
+        file=DataFileMeta(
+            file_name='data-%d.parquet' % index, file_size=1, row_count=1,
+            min_key=empty, max_key=empty, key_stats=stats, value_stats=stats,
+            min_sequence_number=0, max_sequence_number=0, 
schema_id=table.table_schema.id,
+            level=0, extra_files=[], value_stats_cols=[],
+            creation_time=None if value is None else 
Timestamp.from_epoch_millis(value),
+        )) for index, value in enumerate(millis)]
+    manager.write('manifest-creation-time', entries)
+
+    # Fastavro returns aware UTC datetimes even though Paimon's TIMESTAMP
+    # contains local wall fields. Verify the actual wire values independently.
+    with table.file_io.new_input_stream(manager.manifest_path + 
'/manifest-creation-time') as stream:
+        wire_times = [record['_FILE']['_CREATION_TIME'] for record in 
fastavro.reader(stream)]
+    epoch = datetime(1970, 1, 1, tzinfo=timezone.utc)
+    assert wire_times == [None if value is None else epoch + 
timedelta(milliseconds=value) for value in millis]
+
+    options = {'early_entry_filter': lambda bucket, total: bucket == 0 and 
total == 1} if prefix_filter else {}
+    decoded = manager.read('manifest-creation-time', drop_stats=False, 
**options)
+    assert [None if entry.file.creation_time is None else 
entry.file.creation_time.get_millisecond()
+            for entry in decoded] == millis
+
+    # Rewriting a manifest must not alter metadata handed over by a writer.
+    manager.write('manifest-creation-time-rewritten', decoded)
+    rewritten = manager.read('manifest-creation-time-rewritten', **options)
+    assert [entry.file.creation_time for entry in rewritten] == 
[entry.file.creation_time for entry in entries]
diff --git a/paimon-python/pypaimon/tests/native_commit_test.py 
b/paimon-python/pypaimon/tests/native_commit_test.py
index ff369e8d65..d677751a2e 100644
--- a/paimon-python/pypaimon/tests/native_commit_test.py
+++ b/paimon-python/pypaimon/tests/native_commit_test.py
@@ -555,6 +555,16 @@ def 
test_incompatible_publication_environment_is_not_reconstructed(
         resolve.assert_not_called()
 
 
[email protected]('option', ['hadoop_conf', 'prefer_io_loader', 
'fallback_io_loader'])
+def test_inactive_context_loader_fields_do_not_disable_native_commit(tmp_path, 
native_rest_catalog, option):
+    from pypaimon.write.native_commit import _rest_catalog_supported
+    table = _table(tmp_path, catalog=native_rest_catalog)
+    unused = Mock(side_effect=AssertionError('Inactive IO loader was invoked'))
+    setattr(table.catalog_environment.catalog_loader.context(), option, unused)
+    assert _rest_catalog_supported(table)
+    unused.assert_not_called()
+
+
 def test_missing_runtime_falls_back_without_reconstructing_table(
         tmp_path, native_rest_catalog):
     table = _table(tmp_path, catalog=native_rest_catalog)
diff --git 
a/paimon-python/pypaimon/tests/native_filtered_write_validation_test.py 
b/paimon-python/pypaimon/tests/native_filtered_write_validation_test.py
new file mode 100644
index 0000000000..ef4cee28a5
--- /dev/null
+++ b/paimon-python/pypaimon/tests/native_filtered_write_validation_test.py
@@ -0,0 +1,94 @@
+# 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.
+
+"""Ignored events do not reach nested value validation or physical writers."""
+
+import pyarrow as pa
+import pytest
+
+from pypaimon import CatalogFactory, Schema
+from pypaimon.read.native_plan import native_runtime_available
+from pypaimon.write.native_write import NativeTableWrite
+
+
+pytestmark = [pytest.mark.python_write, pytest.mark.python_commit]
+_NATIVE = [False, pytest.param(True, marks=pytest.mark.skipif(
+    not native_runtime_available(), reason='pypaimon-rust not installed'))]
+
+
+def _payload(mode, value):
+    if mode == 'array':
+        return pa.list_(pa.field('item', pa.int32(), nullable=False)), [value]
+    if mode == 'row':
+        return pa.struct([pa.field('value', pa.int32(), nullable=False)]), 
{'value': value}
+    return pa.map_(pa.string(), pa.field('value', pa.int32(), 
nullable=False)), [('key', value)]
+
+
+def _table(tmp_path, native, mode):
+    payload_type, _ = _payload(mode, None)
+    schema = pa.schema([('id', pa.int32()), ('op', pa.string()), ('payload', 
payload_type)])
+    catalog = CatalogFactory.create({'warehouse': str(tmp_path)})
+    catalog.create_database('default', True)
+    catalog.create_table('default.t', Schema.from_pyarrow_schema(schema, 
primary_keys=['id'], options={
+        'bucket': '1', 'rowkind.field': 'op', 'ignore-delete': 'true',
+        'write.native.enabled': str(native).lower(), 'commit.native.enabled': 
'false',
+        'read.native.enabled': 'false', 'scan.native-plan.enabled': 'false'}), 
False)
+    return catalog.get_table('default.t'), schema
+
+
[email protected]('native', _NATIVE)
[email protected]('mode', ['array', 'row', 'map'])
[email protected]('ignored_kind', ['-D', '-U'])
[email protected]('bucket', [None, 0])
+def test_ignored_nested_nulls_leave_only_valid_rows(tmp_path, native, mode, 
ignored_kind, bucket):
+    table, schema = _table(tmp_path, native, mode)
+    _, invalid = _payload(mode, None)
+    _, valid = _payload(mode, 42)
+    data = pa.Table.from_pylist([
+        {'id': 1, 'op': ignored_kind, 'payload': invalid},
+        {'id': 2, 'op': '+I', 'payload': valid},
+    ], schema=schema)
+    builder = table.new_batch_write_builder()
+    writer, commit = builder.new_write(), builder.new_commit()
+    assert isinstance(writer, NativeTableWrite) == native
+    try:
+        writer.write_arrow(data, bucket)
+        messages = writer.prepare_commit()
+        assert sum(file.row_count for message in messages for file in 
message.new_files) == 1
+        commit.commit(messages)
+    finally:
+        writer.close()
+        commit.close()
+    reader = table.new_read_builder()
+    assert 
reader.new_read().to_arrow(reader.new_scan().plan().splits()).to_pylist() == [
+        {'id': 2, 'op': '+I', 'payload': valid}]
+
+
[email protected]('native', _NATIVE)
[email protected]('mode', ['array', 'row', 'map'])
+def test_surviving_nested_nulls_are_rejected(tmp_path, native, mode):
+    table, schema = _table(tmp_path, native, mode)
+    _, invalid = _payload(mode, None)
+    data = pa.Table.from_pylist([{'id': 1, 'op': '+I', 'payload': invalid}], 
schema=schema)
+    writer = table.new_batch_write_builder().new_write()
+    assert isinstance(writer, NativeTableWrite) == native
+    try:
+        with pytest.raises((ValueError, RuntimeError, pa.ArrowInvalid)):
+            writer.write_arrow(data)
+            writer.prepare_commit()
+    finally:
+        writer.abort()
diff --git a/paimon-python/pypaimon/tests/native_partition_types_test.py 
b/paimon-python/pypaimon/tests/native_partition_types_test.py
index a60295907a..cfe49286c8 100644
--- a/paimon-python/pypaimon/tests/native_partition_types_test.py
+++ b/paimon-python/pypaimon/tests/native_partition_types_test.py
@@ -85,10 +85,12 @@ def _write(table, schema, rows, stream=False):
 
 @pytest.mark.native_plan
 @pytest.mark.parametrize('partition_type,values,names', _PARTITIONS)
[email protected]('legacy', [False, True])
 @pytest.mark.parametrize('mode', ['append', 'pk', 'evolution'])
 @pytest.mark.parametrize('placement', ['local', 'directory', 'external'])
-def test_native_partition_write_read_and_file_paths(tmp_path, partition_type, 
values, names, mode, placement):
-    table, schema = _table(tmp_path, partition_type, mode, placement=placement)
+def test_native_partition_write_read_and_file_paths(tmp_path, partition_type, 
values, names, legacy, mode, placement):
+    table, schema = _table(tmp_path, partition_type, mode, placement=placement,
+                           extra_options={'partition.legacy-name': 
str(legacy).lower()})
     rows = [dict(id=i, p=value, value=10 + i) for i, value in 
enumerate(values)]
     if mode == 'pk':
         rows = [row for row in rows if row['p'] is not None]
@@ -149,8 +151,10 @@ def 
test_native_non_finite_partitions_use_java_names_for_python_reads(tmp_path,
 @pytest.mark.python_write
 @pytest.mark.native_plan
 @pytest.mark.parametrize('partition_type,values,names', _PARTITIONS[2:])
-def test_python_binary_partitions_use_java_paths_and_native_reads(tmp_path, 
partition_type, values, names):
-    table, schema = _table(tmp_path, partition_type, native=False)
[email protected]('legacy', [False, True])
+def test_python_binary_partitions_use_java_paths_and_native_reads(tmp_path, 
partition_type, values, names, legacy):
+    table, schema = _table(tmp_path, partition_type, native=False,
+                           extra_options={'partition.legacy-name': 
str(legacy).lower()})
     rows = [dict(id=i, p=value, value=i) for i, value in enumerate(values)]
     messages = _write(table, schema, rows)
     for message in messages:
@@ -202,8 +206,10 @@ def 
test_native_cow_merge_uses_java_partition_path(tmp_path, action):
     (b'', '__DEFAULT_PARTITION__'), (b' \t\x1c', '__DEFAULT_PARTITION__'),
     ('\u00a0'.encode(), '\u00a0'), ('\u0085'.encode(), '\u0085'),
 ])
-def test_binary_partition_string_conversion_matches_java(tmp_path, value, 
expected):
-    table, _ = _table(tmp_path, pa.binary(), native=False)
[email protected]('legacy', [False, True])
+def test_binary_partition_string_conversion_matches_java(tmp_path, value, 
expected, legacy):
+    table, _ = _table(tmp_path, pa.binary(), native=False,
+                      extra_options={'partition.legacy-name': 
str(legacy).lower()})
     assert table.path_factory().relative_bucket_path((value,), 0, 
canonical_partition=True) == (
         'p=' + expected + '/bucket-0')
 
@@ -261,9 +267,31 @@ def 
test_native_commit_binary_partition_capability_precedes_execution(tmp_path,
             patch('pypaimon.write.native_commit.native_runtime_available', 
return_value=True), \
             patch('pypaimon.write.native_commit.create_native_write_table') as 
native:
         committer = create_native_commit(table, 'user')
-        if legacy:
-            assert committer is None
-            native.assert_not_called()
-        else:
-            assert committer is not None
-            native.assert_called_once_with(table)
+        assert committer is not None
+        native.assert_called_once_with(table)
+
+
[email protected]_plan
[email protected]_read
[email protected]_write
[email protected]('legacy', [False, True])
[email protected]('mode', ['append', 'pk', 'evolution'])
+def test_python_binary_partition_write_commit_and_reopen(tmp_path, legacy, 
mode):
+    table, schema = _table(tmp_path, pa.binary(), mode, native=False, 
extra_options={
+        'partition.legacy-name': str(legacy).lower(),
+        'scan.native-plan.enabled': 'false', 'read.native.enabled': 'false',
+        'commit.native.enabled': 'false'})
+    values = [b'a/b=c', '汉字'.encode('utf-8'), b'\xed\xa0\x80', b' \t\x1c', b'']
+    rows = [dict(id=i, p=value, value=i) for i, value in enumerate(values)]
+    messages = _write(table, schema, rows)
+    for message in messages:
+        for file in message.new_files:
+            assert table.file_io.exists(file.file_path)
+            # Resolve independently from the typed partition bytes, as a
+            # reopened reader does; no Python bytes repr or object identity.
+            from pypaimon.utils.file_store_path_factory import 
canonical_data_file_path
+            assert file.file_path == canonical_data_file_path(
+                table, message.partition, message.bucket, file.file_name)
+    catalog = CatalogFactory.create({'warehouse': str(tmp_path / 'warehouse')})
+    reopened = catalog.get_table('db.t')
+    assert _read(reopened) == rows
diff --git a/paimon-python/pypaimon/tests/native_vector_context_test.py 
b/paimon-python/pypaimon/tests/native_vector_context_test.py
new file mode 100644
index 0000000000..d42dc8a92b
--- /dev/null
+++ b/paimon-python/pypaimon/tests/native_vector_context_test.py
@@ -0,0 +1,78 @@
+# 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.
+
+"""Unused CatalogContext attributes must not select Python vector search."""
+
+from types import SimpleNamespace
+from unittest.mock import Mock, patch
+
+import pytest
+
+from pypaimon.catalog.catalog_context import CatalogContext
+from pypaimon.catalog.rest.rest_catalog_loader import RESTCatalogLoader
+from pypaimon.common.predicate import Predicate
+from pypaimon.table.source import native_vector_search
+
+
[email protected]('option', ['hadoop_conf', 'prefer_io_loader', 
'fallback_io_loader'])
[email protected]('batch', [False, True], ids=['single', 'batch'])
+def test_unused_context_attributes_preserve_native_vector_execution(option, 
batch):
+    context = CatalogContext.create_from_options({'metastore': 'rest', 'uri': 
'http://unused'})
+    unused = Mock(side_effect=AssertionError('An unused IO loader was 
invoked'))
+    setattr(context, option, {'unused': 'value'} if option == 'hadoop_conf' 
else unused)
+    loader = RESTCatalogLoader(context)
+    table = SimpleNamespace(
+        options=SimpleNamespace(native_read_enabled=lambda: True, 
query_auth_enabled=False),
+        catalog_environment=SimpleNamespace(catalog_loader=loader))
+    builder = SimpleNamespace(
+        _table=table, _vector_column=SimpleNamespace(name='embedding'), 
_limit=2,
+        _options={'ivf-flat.metric': 'l2'}, _filter=Predicate('equal', 0, 
'id', [7]),
+        _partition_filter=Predicate('equal', 1, 'pt', [1]), 
_is_primary_key_vector_search=lambda: False)
+    core = Mock()
+    core.with_limit.return_value = core
+    core.with_options.return_value = core
+    native_table = Mock()
+    native_table.new_vector_search_builder.return_value = core
+    native_table.new_batch_vector_search_builder.return_value = core
+    expected = [{7: -0.25, 9: -1.5}, {11: -2.0}]
+    core.execute_local.return_value.row_ids.return_value = expected[0]
+    core.execute_batch_local.return_value = 
[Mock(row_ids=Mock(return_value=scores)) for scores in expected]
+    queries = [[1.0, 0.0], [0.0, 1.0]] if batch else [1.0, 0.0]
+    with patch.object(loader, 'load', side_effect=AssertionError('Python 
catalog was reconstructed')), \
+            patch.object(native_vector_search, 'native_runtime_available', 
return_value=True), \
+            patch.object(native_vector_search, '_native_table', 
return_value=native_table) as resolve:
+        result = native_vector_search.try_native_vector_search(builder, 
queries, batch=batch)
+    assert result is not None
+    resolve.assert_called_once_with(table)
+    core.with_vector_column.assert_called_once_with('embedding')
+    core.with_limit.assert_called_once_with(2)
+    core.with_options.assert_called_once_with({'ivf-flat.metric': 'l2'})
+    core.with_filter.assert_called_once_with({'method': 'equal', 'field': 
'id', 'literals': [7]})
+    core.with_partition_filter.assert_called_once_with({'method': 'equal', 
'field': 'pt', 'literals': [1]})
+    unused.assert_not_called()
+    if batch:
+        core.with_query_vectors.assert_called_once_with(queries)
+        core.execute_batch_local.assert_called_once_with()
+        core.execute_local.assert_not_called()
+        results = result
+    else:
+        core.with_query_vector.assert_called_once_with(queries)
+        core.execute_local.assert_called_once_with()
+        core.execute_batch_local.assert_not_called()
+        results = [result]
+        expected = expected[:1]
+    assert [{row_id: item.score_getter()(row_id) for row_id in item.results()} 
for item in results] == expected
diff --git a/paimon-python/pypaimon/tests/native_vector_search_test.py 
b/paimon-python/pypaimon/tests/native_vector_search_test.py
index d493962e8b..99cf6639f4 100644
--- a/paimon-python/pypaimon/tests/native_vector_search_test.py
+++ b/paimon-python/pypaimon/tests/native_vector_search_test.py
@@ -19,7 +19,7 @@
 
 import shutil
 from pathlib import Path
-from unittest.mock import patch
+from unittest.mock import Mock, patch
 
 import pyarrow as pa
 import pytest
@@ -308,18 +308,37 @@ def test_mixed_index_metrics_are_rejected(vector_table, 
filtered, mode):
 
 
 @pytest.mark.parametrize('option', ['hadoop_conf', 'prefer_io_loader', 
'fallback_io_loader'])
-def test_custom_io_context_keeps_python_search(vector_table, option):
[email protected]('batch', [False, True], ids=['single', 'batch'])
+def test_unused_context_attributes_preserve_native_search(vector_table, 
option, batch):
     from copy import copy
-    from pypaimon.table.source import native_vector_search
+    from pypaimon.table.source.vector_search_read import 
DataEvolutionVectorRead, BatchVectorSearchReadImpl
     table = vector_table.copy({'read.native.enabled': 'true'})
     loader = table.catalog_environment.catalog_loader
     context = copy(loader.context())
-    setattr(context, option, object())
-    expected = _scores(_single(table.copy({'read.native.enabled': 
'false'})).execute_local())
+    unused = Mock(side_effect=AssertionError('An unused IO loader was 
invoked'))
+    setattr(context, option, {'unused': 'value'} if option == 'hadoop_conf' 
else unused)
+    classic = table.copy({'read.native.enabled': 'false', 
'scan.native-plan.enabled': 'false'})
+    predicate = classic.new_read_builder().new_predicate_builder()
+    queries = [[1, 0], [0, 1]] if batch else [[1, 0]]
+
+    def configure(builder):
+        return (builder.with_options({'ivf-flat.metric': 'l2'})
+                .with_filter(predicate.greater_than('id', 
1)).with_partition_filter(predicate.equal('pt', 1)))
+
+    expected = [_scores(configure(_single(classic, query, 2)).execute_local()) 
for query in queries]
     with patch.object(loader, 'context', return_value=context), \
-            patch.object(native_vector_search, '_native_table',
-                         side_effect=AssertionError('Unsupported Native 
context')):
-        _assert_scores(_scores(_single(table).execute_local()), expected)
+            patch.object(DataEvolutionVectorRead, 'read_plan', 
side_effect=AssertionError('Python search ran')), \
+            patch.object(BatchVectorSearchReadImpl, 'read_batch_plan',
+                         side_effect=AssertionError('Python batch search 
ran')):
+        if batch:
+            actual = 
configure(table.new_batch_vector_search_builder().with_vector_column('embedding')
+                               
.with_query_vectors(queries).with_limit(2)).execute_batch_local()
+        else:
+            actual = [configure(_single(table, queries[0], 2)).execute_local()]
+    assert len(actual) == len(expected)
+    for result, scores in zip(actual, expected):
+        _assert_scores(_scores(result), scores)
+    unused.assert_not_called()
 
 
 @pytest.mark.parametrize('mode', ['fast', 'full', 'detail'])
diff --git a/paimon-python/pypaimon/tests/pure_writer_creation_time_test.py 
b/paimon-python/pypaimon/tests/pure_writer_creation_time_test.py
new file mode 100644
index 0000000000..d4f85ac0ac
--- /dev/null
+++ b/paimon-python/pypaimon/tests/pure_writer_creation_time_test.py
@@ -0,0 +1,149 @@
+# 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.
+
+"""Prepared data and dedicated files use Java DataFileMeta's TIMESTAMP(3)."""
+
+from datetime import datetime, timedelta
+from unittest.mock import patch
+
+import pyarrow as pa
+import pyarrow.parquet as pq
+import pytest
+
+from pypaimon import CatalogFactory, Schema
+from pypaimon.data.timestamp import Timestamp
+from pypaimon.manifest.manifest_file_manager import ManifestFileManager
+from pypaimon.manifest.manifest_list_manager import ManifestListManager
+from pypaimon.write.native_write import NativeTableWrite
+
+
+pytestmark = [pytest.mark.python_write, pytest.mark.python_commit]
+
+
[email protected]('stream', [False, True], ids=['batch', 'stream'])
[email protected]('mode', ['append', 'pk-input', 'postpone', 'blob', 
'vector'])
+def test_prepared_file_creation_times_have_millisecond_precision(tmp_path, 
stream, mode):
+    catalog = CatalogFactory.create({'warehouse': str(tmp_path)})
+    catalog.create_database('default', True)
+    schema = pa.schema([('id', pa.int32()), ('value', pa.int32())])
+    rows = [dict(id=1, value=10), dict(id=2, value=20)]
+    options = {'file.format': 'parquet', 'write.native.enabled': 'false',
+               'commit.native.enabled': 'false', 'read.native.enabled': 
'false',
+               'scan.native-plan.enabled': 'false'}
+    if mode in ('pk-input', 'postpone'):
+        options.update({'bucket': '-2' if mode == 'postpone' else '1',
+                        'changelog-producer': 'input'})
+    if mode in ('blob', 'vector'):
+        options.update({'data-evolution.enabled': 'true', 
'row-tracking.enabled': 'true'})
+        if mode == 'blob':
+            schema = schema.append(pa.field('payload', pa.large_binary()))
+            for row in rows:
+                row['payload'] = ('blob-%d' % row['id']).encode()
+        else:
+            options['vector.file.format'] = 'parquet'
+            schema = schema.append(pa.field('embedding', 
pa.list_(pa.float32(), 2)))
+            for row in rows:
+                row['embedding'] = [float(row['id']), 2.0]
+    catalog.create_table('default.t', Schema.from_pyarrow_schema(
+        schema, primary_keys=['id'] if mode in ('pk-input', 'postpone') else 
[], options=options), False)
+    table = catalog.get_table('default.t')
+    builder = table.new_stream_write_builder() if stream else 
table.new_batch_write_builder()
+    writer, commit = builder.new_write(), builder.new_commit()
+    assert not isinstance(writer, NativeTableWrite)
+    now = Timestamp.from_local_date_time(datetime(2024, 6, 1, 12, 30, 40, 
123456))
+    expected = now.to_millis_timestamp()
+    try:
+        with patch.object(Timestamp, 'now', return_value=now):
+            writer.write_arrow(pa.Table.from_pylist(rows, schema=schema))
+            messages = writer.prepare_commit(1) if stream else 
writer.prepare_commit()
+        files = [file for message in messages for file in message.new_files + 
message.changelog_files]
+        assert files
+        assert all(file.creation_time.get_nano_of_millisecond() == 0 for file 
in files)
+        assert all(file.creation_time == expected for file in files)
+        assert now.get_nano_of_millisecond() == 456000
+        if mode == 'blob':
+            assert any(file.file_name.endswith('.blob') for file in files)
+            assert any(file.file_name.endswith('.parquet') for file in files)
+        if mode == 'vector':
+            assert any('.vector.parquet' in file.file_name for file in files)
+        if mode == 'pk-input':
+            assert any(message.changelog_files for message in messages)
+        if mode == 'postpone':
+            assert all(message.bucket == -2 and not message.changelog_files 
for message in messages)
+            # Pending events are invisible to normal scans until assigned real
+            # buckets; verify their physical Parquet rows before publication.
+            with table.file_io.new_input_stream(files[0].file_path) as stream_:
+                physical = 
pq.ParquetFile(stream_).read().select(schema.names).to_pylist()
+            assert physical == rows
+        commit.commit(messages, 1) if stream else commit.commit(messages)
+    finally:
+        writer.close()
+        commit.close()
+
+    snapshot = table.snapshot_manager().get_latest_snapshot()
+    entries = 
ManifestFileManager(table).read_entries_parallel(ManifestListManager(table).read_all(snapshot))
+    assert entries and all(entry.file.creation_time == expected for entry in 
entries)
+    read = table.new_read_builder()
+    actual = 
read.new_read().to_arrow(read.new_scan().plan().splits()).sort_by('id').to_pylist()
+    assert actual == ([] if mode == 'postpone' else rows)
+
+
[email protected]('stream', [False, True], ids=['batch', 'stream'])
+def test_input_changelog_creation_time_is_taken_after_its_write(tmp_path, 
stream):
+    catalog = CatalogFactory.create({'warehouse': str(tmp_path)})
+    catalog.create_database('default', True)
+    schema = pa.schema([('id', pa.int32()), ('value', pa.int32())])
+    catalog.create_table('default.t', Schema.from_pyarrow_schema(
+        schema, primary_keys=['id'], options={
+            'bucket': '1', 'changelog-producer': 'input',
+            'write.native.enabled': 'false', 'commit.native.enabled': 'false',
+            'read.native.enabled': 'false', 'scan.native-plan.enabled': 
'false'}), False)
+    table = catalog.get_table('default.t')
+    builder = table.new_stream_write_builder() if stream else 
table.new_batch_write_builder()
+    writer, commit = builder.new_write(), builder.new_commit()
+    clock = [datetime(2024, 6, 1, 12, 30, 40, 123456)]
+    completed = {}
+    write_parquet = table.file_io.write_parquet
+
+    def write_and_advance(path, *args, **kwargs):
+        result = write_parquet(path, *args, **kwargs)
+        # Advance a deterministic clock only after a real file was written.
+        # Each file's result must use its own completion time, including the
+        # changelog whose statistics were collected before the I/O.
+        clock[0] += timedelta(seconds=1)
+        completed[path] = 
Timestamp.from_local_date_time(clock[0]).to_millis_timestamp()
+        return result
+
+    try:
+        with patch.object(table.file_io, 'write_parquet', 
side_effect=write_and_advance), \
+                patch.object(Timestamp, 'now', side_effect=lambda: 
Timestamp.from_local_date_time(clock[0])):
+            writer.write_arrow(pa.Table.from_pylist([
+                {'id': 1, 'value': 10}, {'id': 1, 'value': 20}], 
schema=schema))
+            messages = writer.prepare_commit(1) if stream else 
writer.prepare_commit()
+        data_files = [file for message in messages for file in 
message.new_files]
+        changelogs = [file for message in messages for file in 
message.changelog_files]
+        assert data_files and changelogs
+        assert sum(file.row_count for file in data_files) == 1
+        assert sum(file.row_count for file in changelogs) == 2
+        for file in data_files + changelogs:
+            assert file.creation_time == completed[file.file_path]
+        commit.commit(messages, 1) if stream else commit.commit(messages)
+    finally:
+        writer.close()
+        commit.close()
+    read = table.new_read_builder()
+    assert 
read.new_read().to_arrow(read.new_scan().plan().splits()).to_pylist() == 
[{'id': 1, 'value': 20}]
diff --git a/paimon-python/pypaimon/tests/reader_primary_key_test.py 
b/paimon-python/pypaimon/tests/reader_primary_key_test.py
index 438e57ac64..c8927429cb 100644
--- a/paimon-python/pypaimon/tests/reader_primary_key_test.py
+++ b/paimon-python/pypaimon/tests/reader_primary_key_test.py
@@ -20,6 +20,7 @@ import shutil
 import tempfile
 import time
 import unittest
+from datetime import datetime, timezone
 
 import pyarrow as pa
 
@@ -562,14 +563,12 @@ class PkReaderTest(unittest.TestCase):
                     epoch_millis = entry.file.creation_time_epoch_millis()
                     self.assertIsNotNone(epoch_millis)
                     self.assertGreater(epoch_millis, 0)
-                    import time
-                    expected_epoch_millis = creation_time.get_millisecond()
-                    local_dt = creation_time.to_local_date_time()
-                    local_time_struct = local_dt.timetuple()
-                    local_timestamp = time.mktime(local_time_struct)
-                    local_time_struct_utc = time.gmtime(local_timestamp)
-                    utc_timestamp = time.mktime(local_time_struct_utc)
-                    expected_epoch_millis = int(utc_timestamp * 1000)
+                    # Java resolves the stored local TIMESTAMP in the system
+                    # zone once and retains all millisecond digits.
+                    instant = 
creation_time.to_local_date_time().astimezone(timezone.utc)
+                    delta = instant - datetime(1970, 1, 1, tzinfo=timezone.utc)
+                    expected_epoch_millis = ((delta.days * 86400 + 
delta.seconds) * 1000
+                                             + delta.microseconds // 1000)
                     self.assertEqual(epoch_millis, expected_epoch_millis)
                     creation_times_found.append(epoch_millis)
 
diff --git a/paimon-python/pypaimon/tests/write/row_kind_nullability_test.py 
b/paimon-python/pypaimon/tests/write/row_kind_nullability_test.py
new file mode 100644
index 0000000000..62652600f1
--- /dev/null
+++ b/paimon-python/pypaimon/tests/write/row_kind_nullability_test.py
@@ -0,0 +1,295 @@
+# 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.
+
+"""Filtered events retain Java's row-kind-specific top-level NULL checks."""
+
+from types import SimpleNamespace
+from unittest.mock import Mock, patch
+
+import pyarrow as pa
+import pytest
+
+from pypaimon import CatalogFactory, Schema
+from pypaimon.read.native_plan import native_runtime_available
+from pypaimon.table.row.generic_row import GenericRow
+from pypaimon.table.row.row_kind import RowKind
+from pypaimon.write.native_write import NativeTableWrite
+from pypaimon.write.row_kind import skip_write_row
+
+
+pytestmark = [pytest.mark.python_write, pytest.mark.python_commit]
+_NATIVE = pytest.mark.skipif(not native_runtime_available(), 
reason='pypaimon-rust not installed')
+_ENGINES = [False, pytest.param(True, marks=[pytest.mark.native_plan, 
_NATIVE])]
+
+
+def _table(tmp_path, **options):
+    schema = pa.schema([pa.field('id', pa.int32(), nullable=False),
+                        pa.field('op', pa.string()),
+                        pa.field('seq', pa.int64(), nullable=False),
+                        pa.field('value', pa.int32(), nullable=False)])
+    config = {'bucket': '1', 'rowkind.field': 'op', 'ignore-delete': 'true',
+              'sequence.field': 'seq', 'write.native.enabled': 'false',
+              'commit.native.enabled': 'false', 'read.native.enabled': 'false',
+              'scan.native-plan.enabled': 'false'}
+    config.update(options)
+    catalog = CatalogFactory.create({'warehouse': str(tmp_path)})
+    catalog.create_database('default', True)
+    catalog.create_table('default.t', Schema.from_pyarrow_schema(
+        schema, primary_keys=['id'], options=config), False)
+    return catalog.get_table('default.t'), schema
+
+
+def _write(writer, table, schema, rows, input_):
+    if input_ == 'row':
+        for row in rows:
+            writer.write_row(GenericRow([row[field.name] for field in 
table.fields], table.fields))
+    elif input_ == 'batch':
+        writer.write_arrow_batch(pa.RecordBatch.from_pylist(rows, 
schema=schema))
+    elif input_ == 'pandas':
+        import pandas as pd
+        writer.write_pandas(pd.DataFrame.from_records(rows, 
columns=schema.names))
+    else:
+        writer.write_arrow(pa.Table.from_pylist(rows, schema=schema))
+
+
[email protected]('input_', ['row', 'batch', 'table', 'pandas'])
[email protected]('stream', [False, True], ids=['batch', 'stream'])
[email protected]('kind,field', [('-D', 'id'), ('-D', 'seq'), ('-U', 
'value'), ('+I', 'value')])
+def test_filtered_events_check_required_nulls_before_routing(tmp_path, input_, 
stream, kind, field):
+    table, schema = _table(tmp_path)
+    builder = table.new_stream_write_builder() if stream else 
table.new_batch_write_builder()
+    writer = builder.new_write()
+    invalid = dict(id=1, op=kind, seq=1, value=10)
+    invalid[field] = None
+    try:
+        with pytest.raises(ValueError, match=r'non-null column\(%s\)' % 
field), \
+                patch.object(writer.row_key_extractor, 
'extract_partition_bucket_groups',
+                             side_effect=AssertionError('invalid event reached 
bucket routing')):
+            _write(writer, table, schema, [invalid], input_)
+        assert not writer.file_store_write.data_writers
+    finally:
+        writer.close()
+
+
[email protected]('input_', ['row', 'batch', 'table', 'pandas'])
[email protected]('stream', [False, True], ids=['batch', 'stream'])
[email protected]('native', _ENGINES, ids=['python', 'native'])
+def test_ignored_delete_may_omit_non_key_not_null_values(tmp_path, input_, 
stream, native):
+    table, schema = _table(tmp_path, **{'write.native.enabled': 
str(native).lower()})
+    builder = table.new_stream_write_builder() if stream else 
table.new_batch_write_builder()
+    writer, commit = builder.new_write(), builder.new_commit()
+    expected = dict(id=2, op='+I', seq=2, value=42)
+    try:
+        assert isinstance(writer, NativeTableWrite) == native
+        _write(writer, table, schema, [dict(id=1, op='-D', seq=1, value=None), 
expected], input_)
+        if native:
+            assert writer._python_writer is None
+        messages = writer.prepare_commit(1) if stream else 
writer.prepare_commit()
+        commit.commit(messages, 1) if stream else commit.commit(messages)
+    finally:
+        writer.close()
+        commit.close()
+    builder = table.new_read_builder()
+    assert 
builder.new_read().to_arrow(builder.new_scan().plan().splits()).to_pylist() == 
[expected]
+
+
[email protected]('input_', ['batch', 'table'])
[email protected]('stream', [False, True], ids=['batch', 'stream'])
[email protected]('native', _ENGINES, ids=['python', 'native'])
+def test_large_string_row_kinds_filter_before_casting_payloads(tmp_path, 
input_, stream, native):
+    table, schema = _table(tmp_path, **{'write.native.enabled': 
str(native).lower()})
+    transport = pa.schema([field.with_type(pa.large_string()) if field.name == 
'op' else field
+                           for field in schema])
+    builder = table.new_stream_write_builder() if stream else 
table.new_batch_write_builder()
+    writer, commit = builder.new_write(), builder.new_commit()
+    expected = dict(id=2, op='+I', seq=2, value=42)
+    try:
+        assert isinstance(writer, NativeTableWrite) == native
+        # A whole-table STRING cast checks unrelated top-level NOT NULL
+        # fields, and would reject this DELETE before its row kind is applied.
+        _write(writer, table, transport, [dict(id=1, op='-D', seq=1, 
value=None), expected], input_)
+        if native:
+            assert writer._python_writer is None
+        messages = writer.prepare_commit(1) if stream else 
writer.prepare_commit()
+        commit.commit(messages, 1) if stream else commit.commit(messages)
+    finally:
+        writer.close()
+        commit.close()
+    builder = table.new_read_builder()
+    assert 
builder.new_read().to_arrow(builder.new_scan().plan().splits()).to_pylist() == 
[expected]
+
+
[email protected]_plan
+@_NATIVE
[email protected]('input_', ['row', 'batch', 'table', 'pandas'])
[email protected]('stream', [False, True], ids=['batch', 'stream'])
[email protected]('kind,field', [('-D', 'id'), ('-D', 'seq'), ('-U', 
'value'), ('+I', 'value')])
+def test_native_filtered_events_keep_required_null_checks(tmp_path, input_, 
stream, kind, field):
+    table, schema = _table(tmp_path, **{'write.native.enabled': 'true'})
+    builder = table.new_stream_write_builder() if stream else 
table.new_batch_write_builder()
+    writer = builder.new_write()
+    invalid = dict(id=1, op=kind, seq=1, value=10)
+    invalid[field] = None
+    try:
+        assert isinstance(writer, NativeTableWrite)
+        with pytest.raises(ValueError, match=r'non-null column\(%s\)' % field):
+            _write(writer, table, schema, [invalid], input_)
+        assert writer._python_writer is None
+        assert not list(tmp_path.rglob('*.parquet'))
+        assert table.snapshot_manager().get_latest_snapshot() is None
+    finally:
+        writer.close()
+
+
+def 
test_native_pandas_facade_preserves_null_payload_for_core_filtering(tmp_path):
+    import pandas as pd
+
+    table, schema = _table(tmp_path)
+    native = Mock()
+    writer = NativeTableWrite(table, 'user', None, False, native)
+    rows = [dict(id=1, op='-D', seq=1, value=None), dict(id=2, op='+I', seq=2, 
value=42)]
+    writer.write_pandas(pd.DataFrame.from_records(rows, columns=schema.names))
+    native.write_arrow.assert_called_once()
+    transported = native.write_arrow.call_args[0][0]
+    assert transported.to_pylist() == rows
+    assert transported.schema.field('value').nullable
+    assert not writer._schema.field('value').nullable
+    assert writer._python_writer is None
+
+
[email protected]('primary_key', [False, True], ids=['append', 
'pk-without-filter'])
+def test_pandas_non_filtering_writers_keep_not_null_validation(tmp_path, 
primary_key):
+    import pandas as pd
+
+    catalog = CatalogFactory.create({'warehouse': str(tmp_path)})
+    catalog.create_database('default', True)
+    schema = pa.schema([pa.field('id', pa.int32(), nullable=False),
+                        pa.field('value', pa.int32(), nullable=False)])
+    catalog.create_table('default.t', Schema.from_pyarrow_schema(
+        schema, primary_keys=['id'] if primary_key else [], options={
+            'bucket': '1', 'write.native.enabled': 'false'}), False)
+    table = catalog.get_table('default.t')
+    data = pd.DataFrame([dict(id=1, value=None)])
+    writer = table.new_batch_write_builder().new_write()
+    facade = NativeTableWrite(table, 'native-user', None, False, Mock())
+    try:
+        for candidate in [writer, facade]:
+            with pytest.raises(ValueError, match='non-nullable'):
+                candidate.write_pandas(data)
+        assert not writer.file_store_write.data_writers
+        facade._native_writer.write_arrow.assert_not_called()
+    finally:
+        writer.close()
+
+
+def 
test_delete_null_exception_is_limited_to_non_cross_partition_deduplicate(tmp_path):
+    table, _ = _table(tmp_path)
+    values = dict(id=1, op='-D', seq=1, value=None)
+    assert skip_write_row(table, values)
+    for variant, cross_partition in [(table.copy({'merge-engine': 
'first-row'}), False), (table, True)]:
+        with patch.object(variant, 'cross_partition_update', cross_partition), 
\
+                pytest.raises(ValueError, match=r'non-null column\(value\)'):
+            skip_write_row(variant, values)
+
+
[email protected]('kind,field', [
+    (RowKind.DELETE, 'id'), (RowKind.DELETE, 'seq'), (RowKind.UPDATE_BEFORE, 
'value')])
+def test_internal_row_kinds_check_required_nulls_before_filtering(tmp_path, 
kind, field):
+    table, _ = _table(tmp_path, **{'rowkind.field': None})
+    writer = table.new_batch_write_builder().new_write()
+    values = dict(id=1, op=None, seq=1, value=10)
+    values[field] = None
+    try:
+        with pytest.raises(ValueError, match=r'non-null column\(%s\)' % field):
+            writer.write_row(GenericRow([values[field.name] for field in 
table.fields], table.fields, kind))
+        assert not writer.file_store_write.data_writers
+    finally:
+        writer.close()
+
+
+def test_native_bridge_leaves_nested_value_validation_to_core(tmp_path):
+    transport = pa.schema([('items', pa.list_(pa.int32()))])
+    declared = pa.schema([('items', pa.list_(pa.field('element', pa.int32(), 
nullable=False)))])
+    data = pa.RecordBatch.from_arrays([pa.array([[None]], 
type=transport.field(0).type)], schema=transport)
+    writer = NativeTableWrite.__new__(NativeTableWrite)
+    writer._table_schema = writer._schema = declared
+    writer._write_cols = None
+    with patch('pypaimon.write.native_write.normalize_arrow_write_nullability',
+               side_effect=AssertionError('bridge inspected values before core 
filtering'), create=True), \
+            patch('pypaimon.write.native_write.normalize_arrow_strings',
+                  side_effect=AssertionError('bridge cast values before core 
filtering'), create=True):
+        assert writer._prepare_native_arrow_data(data) is data
+
+
+def test_schema_only_writer_keeps_strict_nested_validation():
+    from pypaimon.write.table_write import TableWrite
+
+    writer = TableWrite.__new__(TableWrite)
+    writer.file_store_write = SimpleNamespace(write_cols=None)
+    writer.table_pyarrow_schema = pa.schema([
+        ('items', pa.list_(pa.field('element', pa.int32(), nullable=False)))])
+    data = pa.table({'items': [[None]]}, schema=pa.schema([('items', 
pa.list_(pa.int32()))]))
+    with pytest.raises(ValueError, match='consistent'):
+        writer._prepare_arrow_data(data)
+
+
+def test_python_filters_before_nested_normalization(tmp_path):
+    from pypaimon.write.table_write import TableWrite
+
+    table, _ = _table(tmp_path)
+    writer = table.new_batch_write_builder().new_write()
+    declared = pa.schema([field if field.name != 'value' else field.with_type(
+        pa.list_(pa.field('element', pa.int32(), nullable=False))) for field 
in writer.table_pyarrow_schema])
+    # Use a real table for options and routing, but a schema-only writer facade
+    # to observe normalization before any actual Parquet output is attempted.
+    writer.table_pyarrow_schema = declared
+    transport = pa.schema([field if field.name != 'value' else 
field.with_type(pa.list_(pa.int32()))
+                           for field in declared])
+    data = pa.table({'id': [1, 2], 'op': ['-D', '+I'], 'seq': [1, 2], 'value': 
[[None], [42]]}, schema=transport)
+
+    def normalize(selected, expected):
+        assert selected['id'].to_pylist() == [2]
+        assert expected == declared
+        return selected.cast(expected)
+
+    try:
+        with 
patch('pypaimon.write.table_write.normalize_arrow_write_nullability', 
side_effect=normalize):
+            selected = TableWrite._prepare_arrow_data(writer, data)
+        assert selected.to_pydict() == {'id': [2], 'op': ['+I'], 'seq': [2], 
'value': [[42]]}
+    finally:
+        writer.close()
+
+
[email protected]('ignored', [False, True])
+def 
test_python_retained_nested_schema_stays_strict_without_native_extension(tmp_path,
 ignored):
+    table, _ = _table(tmp_path)
+    writer = table.new_batch_write_builder().new_write()
+    writer.table_pyarrow_schema = pa.schema([field if field.name != 'value' 
else field.with_type(
+        pa.list_(pa.field('element', pa.int32(), nullable=False))) for field 
in writer.table_pyarrow_schema])
+    transport = pa.schema([field if field.name != 'value' else 
field.with_type(pa.list_(pa.int32()))
+                           for field in writer.table_pyarrow_schema])
+    data = pa.table({'id': [1], 'op': ['-D' if ignored else '+I'], 'seq': [1], 
'value': [[None]]}, schema=transport)
+    try:
+        with patch('pypaimon.read.native_plan.native_runtime_available', 
return_value=False):
+            if ignored:
+                assert writer._prepare_arrow_data(data).num_rows == 0
+            else:
+                with pytest.raises(ValueError, match='consistent'):
+                    writer._prepare_arrow_data(data)
+        assert not writer.file_store_write.data_writers
+    finally:
+        writer.close()
diff --git a/paimon-python/pypaimon/utils/file_store_path_factory.py 
b/paimon-python/pypaimon/utils/file_store_path_factory.py
index c1c956e560..d5920e1cdb 100644
--- a/paimon-python/pypaimon/utils/file_store_path_factory.py
+++ b/paimon-python/pypaimon/utils/file_store_path_factory.py
@@ -163,7 +163,7 @@ class FileStorePathFactory:
         for i, value in enumerate(partition):
             data_type = self.partition_types[i] if self.partition_types is not 
None else None
             type_name = str(data_type).split('(', 1)[0].split()[0]
-            if value is not None and type_name in ('BINARY', 'VARBINARY', 
'BYTES') and not self.legacy_partition_name:
+            if value is not None and type_name in ('BINARY', 'VARBINARY', 
'BYTES'):
                 value = _decode_java_utf8(value)
             if value is not None and _is_ltz_type(str(data_type).upper()):
                 # Legacy Timestamp.toString() uses UTC fields. Java's 
non-legacy
@@ -189,7 +189,9 @@ class FileStorePathFactory:
             elif self.legacy_partition_name and type_name.startswith('TIME'):
                 text = str(((value.hour * 60 + value.minute) * 60 + 
value.second) * 1000
                            + value.microsecond // 1000)
-            elif type_name in ('BINARY', 'VARBINARY', 'BYTES') and not 
self.legacy_partition_name:
+            elif type_name in ('BINARY', 'VARBINARY', 'BYTES'):
+                # Java uses the binary-to-string cast even in legacy mode:
+                # byte[].toString contains a fresh allocation identity.
                 text = value
             elif data_type is not None and not _is_unsupported(data_type):
                 text = cast_value_to_string(value, data_type)
diff --git a/paimon-python/pypaimon/write/file_store_commit.py 
b/paimon-python/pypaimon/write/file_store_commit.py
index adc230e51a..ac07c73312 100644
--- a/paimon-python/pypaimon/write/file_store_commit.py
+++ b/paimon-python/pypaimon/write/file_store_commit.py
@@ -1079,31 +1079,30 @@ class FileStoreCommit:
             partition_spec = dict(zip(self.table.partition_keys, 
path_factory._canonical_partition(partition_value)))
             partition_key = GenericRowSerializer.to_bytes(entry.partition)
 
+            file_meta = entry.file
+            file_creation_time = (file_meta.creation_time_epoch_millis()
+                                  if file_meta.creation_time is not None else 
int(time.time() * 1000))
+
             if partition_key not in partition_stats:
                 partition_stats[partition_key] = {
                     'partition_spec': partition_spec,
                     'record_count': 0,
                     'file_count': 0,
                     'file_size_in_bytes': 0,
-                    'last_file_creation_time': 0,
+                    # Java starts with the first entry, including times before
+                    # the epoch, rather than clamping negative times to zero.
+                    'last_file_creation_time': file_creation_time,
                     'total_buckets': entry.total_buckets
                 }
             partition_stats[partition_key]['total_buckets'] = (
                 entry.total_buckets)
 
             # Following Java implementation: PartitionEntry.fromDataFile()
-            file_meta = entry.file
             # Extract actual file metadata (following Java DataFileMeta 
pattern)
             record_count = file_meta.row_count if entry.kind == 0 else 
file_meta.row_count * -1
             file_size_in_bytes = file_meta.file_size if entry.kind == 0 else 
file_meta.file_size * -1
             file_count = 1 if entry.kind == 0 else -1
 
-            # Use epoch millis
-            if file_meta.creation_time:
-                file_creation_time = file_meta.creation_time_epoch_millis()
-            else:
-                file_creation_time = int(time.time() * 1000)
-
             # Accumulate statistics (following Java PartitionEntry.merge() 
logic)
             partition_stats[partition_key]['record_count'] += record_count
             partition_stats[partition_key]['file_size_in_bytes'] += 
file_size_in_bytes
diff --git a/paimon-python/pypaimon/write/native_commit.py 
b/paimon-python/pypaimon/write/native_commit.py
index d3cb5235dd..a1731a528a 100644
--- a/paimon-python/pypaimon/write/native_commit.py
+++ b/paimon-python/pypaimon/write/native_commit.py
@@ -51,13 +51,6 @@ def create_native_commit(table, commit_user, 
overwrite_partition=None, *, batch=
     """Return a native committer only when its publication protocol matches 
Python."""
     if not _rest_catalog_supported(table) or not native_runtime_available():
         return None
-    from pypaimon.common.options.core_options import CoreOptions
-    from pypaimon.schema.data_types import PyarrowFieldParser
-    from pypaimon.write.native_write import _native_partition_types_supported
-    if not _native_partition_types_supported(
-            PyarrowFieldParser.from_paimon_schema(table.table_schema.fields), 
table.partition_keys,
-            
table.options.options.get(CoreOptions.PARTITION_GENERATE_LEGACY_NAME)):
-        return None
     native_table = create_native_write_table(table)
     if native_table is None:
         return None
@@ -94,9 +87,7 @@ def _rest_catalog_supported(table):
             and environment.supports_version_management
             and _rest_source_uuid(table) is not None
             and context is not None
-            and context.options is not None
-            and all(getattr(context, attr, None) is None for attr in (
-                'hadoop_conf', 'prefer_io_loader', 'fallback_io_loader')))
+            and context.options is not None)
 
 
 def _rest_source_uuid(table):
diff --git a/paimon-python/pypaimon/write/native_merge_into.py 
b/paimon-python/pypaimon/write/native_merge_into.py
index 717950763f..c9999217ca 100644
--- a/paimon-python/pypaimon/write/native_merge_into.py
+++ b/paimon-python/pypaimon/write/native_merge_into.py
@@ -19,15 +19,12 @@
 
 import pyarrow as pa
 
-from pypaimon.common.options.core_options import CoreOptions
-
 from pypaimon.snapshot.snapshot import BATCH_COMMIT_IDENTIFIER
 from pypaimon.write.native_commit import create_native_write_table, 
from_native_commit_messages
 from pypaimon.write.native_update import (
     _dictionary_value_type, _native_row_id_table, 
_native_update_columns_supported,
     _native_update_formats_supported, _supported_upsert_key_type,
 )
-from pypaimon.write.native_write import _native_partition_types_supported
 
 
 def _callable_assignment(function, on_pairs):
@@ -76,10 +73,6 @@ def create_native_merge_into(table, source, on, matched, 
not_matched, commit_use
             return None
         from pypaimon.schema.data_types import PyarrowFieldParser
         source_schema = 
PyarrowFieldParser.from_paimon_schema(source.table_schema.fields)
-        if not _native_partition_types_supported(
-                source_schema, source.partition_keys,
-                
source.options.options.get(CoreOptions.PARTITION_GENERATE_LEGACY_NAME)):
-            return None
         if source.options.query_auth_enabled:
             from pypaimon.read.native_plan import 
_native_query_auth_supported, _native_table
             from pypaimon.write.native_commit import _rest_catalog_supported
diff --git a/paimon-python/pypaimon/write/native_update.py 
b/paimon-python/pypaimon/write/native_update.py
index a7825c13dd..dd1bb33b83 100644
--- a/paimon-python/pypaimon/write/native_update.py
+++ b/paimon-python/pypaimon/write/native_update.py
@@ -25,14 +25,13 @@ import pyarrow.compute as pc
 from pypaimon.read.native_plan import _native_query_auth_supported, 
native_runtime_available
 from pypaimon.schema.arrow_schema import arrow_schemas_compatible
 from pypaimon.schema.data_types import MapType, PyarrowFieldParser, RowType, 
VectorType, is_blob_file_field
-from pypaimon.common.options.core_options import ChangelogProducer, CoreOptions
+from pypaimon.common.options.core_options import ChangelogProducer
 from pypaimon.snapshot.snapshot import BATCH_COMMIT_IDENTIFIER
 from pypaimon.snapshot.time_travel_util import SCAN_KEYS
 from pypaimon.table.file_store_table import FileStoreTable
 from pypaimon.write.native_commit import (
     create_native_write_table, from_native_commit_messages,
 )
-from pypaimon.write.native_write import _native_partition_types_supported
 from pypaimon.write.table_update_by_row_id import TableUpdateByRowId
 from pypaimon.write.row_utils import _contains_blob_value, value_for_arrow
 
@@ -53,10 +52,6 @@ def _native_update_table(table, *, allow_changelog=False):
             or not native_runtime_available()
             or not _native_query_auth_supported(table)):
         return None
-    schema = PyarrowFieldParser.from_paimon_schema(table.table_schema.fields)
-    if not _native_partition_types_supported(
-            schema, table.partition_keys, 
table.options.options.get(CoreOptions.PARTITION_GENERATE_LEGACY_NAME)):
-        return None
     return create_native_write_table(table)
 
 
diff --git a/paimon-python/pypaimon/write/native_write.py 
b/paimon-python/pypaimon/write/native_write.py
index ce74056e24..ef2efe179e 100644
--- a/paimon-python/pypaimon/write/native_write.py
+++ b/paimon-python/pypaimon/write/native_write.py
@@ -18,10 +18,9 @@
 
 import pyarrow as pa
 
-from pypaimon.common.options.core_options import ChangelogProducer, CoreOptions
+from pypaimon.common.options.core_options import ChangelogProducer
 from pypaimon.read.native_plan import native_runtime_available
-from pypaimon.schema.arrow_schema import (
-    arrow_schemas_compatible, normalize_arrow_strings, 
normalize_arrow_write_nullability)
+from pypaimon.schema.arrow_schema import arrow_schemas_compatible
 from pypaimon.schema.data_types import PyarrowFieldParser, is_blob_file_field
 from pypaimon.table.bucket_mode import BucketMode
 from pypaimon.write.native_commit import (
@@ -30,20 +29,9 @@ from pypaimon.write.native_commit import (
 from pypaimon.write.row_utils import require_columns, row_to_named_values, 
row_values_to_arrow_table
 
 
-def _native_partition_types_supported(schema, partition_keys, 
legacy_partition_name=True):
-    """Partition keys which Rust can encode and use to locate existing 
files."""
-    # Java's legacy byte[].toString() contains an allocation identity, so 
binary
-    # partitions only have portable names with the cast-based option.
-    return not legacy_partition_name or not any(
-        pa.types.is_binary(data_type) or pa.types.is_large_binary(data_type)
-        or pa.types.is_fixed_size_binary(data_type)
-        for data_type in (schema.field(name).type for name in partition_keys))
-
-
 def create_native_write(table, commit_user, static_partition=None, 
stream=False,
                         *, fixed_bucket=False, bucket_plan=None, 
restore_snapshot_id=None):
     """Return a native writer if the table can use the filesystem write 
path."""
-    schema = PyarrowFieldParser.from_paimon_schema(table.table_schema.fields)
     # DV tables flush normal level-0 files using the configured merge engine.
     # Batch visibility is controlled by deletion-vectors.merge-on-read.
     if (not native_runtime_available()
@@ -57,8 +45,6 @@ def create_native_write(table, commit_user, 
static_partition=None, stream=False,
                 and table.options.changelog_producer() == 
ChangelogProducer.INPUT
                 and (stream or static_partition is None))
             or table.options.file_format() != 'parquet'
-            or not _native_partition_types_supported(
-                schema, table.partition_keys, 
table.options.options.get(CoreOptions.PARTITION_GENERATE_LEGACY_NAME))
             or table.options.video_frame_fields()):
         return None
     native_table = create_native_write_table(table)
@@ -226,10 +212,10 @@ class NativeTableWrite:
         self._native_writer.write_arrow(data, bucket)
 
     def _prepare_native_arrow_data(self, data):
-        data = normalize_arrow_strings(data)
-        target = self._table_schema if data.schema.names == 
self._table_schema.names else self._schema
-        data = normalize_arrow_write_nullability(data, target)
-        compatible = dict(check_top_level_nullability=False, 
allow_binary_compatibility=True)
+        # Core resolves row kinds and filters ignored events before inspecting
+        # payload values. This facade checks only the logical Arrow schema.
+        compatible = dict(check_top_level_nullability=False, 
allow_binary_compatibility=True,
+                          check_nested_nullability=False)
         if not (arrow_schemas_compatible(data.schema, self._schema, 
**compatible)
                 or (self._write_cols is not None
                     and arrow_schemas_compatible(data.schema, 
self._table_schema, **compatible))):
@@ -244,7 +230,9 @@ class NativeTableWrite:
     def write_pandas(self, dataframe):
         if self._python_writer is not None:
             return self._python_writer.write_pandas(dataframe)
-        self.write_arrow_batch(pa.RecordBatch.from_pandas(dataframe, 
schema=self._schema))
+        from pypaimon.write.table_write import _pandas_write_schema
+        transport = _pandas_write_schema(self.table, self._schema)
+        self.write_arrow_batch(pa.RecordBatch.from_pandas(dataframe, 
schema=transport))
 
     def write_ray(self, dataset, overwrite=False, concurrency=None, 
ray_remote_args=None,
                   hash_fixed_precluster='auto', static_partition=None):
diff --git a/paimon-python/pypaimon/write/row_kind.py 
b/paimon-python/pypaimon/write/row_kind.py
index df279aadf6..ad66bf0cbc 100644
--- a/paimon-python/pypaimon/write/row_kind.py
+++ b/paimon-python/pypaimon/write/row_kind.py
@@ -21,7 +21,7 @@ from typing import List, Union
 import pyarrow as pa
 import pyarrow.compute as pc
 
-from pypaimon.common.options.core_options import CoreOptions
+from pypaimon.common.options.core_options import CoreOptions, MergeEngine
 from pypaimon.table.row.row_kind import RowKind
 from pypaimon.table.special_fields import SpecialFields
 
@@ -41,7 +41,7 @@ def row_kinds(
             '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):
+    if not (pa.types.is_string(column.type) or 
pa.types.is_large_string(column.type)):
         raise ValueError('rowkind.field column must be a string')
     kinds: List[int] = []
     for value in column.to_pylist():
@@ -63,6 +63,15 @@ 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
+    # Java checks top-level NULLs before its RowKindFilter. A DELETE in a
+    # non-cross-partition deduplicate table only requires non-NULL keys and
+    # sequence fields; UPDATE_BEFORE retains all declared NOT NULL fields.
+    for name, check_delete in _not_null_fields(table):
+        invalid = data.column(name).is_null()
+        if not check_delete:
+            invalid = pc.and_(invalid, pc.not_equal(kinds, 
RowKind.DELETE.value))
+        if pc.any(invalid).as_py():
+            raise ValueError('Cannot write null to non-null column(%s)' % name)
     ignored = [RowKind.UPDATE_BEFORE.value]
     if ignore_delete:
         ignored.append(RowKind.DELETE.value)
@@ -100,9 +109,23 @@ def skip_write_row(table, values, row_kind=RowKind.INSERT):
         if row_kind_field_name is None
         else RowKind.from_string(values[row_kind_field_name]).value
     )
+    if options.ignore_delete() or str(raw.get('ignore-update-before', 
'false')).lower() == 'true':
+        for name, check_delete in _not_null_fields(table):
+            if (check_delete or kind != RowKind.DELETE.value) and values[name] 
is None:
+                raise ValueError('Cannot write null to non-null column(%s)' % 
name)
     return _is_filtered(kind, options.ignore_delete(),
                         str(raw.get('ignore-update-before', 'false')).lower() 
== 'true')
 
 
+def _not_null_fields(table):
+    """Fields Java checks before filtering, with its DELETE-only exception."""
+    delete_fields = None
+    if (table.options.merge_engine() == MergeEngine.DEDUPLICATE
+            and not table.cross_partition_update):
+        delete_fields = set(table.table_schema.primary_keys + 
table.options.sequence_field())
+    return [(field.name, delete_fields is None or field.name in delete_fields)
+            for field in table.fields if not field.type.nullable]
+
+
 def _is_filtered(kind, ignore_delete, ignore_before):
     return (ignore_delete and kind in (1, 3)) or (ignore_before and kind == 1)
diff --git a/paimon-python/pypaimon/write/table_write.py 
b/paimon-python/pypaimon/write/table_write.py
index 65c9996326..6c02c33a0b 100644
--- a/paimon-python/pypaimon/write/table_write.py
+++ b/paimon-python/pypaimon/write/table_write.py
@@ -36,6 +36,20 @@ if TYPE_CHECKING:
     from ray.data import Dataset
 
 
+def _pandas_write_schema(table, schema):
+    """Keep ignored events available for the normal row-kind NULL checks."""
+    if table is None or not table.is_primary_key_table:
+        return schema
+    options = table.options
+    raw = options.options.to_map()
+    if (raw.get('rowkind.field') is None
+            or not (options.ignore_delete() or 
str(raw.get('ignore-update-before', 'false')).lower() == 'true')):
+        return schema
+    # Pandas checks top-level NOT NULL before Arrow reaches the writer. Only
+    # relax the transport; the declared schema and nested fields stay intact.
+    return pa.schema([field.with_nullable(True) for field in schema], 
metadata=schema.metadata)
+
+
 def _write_ray(writer, dataset, overwrite=False, concurrency=None, 
ray_remote_args=None,
                hash_fixed_precluster='auto', static_partition=None):
     """Share Ray coordination without constructing another local data 
writer."""
@@ -205,7 +219,8 @@ class TableWrite:
             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)
+        transport = _pandas_write_schema(getattr(self, 'table', None), 
pa_schema)
+        record_batch = pa.RecordBatch.from_pandas(dataframe, schema=transport)
         return self.write_arrow_batch(record_batch)
 
     def with_write_type(self, write_cols: List[str]):
@@ -251,7 +266,7 @@ class TableWrite:
     ) -> None:
         """
         Write a Ray Dataset to Paimon table.
-        
+
         Args:
             dataset: Ray Dataset to write. This is a distributed data 
collection
                 from Ray Data (ray.data.Dataset).
@@ -328,18 +343,26 @@ class TableWrite:
             return data
         data = self._normalize_arrow_write_schema(data)
         from pypaimon.write.row_kind import filter_write_batch
-        return filter_write_batch(table, data)
-
-    def _prepare_arrow_schema(self, data):
+        data = filter_write_batch(table, data)
+        if not data.num_rows:
+            return data
         data = normalize_arrow_strings(data)
         target = self.table_pyarrow_schema
         if data.schema.names != target.names and 
self.file_store_write.write_cols is not None:
             target = 
self._write_cols_pyarrow_schema(self.file_store_write.write_cols)
+        # Validate only surviving values. Keep the input transport metadata:
+        # PyArrow otherwise rejects hidden NULL children of NULL ROW parents.
         validated = normalize_arrow_write_nullability(data, target)
         self._validate_pyarrow_schema(validated.schema)
-        # Core checks only visible NOT NULL children. PyArrow's Parquet writer
-        # also rejects hidden NULL children under NULL ROW parents when their
-        # Arrow field is nonnullable, so retain the input's transport metadata.
+        return data
+
+    def _prepare_arrow_schema(self, data):
+        # Legacy schema-only callers have no row-kind options to evaluate.
+        # A real writer must defer casts until ignored events have been 
removed.
+        if getattr(self, 'table', None) is None:
+            data = normalize_arrow_strings(data)
+        self._validate_pyarrow_schema(
+            data.schema, check_nested_nullability=getattr(self, 'table', None) 
is None)
         return data
 
     def _normalize_arrow_write_schema(self, data):
@@ -358,22 +381,25 @@ class TableWrite:
             return pa.RecordBatch.from_arrays(data.columns, schema=schema)
         return pa.Table.from_arrays(data.columns, schema=schema)
 
-    def _validate_pyarrow_schema(self, data_schema: pa.Schema):
-        if self._is_compatible_pyarrow_schema(data_schema, 
self.table_pyarrow_schema):
+    def _validate_pyarrow_schema(self, data_schema: pa.Schema, *, 
check_nested_nullability=True):
+        if self._is_compatible_pyarrow_schema(
+                data_schema, self.table_pyarrow_schema, 
check_nested_nullability=check_nested_nullability):
             return
 
         write_cols = self.file_store_write.write_cols
         if write_cols is not None:
             write_cols_schema = self._write_cols_pyarrow_schema(write_cols)
-            if self._is_compatible_pyarrow_schema(data_schema, 
write_cols_schema):
+            if self._is_compatible_pyarrow_schema(
+                    data_schema, write_cols_schema, 
check_nested_nullability=check_nested_nullability):
                 return
 
         self._raise_inconsistent_schema(data_schema)
 
     def _is_compatible_pyarrow_schema(
-            self, data_schema: pa.Schema, expected_schema: pa.Schema) -> bool:
+            self, data_schema: pa.Schema, expected_schema: pa.Schema, *, 
check_nested_nullability=True) -> bool:
         return arrow_schemas_compatible(
-            data_schema, expected_schema, check_top_level_nullability=False, 
allow_binary_compatibility=True)
+            data_schema, expected_schema, check_top_level_nullability=False,
+            allow_binary_compatibility=True, 
check_nested_nullability=check_nested_nullability)
 
     def _write_cols_pyarrow_schema(self, write_cols: List[str]) -> pa.Schema:
         table_fields = {
diff --git a/paimon-python/pypaimon/write/writer/blob_writer.py 
b/paimon-python/pypaimon/write/writer/blob_writer.py
index 166144e5c6..47ba95778c 100644
--- a/paimon-python/pypaimon/write/writer/blob_writer.py
+++ b/paimon-python/pypaimon/write/writer/blob_writer.py
@@ -268,7 +268,7 @@ class BlobWriter(AppendOnlyDataWriter):
             schema_id=self.table.table_schema.id,
             level=0,
             extra_files=[],
-            creation_time=Timestamp.now(),
+            creation_time=Timestamp.now().to_millis_timestamp(),
             delete_row_count=0,
             file_source=0,  # FileSource.APPEND = 0
             value_stats_cols=None,
diff --git a/paimon-python/pypaimon/write/writer/composite_data_writer.py 
b/paimon-python/pypaimon/write/writer/composite_data_writer.py
index a9d5c66e6f..7887582cd9 100644
--- a/paimon-python/pypaimon/write/writer/composite_data_writer.py
+++ b/paimon-python/pypaimon/write/writer/composite_data_writer.py
@@ -169,7 +169,7 @@ class CompositeDataWriter(DataWriter):
                 schema_id=self.table.table_schema.id,
                 level=0,
                 extra_files=[],
-                creation_time=Timestamp.now(),
+                creation_time=Timestamp.now().to_millis_timestamp(),
                 delete_row_count=0,
                 file_source=0,
                 value_stats_cols=[column.name for column in stats_columns],
diff --git a/paimon-python/pypaimon/write/writer/data_writer.py 
b/paimon-python/pypaimon/write/writer/data_writer.py
index 98cdd66d2b..f2a68b1218 100644
--- a/paimon-python/pypaimon/write/writer/data_writer.py
+++ b/paimon-python/pypaimon/write/writer/data_writer.py
@@ -323,7 +323,6 @@ class DataWriter(ABC):
                 min_sequence_number=statistics['min_seq'],
                 max_sequence_number=statistics['max_seq'],
                 extra_files=extra_files,
-                creation_time=statistics['creation_time'],
                 delete_row_count=self._count_delete_rows(data),
             )
 
@@ -382,10 +381,9 @@ class DataWriter(ABC):
         else:
             min_seq = self.sequence_generator.start
             max_seq = self.sequence_generator.current
-        creation_time = Timestamp.now()
         return dict(min_key=min_key, max_key=max_key, key_stats=key_stats,
                     value_stats=value_stats, min_seq=min_seq, max_seq=max_seq,
-                    creation_time=creation_time, 
value_stats_enabled=value_stats_enabled)
+                    value_stats_enabled=value_stats_enabled)
 
     def _writes_changelog_before_merge(self):
         return False
@@ -424,7 +422,7 @@ class DataWriter(ABC):
             max_sequence_number=max_sequence_number,
             schema_id=self.table.table_schema.id, level=0,
             extra_files=extra_files if extra_files is not None else [],
-            creation_time=creation_time if creation_time is not None else 
Timestamp.now(),
+            creation_time=creation_time if creation_time is not None else 
Timestamp.now().to_millis_timestamp(),
             delete_row_count=delete_row_count, file_source=0,
             value_stats_cols=None if self.options.metadata_stats_enabled() 
else [],
             external_path=file_path if self.external_path_provider is not None 
else None,
@@ -468,7 +466,7 @@ class DataWriter(ABC):
         return pa.Table.from_arrays(columns, schema=pa.schema(fields))
 
     def _write_changelog_file(self, data, min_key, max_key, key_stats, 
value_stats,
-                              min_seq, max_seq, creation_time,
+                              min_seq, max_seq,
                               value_stats_enabled, is_external) -> 
DataFileMeta:
         """Write a complete changelog file and return its metadata.
 
@@ -502,7 +500,7 @@ class DataWriter(ABC):
                 schema_id=self.table.table_schema.id,
                 level=0,
                 extra_files=[],
-                creation_time=creation_time,
+                creation_time=Timestamp.now().to_millis_timestamp(),
                 delete_row_count=self._count_delete_rows(data),
                 file_source=0,
                 value_stats_cols=None if value_stats_enabled else [],
diff --git a/paimon-python/pypaimon/write/writer/vector_writer.py 
b/paimon-python/pypaimon/write/writer/vector_writer.py
index 50663af7b0..68010ce4a4 100644
--- a/paimon-python/pypaimon/write/writer/vector_writer.py
+++ b/paimon-python/pypaimon/write/writer/vector_writer.py
@@ -117,7 +117,7 @@ class VectorWriter(AppendOnlyDataWriter):
             schema_id=self.table.table_schema.id,
             level=0,
             extra_files=[],
-            creation_time=Timestamp.now(),
+            creation_time=Timestamp.now().to_millis_timestamp(),
             delete_row_count=0,
             file_source=0,
             value_stats_cols=None,

Reply via email to