This is an automated email from the ASF dual-hosted git repository.
lxy-9602 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-cpp.git
The following commit(s) were added to refs/heads/main by this push:
new 09e7f75f fix(read): support Parquet TIME columns (#378)
09e7f75f is described below
commit 09e7f75fbd728b188e3f18a2ee4f113101e59bff
Author: QuakeWang <[email protected]>
AuthorDate: Tue Sep 22 16:44:51 2026 +0800
fix(read): support Parquet TIME columns (#378)
---
src/paimon/common/types/data_type.cpp | 28 ++++++++++
src/paimon/common/types/data_type.h | 12 ++++
src/paimon/common/types/data_type_json_parser.cpp | 29 +++++++++-
.../common/types/data_type_json_parser_test.cpp | 33 +++++++++++
src/paimon/core/schema/arrow_schema_validator.cpp | 5 ++
.../core/schema/arrow_schema_validator_test.cpp | 34 ++++++++++++
src/paimon/core/schema/schema_manager_test.cpp | 64 ++++++++++++++++++++++
src/paimon/core/schema/schema_validation.cpp | 4 ++
src/paimon/core/schema/schema_validation_test.cpp | 30 ++++++++--
src/paimon/core/utils/field_mapping.cpp | 9 ++-
src/paimon/core/utils/field_mapping_test.cpp | 18 ++++++
test/inte/paimon_read_compat_inte_test.cpp | 37 +++++++++++--
12 files changed, 286 insertions(+), 17 deletions(-)
diff --git a/src/paimon/common/types/data_type.cpp
b/src/paimon/common/types/data_type.cpp
index 2bf5d73c..f5e56295 100644
--- a/src/paimon/common/types/data_type.cpp
+++ b/src/paimon/common/types/data_type.cpp
@@ -41,6 +41,27 @@
namespace paimon {
+Result<int32_t> DataType::GetTimePrecision(const arrow::Field& field) {
+ return GetTimePrecision(field.type(), field.metadata());
+}
+
+Result<int32_t> DataType::GetTimePrecision(
+ const std::shared_ptr<arrow::DataType>& type,
+ const std::shared_ptr<const arrow::KeyValueMetadata>& metadata) {
+ if (type->id() != arrow::Type::TIME32 ||
+ checked_cast<const arrow::Time32Type&>(*type).unit() !=
arrow::TimeUnit::MILLI) {
+ return Status::Invalid("Only millisecond TIME is supported: ",
type->ToString());
+ }
+ if (!metadata || !metadata->Contains(kTimePrecision)) {
+ return 0;
+ }
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::string precision,
metadata->Get(kTimePrecision));
+ if (precision.size() != 1 || precision[0] < '0' || precision[0] > '9') {
+ return Status::Invalid("Invalid TIME precision metadata: ", precision);
+ }
+ return precision[0] - '0';
+}
+
DataType::DataType(const std::shared_ptr<arrow::DataType>& type, bool nullable,
const std::shared_ptr<const arrow::KeyValueMetadata>&
metadata)
: type_(type), nullable_(nullable), metadata_(metadata) {}
@@ -112,6 +133,13 @@ std::string DataType::DataTypeToString(const
std::shared_ptr<arrow::DataType>& t
return "BYTES";
case arrow::Type::type::DATE32:
return "DATE";
+ case arrow::Type::type::TIME32: {
+ auto precision = GetTimePrecision(type, metadata_);
+ if (!precision.ok()) {
+ throw std::invalid_argument(precision.status().ToString());
+ }
+ return fmt::format("TIME({})", precision.value());
+ }
case arrow::Type::type::DECIMAL128: {
auto status = DecimalUtils::CheckDecimalType(*type);
if (!status.ok()) {
diff --git a/src/paimon/common/types/data_type.h
b/src/paimon/common/types/data_type.h
index 173960b4..1e49ce7e 100644
--- a/src/paimon/common/types/data_type.h
+++ b/src/paimon/common/types/data_type.h
@@ -19,6 +19,7 @@
#pragma once
+#include <cstdint>
#include <memory>
#include <string>
@@ -29,6 +30,7 @@
namespace arrow {
class DataType;
+class Field;
class TimestampType;
class KeyValueMetadata;
} // namespace arrow
@@ -37,6 +39,12 @@ namespace paimon {
class DataType : public Jsonizable<DataType> {
public:
+ static constexpr char kTimePrecision[] = "paimon.time.precision";
+
+ // Arrow carries milliseconds, while metadata preserves the declared TIME
precision.
+ // Arrow-only schemas use Paimon's default precision of zero.
+ static Result<int32_t> GetTimePrecision(const arrow::Field& field);
+
static std::unique_ptr<DataType> Create(
const std::shared_ptr<arrow::DataType>& type, bool nullable,
const std::shared_ptr<const arrow::KeyValueMetadata>& metadata);
@@ -58,6 +66,10 @@ class DataType : public Jsonizable<DataType> {
std::shared_ptr<const arrow::KeyValueMetadata> metadata_;
private:
+ static Result<int32_t> GetTimePrecision(
+ const std::shared_ptr<arrow::DataType>& type,
+ const std::shared_ptr<const arrow::KeyValueMetadata>& metadata);
+
std::string TimestampToString(const std::shared_ptr<arrow::TimestampType>&
type) const;
std::string DataTypeToString(const std::shared_ptr<arrow::DataType>& type)
const;
};
diff --git a/src/paimon/common/types/data_type_json_parser.cpp
b/src/paimon/common/types/data_type_json_parser.cpp
index adcf5b73..1416ae33 100644
--- a/src/paimon/common/types/data_type_json_parser.cpp
+++ b/src/paimon/common/types/data_type_json_parser.cpp
@@ -33,6 +33,7 @@
#include "paimon/common/data/blob_utils.h"
#include "paimon/common/data/variant/variant_type_utils.h"
#include "paimon/common/types/data_field.h"
+#include "paimon/common/types/data_type.h"
#include "paimon/common/types/vector_type.h"
#include "paimon/common/utils/date_time_utils.h"
#include "paimon/common/utils/string_utils.h"
@@ -84,11 +85,11 @@ struct Token {
std::string value;
};
-// Extension type attributes of a parsed atomic type. BLOB and VARIANT parse
to plain arrow
-// types (large_binary / struct) and need field-level metadata markers applied
by the caller.
+// Logical attributes not represented by the Arrow type are attached to the
parsed field.
struct AtomicTypeAttributes {
bool is_blob = false;
bool is_variant = false;
+ std::optional<int32_t> time_precision;
};
// nullptr is returned in the case of parsing failed
@@ -249,6 +250,7 @@ class TokenParser {
Result<std::shared_ptr<arrow::DataType>> ParseStringType();
Result<std::shared_ptr<arrow::DataType>> ParseDecimalType();
Result<std::shared_ptr<arrow::DataType>> ParseDoubleType();
+ Result<std::shared_ptr<arrow::DataType>>
ParseTimeType(AtomicTypeAttributes* attributes);
Result<std::shared_ptr<arrow::DataType>> ParseTimestampType();
Result<std::shared_ptr<arrow::DataType>> ParseTimestampLtzType();
Result<std::shared_ptr<arrow::DataType>> ParseVectorType();
@@ -522,6 +524,8 @@ Result<std::shared_ptr<arrow::DataType>>
TokenParser::ParseTypeByKeyword(
return ParseDoubleType();
case Keyword::DATE:
return arrow::date32();
+ case Keyword::TIME:
+ return ParseTimeType(attributes);
case Keyword::TIMESTAMP:
return ParseTimestampType();
case Keyword::TIMESTAMP_LTZ:
@@ -581,6 +585,22 @@ Result<std::shared_ptr<arrow::DataType>>
TokenParser::ParseDoubleType() {
return arrow::float64();
}
+Result<std::shared_ptr<arrow::DataType>> TokenParser::ParseTimeType(
+ AtomicTypeAttributes* attributes) {
+ PAIMON_ASSIGN_OR_RAISE(int32_t precision,
ParseOptionalPrecision(/*default_precision=*/0));
+ if (precision < 0 || precision > 9) {
+ return Status::Invalid("Time precision must be between 0 and 9 (both
inclusive)");
+ }
+ if (HasNextToken({Keyword::WITHOUT})) {
+ PAIMON_RETURN_NOT_OK(NextToken(Keyword::WITHOUT));
+ PAIMON_RETURN_NOT_OK(NextToken(Keyword::TIME));
+ PAIMON_RETURN_NOT_OK(NextToken(Keyword::ZONE));
+ }
+ // Paimon stores TIME as milliseconds of the day, including PyPaimon's
TIME(0).
+ attributes->time_precision = precision;
+ return arrow::time32(arrow::TimeUnit::MILLI);
+}
+
Result<std::shared_ptr<arrow::DataType>> TokenParser::ParseTimestampType() {
PAIMON_ASSIGN_OR_RAISE(int32_t precision,
ParseOptionalPrecision(Timestamp::DEFAULT_PRECISION));
bool with_timezone = false;
@@ -743,6 +763,11 @@ Result<std::shared_ptr<arrow::Field>>
DataTypeJsonParser::ParseAtomicTypeField(
return BlobUtils::ToArrowField(name, nullable);
} else if (attributes.is_variant) {
return VariantTypeUtils::ToArrowField(name, nullable);
+ } else if (attributes.time_precision) {
+ return arrow::field(
+ name, type, nullable,
+ arrow::KeyValueMetadata::Make({DataType::kTimePrecision},
+
{std::to_string(attributes.time_precision.value())}));
} else {
return arrow::field(name, type, nullable);
}
diff --git a/src/paimon/common/types/data_type_json_parser_test.cpp
b/src/paimon/common/types/data_type_json_parser_test.cpp
index 5049afc3..7c70289e 100644
--- a/src/paimon/common/types/data_type_json_parser_test.cpp
+++ b/src/paimon/common/types/data_type_json_parser_test.cpp
@@ -22,9 +22,11 @@
#include <utility>
#include <vector>
+#include "fmt/format.h"
#include "gtest/gtest.h"
#include "paimon/common/data/variant/variant_type_utils.h"
#include "paimon/common/types/data_field.h"
+#include "paimon/common/types/data_type.h"
#include "paimon/common/utils/checked_cast.h"
#include "paimon/common/utils/date_time_utils.h"
#include "paimon/status.h"
@@ -356,4 +358,35 @@ TEST(DataTypeJsonParserTest, ParseTypeAtomicTypeSuccess) {
}
}
+TEST(DataTypeJsonParserTest, ParseTimeType) {
+ std::vector<std::string> types = {"TIME", "TIME WITHOUT TIME ZONE"};
+ for (int32_t precision = 0; precision <= 9; ++precision) {
+ types.push_back(fmt::format("TIME({})", precision));
+ types.push_back(fmt::format("TIME({}) WITHOUT TIME ZONE", precision));
+ }
+ for (const auto& type : types) {
+ for (bool nullable : {true, false}) {
+ std::string type_str = nullable ? type : type + " NOT NULL";
+ SCOPED_TRACE(type_str);
+ rapidjson::Document doc;
+ rapidjson::Value value(type_str.data(), doc.GetAllocator());
+ ASSERT_OK_AND_ASSIGN(auto field,
DataTypeJsonParser::ParseType("time", value));
+
ASSERT_TRUE(field->type()->Equals(arrow::time32(arrow::TimeUnit::MILLI)));
+ ASSERT_EQ(field->nullable(), nullable);
+ auto logical_type = DataType::Create(field->type(), nullable,
field->metadata());
+ ASSERT_OK_AND_ASSIGN(auto serialized,
logical_type->ToJsonString());
+ int32_t precision = type.find('(') == std::string::npos ? 0 :
type[5] - '0';
+ ASSERT_EQ(serialized,
+ fmt::format("\"TIME({}){}\"", precision, nullable ? "" :
" NOT NULL"));
+ }
+ }
+ for (const char* type : {"TIME(-1)", "TIME(10)", "TIME(2147483648)",
"TIME()", "TIME(3, 0)",
+ "TIME WITH TIME ZONE", "TIME(3) WITHOUT TIME"}) {
+ SCOPED_TRACE(type);
+ rapidjson::Document doc;
+ rapidjson::Value value(type, doc.GetAllocator());
+ ASSERT_NOK(DataTypeJsonParser::ParseType("time", value));
+ }
+}
+
} // namespace paimon::test
diff --git a/src/paimon/core/schema/arrow_schema_validator.cpp
b/src/paimon/core/schema/arrow_schema_validator.cpp
index c008d222..daf364c6 100644
--- a/src/paimon/core/schema/arrow_schema_validator.cpp
+++ b/src/paimon/core/schema/arrow_schema_validator.cpp
@@ -28,6 +28,7 @@
#include "paimon/common/data/variant/variant_access_utils.h"
#include "paimon/common/data/variant/variant_type_utils.h"
#include "paimon/common/types/data_field.h"
+#include "paimon/common/types/data_type.h"
#include "paimon/common/types/vector_type.h"
#include "paimon/common/utils/checked_cast.h"
#include "paimon/common/utils/decimal_utils.h"
@@ -123,6 +124,7 @@ Status ArrowSchemaValidator::ValidateDataTypeWithFieldId(
case arrow::Type::type::DATE32:
case arrow::Type::type::DECIMAL128:
case arrow::Type::type::TIMESTAMP:
+ case arrow::Type::type::TIME32:
return Status::OK();
case arrow::Type::type::LIST: {
const auto& value_field =
checked_cast<arrow::BaseListType*>(type.get())->value_field();
@@ -208,6 +210,9 @@ Status ArrowSchemaValidator::ValidateField(const
std::shared_ptr<arrow::Field>&
case arrow::Type::type::DATE32:
case arrow::Type::type::TIMESTAMP:
break;
+ case arrow::Type::type::TIME32:
+ PAIMON_RETURN_NOT_OK(DataType::GetTimePrecision(*field));
+ break;
case arrow::Type::type::DECIMAL128:
PAIMON_RETURN_NOT_OK(DecimalUtils::CheckDecimalType(*field->type()));
break;
diff --git a/src/paimon/core/schema/arrow_schema_validator_test.cpp
b/src/paimon/core/schema/arrow_schema_validator_test.cpp
index 283b61ec..6a9a55f3 100644
--- a/src/paimon/core/schema/arrow_schema_validator_test.cpp
+++ b/src/paimon/core/schema/arrow_schema_validator_test.cpp
@@ -29,6 +29,7 @@
#include "paimon/common/data/variant/variant_defs.h"
#include "paimon/common/data/variant/variant_type_utils.h"
#include "paimon/common/types/data_field.h"
+#include "paimon/common/types/data_type.h"
#include "paimon/common/utils/date_time_utils.h"
#include "paimon/testing/utils/testharness.h"
@@ -76,6 +77,39 @@ TEST(ArrowSchemaValidatorTest, TestVectorElementType) {
}
}
+TEST(ArrowSchemaValidatorTest, TestTimeType) {
+ for (const auto& type :
+ {arrow::time32(arrow::TimeUnit::MILLI),
arrow::time32(arrow::TimeUnit::SECOND),
+ arrow::time64(arrow::TimeUnit::MICRO),
arrow::time64(arrow::TimeUnit::NANO)}) {
+ SCOPED_TRACE(type->ToString());
+ for (const auto& field_type : {type, arrow::list(type)}) {
+ auto schema = DataField::ConvertDataFieldsToArrowSchema(
+ {DataField(0, arrow::field("time", field_type))});
+ if (type->Equals(arrow::time32(arrow::TimeUnit::MILLI))) {
+ ASSERT_OK(ArrowSchemaValidator::ValidateSchema(*schema));
+
ASSERT_OK(ArrowSchemaValidator::ValidateSchemaWithFieldId(*schema));
+ } else {
+ ASSERT_NOK(ArrowSchemaValidator::ValidateSchema(*schema));
+
ASSERT_NOK(ArrowSchemaValidator::ValidateSchemaWithFieldId(*schema));
+ }
+ }
+ }
+}
+
+TEST(ArrowSchemaValidatorTest, TestInvalidTimePrecision) {
+ for (const char* precision : {"", "-1", "10", "3x", "1.5", "2147483648"}) {
+ SCOPED_TRACE(precision);
+ auto field =
+ arrow::field("time", arrow::time32(arrow::TimeUnit::MILLI), true,
+
arrow::KeyValueMetadata::Make({DataType::kTimePrecision}, {precision}));
+
ASSERT_NOK_WITH_MSG(ArrowSchemaValidator::ValidateSchema(*arrow::schema({field})),
+ "Invalid TIME precision metadata");
+ ASSERT_NOK_WITH_MSG(DataField(0, field).ToJsonString(), "Invalid TIME
precision metadata");
+ }
+ auto seconds = DataType::Create(arrow::time32(arrow::TimeUnit::SECOND),
true, nullptr);
+ ASSERT_NOK_WITH_MSG(seconds->ToJsonString(), "Only millisecond TIME is
supported");
+}
+
TEST(ArrowSchemaValidatorTest, TestValidateNoRedundantFields) {
auto col1_field = arrow::field("col1", arrow::int64());
auto col2_field = arrow::field("col2", arrow::int32());
diff --git a/src/paimon/core/schema/schema_manager_test.cpp
b/src/paimon/core/schema/schema_manager_test.cpp
index 0b078462..cd482389 100644
--- a/src/paimon/core/schema/schema_manager_test.cpp
+++ b/src/paimon/core/schema/schema_manager_test.cpp
@@ -25,12 +25,76 @@
#include "arrow/type.h"
#include "gtest/gtest.h"
+#include "paimon/common/types/data_type.h"
+#include "paimon/common/types/data_type_json_parser.h"
#include "paimon/fs/local/local_file_system.h"
#include "paimon/status.h"
#include "paimon/testing/utils/testharness.h"
namespace paimon::test {
+TEST(SchemaManagerTest, TimePrecisionRoundTrip) {
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ auto fs = std::make_shared<LocalFileSystem>();
+ SchemaManager manager(fs, dir->Str());
+ arrow::FieldVector fields;
+ for (int32_t precision = 0; precision <= 9; ++precision) {
+ for (bool nullable : {true, false}) {
+ std::string name = "t" + std::to_string(fields.size());
+ std::string type =
+ "TIME(" + std::to_string(precision) + ")" + (nullable ? "" : "
NOT NULL");
+ rapidjson::Document doc;
+ rapidjson::Value value(type.c_str(), doc.GetAllocator());
+ ASSERT_OK_AND_ASSIGN(auto field,
DataTypeJsonParser::ParseType(name, value));
+ fields.push_back(field);
+ }
+ }
+ fields.push_back(arrow::field("default_time",
arrow::time32(arrow::TimeUnit::MILLI)));
+ fields.push_back(arrow::field("times",
arrow::list(fields[6]->WithName("item"))));
+ fields.push_back(
+ arrow::field("mapping",
std::make_shared<arrow::MapType>(fields[13]->WithName("key"),
+
fields[18]->WithName("value"))));
+ fields.push_back(arrow::field("nested", arrow::struct_({fields[6],
fields[13], fields[18]})));
+ ASSERT_OK_AND_ASSIGN(auto created,
+ manager.CreateTable(arrow::schema(fields), {}, {},
+ {{"file.format", "parquet"},
{"bucket", "-1"}}));
+ ASSERT_OK_AND_ASSIGN(auto serialized, created->ToJsonString());
+ SchemaManager reloaded_manager(fs, dir->Str());
+ ASSERT_OK_AND_ASSIGN(auto reloaded, reloaded_manager.ReadSchema(0));
+ ASSERT_OK_AND_ASSIGN(auto reserialized, reloaded->ToJsonString());
+ ASSERT_EQ(serialized, reserialized);
+ const auto& restored_fields = reloaded->Fields();
+ for (int32_t i = 0; i < 20; ++i) {
+ ASSERT_OK_AND_ASSIGN(auto precision,
+
DataType::GetTimePrecision(*restored_fields[i].ArrowField()));
+ ASSERT_EQ(precision, i / 2);
+ ASSERT_EQ(restored_fields[i].ArrowField()->nullable(), i % 2 == 0);
+ }
+ ASSERT_OK_AND_ASSIGN(auto default_precision,
+
DataType::GetTimePrecision(*restored_fields[20].ArrowField()));
+ ASSERT_EQ(default_precision, 0);
+ for (int32_t i = 21; i < 24; ++i) {
+ SCOPED_TRACE(i);
+
ASSERT_TRUE(DataField::ConvertDataFieldToArrowField(created->Fields()[i])
+
->Equals(DataField::ConvertDataFieldToArrowField(restored_fields[i]),
+ /*check_metadata=*/true));
+ }
+}
+
+TEST(SchemaManagerTest, RejectTimePartitionKey) {
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ SchemaManager manager(std::make_shared<LocalFileSystem>(), dir->Str());
+ auto schema = arrow::schema({arrow::field("id", arrow::int32()),
+ arrow::field("time",
arrow::time32(arrow::TimeUnit::MILLI))});
+ ASSERT_NOK_WITH_MSG(
+ manager.CreateTable(schema, {"time"}, {}, {{"file.format", "parquet"},
{"bucket", "-1"}}),
+ "partition field time cannot be TIME");
+ ASSERT_OK_AND_ASSIGN(auto latest, manager.Latest());
+ ASSERT_FALSE(latest.has_value());
+}
+
TEST(SchemaManagerTest, ConcurrentHistoricalSchemaReads) {
SchemaManager manager(
std::make_shared<LocalFileSystem>(),
diff --git a/src/paimon/core/schema/schema_validation.cpp
b/src/paimon/core/schema/schema_validation.cpp
index d97d73a8..2a9c3322 100644
--- a/src/paimon/core/schema/schema_validation.cpp
+++ b/src/paimon/core/schema/schema_validation.cpp
@@ -397,6 +397,10 @@ Status SchemaValidation::ValidateNotContainSpecificType(
auto it = fields_map.find(field_name);
if (it != fields_map.end()) {
auto field = it->second;
+ if (field->type()->id() == arrow::Type::TIME32) {
+ return Status::Invalid(
+ fmt::format("partition field {} cannot be TIME",
field_name));
+ }
if (IsComplexType(field)) {
return Status::Invalid(
fmt::format("partition field {} cannot be
TIMESTAMP/DECIMAL/BLOB", field_name));
diff --git a/src/paimon/core/schema/schema_validation_test.cpp
b/src/paimon/core/schema/schema_validation_test.cpp
index 54728987..f6c08b86 100644
--- a/src/paimon/core/schema/schema_validation_test.cpp
+++ b/src/paimon/core/schema/schema_validation_test.cpp
@@ -289,9 +289,13 @@ TEST(SchemaValidationTest, TestLanceDataTypes) {
arrow::field("map", arrow::map(arrow::int32(), arrow::utf8())),
arrow::field("ltz", arrow::timestamp(arrow::TimeUnit::MICRO, "UTC")),
VariantTypeUtils::ToArrowField("variant"),
+ arrow::field("time_millis", arrow::time32(arrow::TimeUnit::MILLI)),
+ arrow::field("nested_time",
+ arrow::struct_({arrow::field(
+ "values",
arrow::list(arrow::time32(arrow::TimeUnit::MILLI)))})),
};
- std::vector<std::string> expected_errors = {"type MAP",
"LOCAL_ZONED_TIMESTAMP",
- "type VARIANT"};
+ std::vector<std::string> expected_errors = {"type MAP",
"LOCAL_ZONED_TIMESTAMP", "type VARIANT",
+ "type time32", "type time32"};
for (size_t i = 0; i < unsupported_fields.size(); ++i) {
ASSERT_OK_AND_ASSIGN(
table_schema,
@@ -303,14 +307,13 @@ TEST(SchemaValidationTest, TestLanceDataTypes) {
for (const auto& field : arrow::FieldVector{
arrow::field("time_seconds",
arrow::time32(arrow::TimeUnit::SECOND)),
- arrow::field("time_millis",
arrow::time32(arrow::TimeUnit::MILLI)),
arrow::field("nested_time",
arrow::struct_({arrow::field(
- "values",
arrow::list(arrow::time32(arrow::TimeUnit::MILLI)))}))}) {
+ "values",
arrow::list(arrow::time32(arrow::TimeUnit::SECOND)))}))}) {
ASSERT_NOK_WITH_MSG(
TableSchema::Create(/*schema_id=*/0, arrow::schema({field}),
/*partition_keys=*/{}, /*primary_keys=*/{},
options),
- "Unknown or unsupported arrow type: time32");
+ "Only millisecond TIME is supported");
}
for (const auto& [option_key, option_value] :
std::vector<std::pair<std::string, std::string>>{
@@ -691,6 +694,23 @@ TEST(SchemaValidationTest, TestSpecificPartitionKey) {
}
}
+TEST(SchemaValidationTest, TestTimePartitionKey) {
+ auto schema = arrow::schema({arrow::field("id", arrow::int32()),
+ arrow::field("time",
arrow::time32(arrow::TimeUnit::MILLI))});
+ for (const std::vector<std::string>& partition_keys :
+ {std::vector<std::string>{}, std::vector<std::string>{"time"}}) {
+ ASSERT_OK_AND_ASSIGN(auto table_schema,
+ TableSchema::Create(0, schema, partition_keys, {},
+ {{"file.format", "parquet"},
{"bucket", "-1"}}));
+ if (partition_keys.empty()) {
+ ASSERT_OK(SchemaValidation::ValidateTableSchema(*table_schema));
+ } else {
+
ASSERT_NOK_WITH_MSG(SchemaValidation::ValidateTableSchema(*table_schema),
+ "partition field time cannot be TIME");
+ }
+ }
+}
+
TEST(SchemaValidationTest, TestComplexPartitionKeyWithBlob) {
auto f0 = arrow::field("f0", arrow::utf8());
auto f1 = BlobUtils::ToArrowField("f1");
diff --git a/src/paimon/core/utils/field_mapping.cpp
b/src/paimon/core/utils/field_mapping.cpp
index be7287dd..61a40365 100644
--- a/src/paimon/core/utils/field_mapping.cpp
+++ b/src/paimon/core/utils/field_mapping.cpp
@@ -177,11 +177,6 @@ Result<std::vector<std::shared_ptr<CastExecutor>>>
FieldMappingBuilder::CreateDa
std::vector<std::shared_ptr<CastExecutor>> cast_executors;
cast_executors.reserve(read_fields.size());
for (size_t i = 0; i < read_fields.size(); i++) {
- PAIMON_ASSIGN_OR_RAISE(FieldType read_type,
-
FieldTypeUtils::ConvertToFieldType(read_fields[i].Type()->id()));
- PAIMON_ASSIGN_OR_RAISE(FieldType data_type,
-
FieldTypeUtils::ConvertToFieldType(data_fields[i].Type()->id()));
-
if (!read_fields[i].Type()->Equals(data_fields[i].Type())) {
auto read_type_id = read_fields[i].Type()->id();
if (read_type_id == arrow::Type::STRUCT || read_type_id ==
arrow::Type::LIST ||
@@ -191,6 +186,10 @@ Result<std::vector<std::shared_ptr<CastExecutor>>>
FieldMappingBuilder::CreateDa
cast_executors.push_back(nullptr);
continue;
}
+ PAIMON_ASSIGN_OR_RAISE(FieldType read_type,
+
FieldTypeUtils::ConvertToFieldType(read_fields[i].Type()->id()));
+ PAIMON_ASSIGN_OR_RAISE(FieldType data_type,
+
FieldTypeUtils::ConvertToFieldType(data_fields[i].Type()->id()));
auto executor_factory =
CastExecutorFactory::GetCastExecutorFactory();
auto cast_executor =
executor_factory->GetCastExecutor(/*src=*/data_type,
/*target=*/read_type);
diff --git a/src/paimon/core/utils/field_mapping_test.cpp
b/src/paimon/core/utils/field_mapping_test.cpp
index 1ba674cd..a970a31e 100644
--- a/src/paimon/core/utils/field_mapping_test.cpp
+++ b/src/paimon/core/utils/field_mapping_test.cpp
@@ -677,4 +677,22 @@ TEST_F(FieldMappingTest,
TestMapSelectedKeysMetadataPropagatedToDataSchema) {
ASSERT_FALSE(custom_metadata_result.ok());
}
+TEST_F(FieldMappingTest, TestTimeWithoutCast) {
+ std::vector<DataField> fields = {
+ DataField(0, arrow::field("time",
arrow::time32(arrow::TimeUnit::MILLI)))};
+ auto schema = DataField::ConvertDataFieldsToArrowSchema(fields);
+ ASSERT_OK_AND_ASSIGN(auto builder, FieldMappingBuilder::Create(schema,
/*partition_keys=*/{},
+
/*predicate=*/nullptr));
+ ASSERT_OK_AND_ASSIGN(auto mapping, builder->CreateFieldMapping(fields));
+ const auto& info = mapping->non_partition_info;
+ ASSERT_EQ(info.non_partition_read_schema, fields);
+ ASSERT_EQ(info.non_partition_data_schema, fields);
+ ASSERT_EQ(info.cast_executors.size(), 1);
+ ASSERT_EQ(info.cast_executors[0], nullptr);
+
+ std::vector<DataField> int_fields = {DataField(0, arrow::field("time",
arrow::int32()))};
+ ASSERT_NOK(FieldMappingBuilder::CreateDataCastExecutors(fields,
int_fields));
+ ASSERT_NOK(FieldMappingBuilder::CreateDataCastExecutors(int_fields,
fields));
+}
+
} // namespace paimon::test
diff --git a/test/inte/paimon_read_compat_inte_test.cpp
b/test/inte/paimon_read_compat_inte_test.cpp
index 0af1dd1f..f6468b6f 100644
--- a/test/inte/paimon_read_compat_inte_test.cpp
+++ b/test/inte/paimon_read_compat_inte_test.cpp
@@ -406,6 +406,23 @@ TEST_P(PaimonReadCompatInteTest,
ReadsCompatibleTypeValues) {
}
}
+TEST_P(PaimonReadCompatInteTest, ReadsTimeValues) {
+ const CompatibilityParam& param = GetParam();
+ if (param.file_format != "parquet") {
+ GTEST_SKIP() << "TIME reading is only supported for Parquet";
+ }
+ for (int32_t precision : {0, 3, 6, 9}) {
+ std::string field_name = "f_time_" + std::to_string(precision);
+ ASSERT_OK_AND_ASSIGN(auto result,
+ ReadTable(param, param.writer_prefix +
"_time_types", {field_name}));
+ auto rows = GetOnlyStructChunk(result);
+ ASSERT_TRUE(rows);
+ ASSERT_EQ(rows->length(), 2);
+ AssertFieldEqualsJson(rows, field_name,
arrow::time32(arrow::TimeUnit::MILLI),
+ precision == 0 ? "[45296000, null]" :
"[45296123, null]");
+ }
+}
+
TEST_P(PaimonReadCompatInteTest, ReadsBlobValues) {
const CompatibilityParam& param = GetParam();
@@ -566,20 +583,30 @@ std::vector<UnsupportedReadParam> UnsupportedReadParams()
{
const std::vector<UnsupportedReadCase> read_cases = {
{"ArrayBlob", "array_blob_types", "f_array_blob",
"BLOB field must be a top-level field or the direct value of a
top-level MAP field"},
- {"TimePrecision0", "time_types", "f_time_0", "Unsupported type: TIME"},
- {"TimePrecision3", "time_types", "f_time_3", "Unsupported type: TIME"},
- {"TimePrecision6", "time_types", "f_time_6", "Unsupported type: TIME"},
- {"TimePrecision9", "time_types", "f_time_9", "Unsupported type: TIME"},
+ {"TimePrecision0", "time_types", "f_time_0", ""},
+ {"TimePrecision3", "time_types", "f_time_3", ""},
+ {"TimePrecision6", "time_types", "f_time_6", ""},
+ {"TimePrecision9", "time_types", "f_time_9", ""},
};
std::vector<UnsupportedReadParam> result;
for (const CompatibilityParam& param : CompatibilityParams()) {
for (const UnsupportedReadCase& read_case : read_cases) {
+ bool is_time = read_case.table_suffix == "time_types";
+ if (is_time && param.file_format == "parquet") {
+ continue;
+ }
// Java Avro cannot create TIME(6/9), so its negative table
contains TIME(0/3) only.
if (param.file_format == "avro" &&
(read_case.name == "TimePrecision6" || read_case.name ==
"TimePrecision9")) {
continue;
}
- result.push_back({param.file_format, param.writer_prefix,
read_case});
+ auto expected_case = read_case;
+ if (is_time) {
+ expected_case.expected_error = param.file_format == "orc"
+ ? "Unknown or unsupported
Arrow type: time32[ms]"
+ : "invalid avro logical
type";
+ }
+ result.push_back({param.file_format, param.writer_prefix,
expected_case});
}
}
return result;