@@ -432,3 +432,53 @@ def test_avro_nested_timestamp_roundtrips_on_non_utc_host(self):
432432 self .assertEqual (rows [0 ]["ts" ].replace (tzinfo = None ), ts )
433433 self .assertEqual (rows [0 ]["r" ]["ts" ].replace (tzinfo = None ), ts )
434434 self .assertEqual (rows [0 ]["arr" ][0 ].replace (tzinfo = None ), ts )
435+
436+ def test_pyarrow_file_io_avro_normalizes_nested_naive_timestamps (self ):
437+ # Same contract as the LocalFileIO case, but for the object-store base
438+ # PyArrowFileIO (OSS/S3/GCS): it previously normalized only top-level
439+ # column values, so a naive TIMESTAMP nested in a ROW/ARRAY written as
440+ # Avro to object storage on a non-UTC host was shifted by the host
441+ # offset. The output stream is mocked to a local file so the real
442+ # write_avro record path runs without cloud credentials.
443+ import datetime
444+ import os
445+ import tempfile
446+ import time
447+ from unittest import mock
448+
449+ import fastavro
450+
451+ from pypaimon .filesystem .pyarrow_file_io import PyArrowFileIO
452+
453+ if not hasattr (time , "tzset" ):
454+ self .skipTest ("time.tzset is unavailable on this platform" )
455+
456+ ts = datetime .datetime (2024 , 1 , 2 , 3 , 4 , 5 )
457+ table = pa .table ({
458+ "ts" : pa .array ([ts ], pa .timestamp ('s' )),
459+ "r" : pa .array ([{"ts" : ts }], pa .struct ([("ts" , pa .timestamp ('s' ))])),
460+ "arr" : pa .array ([[ts ]], pa .list_ (pa .timestamp ('s' ))),
461+ })
462+ file_io = PyArrowFileIO .__new__ (PyArrowFileIO )
463+ previous_tz = os .environ .get ("TZ" )
464+ os .environ ["TZ" ] = "Asia/Shanghai"
465+ time .tzset ()
466+ try :
467+ with tempfile .TemporaryDirectory () as d :
468+ path = os .path .join (d , "data.avro" )
469+ with mock .patch .object (
470+ PyArrowFileIO , "new_output_stream" ,
471+ side_effect = lambda p : open (p , "wb" )):
472+ file_io .write_avro (path , table )
473+ with open (path , 'rb' ) as f :
474+ rows = list (fastavro .reader (f ))
475+ finally :
476+ if previous_tz is None :
477+ os .environ .pop ("TZ" , None )
478+ else :
479+ os .environ ["TZ" ] = previous_tz
480+ time .tzset ()
481+
482+ self .assertEqual (rows [0 ]["ts" ].replace (tzinfo = None ), ts )
483+ self .assertEqual (rows [0 ]["r" ]["ts" ].replace (tzinfo = None ), ts )
484+ self .assertEqual (rows [0 ]["arr" ][0 ].replace (tzinfo = None ), ts )
0 commit comments