| /* |
| * 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. |
| */ |
| |
| #include "iceberg/util/temporal_util.h" |
| |
| #include <chrono> |
| #include <cstdint> |
| #include <limits> |
| #include <utility> |
| |
| #include "iceberg/expression/literal.h" |
| #include "iceberg/util/int128.h" |
| #include "iceberg/util/macros.h" |
| #include "iceberg/util/math_util_internal.h" |
| #include "iceberg/util/string_util.h" |
| |
| namespace iceberg { |
| |
| namespace { |
| |
| using namespace std::chrono; // NOLINT |
| |
| /// Parse a timezone offset of the form "+HH:mm" or "-HH:mm" and return the |
| /// offset in microseconds (positive for east of UTC, negative for west). |
| Result<int64_t> ParseTimezoneOffset(std::string_view offset) { |
| if (offset.size() != 6 || (offset[0] != '+' && offset[0] != '-') || offset[3] != ':') { |
| return InvalidArgument("Invalid timezone offset: '{}'", offset); |
| } |
| bool negative = offset[0] == '-'; |
| ICEBERG_ASSIGN_OR_RAISE(auto hours, |
| StringUtils::ParseNumber<int64_t>(offset.substr(1, 2))); |
| ICEBERG_ASSIGN_OR_RAISE(auto minutes, |
| StringUtils::ParseNumber<int64_t>(offset.substr(4, 2))); |
| if (hours > 18 || minutes > 59) [[unlikely]] { |
| return InvalidArgument("Invalid timezone offset: '{}'", offset); |
| } |
| |
| if (hours == 18 && minutes != 0) [[unlikely]] { |
| return InvalidArgument("Timezone offset '{}' not in range [-18:00, +18:00]", offset); |
| } |
| |
| auto micros = hours * internal::kSecondsPerHour * internal::kMicrosPerSecond + |
| minutes * internal::kSecondsPerMinute * internal::kMicrosPerSecond; |
| return negative ? -micros : micros; |
| } |
| |
| Result<std::pair<std::string_view, int64_t>> ParseTimestampWithZoneSuffix( |
| std::string_view str) { |
| if (str.empty()) [[unlikely]] { |
| return InvalidArgument("Invalid timestamptz string: '{}'", str); |
| } |
| |
| int64_t offset_micros = 0; |
| std::string_view timestamp_part; |
| |
| if (str.back() == 'Z') { |
| timestamp_part = str.substr(0, str.size() - 1); |
| } else if (str.size() >= 6 && |
| (str[str.size() - 6] == '+' || str[str.size() - 6] == '-')) { |
| ICEBERG_ASSIGN_OR_RAISE(offset_micros, |
| ParseTimezoneOffset(str.substr(str.size() - 6))); |
| timestamp_part = str.substr(0, str.size() - 6); |
| } else { |
| return InvalidArgument("Invalid timestamptz string (missing timezone suffix): '{}'", |
| str); |
| } |
| |
| return std::make_pair(timestamp_part, offset_micros); |
| } |
| |
| Result<int64_t> TimestampFromDayTime(int32_t days, int64_t time_units, |
| int64_t units_per_day, int64_t offset_micros, |
| int64_t units_per_micro) { |
| const auto offset_units = |
| static_cast<int128_t>(offset_micros) * static_cast<int128_t>(units_per_micro); |
| const auto timestamp = |
| static_cast<int128_t>(days) * static_cast<int128_t>(units_per_day) + |
| static_cast<int128_t>(time_units) - offset_units; |
| |
| if (timestamp > std::numeric_limits<int64_t>::max() || |
| timestamp < std::numeric_limits<int64_t>::min()) [[unlikely]] { |
| return InvalidArgument("Timestamp value is out of int64 range"); |
| } |
| |
| return static_cast<int64_t>(timestamp); |
| } |
| |
| /// Parse fractional seconds (after '.') and return micros. |
| /// Digits beyond 6 are truncated. |
| Result<int64_t> ParseFractionalMicros(std::string_view frac) { |
| if (frac.empty() || frac.size() > 9) [[unlikely]] { |
| return InvalidArgument("Invalid fractional seconds: '{}'", frac); |
| } |
| if (frac.size() > 6) frac = frac.substr(0, 6); |
| ICEBERG_ASSIGN_OR_RAISE(auto val, StringUtils::ParseNumber<int32_t>(frac)); |
| for (size_t i = frac.size(); i < 6; ++i) { |
| val *= 10; |
| } |
| return static_cast<int64_t>(val); |
| } |
| |
| /// Parse fractional seconds (after '.') and return nanos. |
| Result<int64_t> ParseFractionalNanos(std::string_view frac) { |
| if (frac.empty() || frac.size() > 9) [[unlikely]] { |
| return InvalidArgument("Invalid fractional seconds: '{}'", frac); |
| } |
| ICEBERG_ASSIGN_OR_RAISE(auto val, StringUtils::ParseNumber<int32_t>(frac)); |
| for (size_t i = frac.size(); i < 9; ++i) { |
| val *= 10; |
| } |
| return static_cast<int64_t>(val); |
| } |
| |
| template <typename TimeScaleParser> |
| Result<int64_t> ParseTimeWithFraction(std::string_view str, int64_t units_per_second, |
| TimeScaleParser&& parse_fraction) { |
| if (str.size() < 5 || str[2] != ':') [[unlikely]] { |
| return InvalidArgument("Invalid time string: '{}'", str); |
| } |
| |
| ICEBERG_ASSIGN_OR_RAISE(auto hours, |
| StringUtils::ParseNumber<int64_t>(str.substr(0, 2))); |
| ICEBERG_ASSIGN_OR_RAISE(auto minutes, |
| StringUtils::ParseNumber<int64_t>(str.substr(3, 2))); |
| int64_t seconds = 0; |
| |
| int64_t frac_units = 0; |
| if (str.size() > 5) { |
| if (str[5] != ':' || str.size() < 8) [[unlikely]] { |
| return InvalidArgument("Invalid time string: '{}'", str); |
| } |
| ICEBERG_ASSIGN_OR_RAISE(seconds, StringUtils::ParseNumber<int64_t>(str.substr(6, 2))); |
| if (str.size() > 8) { |
| if (str[8] != '.') [[unlikely]] { |
| return InvalidArgument("Invalid time string: '{}'", str); |
| } |
| ICEBERG_ASSIGN_OR_RAISE(frac_units, parse_fraction(str.substr(9))); |
| } |
| } |
| |
| if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59 || seconds < 0 || |
| seconds > 59) [[unlikely]] { |
| return InvalidArgument("Invalid time string: '{}'", str); |
| } |
| |
| return hours * internal::kSecondsPerHour * units_per_second + |
| minutes * internal::kSecondsPerMinute * units_per_second + |
| seconds * units_per_second + frac_units; |
| } |
| |
| inline constexpr year_month_day DateToYmd(int32_t days_since_epoch) { |
| return {internal::kEpochDays + days{days_since_epoch}}; |
| } |
| |
| inline constexpr year_month_day TimestampToYmd(int64_t micros_since_epoch) { |
| return {floor<days>(sys_time<microseconds>(microseconds{micros_since_epoch}))}; |
| } |
| |
| inline constexpr year_month_day TimestampNsToYmd(int64_t nanos_since_epoch) { |
| return {floor<days>(sys_time<nanoseconds>(nanoseconds{nanos_since_epoch}))}; |
| } |
| |
| template <typename Duration> |
| requires std::is_same_v<Duration, days> || std::is_same_v<Duration, hours> |
| inline constexpr int32_t TimestampToDuration(int64_t micros_since_epoch) { |
| return static_cast<int32_t>( |
| floor<Duration>( |
| sys_time<microseconds>(microseconds{micros_since_epoch}).time_since_epoch()) |
| .count()); |
| } |
| |
| template <typename Duration> |
| requires std::is_same_v<Duration, days> || std::is_same_v<Duration, hours> |
| inline constexpr int32_t TimestampNsToDuration(int64_t nanos_since_epoch) { |
| return static_cast<int32_t>( |
| floor<Duration>( |
| sys_time<nanoseconds>(nanoseconds{nanos_since_epoch}).time_since_epoch()) |
| .count()); |
| } |
| |
| inline constexpr int32_t MonthsSinceEpoch(const year_month_day& ymd) { |
| auto delta = ymd.year() - internal::kEpochYmd.year(); |
| // Calculate the month as months from 1970-01 |
| // Note: January is month 1, so we subtract 1 to get zero-based month count. |
| return static_cast<int32_t>(delta.count() * 12 + static_cast<unsigned>(ymd.month()) - |
| 1); |
| } |
| |
| template <TypeId type_id> |
| Result<Literal> ExtractYearImpl(const Literal& literal) { |
| std::unreachable(); |
| } |
| |
| template <> |
| Result<Literal> ExtractYearImpl<TypeId::kDate>(const Literal& literal) { |
| auto value = std::get<int32_t>(literal.value()); |
| auto ymd = DateToYmd(value); |
| return Literal::Int((ymd.year() - internal::kEpochYmd.year()).count()); |
| } |
| |
| template <> |
| Result<Literal> ExtractYearImpl<TypeId::kTimestamp>(const Literal& literal) { |
| auto value = std::get<int64_t>(literal.value()); |
| auto ymd = TimestampToYmd(value); |
| return Literal::Int((ymd.year() - internal::kEpochYmd.year()).count()); |
| } |
| |
| template <> |
| Result<Literal> ExtractYearImpl<TypeId::kTimestampNs>(const Literal& literal) { |
| auto value = std::get<int64_t>(literal.value()); |
| auto ymd = TimestampNsToYmd(value); |
| return Literal::Int((ymd.year() - internal::kEpochYmd.year()).count()); |
| } |
| |
| template <> |
| Result<Literal> ExtractYearImpl<TypeId::kTimestampTz>(const Literal& literal) { |
| return ExtractYearImpl<TypeId::kTimestamp>(literal); |
| } |
| |
| template <> |
| Result<Literal> ExtractYearImpl<TypeId::kTimestampTzNs>(const Literal& literal) { |
| return ExtractYearImpl<TypeId::kTimestampNs>(literal); |
| } |
| |
| template <TypeId type_id> |
| Result<Literal> ExtractMonthImpl(const Literal& literal) { |
| std::unreachable(); |
| } |
| |
| template <> |
| Result<Literal> ExtractMonthImpl<TypeId::kDate>(const Literal& literal) { |
| auto value = std::get<int32_t>(literal.value()); |
| auto ymd = DateToYmd(value); |
| return Literal::Int(MonthsSinceEpoch(ymd)); |
| } |
| |
| template <> |
| Result<Literal> ExtractMonthImpl<TypeId::kTimestamp>(const Literal& literal) { |
| auto value = std::get<int64_t>(literal.value()); |
| auto ymd = TimestampToYmd(value); |
| return Literal::Int(MonthsSinceEpoch(ymd)); |
| } |
| |
| template <> |
| Result<Literal> ExtractMonthImpl<TypeId::kTimestampNs>(const Literal& literal) { |
| auto value = std::get<int64_t>(literal.value()); |
| auto ymd = TimestampNsToYmd(value); |
| return Literal::Int(MonthsSinceEpoch(ymd)); |
| } |
| |
| template <> |
| Result<Literal> ExtractMonthImpl<TypeId::kTimestampTz>(const Literal& literal) { |
| return ExtractMonthImpl<TypeId::kTimestamp>(literal); |
| } |
| |
| template <> |
| Result<Literal> ExtractMonthImpl<TypeId::kTimestampTzNs>(const Literal& literal) { |
| return ExtractMonthImpl<TypeId::kTimestampNs>(literal); |
| } |
| |
| template <TypeId type_id> |
| Result<Literal> ExtractDayImpl(const Literal& literal) { |
| std::unreachable(); |
| } |
| |
| template <> |
| Result<Literal> ExtractDayImpl<TypeId::kDate>(const Literal& literal) { |
| return Literal::Int(std::get<int32_t>(literal.value())); |
| } |
| |
| template <> |
| Result<Literal> ExtractDayImpl<TypeId::kTimestamp>(const Literal& literal) { |
| auto value = std::get<int64_t>(literal.value()); |
| return Literal::Int(TimestampToDuration<days>(value)); |
| } |
| |
| template <> |
| Result<Literal> ExtractDayImpl<TypeId::kTimestampNs>(const Literal& literal) { |
| auto value = std::get<int64_t>(literal.value()); |
| return Literal::Int(TimestampNsToDuration<days>(value)); |
| } |
| |
| template <> |
| Result<Literal> ExtractDayImpl<TypeId::kTimestampTz>(const Literal& literal) { |
| return ExtractDayImpl<TypeId::kTimestamp>(literal); |
| } |
| |
| template <> |
| Result<Literal> ExtractDayImpl<TypeId::kTimestampTzNs>(const Literal& literal) { |
| return ExtractDayImpl<TypeId::kTimestampNs>(literal); |
| } |
| |
| template <TypeId type_id> |
| Result<Literal> ExtractHourImpl(const Literal& literal) { |
| std::unreachable(); |
| } |
| |
| template <> |
| Result<Literal> ExtractHourImpl<TypeId::kTimestamp>(const Literal& literal) { |
| auto value = std::get<int64_t>(literal.value()); |
| return Literal::Int(TimestampToDuration<hours>(value)); |
| } |
| |
| template <> |
| Result<Literal> ExtractHourImpl<TypeId::kTimestampNs>(const Literal& literal) { |
| auto value = std::get<int64_t>(literal.value()); |
| return Literal::Int(TimestampNsToDuration<hours>(value)); |
| } |
| |
| template <> |
| Result<Literal> ExtractHourImpl<TypeId::kTimestampTz>(const Literal& literal) { |
| return ExtractHourImpl<TypeId::kTimestamp>(literal); |
| } |
| |
| template <> |
| Result<Literal> ExtractHourImpl<TypeId::kTimestampTzNs>(const Literal& literal) { |
| return ExtractHourImpl<TypeId::kTimestampNs>(literal); |
| } |
| |
| } // namespace |
| |
| int64_t TemporalUtils::NanosToMicros(int64_t nanos) { |
| return FloorDiv(nanos, internal::kNanosPerMicro); |
| } |
| |
| Result<int64_t> TemporalUtils::MicrosToNanos(int64_t micros) { |
| return MultiplyExact(micros, internal::kNanosPerMicro); |
| } |
| |
| Result<int32_t> TemporalUtils::ParseDay(std::string_view str) { |
| auto dash1 = str.find('-', (!str.empty() && (str[0] == '-' || str[0] == '+')) ? 1 : 0); |
| auto dash2 = str.find('-', dash1 + 1); |
| if (str.size() < 10 || dash1 == std::string_view::npos || |
| dash2 == std::string_view::npos) [[unlikely]] { |
| return InvalidArgument("Invalid date string: '{}'", str); |
| } |
| auto year_str = str.substr(0, dash1); |
| if (!year_str.empty() && year_str[0] == '+') { |
| year_str = year_str.substr(1); |
| } |
| ICEBERG_ASSIGN_OR_RAISE(auto year_value, StringUtils::ParseNumber<int32_t>(year_str)); |
| ICEBERG_ASSIGN_OR_RAISE(auto month_value, StringUtils::ParseNumber<int32_t>(str.substr( |
| dash1 + 1, dash2 - dash1 - 1))); |
| ICEBERG_ASSIGN_OR_RAISE(auto day_value, |
| StringUtils::ParseNumber<int32_t>(str.substr(dash2 + 1))); |
| |
| auto ymd = std::chrono::year{year_value} / |
| std::chrono::month{static_cast<unsigned>(month_value)} / |
| std::chrono::day{static_cast<unsigned>(day_value)}; |
| if (!ymd.ok()) [[unlikely]] { |
| return InvalidArgument("Invalid date: '{}'", str); |
| } |
| |
| auto days_since_epoch = std::chrono::sys_days{ymd} - internal::kEpochDays; |
| return static_cast<int32_t>(days_since_epoch.count()); |
| } |
| |
| Result<int64_t> TemporalUtils::ParseTime(std::string_view str) { |
| return ParseTimeWithFraction(str, internal::kMicrosPerSecond, ParseFractionalMicros); |
| } |
| |
| Result<int64_t> TemporalUtils::ParseTimeNs(std::string_view str) { |
| return ParseTimeWithFraction(str, internal::kNanosPerSecond, ParseFractionalNanos); |
| } |
| |
| Result<int64_t> TemporalUtils::ParseTimestamp(std::string_view str) { |
| auto t_pos = str.find('T'); |
| if (t_pos == std::string_view::npos) [[unlikely]] { |
| return InvalidArgument("Invalid timestamp string (missing 'T'): '{}'", str); |
| } |
| |
| ICEBERG_ASSIGN_OR_RAISE(auto days_since_epoch, ParseDay(str.substr(0, t_pos))); |
| ICEBERG_ASSIGN_OR_RAISE(auto time_micros, ParseTime(str.substr(t_pos + 1))); |
| |
| return TimestampFromDayTime(days_since_epoch, time_micros, internal::kMicrosPerDay, |
| /*offset_micros=*/0, /*units_per_micro=*/1); |
| } |
| |
| Result<int64_t> TemporalUtils::ParseTimestampNs(std::string_view str) { |
| auto t_pos = str.find('T'); |
| if (t_pos == std::string_view::npos) [[unlikely]] { |
| return InvalidArgument("Invalid timestamp string (missing 'T'): '{}'", str); |
| } |
| |
| ICEBERG_ASSIGN_OR_RAISE(auto days_since_epoch, ParseDay(str.substr(0, t_pos))); |
| ICEBERG_ASSIGN_OR_RAISE(auto time_nanos, ParseTimeNs(str.substr(t_pos + 1))); |
| |
| return TimestampFromDayTime(days_since_epoch, time_nanos, internal::kNanosPerDay, |
| /*offset_micros=*/0, |
| /*units_per_micro=*/internal::kNanosPerMicro); |
| } |
| |
| Result<int64_t> TemporalUtils::ParseTimestampWithZone(std::string_view str) { |
| ICEBERG_ASSIGN_OR_RAISE(auto timestamp_with_offset, ParseTimestampWithZoneSuffix(str)); |
| const auto [timestamp_part, offset_micros] = timestamp_with_offset; |
| |
| auto t_pos = timestamp_part.find('T'); |
| if (t_pos == std::string_view::npos) [[unlikely]] { |
| return InvalidArgument("Invalid timestamp string (missing 'T'): '{}'", |
| timestamp_part); |
| } |
| |
| ICEBERG_ASSIGN_OR_RAISE(auto days_since_epoch, |
| ParseDay(timestamp_part.substr(0, t_pos))); |
| ICEBERG_ASSIGN_OR_RAISE(auto time_micros, ParseTime(timestamp_part.substr(t_pos + 1))); |
| |
| return TimestampFromDayTime(days_since_epoch, time_micros, internal::kMicrosPerDay, |
| offset_micros, |
| /*units_per_micro=*/1); |
| } |
| |
| Result<int64_t> TemporalUtils::ParseTimestampNsWithZone(std::string_view str) { |
| ICEBERG_ASSIGN_OR_RAISE(auto timestamp_with_offset, ParseTimestampWithZoneSuffix(str)); |
| const auto [timestamp_part, offset_micros] = timestamp_with_offset; |
| |
| auto t_pos = timestamp_part.find('T'); |
| if (t_pos == std::string_view::npos) [[unlikely]] { |
| return InvalidArgument("Invalid timestamp string (missing 'T'): '{}'", |
| timestamp_part); |
| } |
| |
| ICEBERG_ASSIGN_OR_RAISE(auto days_since_epoch, |
| ParseDay(timestamp_part.substr(0, t_pos))); |
| ICEBERG_ASSIGN_OR_RAISE(auto time_nanos, ParseTimeNs(timestamp_part.substr(t_pos + 1))); |
| |
| return TimestampFromDayTime(days_since_epoch, time_nanos, internal::kNanosPerDay, |
| offset_micros, |
| /*units_per_micro=*/internal::kNanosPerMicro); |
| } |
| |
| #define DISPATCH_EXTRACT_YEAR(type_id) \ |
| case type_id: \ |
| return ExtractYearImpl<type_id>(literal); |
| |
| Result<Literal> TemporalUtils::ExtractYear(const Literal& literal) { |
| if (literal.IsNull()) [[unlikely]] { |
| return Literal::Null(int32()); |
| } |
| |
| if (literal.IsAboveMax() || literal.IsBelowMin()) [[unlikely]] { |
| return NotSupported("Cannot extract year from {}", literal.ToString()); |
| } |
| |
| switch (literal.type()->type_id()) { |
| DISPATCH_EXTRACT_YEAR(TypeId::kDate) |
| DISPATCH_EXTRACT_YEAR(TypeId::kTimestamp) |
| DISPATCH_EXTRACT_YEAR(TypeId::kTimestampTz) |
| DISPATCH_EXTRACT_YEAR(TypeId::kTimestampNs) |
| DISPATCH_EXTRACT_YEAR(TypeId::kTimestampTzNs) |
| default: |
| return NotSupported("Extract year from type {} is not supported", |
| literal.type()->ToString()); |
| } |
| } |
| |
| #define DISPATCH_EXTRACT_MONTH(type_id) \ |
| case type_id: \ |
| return ExtractMonthImpl<type_id>(literal); |
| |
| Result<Literal> TemporalUtils::ExtractMonth(const Literal& literal) { |
| if (literal.IsNull()) [[unlikely]] { |
| return Literal::Null(int32()); |
| } |
| |
| if (literal.IsAboveMax() || literal.IsBelowMin()) [[unlikely]] { |
| return NotSupported("Cannot extract month from {}", literal.ToString()); |
| } |
| |
| switch (literal.type()->type_id()) { |
| DISPATCH_EXTRACT_MONTH(TypeId::kDate) |
| DISPATCH_EXTRACT_MONTH(TypeId::kTimestamp) |
| DISPATCH_EXTRACT_MONTH(TypeId::kTimestampTz) |
| DISPATCH_EXTRACT_MONTH(TypeId::kTimestampNs) |
| DISPATCH_EXTRACT_MONTH(TypeId::kTimestampTzNs) |
| default: |
| return NotSupported("Extract month from type {} is not supported", |
| literal.type()->ToString()); |
| } |
| } |
| |
| #define DISPATCH_EXTRACT_DAY(type_id) \ |
| case type_id: \ |
| return ExtractDayImpl<type_id>(literal); |
| |
| Result<Literal> TemporalUtils::ExtractDay(const Literal& literal) { |
| if (literal.IsNull()) [[unlikely]] { |
| return Literal::Null(int32()); |
| } |
| |
| if (literal.IsAboveMax() || literal.IsBelowMin()) [[unlikely]] { |
| return NotSupported("Cannot extract day from {}", literal.ToString()); |
| } |
| |
| switch (literal.type()->type_id()) { |
| DISPATCH_EXTRACT_DAY(TypeId::kDate) |
| DISPATCH_EXTRACT_DAY(TypeId::kTimestamp) |
| DISPATCH_EXTRACT_DAY(TypeId::kTimestampTz) |
| DISPATCH_EXTRACT_DAY(TypeId::kTimestampNs) |
| DISPATCH_EXTRACT_DAY(TypeId::kTimestampTzNs) |
| default: |
| return NotSupported("Extract day from type {} is not supported", |
| literal.type()->ToString()); |
| } |
| } |
| |
| #define DISPATCH_EXTRACT_HOUR(type_id) \ |
| case type_id: \ |
| return ExtractHourImpl<type_id>(literal); |
| |
| Result<Literal> TemporalUtils::ExtractHour(const Literal& literal) { |
| if (literal.IsNull()) [[unlikely]] { |
| return Literal::Null(int32()); |
| } |
| |
| if (literal.IsAboveMax() || literal.IsBelowMin()) [[unlikely]] { |
| return NotSupported("Cannot extract hour from {}", literal.ToString()); |
| } |
| |
| switch (literal.type()->type_id()) { |
| DISPATCH_EXTRACT_HOUR(TypeId::kTimestamp) |
| DISPATCH_EXTRACT_HOUR(TypeId::kTimestampTz) |
| DISPATCH_EXTRACT_HOUR(TypeId::kTimestampNs) |
| DISPATCH_EXTRACT_HOUR(TypeId::kTimestampTzNs) |
| default: |
| return NotSupported("Extract hour from type {} is not supported", |
| literal.type()->ToString()); |
| } |
| } |
| |
| } // namespace iceberg |