diff --git a/src/iceberg/test/bucket_util_test.cc b/src/iceberg/test/bucket_util_test.cc index 69a04ef54..8c80f04c2 100644 --- a/src/iceberg/test/bucket_util_test.cc +++ b/src/iceberg/test/bucket_util_test.cc @@ -25,6 +25,7 @@ #include "iceberg/util/decimal.h" #include "iceberg/util/uuid.h" +#include "temporal_test_helper.h" namespace iceberg { @@ -41,27 +42,55 @@ TEST(BucketUtilsTest, HashHelper) { EXPECT_EQ(BucketUtils::HashBytes(decimal->ToBigEndian()), -500754589); // date hash - std::chrono::sys_days sd = std::chrono::year{2017} / 11 / 16; - std::chrono::sys_days epoch{std::chrono::year{1970} / 1 / 1}; - int32_t days = (sd - epoch).count(); - EXPECT_EQ(BucketUtils::HashInt(days), -653330422); + EXPECT_EQ(BucketUtils::HashInt( + TemporalTestHelper::CreateDate({.year = 2017, .month = 11, .day = 16})), + -653330422); // time - // 22:31:08 in microseconds - int64_t time_micros = (22 * 3600 + 31 * 60 + 8) * 1000000LL; - EXPECT_EQ(BucketUtils::HashLong(time_micros), -662762989); + EXPECT_EQ(BucketUtils::HashLong( + TemporalTestHelper::CreateTime({.hour = 22, .minute = 31, .second = 8})), + -662762989); // timestamp // 2017-11-16T22:31:08 in microseconds - std::chrono::system_clock::time_point tp = - std::chrono::sys_days{std::chrono::year{2017} / 11 / 16} + std::chrono::hours{22} + - std::chrono::minutes{31} + std::chrono::seconds{8}; - int64_t timestamp_micros = - std::chrono::duration_cast(tp.time_since_epoch()) - .count(); - EXPECT_EQ(BucketUtils::HashLong(timestamp_micros), -2047944441); + EXPECT_EQ( + BucketUtils::HashLong(TemporalTestHelper::CreateTimestamp( + {.year = 2017, .month = 11, .day = 16, .hour = 22, .minute = 31, .second = 8})), + -2047944441); + // 2017-11-16T22:31:08.000001 in microseconds - EXPECT_EQ(BucketUtils::HashLong(timestamp_micros + 1), -1207196810); + EXPECT_EQ( + BucketUtils::HashLong(TemporalTestHelper::CreateTimestamp({.year = 2017, + .month = 11, + .day = 16, + .hour = 22, + .minute = 31, + .second = 8, + .microsecond = 1})), + -1207196810); + + // 2017-11-16T14:31:08-08:00 in microseconds + EXPECT_EQ(BucketUtils::HashLong( + TemporalTestHelper::CreateTimestampTz({.year = 2017, + .month = 11, + .day = 16, + .hour = 14, + .minute = 31, + .second = 8, + .tz_offset_minutes = -480})), + -2047944441); + + // 2017-11-16T14:31:08.000001-08:00 in microseconds + EXPECT_EQ(BucketUtils::HashLong( + TemporalTestHelper::CreateTimestampTz({.year = 2017, + .month = 11, + .day = 16, + .hour = 14, + .minute = 31, + .second = 8, + .microsecond = 1, + .tz_offset_minutes = -480})), + -1207196810); // string std::string str = "iceberg"; diff --git a/src/iceberg/test/temporal_test_helper.h b/src/iceberg/test/temporal_test_helper.h new file mode 100644 index 000000000..0f2904891 --- /dev/null +++ b/src/iceberg/test/temporal_test_helper.h @@ -0,0 +1,128 @@ +/* + * 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. + */ + +#pragma once + +#include +#include + +namespace iceberg { + +using namespace std::chrono; // NOLINT + +struct DateParts { + int32_t year{0}; + uint8_t month{0}; + uint8_t day{0}; +}; + +struct TimeParts { + int32_t hour{0}; + int32_t minute{0}; + int32_t second{0}; + int32_t microsecond{0}; +}; + +struct TimestampParts { + int32_t year{0}; + uint8_t month{0}; + uint8_t day{0}; + int32_t hour{0}; + int32_t minute{0}; + int32_t second{0}; + int32_t microsecond{0}; + // e.g. -480 for PST (UTC-8:00), +480 for Asia/Shanghai (UTC+8:00) + int32_t tz_offset_minutes{0}; +}; + +struct TimestampNanosParts { + int32_t year{0}; + uint8_t month{0}; + uint8_t day{0}; + int32_t hour{0}; + int32_t minute{0}; + int32_t second{0}; + int32_t nanosecond{0}; + // e.g. -480 for PST (UTC-8:00), +480 for Asia/Shanghai (UTC+8:00) + int32_t tz_offset_minutes{0}; +}; + +class TemporalTestHelper { + static constexpr auto kEpochDays = sys_days(year{1970} / January / 1); + + public: + /// \brief Construct a Calendar date without timezone or time + static int32_t CreateDate(const DateParts& parts) { + return static_cast( + (sys_days(year{parts.year} / month{parts.month} / day{parts.day}) - kEpochDays) + .count()); + } + + /// \brief Construct a time-of-day, microsecond precision, without date, timezone + static int64_t CreateTime(const TimeParts& parts) { + return duration_cast(hours(parts.hour) + minutes(parts.minute) + + seconds(parts.second) + + microseconds(parts.microsecond)) + .count(); + } + + /// \brief Construct a timestamp, microsecond precision, without timezone + static int64_t CreateTimestamp(const TimestampParts& parts) { + year_month_day ymd{year{parts.year}, month{parts.month}, day{parts.day}}; + auto tp = sys_time{(sys_days(ymd) + hours{parts.hour} + + minutes{parts.minute} + seconds{parts.second} + + microseconds{parts.microsecond}) + .time_since_epoch()}; + return tp.time_since_epoch().count(); + } + + /// \brief Construct a timestamp, microsecond precision, with timezone + static int64_t CreateTimestampTz(const TimestampParts& parts) { + year_month_day ymd{year{parts.year}, month{parts.month}, day{parts.day}}; + auto tp = sys_time{(sys_days(ymd) + hours{parts.hour} + + minutes{parts.minute} + seconds{parts.second} + + microseconds{parts.microsecond} - + minutes{parts.tz_offset_minutes}) + .time_since_epoch()}; + return tp.time_since_epoch().count(); + } + + /// \brief Construct a timestamp, nanosecond precision, without timezone + static int64_t CreateTimestampNanos(const TimestampNanosParts& parts) { + year_month_day ymd{year{parts.year}, month{parts.month}, day{parts.day}}; + auto tp = + sys_time{(sys_days(ymd) + hours{parts.hour} + minutes{parts.minute} + + seconds{parts.second} + nanoseconds{parts.nanosecond}) + .time_since_epoch()}; + return tp.time_since_epoch().count(); + } + + /// \brief Construct a timestamp, nanosecond precision, with timezone + static int64_t CreateTimestampTzNanos(const TimestampNanosParts& parts) { + year_month_day ymd{year{parts.year}, month{parts.month}, day{parts.day}}; + auto tp = + sys_time{(sys_days(ymd) + hours{parts.hour} + minutes{parts.minute} + + seconds{parts.second} + nanoseconds{parts.nanosecond} - + minutes{parts.tz_offset_minutes}) + .time_since_epoch()}; + return tp.time_since_epoch().count(); + } +}; + +} // namespace iceberg diff --git a/src/iceberg/test/transform_test.cc b/src/iceberg/test/transform_test.cc index 1003b9532..6d72bdce9 100644 --- a/src/iceberg/test/transform_test.cc +++ b/src/iceberg/test/transform_test.cc @@ -27,10 +27,10 @@ #include #include "iceberg/expression/literal.h" -#include "iceberg/transform_function.h" #include "iceberg/type.h" #include "iceberg/util/formatter.h" // IWYU pragma: keep #include "matchers.h" +#include "temporal_test_helper.h" namespace iceberg { @@ -315,25 +315,40 @@ INSTANTIATE_TEST_SUITE_P( .source = Literal::Decimal(1420, 4, 2), .expected = Literal::Int(3)}, TransformParam{.str = "Date", - // 2017-11-16 .source_type = iceberg::date(), - .source = Literal::Date(17486), + .source = Literal::Date(TemporalTestHelper::CreateDate( + {.year = 2017, .month = 11, .day = 16})), .expected = Literal::Int(2)}, TransformParam{.str = "Time", - // 22:31:08 in microseconds .source_type = iceberg::time(), - .source = Literal::Time(81068000000), + .source = Literal::Time(TemporalTestHelper::CreateTime( + {.hour = 22, .minute = 31, .second = 8})), .expected = Literal::Int(3)}, TransformParam{.str = "Timestamp", // 2017-11-16T22:31:08 in microseconds .source_type = iceberg::timestamp(), - .source = Literal::Timestamp(1510871468000000), + .source = Literal::Timestamp( + TemporalTestHelper::CreateTimestamp({.year = 2017, + .month = 11, + .day = 16, + .hour = 22, + .minute = 31, + .second = 8})), .expected = Literal::Int(3)}, - TransformParam{.str = "TimestampTz", - // 2017-11-16T22:31:08.000001 in microseconds - .source_type = iceberg::timestamp_tz(), - .source = Literal::TimestampTz(1510871468000001), - .expected = Literal::Int(2)}, + TransformParam{ + .str = "TimestampTz", + // 2017-11-16T14:31:08.000001-08:00 in microseconds + .source_type = iceberg::timestamp_tz(), + .source = Literal::TimestampTz( + TemporalTestHelper::CreateTimestampTz({.year = 2017, + .month = 11, + .day = 16, + .hour = 14, + .minute = 31, + .second = 8, + .microsecond = 1, + .tz_offset_minutes = -480})), + .expected = Literal::Int(2)}, TransformParam{.str = "String", .source_type = iceberg::string(), .source = Literal::String("iceberg"), @@ -428,19 +443,36 @@ TEST_P(YearTransformTest, YearTransform) { INSTANTIATE_TEST_SUITE_P( YearTransformTests, YearTransformTest, - ::testing::Values(TransformParam{.str = "Timestamp", - // 2021-06-01T11:43:20Z - .source_type = iceberg::timestamp(), - .source = Literal::Timestamp(1622547800000000), - .expected = Literal::Int(2021)}, - TransformParam{.str = "TimestampTz", - .source_type = iceberg::timestamp_tz(), - .source = Literal::TimestampTz(1622547800000000), - .expected = Literal::Int(2021)}, - TransformParam{.str = "Date", - .source_type = iceberg::date(), - .source = Literal::Date(30000), - .expected = Literal::Int(2052)}), + ::testing::Values( + TransformParam{.str = "Timestamp", + // 2021-06-01T11:43:20Z + .source_type = iceberg::timestamp(), + .source = Literal::Timestamp( + TemporalTestHelper::CreateTimestamp({.year = 2021, + .month = 6, + .day = 1, + .hour = 11, + .minute = 43, + .second = 20})), + .expected = Literal::Int(2021)}, + TransformParam{ + .str = "TimestampTz", + // 2021-01-01T07:43:20+08:00, which is 2020-12-31T23:43:20Z + .source_type = iceberg::timestamp_tz(), + .source = Literal::TimestampTz( + TemporalTestHelper::CreateTimestampTz({.year = 2021, + .month = 1, + .day = 1, + .hour = 7, + .minute = 43, + .second = 20, + .tz_offset_minutes = 480})), + .expected = Literal::Int(2020)}, + TransformParam{.str = "Date", + .source_type = iceberg::date(), + .source = Literal::Date(TemporalTestHelper::CreateDate( + {.year = 2052, .month = 2, .day = 20})), + .expected = Literal::Int(2052)}), [](const ::testing::TestParamInfo& info) { return info.param.str; }); class MonthTransformTest : public ::testing::TestWithParam {}; @@ -495,18 +527,35 @@ TEST_P(DayTransformTest, DayTransform) { INSTANTIATE_TEST_SUITE_P( DayTransformTests, DayTransformTest, - ::testing::Values(TransformParam{.str = "Timestamp", - .source_type = iceberg::timestamp(), - .source = Literal::Timestamp(1622547800000000), - .expected = Literal::Int(18779)}, - TransformParam{.str = "TimestampTz", - .source_type = iceberg::timestamp_tz(), - .source = Literal::TimestampTz(1622547800000000), - .expected = Literal::Int(18779)}, - TransformParam{.str = "Date", - .source_type = iceberg::date(), - .source = Literal::Date(30000), - .expected = Literal::Int(30000)}), + ::testing::Values( + TransformParam{.str = "Timestamp", + .source_type = iceberg::timestamp(), + .source = Literal::Timestamp( + TemporalTestHelper::CreateTimestamp({.year = 2021, + .month = 6, + .day = 1, + .hour = 11, + .minute = 43, + .second = 20})), + .expected = Literal::Int(TemporalTestHelper::CreateDate( + {.year = 2021, .month = 6, .day = 1}))}, + TransformParam{ + .str = "TimestampTz", + .source_type = iceberg::timestamp_tz(), + .source = Literal::TimestampTz( + TemporalTestHelper::CreateTimestampTz({.year = 2021, + .month = 1, + .day = 1, + .hour = 7, + .minute = 43, + .second = 20, + .tz_offset_minutes = 480})), + .expected = Literal::Int( + TemporalTestHelper::CreateDate({.year = 2020, .month = 12, .day = 31}))}, + TransformParam{.str = "Date", + .source_type = iceberg::date(), + .source = Literal::Date(30000), + .expected = Literal::Int(30000)}), [](const ::testing::TestParamInfo& info) { return info.param.str; }); class HourTransformTest : public ::testing::TestWithParam {}; diff --git a/src/iceberg/util/temporal_util.cc b/src/iceberg/util/temporal_util.cc index 41748c920..0112e4925 100644 --- a/src/iceberg/util/temporal_util.cc +++ b/src/iceberg/util/temporal_util.cc @@ -20,6 +20,7 @@ #include "iceberg/util/temporal_util.h" #include +#include #include #include "iceberg/expression/literal.h"