From 7e8b0297af716c87a31dd954f07485695b15f956 Mon Sep 17 00:00:00 2001
From: Edmond Abraham <54119952+gitedmond@users.noreply.github.com>
Date: Sat, 10 Oct 2026 22:12:14 -0400
Subject: [PATCH] GH-50142: [C++][Python] Honor Parquet time coercion and
format version
---
.../parquet/arrow/arrow_reader_writer_test.cc | 35 +++++-
cpp/src/parquet/arrow/arrow_schema_test.cc | 32 +++++
cpp/src/parquet/arrow/schema.cc | 63 +++++++---
cpp/src/parquet/column_writer.cc | 119 +++++++++++++++---
cpp/src/parquet/properties.h | 10 +-
.../python/parquet/parquet_type_handling.rst | 20 +--
python/pyarrow/parquet/core.py | 12 +-
python/pyarrow/tests/parquet/test_datetime.py | 110 ++++++++++++++++
8 files changed, 352 insertions(+), 49 deletions(-)
diff --git a/cpp/src/parquet/arrow/arrow_reader_writer_test.cc b/cpp/src/parquet/arrow/arrow_reader_writer_test.cc
index d858962d4728..9ded03867bf5 100644
--- a/cpp/src/parquet/arrow/arrow_reader_writer_test.cc
+++ b/cpp/src/parquet/arrow/arrow_reader_writer_test.cc
@@ -1873,7 +1873,8 @@ void MakeDateTimeTypesTable(std::shared_ptr
* out, bool expected_micro = f
auto f3_x = field("f3", ::arrow::timestamp(TimeUnit::MICRO));
auto f4 = field("f4", ::arrow::time32(TimeUnit::MILLI));
auto f5 = field("f5", ::arrow::time64(TimeUnit::MICRO));
- auto f6 = field("f6", ::arrow::time64(TimeUnit::NANO));
+ auto f6 =
+ field("f6", ::arrow::time64(expected_micro ? TimeUnit::MICRO : TimeUnit::NANO));
std::shared_ptr<::arrow::Schema> schema(
new ::arrow::Schema({f0, f1, f2, (expected_micro ? f3_x : f3), f4, f5, f6}));
@@ -1899,7 +1900,8 @@ void MakeDateTimeTypesTable(std::shared_ptr* out, bool expected_micro = f
&a3_x);
ArrayFromVector<::arrow::Time32Type, int32_t>(f4->type(), is_valid, t32_values, &a4);
ArrayFromVector<::arrow::Time64Type, int64_t>(f5->type(), is_valid, t64_us_values, &a5);
- ArrayFromVector<::arrow::Time64Type, int64_t>(f6->type(), is_valid, t64_ns_values, &a6);
+ ArrayFromVector<::arrow::Time64Type, int64_t>(
+ f6->type(), is_valid, expected_micro ? t64_us_values : t64_ns_values, &a6);
*out = Table::Make(schema, {a0, a1, a2, expected_micro ? a3_x : a3, a4, a5, a6});
}
@@ -1927,6 +1929,35 @@ TEST(TestArrowReadWrite, DateTimeTypes) {
CheckConfiguredRoundtrip(table, expected, parquet_version_2_4_properties));
}
+TEST(TestArrowReadWrite, WriteUnannotatedTimeArrays) {
+ for (bool wide : {false, true}) {
+ ASSERT_OK_AND_ASSIGN(auto sink, ::arrow::io::BufferOutputStream::Create());
+ auto node = PrimitiveNode::Make("time", Repetition::OPTIONAL,
+ wide ? ParquetType::INT64 : ParquetType::INT32);
+ auto schema = std::static_pointer_cast(
+ GroupNode::Make("schema", Repetition::REQUIRED, {node}));
+ auto array = ArrayFromJSON(
+ wide ? ::arrow::time64(TimeUnit::NANO) : ::arrow::time32(TimeUnit::SECOND),
+ "[1000]");
+ auto properties = default_arrow_writer_properties();
+ ArrowWriteContext ctx(::arrow::default_memory_pool(), properties.get());
+ auto writer = ParquetFileWriter::Open(sink, schema);
+ auto row_group = writer->AppendRowGroup();
+ const int16_t def_level = 1;
+ ASSERT_OK(
+ row_group->NextColumn()->WriteArrow(&def_level, nullptr, 1, *array, &ctx, true));
+ row_group->Close();
+ writer->Close();
+ ASSERT_OK_AND_ASSIGN(auto buffer, sink->Finish());
+ ASSERT_OK_AND_ASSIGN(auto reader, OpenFile(std::make_shared(buffer),
+ ::arrow::default_memory_pool()));
+ ASSERT_OK_AND_ASSIGN(auto result, reader->ReadTable());
+ auto expected = ArrayFromJSON(wide ? ::arrow::int64() : ::arrow::int32(),
+ wide ? "[1000]" : "[1000000]");
+ ::arrow::AssertArraysEqual(*expected, *result->column(0)->chunk(0));
+ }
+}
+
TEST(TestArrowReadWrite, UseDeprecatedInt96) {
using ::arrow::ArrayFromVector;
using ::arrow::field;
diff --git a/cpp/src/parquet/arrow/arrow_schema_test.cc b/cpp/src/parquet/arrow/arrow_schema_test.cc
index 0ec943841616..85e33152acc8 100644
--- a/cpp/src/parquet/arrow/arrow_schema_test.cc
+++ b/cpp/src/parquet/arrow/arrow_schema_test.cc
@@ -1871,6 +1871,38 @@ TEST_F(TestConvertArrowSchema, ParquetFlatDecimals) {
ASSERT_NO_FATAL_FAILURE(CheckFlatSchema(parquet_fields));
}
+TEST_F(TestConvertArrowSchema, ParquetTimeCoercion) {
+ auto schema =
+ ::arrow::schema({::arrow::field("seconds", ::arrow::time32(TimeUnit::SECOND)),
+ ::arrow::field("nanos", ::arrow::time64(TimeUnit::NANO))});
+ auto arrow_properties = ArrowWriterProperties::Builder()
+ .coerce_timestamps(TimeUnit::NANO)
+ ->set_time_adjusted_to_utc(true)
+ ->build();
+ for (auto version : {ParquetVersion::PARQUET_1_0, ParquetVersion::PARQUET_2_4,
+ ParquetVersion::PARQUET_2_6}) {
+ auto properties = WriterProperties::Builder().version(version)->build();
+ auto status =
+ ToParquetSchema(schema.get(), *properties, *arrow_properties, &result_schema_);
+ if (version != ParquetVersion::PARQUET_2_6) {
+ ASSERT_RAISES(NotImplemented, status);
+ continue;
+ }
+ ASSERT_OK(status);
+ const auto logical_type = LogicalType::Time(true, LogicalType::TimeUnit::NANOS);
+ for (int i = 0; i < schema->num_fields(); ++i) {
+ const auto* column = result_schema_->Column(i);
+ ASSERT_EQ(column->physical_type(), ParquetType::INT64);
+ ASSERT_TRUE(column->logical_type()->Equals(*logical_type));
+ }
+ }
+ arrow_properties =
+ ArrowWriterProperties::Builder().coerce_timestamps(TimeUnit::SECOND)->build();
+ ASSERT_RAISES(NotImplemented,
+ ToParquetSchema(schema.get(), *default_writer_properties(),
+ *arrow_properties, &result_schema_));
+}
+
TEST_F(TestConvertArrowSchema, ParquetTimeAdjustedToUTC) {
// Verify Parquet Time types have the appropriate isAdjustedToUTC value, depending
// on the return value of ArrowWriterProperties::write_time_adjusted_to_utc()
diff --git a/cpp/src/parquet/arrow/schema.cc b/cpp/src/parquet/arrow/schema.cc
index bc4de6c39b5e..cac4d423465a 100644
--- a/cpp/src/parquet/arrow/schema.cc
+++ b/cpp/src/parquet/arrow/schema.cc
@@ -204,6 +204,48 @@ static std::shared_ptr TimestampLogicalTypeFromArrowTimestamp
return LogicalType::None();
}
+static Status GetTimeMetadata(const ::arrow::TimeType& type,
+ const WriterProperties& properties,
+ const ArrowWriterProperties& arrow_properties,
+ ParquetType::type* physical_type,
+ std::shared_ptr* logical_type) {
+ const bool coerce = arrow_properties.coerce_timestamps_enabled();
+ auto unit = coerce ? arrow_properties.coerce_timestamps_unit() : type.unit();
+ const bool supports_nanos = properties.version() == ParquetVersion::PARQUET_2_6;
+ if (!coerce) {
+ if (unit == ::arrow::TimeUnit::SECOND) {
+ unit = ::arrow::TimeUnit::MILLI;
+ } else if (unit == ::arrow::TimeUnit::NANO && !supports_nanos) {
+ unit = ::arrow::TimeUnit::MICRO;
+ }
+ }
+
+ *physical_type =
+ unit == ::arrow::TimeUnit::MILLI ? ParquetType::INT32 : ParquetType::INT64;
+ LogicalType::TimeUnit::unit parquet_unit;
+ switch (unit) {
+ case ::arrow::TimeUnit::MILLI:
+ parquet_unit = LogicalType::TimeUnit::MILLIS;
+ break;
+ case ::arrow::TimeUnit::MICRO:
+ parquet_unit = LogicalType::TimeUnit::MICROS;
+ break;
+ case ::arrow::TimeUnit::NANO:
+ if (!supports_nanos) {
+ return Status::NotImplemented("Nanosecond times require Parquet version 2.6");
+ }
+ parquet_unit = LogicalType::TimeUnit::NANOS;
+ break;
+ case ::arrow::TimeUnit::SECOND:
+ return Status::NotImplemented("Parquet does not support times in seconds");
+ default:
+ return Status::NotImplemented("Unsupported Arrow time unit");
+ }
+ *logical_type =
+ LogicalType::Time(arrow_properties.write_time_adjusted_to_utc(), parquet_unit);
+ return Status::OK();
+}
+
static Status GetTimestampMetadata(const ::arrow::TimestampType& type,
const WriterProperties& properties,
const ArrowWriterProperties& arrow_properties,
@@ -423,24 +465,11 @@ Status FieldToNode(const std::string& name, const std::shared_ptr& field,
properties, arrow_properties, &type, &logical_type));
break;
case ArrowTypeId::TIME32:
- type = ParquetType::INT32;
- logical_type = LogicalType::Time(
- /*is_adjusted_to_utc=*/arrow_properties.write_time_adjusted_to_utc(),
- LogicalType::TimeUnit::MILLIS);
+ case ArrowTypeId::TIME64:
+ RETURN_NOT_OK(
+ GetTimeMetadata(checked_cast(*field->type()),
+ properties, arrow_properties, &type, &logical_type));
break;
- case ArrowTypeId::TIME64: {
- type = ParquetType::INT64;
- auto time_type = static_cast<::arrow::Time64Type*>(field->type().get());
- if (time_type->unit() == ::arrow::TimeUnit::NANO) {
- logical_type = LogicalType::Time(
- /*is_adjusted_to_utc=*/arrow_properties.write_time_adjusted_to_utc(),
- LogicalType::TimeUnit::NANOS);
- } else {
- logical_type = LogicalType::Time(
- /*is_adjusted_to_utc=*/arrow_properties.write_time_adjusted_to_utc(),
- LogicalType::TimeUnit::MICROS);
- }
- } break;
case ArrowTypeId::DURATION:
type = ParquetType::INT64;
break;
diff --git a/cpp/src/parquet/column_writer.cc b/cpp/src/parquet/column_writer.cc
index 3296af62f0c4..45f129da3db5 100644
--- a/cpp/src/parquet/column_writer.cc
+++ b/cpp/src/parquet/column_writer.cc
@@ -20,6 +20,7 @@
#include
#include
#include
+#include
#include