You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by ze...@apache.org on 2025/08/11 19:41:11 UTC
(iceberg-cpp) branch main updated: feat: implement Literal Transform (#156)
This is an automated email from the ASF dual-hosted git repository.
zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-cpp.git
The following commit(s) were added to refs/heads/main by this push:
new a13eed2 feat: implement Literal Transform (#156)
a13eed2 is described below
commit a13eed2aa1c917ec20e9c75ac7352802735d74a7
Author: Junwang Zhao <zh...@gmail.com>
AuthorDate: Tue Aug 12 03:41:05 2025 +0800
feat: implement Literal Transform (#156)
---
.github/workflows/cpp-linter.yml | 2 +-
.pre-commit-config.yaml | 2 +-
src/iceberg/expression/literal.cc | 45 +++--
src/iceberg/expression/literal.h | 33 +++-
src/iceberg/transform.h | 10 +-
src/iceberg/transform_function.cc | 268 +++++++++++++++++++++++----
src/iceberg/transform_function.h | 52 +++---
src/iceberg/type_fwd.h | 16 +-
src/iceberg/util/string_utils.h | 2 +-
src/iceberg/util/truncate_utils.h | 72 +++++++
test/transform_test.cc | 381 +++++++++++++++++++++++++++++++++++++-
11 files changed, 772 insertions(+), 111 deletions(-)
diff --git a/.github/workflows/cpp-linter.yml b/.github/workflows/cpp-linter.yml
index 2085a49..90f446c 100644
--- a/.github/workflows/cpp-linter.yml
+++ b/.github/workflows/cpp-linter.yml
@@ -47,7 +47,7 @@ jobs:
with:
style: file
tidy-checks: ''
- version: 19
+ version: 22
files-changed-only: true
lines-changed-only: true
thread-comments: true
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 52c8fe5..04d60cf 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -30,7 +30,7 @@ repos:
- id: check-added-large-files
- repo: https://github.com/pre-commit/mirrors-clang-format
- rev: v19.1.5
+ rev: v20.1.8
hooks:
- id: clang-format
exclude: ^test/resources/.*\.json$
diff --git a/src/iceberg/expression/literal.cc b/src/iceberg/expression/literal.cc
index d053f30..e3abb6a 100644
--- a/src/iceberg/expression/literal.cc
+++ b/src/iceberg/expression/literal.cc
@@ -21,7 +21,6 @@
#include <cmath>
#include <concepts>
-#include <sstream>
#include "iceberg/exception.h"
@@ -126,22 +125,28 @@ Literal::Literal(Value value, std::shared_ptr<PrimitiveType> type)
: value_(std::move(value)), type_(std::move(type)) {}
// Factory methods
-Literal Literal::Boolean(bool value) { return {Value{value}, iceberg::boolean()}; }
+Literal Literal::Boolean(bool value) { return {Value{value}, boolean()}; }
-Literal Literal::Int(int32_t value) { return {Value{value}, iceberg::int32()}; }
+Literal Literal::Int(int32_t value) { return {Value{value}, int32()}; }
-Literal Literal::Long(int64_t value) { return {Value{value}, iceberg::int64()}; }
+Literal Literal::Date(int32_t value) { return {Value{value}, date()}; }
-Literal Literal::Float(float value) { return {Value{value}, iceberg::float32()}; }
+Literal Literal::Long(int64_t value) { return {Value{value}, int64()}; }
-Literal Literal::Double(double value) { return {Value{value}, iceberg::float64()}; }
+Literal Literal::Time(int64_t value) { return {Value{value}, time()}; }
-Literal Literal::String(std::string value) {
- return {Value{std::move(value)}, iceberg::string()};
-}
+Literal Literal::Timestamp(int64_t value) { return {Value{value}, timestamp()}; }
+
+Literal Literal::TimestampTz(int64_t value) { return {Value{value}, timestamp_tz()}; }
+
+Literal Literal::Float(float value) { return {Value{value}, float32()}; }
+
+Literal Literal::Double(double value) { return {Value{value}, float64()}; }
+
+Literal Literal::String(std::string value) { return {Value{std::move(value)}, string()}; }
Literal Literal::Binary(std::vector<uint8_t> value) {
- return {Value{std::move(value)}, iceberg::binary()};
+ return {Value{std::move(value)}, binary()};
}
Result<Literal> Literal::Deserialize(std::span<const uint8_t> data,
@@ -188,8 +193,9 @@ std::partial_ordering Literal::operator<=>(const Literal& other) const {
return std::partial_ordering::unordered;
}
- // If either value is AboveMax or BelowMin, comparison is unordered
- if (IsAboveMax() || IsBelowMin() || other.IsAboveMax() || other.IsBelowMin()) {
+ // If either value is AboveMax, BelowMin or null, comparison is unordered
+ if (IsAboveMax() || IsBelowMin() || other.IsAboveMax() || other.IsBelowMin() ||
+ IsNull() || other.IsNull()) {
return std::partial_ordering::unordered;
}
@@ -202,13 +208,16 @@ std::partial_ordering Literal::operator<=>(const Literal& other) const {
return this_val ? std::partial_ordering::greater : std::partial_ordering::less;
}
- case TypeId::kInt: {
+ case TypeId::kInt:
+ case TypeId::kDate: {
auto this_val = std::get<int32_t>(value_);
auto other_val = std::get<int32_t>(other.value_);
return this_val <=> other_val;
}
- case TypeId::kLong: {
+ case TypeId::kLong:
+ case TypeId::kTimestamp:
+ case TypeId::kTimestampTz: {
auto this_val = std::get<int64_t>(value_);
auto other_val = std::get<int64_t>(other.value_);
return this_val <=> other_val;
@@ -253,6 +262,9 @@ std::string Literal::ToString() const {
if (std::holds_alternative<AboveMax>(value_)) {
return "aboveMax";
}
+ if (std::holds_alternative<std::monostate>(value_)) {
+ return "null";
+ }
switch (type_->type_id()) {
case TypeId::kBoolean: {
@@ -301,6 +313,8 @@ bool Literal::IsBelowMin() const { return std::holds_alternative<BelowMin>(value
bool Literal::IsAboveMax() const { return std::holds_alternative<AboveMax>(value_); }
+bool Literal::IsNull() const { return std::holds_alternative<std::monostate>(value_); }
+
// LiteralCaster implementation
Result<Literal> LiteralCaster::CastTo(const Literal& literal,
@@ -312,7 +326,8 @@ Result<Literal> LiteralCaster::CastTo(const Literal& literal,
// Handle special values
if (std::holds_alternative<Literal::BelowMin>(literal.value_) ||
- std::holds_alternative<Literal::AboveMax>(literal.value_)) {
+ std::holds_alternative<Literal::AboveMax>(literal.value_) ||
+ std::holds_alternative<std::monostate>(literal.value_)) {
// Cannot cast type for special values
return NotSupported("Cannot cast type for {}", literal.ToString());
}
diff --git a/src/iceberg/expression/literal.h b/src/iceberg/expression/literal.h
index 739e20f..4c880ef 100644
--- a/src/iceberg/expression/literal.h
+++ b/src/iceberg/expression/literal.h
@@ -32,7 +32,7 @@ namespace iceberg {
/// \brief Literal is a literal value that is associated with a primitive type.
class ICEBERG_EXPORT Literal {
- private:
+ public:
/// \brief Sentinel value to indicate that the literal value is below the valid range
/// of a specific primitive type. It can happen when casting a literal to a narrower
/// primitive type.
@@ -48,27 +48,35 @@ class ICEBERG_EXPORT Literal {
bool operator==(const AboveMax&) const = default;
std::strong_ordering operator<=>(const AboveMax&) const = default;
};
-
- using Value = std::variant<bool, // for boolean
- int32_t, // for int, date
- int64_t, // for long, timestamp, timestamp_tz, time
- float, // for float
- double, // for double
- std::string, // for string
+ using Value = std::variant<std::monostate, // for null
+ bool, // for boolean
+ int32_t, // for int, date
+ int64_t, // for long, timestamp, timestamp_tz, time
+ float, // for float
+ double, // for double
+ std::string, // for string
std::vector<uint8_t>, // for binary, fixed
std::array<uint8_t, 16>, // for uuid and decimal
BelowMin, AboveMax>;
- public:
/// \brief Factory methods for primitive types
static Literal Boolean(bool value);
static Literal Int(int32_t value);
+ static Literal Date(int32_t value);
static Literal Long(int64_t value);
+ static Literal Time(int64_t value);
+ static Literal Timestamp(int64_t value);
+ static Literal TimestampTz(int64_t value);
static Literal Float(float value);
static Literal Double(double value);
static Literal String(std::string value);
static Literal Binary(std::vector<uint8_t> value);
+ /// \brief Create a literal representing a null value.
+ static Literal Null(std::shared_ptr<PrimitiveType> type) {
+ return {Value{std::monostate{}}, std::move(type)};
+ }
+
/// \brief Restore a literal from single-value serialization.
///
/// See [this spec](https://iceberg.apache.org/spec/#binary-single-value-serialization)
@@ -85,6 +93,9 @@ class ICEBERG_EXPORT Literal {
/// \brief Get the literal type.
const std::shared_ptr<PrimitiveType>& type() const;
+ /// \brief Get the literal value.
+ const Value& value() const { return value_; }
+
/// \brief Converts this literal to a literal of the given type.
///
/// When a predicate is bound to a concrete data column, literals are converted to match
@@ -123,6 +134,10 @@ class ICEBERG_EXPORT Literal {
/// \return true if this literal represents a BelowMin value, false otherwise
bool IsBelowMin() const;
+ /// Check if this literal is null.
+ /// \return true if this literal is null, false otherwise
+ bool IsNull() const;
+
std::string ToString() const;
private:
diff --git a/src/iceberg/transform.h b/src/iceberg/transform.h
index f09f15b..3e6709f 100644
--- a/src/iceberg/transform.h
+++ b/src/iceberg/transform.h
@@ -25,7 +25,7 @@
#include <memory>
#include <variant>
-#include "iceberg/arrow_c_data.h"
+#include "iceberg/expression/literal.h"
#include "iceberg/iceberg_export.h"
#include "iceberg/result.h"
#include "iceberg/type_fwd.h"
@@ -170,14 +170,16 @@ class ICEBERG_EXPORT TransformFunction {
public:
virtual ~TransformFunction() = default;
TransformFunction(TransformType transform_type, std::shared_ptr<Type> source_type);
- /// \brief Transform an input array to a new array
- virtual Result<ArrowArray> Transform(const ArrowArray& data) = 0;
+ /// \brief Transform an input Literal to a new Literal
+ ///
+ /// All transforms must return null for a null input value.
+ virtual Result<Literal> Transform(const Literal& literal) = 0;
/// \brief Get the transform type
TransformType transform_type() const;
/// \brief Get the source type of transform function
const std::shared_ptr<Type>& source_type() const;
/// \brief Get the result type of transform function
- virtual Result<std::shared_ptr<Type>> ResultType() const = 0;
+ virtual std::shared_ptr<Type> ResultType() const = 0;
friend bool operator==(const TransformFunction& lhs, const TransformFunction& rhs) {
return lhs.Equals(rhs);
diff --git a/src/iceberg/transform_function.cc b/src/iceberg/transform_function.cc
index 9ddf6e9..0cc227e 100644
--- a/src/iceberg/transform_function.cc
+++ b/src/iceberg/transform_function.cc
@@ -19,20 +19,25 @@
#include "iceberg/transform_function.h"
+#include <cassert>
+#include <chrono>
+#include <type_traits>
+#include <utility>
+#include <variant>
+
+#include "iceberg/expression/literal.h"
#include "iceberg/type.h"
+#include "iceberg/util/murmurhash3_internal.h"
+#include "iceberg/util/truncate_utils.h"
namespace iceberg {
IdentityTransform::IdentityTransform(std::shared_ptr<Type> const& source_type)
: TransformFunction(TransformType::kIdentity, source_type) {}
-Result<ArrowArray> IdentityTransform::Transform(const ArrowArray& input) {
- return NotImplemented("IdentityTransform::Transform");
-}
+Result<Literal> IdentityTransform::Transform(const Literal& literal) { return literal; }
-Result<std::shared_ptr<Type>> IdentityTransform::ResultType() const {
- return source_type();
-}
+std::shared_ptr<Type> IdentityTransform::ResultType() const { return source_type(); }
Result<std::unique_ptr<TransformFunction>> IdentityTransform::Make(
std::shared_ptr<Type> const& source_type) {
@@ -47,14 +52,52 @@ BucketTransform::BucketTransform(std::shared_ptr<Type> const& source_type,
int32_t num_buckets)
: TransformFunction(TransformType::kBucket, source_type), num_buckets_(num_buckets) {}
-Result<ArrowArray> BucketTransform::Transform(const ArrowArray& input) {
- return NotImplemented("BucketTransform::Transform");
-}
+Result<Literal> BucketTransform::Transform(const Literal& literal) {
+ assert(literal.type() == source_type());
+ if (literal.IsBelowMin() || literal.IsAboveMax()) {
+ return InvalidArgument(
+ "Cannot apply bucket transform to literal with value {} of type {}",
+ literal.ToString(), source_type()->ToString());
+ }
+ if (literal.IsNull()) [[unlikely]] {
+ return Literal::Null(int32());
+ }
-Result<std::shared_ptr<Type>> BucketTransform::ResultType() const {
- return iceberg::int32();
+ int32_t hash_value = 0;
+ std::visit(
+ [&](auto&& value) {
+ using T = std::decay_t<decltype(value)>;
+ if constexpr (std::is_same_v<T, int32_t>) {
+ MurmurHash3_x86_32(&value, sizeof(int32_t), 0, &hash_value);
+ } else if constexpr (std::is_same_v<T, int64_t>) {
+ MurmurHash3_x86_32(&value, sizeof(int64_t), 0, &hash_value);
+ } else if constexpr (std::is_same_v<T, std::array<uint8_t, 16>>) {
+ MurmurHash3_x86_32(value.data(), sizeof(uint8_t) * 16, 0, &hash_value);
+ } else if constexpr (std::is_same_v<T, std::string>) {
+ MurmurHash3_x86_32(value.data(), value.size(), 0, &hash_value);
+ } else if constexpr (std::is_same_v<T, std::vector<uint8_t>>) {
+ MurmurHash3_x86_32(value.data(), value.size(), 0, &hash_value);
+ } else if constexpr (std::is_same_v<T, std::monostate> ||
+ std::is_same_v<T, bool> || std::is_same_v<T, float> ||
+ std::is_same_v<T, double> ||
+ std::is_same_v<T, Literal::BelowMin> ||
+ std::is_same_v<T, Literal::AboveMax>) {
+ std::unreachable();
+ } else {
+ static_assert(false, "Unhandled type in BucketTransform::Transform");
+ }
+ },
+ literal.value());
+
+ // Calculate the bucket index
+ int32_t bucket_index =
+ (hash_value & std::numeric_limits<int32_t>::max()) % num_buckets_;
+
+ return Literal::Int(bucket_index);
}
+std::shared_ptr<Type> BucketTransform::ResultType() const { return int32(); }
+
Result<std::unique_ptr<TransformFunction>> BucketTransform::Make(
std::shared_ptr<Type> const& source_type, int32_t num_buckets) {
if (!source_type) {
@@ -87,14 +130,52 @@ TruncateTransform::TruncateTransform(std::shared_ptr<Type> const& source_type,
int32_t width)
: TransformFunction(TransformType::kTruncate, source_type), width_(width) {}
-Result<ArrowArray> TruncateTransform::Transform(const ArrowArray& input) {
- return NotImplemented("TruncateTransform::Transform");
-}
+Result<Literal> TruncateTransform::Transform(const Literal& literal) {
+ assert(literal.type() == source_type());
+ if (literal.IsBelowMin() || literal.IsAboveMax()) {
+ return InvalidArgument(
+ "Cannot apply truncate transform to literal with value {} of type {}",
+ literal.ToString(), source_type()->ToString());
+ }
+ if (literal.IsNull()) [[unlikely]] {
+ // Return null as is
+ return literal;
+ }
-Result<std::shared_ptr<Type>> TruncateTransform::ResultType() const {
- return source_type();
+ switch (source_type()->type_id()) {
+ case TypeId::kInt: {
+ auto value = std::get<int32_t>(literal.value());
+ return Literal::Int(TruncateUtils::TruncateInteger(value, width_));
+ }
+ case TypeId::kLong: {
+ auto value = std::get<int64_t>(literal.value());
+ return Literal::Long(TruncateUtils::TruncateInteger(value, width_));
+ }
+ case TypeId::kDecimal: {
+ // TODO(zhjwpku): Handle decimal truncation logic here
+ return NotImplemented("Truncate for Decimal is not implemented yet");
+ }
+ case TypeId::kString: {
+ // Strings are truncated to a valid UTF-8 string with no more than L code points.
+ auto value = std::get<std::string>(literal.value());
+ return Literal::String(TruncateUtils::TruncateUTF8(std::move(value), width_));
+ }
+ case TypeId::kBinary: {
+ /// In contrast to strings, binary values do not have an assumed encoding and are
+ /// truncated to L bytes.
+ auto value = std::get<std::vector<uint8_t>>(literal.value());
+ if (value.size() > static_cast<size_t>(width_)) {
+ value.resize(width_);
+ }
+ return Literal::Binary(std::move(value));
+ }
+ default:
+ std::unreachable();
+ }
}
+std::shared_ptr<Type> TruncateTransform::ResultType() const { return source_type(); }
+
Result<std::unique_ptr<TransformFunction>> TruncateTransform::Make(
std::shared_ptr<Type> const& source_type, int32_t width) {
if (!source_type) {
@@ -120,14 +201,39 @@ Result<std::unique_ptr<TransformFunction>> TruncateTransform::Make(
YearTransform::YearTransform(std::shared_ptr<Type> const& source_type)
: TransformFunction(TransformType::kTruncate, source_type) {}
-Result<ArrowArray> YearTransform::Transform(const ArrowArray& input) {
- return NotImplemented("YearTransform::Transform");
-}
+Result<Literal> YearTransform::Transform(const Literal& literal) {
+ assert(literal.type() == source_type());
+ if (literal.IsBelowMin() || literal.IsAboveMax()) {
+ return InvalidArgument(
+ "Cannot apply year transform to literal with value {} of type {}",
+ literal.ToString(), source_type()->ToString());
+ }
+ if (literal.IsNull()) [[unlikely]] {
+ return Literal::Null(int32());
+ }
-Result<std::shared_ptr<Type>> YearTransform::ResultType() const {
- return iceberg::int32();
+ using namespace std::chrono; // NOLINT
+ switch (source_type()->type_id()) {
+ case TypeId::kDate: {
+ auto value = std::get<int32_t>(literal.value());
+ auto epoch = sys_days(year{1970} / January / 1);
+ auto ymd = year_month_day(epoch + days{value});
+ return Literal::Int(static_cast<int32_t>(ymd.year()));
+ }
+ case TypeId::kTimestamp:
+ case TypeId::kTimestampTz: {
+ auto value = std::get<int64_t>(literal.value());
+ // Convert microseconds-since-epoch into a `year_month_day` object
+ auto ymd = year_month_day(floor<days>(sys_time<microseconds>(microseconds{value})));
+ return Literal::Int(static_cast<int32_t>(ymd.year()));
+ }
+ default:
+ std::unreachable();
+ }
}
+std::shared_ptr<Type> YearTransform::ResultType() const { return int32(); }
+
Result<std::unique_ptr<TransformFunction>> YearTransform::Make(
std::shared_ptr<Type> const& source_type) {
if (!source_type) {
@@ -148,14 +254,51 @@ Result<std::unique_ptr<TransformFunction>> YearTransform::Make(
MonthTransform::MonthTransform(std::shared_ptr<Type> const& source_type)
: TransformFunction(TransformType::kMonth, source_type) {}
-Result<ArrowArray> MonthTransform::Transform(const ArrowArray& input) {
- return NotImplemented("MonthTransform::Transform");
-}
+Result<Literal> MonthTransform::Transform(const Literal& literal) {
+ assert(literal.type() == source_type());
+ if (literal.IsBelowMin() || literal.IsAboveMax()) {
+ return InvalidArgument(
+ "Cannot apply month transform to literal with value {} of type {}",
+ literal.ToString(), source_type()->ToString());
+ }
+ if (literal.IsNull()) [[unlikely]] {
+ return Literal::Null(int32());
+ }
-Result<std::shared_ptr<Type>> MonthTransform::ResultType() const {
- return iceberg::int32();
+ using namespace std::chrono; // NOLINT
+ switch (source_type()->type_id()) {
+ case TypeId::kDate: {
+ auto value = std::get<int32_t>(literal.value());
+ auto epoch = sys_days(year{1970} / January / 1);
+ auto ymd = year_month_day(epoch + days{value});
+ auto epoch_ymd = year_month_day(epoch);
+ auto delta = ymd.year() - epoch_ymd.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 Literal::Int(static_cast<int32_t>(delta.count() * 12 +
+ static_cast<unsigned>(ymd.month()) - 1));
+ }
+ case TypeId::kTimestamp:
+ case TypeId::kTimestampTz: {
+ auto value = std::get<int64_t>(literal.value());
+ // Convert microseconds-since-epoch into a `year_month_day` object
+ auto ymd = year_month_day(floor<days>(sys_time<microseconds>(microseconds{value})));
+ auto epoch_ymd = year_month_day(year{1970} / January / 1);
+ auto delta = ymd.year() - epoch_ymd.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 Literal::Int(static_cast<int32_t>(delta.count() * 12 +
+ static_cast<unsigned>(ymd.month()) - 1));
+ }
+ default:
+ std::unreachable();
+ }
}
+std::shared_ptr<Type> MonthTransform::ResultType() const { return int32(); }
+
Result<std::unique_ptr<TransformFunction>> MonthTransform::Make(
std::shared_ptr<Type> const& source_type) {
if (!source_type) {
@@ -176,11 +319,38 @@ Result<std::unique_ptr<TransformFunction>> MonthTransform::Make(
DayTransform::DayTransform(std::shared_ptr<Type> const& source_type)
: TransformFunction(TransformType::kDay, source_type) {}
-Result<ArrowArray> DayTransform::Transform(const ArrowArray& input) {
- return NotImplemented("DayTransform::Transform");
+Result<Literal> DayTransform::Transform(const Literal& literal) {
+ assert(literal.type() == source_type());
+ if (literal.IsBelowMin() || literal.IsAboveMax()) {
+ return InvalidArgument(
+ "Cannot apply day transform to literal with value {} of type {}",
+ literal.ToString(), source_type()->ToString());
+ }
+ if (literal.IsNull()) [[unlikely]] {
+ return Literal::Null(int32());
+ }
+
+ using namespace std::chrono; // NOLINT
+ switch (source_type()->type_id()) {
+ case TypeId::kDate: {
+ return Literal::Int(std::get<int32_t>(literal.value()));
+ }
+ case TypeId::kTimestamp:
+ case TypeId::kTimestampTz: {
+ auto value = std::get<int64_t>(literal.value());
+ // Convert microseconds to `sys_days` (chronological days since epoch)
+ auto timestamp = sys_time<microseconds>(microseconds{value});
+ auto days_since_epoch = floor<days>(timestamp);
+
+ return Literal::Int(
+ static_cast<int32_t>(days_since_epoch.time_since_epoch().count()));
+ }
+ default:
+ std::unreachable();
+ }
}
-Result<std::shared_ptr<Type>> DayTransform::ResultType() const { return iceberg::date(); }
+std::shared_ptr<Type> DayTransform::ResultType() const { return int32(); }
Result<std::unique_ptr<TransformFunction>> DayTransform::Make(
std::shared_ptr<Type> const& source_type) {
@@ -202,14 +372,38 @@ Result<std::unique_ptr<TransformFunction>> DayTransform::Make(
HourTransform::HourTransform(std::shared_ptr<Type> const& source_type)
: TransformFunction(TransformType::kHour, source_type) {}
-Result<ArrowArray> HourTransform::Transform(const ArrowArray& input) {
- return NotImplemented("HourTransform::Transform");
-}
+Result<Literal> HourTransform::Transform(const Literal& literal) {
+ assert(literal.type() == source_type());
+ if (literal.IsBelowMin() || literal.IsAboveMax()) {
+ return InvalidArgument(
+ "Cannot apply hour transform to literal with value {} of type {}",
+ literal.ToString(), source_type()->ToString());
+ }
+
+ if (literal.IsNull()) [[unlikely]] {
+ return Literal::Null(int32());
+ }
+
+ using namespace std::chrono; // NOLINT
+ switch (source_type()->type_id()) {
+ case TypeId::kTimestamp:
+ case TypeId::kTimestampTz: {
+ auto value = std::get<int64_t>(literal.value());
+ // Create a `sys_time` object from the microseconds value
+ auto timestamp = sys_time<microseconds>(microseconds{value});
+
+ // Convert the time since epoch directly into hours
+ auto hours_since_epoch = duration_cast<hours>(timestamp.time_since_epoch()).count();
-Result<std::shared_ptr<Type>> HourTransform::ResultType() const {
- return iceberg::int32();
+ return Literal::Int(static_cast<int32_t>(hours_since_epoch));
+ }
+ default:
+ std::unreachable();
+ }
}
+std::shared_ptr<Type> HourTransform::ResultType() const { return int32(); }
+
Result<std::unique_ptr<TransformFunction>> HourTransform::Make(
std::shared_ptr<Type> const& source_type) {
if (!source_type) {
@@ -229,11 +423,11 @@ Result<std::unique_ptr<TransformFunction>> HourTransform::Make(
VoidTransform::VoidTransform(std::shared_ptr<Type> const& source_type)
: TransformFunction(TransformType::kVoid, source_type) {}
-Result<ArrowArray> VoidTransform::Transform(const ArrowArray& input) {
- return NotImplemented("VoidTransform::Transform");
+Result<Literal> VoidTransform::Transform(const Literal& literal) {
+ return literal.IsNull() ? literal : Literal::Null(literal.type());
}
-Result<std::shared_ptr<Type>> VoidTransform::ResultType() const { return source_type(); }
+std::shared_ptr<Type> VoidTransform::ResultType() const { return source_type(); }
Result<std::unique_ptr<TransformFunction>> VoidTransform::Make(
std::shared_ptr<Type> const& source_type) {
diff --git a/src/iceberg/transform_function.h b/src/iceberg/transform_function.h
index 7fffd61..6d81064 100644
--- a/src/iceberg/transform_function.h
+++ b/src/iceberg/transform_function.h
@@ -30,11 +30,11 @@ class IdentityTransform : public TransformFunction {
/// \param source_type Type of the input data.
explicit IdentityTransform(std::shared_ptr<Type> const& source_type);
- /// \brief Returns the input array without modification.
- Result<ArrowArray> Transform(const ArrowArray& input) override;
+ /// \brief Returns the same Literal as the input.
+ Result<Literal> Transform(const Literal& literal) override;
- /// \brief Returns the same type as the source type if it is valid.
- Result<std::shared_ptr<Type>> ResultType() const override;
+ /// \brief Returns the same type as source_type.
+ std::shared_ptr<Type> ResultType() const override;
/// \brief Create an IdentityTransform.
/// \param source_type Type of the input data.
@@ -50,11 +50,11 @@ class BucketTransform : public TransformFunction {
/// \param num_buckets Number of buckets to hash into.
BucketTransform(std::shared_ptr<Type> const& source_type, int32_t num_buckets);
- /// \brief Applies the bucket hash function to the input array.
- Result<ArrowArray> Transform(const ArrowArray& input) override;
+ /// \brief Applies the bucket hash function to the input Literal.
+ Result<Literal> Transform(const Literal& literal) override;
/// \brief Returns INT32 as the output type.
- Result<std::shared_ptr<Type>> ResultType() const override;
+ std::shared_ptr<Type> ResultType() const override;
/// \brief Create a BucketTransform.
/// \param source_type Type of the input data.
@@ -74,11 +74,11 @@ class TruncateTransform : public TransformFunction {
/// \param width The width to truncate to (e.g., for strings or numbers).
TruncateTransform(std::shared_ptr<Type> const& source_type, int32_t width);
- /// \brief Truncates values in the input array to the specified width.
- Result<ArrowArray> Transform(const ArrowArray& input) override;
+ /// \brief Truncates the input Literal to the specified width.
+ Result<Literal> Transform(const Literal& literal) override;
/// \brief Returns the same type as source_type.
- Result<std::shared_ptr<Type>> ResultType() const override;
+ std::shared_ptr<Type> ResultType() const override;
/// \brief Create a TruncateTransform.
/// \param source_type Type of the input data.
@@ -97,11 +97,11 @@ class YearTransform : public TransformFunction {
/// \param source_type Must be a timestamp type.
explicit YearTransform(std::shared_ptr<Type> const& source_type);
- /// \brief Extracts the year from each timestamp in the input array.
- Result<ArrowArray> Transform(const ArrowArray& input) override;
+ /// \brief Extract a date or timestamp year, as years from 1970.
+ Result<Literal> Transform(const Literal& literal) override;
/// \brief Returns INT32 as the output type.
- Result<std::shared_ptr<Type>> ResultType() const override;
+ std::shared_ptr<Type> ResultType() const override;
/// \brief Create a YearTransform.
/// \param source_type Type of the input data.
@@ -116,11 +116,11 @@ class MonthTransform : public TransformFunction {
/// \param source_type Must be a timestamp type.
explicit MonthTransform(std::shared_ptr<Type> const& source_type);
- /// \brief Extracts the month (1-12) from each timestamp in the input array.
- Result<ArrowArray> Transform(const ArrowArray& input) override;
+ /// \brief Extract a date or timestamp month, as months from 1970-01-01.
+ Result<Literal> Transform(const Literal& literal) override;
/// \brief Returns INT32 as the output type.
- Result<std::shared_ptr<Type>> ResultType() const override;
+ std::shared_ptr<Type> ResultType() const override;
/// \brief Create a MonthTransform.
/// \param source_type Type of the input data.
@@ -135,11 +135,11 @@ class DayTransform : public TransformFunction {
/// \param source_type Must be a timestamp type.
explicit DayTransform(std::shared_ptr<Type> const& source_type);
- /// \brief Extracts the day (1-31) from each timestamp in the input array.
- Result<ArrowArray> Transform(const ArrowArray& input) override;
+ /// \brief Extract a date or timestamp day, as days from 1970-01-01.
+ Result<Literal> Transform(const Literal& literal) override;
/// \brief Returns INT32 as the output type.
- Result<std::shared_ptr<Type>> ResultType() const override;
+ std::shared_ptr<Type> ResultType() const override;
/// \brief Create a DayTransform.
/// \param source_type Type of the input data.
@@ -154,11 +154,11 @@ class HourTransform : public TransformFunction {
/// \param source_type Must be a timestamp type.
explicit HourTransform(std::shared_ptr<Type> const& source_type);
- /// \brief Extracts the hour (0-23) from each timestamp in the input array.
- Result<ArrowArray> Transform(const ArrowArray& input) override;
+ /// \brief Extract a timestamp hour, as hours from 1970-01-01 00:00:00.
+ Result<Literal> Transform(const Literal& literal) override;
/// \brief Returns INT32 as the output type.
- Result<std::shared_ptr<Type>> ResultType() const override;
+ std::shared_ptr<Type> ResultType() const override;
/// \brief Create a HourTransform.
/// \param source_type Type of the input data.
@@ -173,11 +173,11 @@ class VoidTransform : public TransformFunction {
/// \param source_type Input type (ignored).
explicit VoidTransform(std::shared_ptr<Type> const& source_type);
- /// \brief Returns an all-null array of the same length as the input.
- Result<ArrowArray> Transform(const ArrowArray& input) override;
+ /// \brief Returns a null literal.
+ Result<Literal> Transform(const Literal& literal) override;
- /// \brief Returns null type or a sentinel type indicating void.
- Result<std::shared_ptr<Type>> ResultType() const override;
+ /// \brief Returns the same type as source_type.
+ std::shared_ptr<Type> ResultType() const override;
/// \brief Create a VoidTransform.
/// \param source_type Input type (ignored).
diff --git a/src/iceberg/type_fwd.h b/src/iceberg/type_fwd.h
index 83574ee..0135422 100644
--- a/src/iceberg/type_fwd.h
+++ b/src/iceberg/type_fwd.h
@@ -120,6 +120,14 @@ class FileScanTask;
class TableScan;
class TableScanBuilder;
+struct DataFile;
+struct ManifestEntry;
+struct ManifestFile;
+struct ManifestList;
+
+class ManifestReader;
+class ManifestListReader;
+
/// ----------------------------------------------------------------------------
/// TODO: Forward declarations below are not added yet.
/// ----------------------------------------------------------------------------
@@ -131,12 +139,4 @@ class UpdateRequirement;
class AppendFiles;
-struct DataFile;
-struct ManifestEntry;
-struct ManifestFile;
-struct ManifestList;
-
-class ManifestReader;
-class ManifestListReader;
-
} // namespace iceberg
diff --git a/src/iceberg/util/string_utils.h b/src/iceberg/util/string_utils.h
index 9ff250b..558fc29 100644
--- a/src/iceberg/util/string_utils.h
+++ b/src/iceberg/util/string_utils.h
@@ -27,7 +27,7 @@
namespace iceberg {
-ICEBERG_EXPORT class StringUtils {
+class ICEBERG_EXPORT StringUtils {
public:
static std::string ToLower(std::string_view str) {
std::string input(str);
diff --git a/src/iceberg/util/truncate_utils.h b/src/iceberg/util/truncate_utils.h
new file mode 100644
index 0000000..5e76135
--- /dev/null
+++ b/src/iceberg/util/truncate_utils.h
@@ -0,0 +1,72 @@
+/*
+ * 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 <string>
+#include <utility>
+
+#include "iceberg/iceberg_export.h"
+
+namespace iceberg {
+
+class ICEBERG_EXPORT TruncateUtils {
+ public:
+ /// \brief Truncate a UTF-8 string to a specified number of code points.
+ ///
+ /// \param source The input string to truncate.
+ /// \param L The maximum number of code points allowed in the output string.
+ /// \return A valid UTF-8 string truncated to L code points.
+ /// If the input string is already valid and has fewer than L code points, it is
+ /// returned unchanged.
+ static std::string TruncateUTF8(std::string source, size_t L) {
+ size_t code_point_count = 0;
+ size_t safe_point = 0;
+
+ for (size_t i = 0; i < source.size(); ++i) {
+ // Start of a new UTF-8 code point
+ if ((source[i] & 0xC0) != 0x80) {
+ code_point_count++;
+ if (code_point_count > static_cast<size_t>(L)) {
+ safe_point = i;
+ break;
+ }
+ }
+ }
+
+ if (safe_point != 0) {
+ // Resize the string to the safe point
+ source.resize(safe_point);
+ }
+
+ return std::move(source);
+ }
+
+ /// \brief Truncate an integer v, either int32_t or int64_t, to v - (v % W).
+ ///
+ /// The remainder, v % W, must be positive. For languages where % can produce negative
+ /// values, the correct truncate function is: v - (((v % W) + W) % W)
+ template <typename T>
+ requires std::is_same_v<T, int32_t> || std::is_same_v<T, int64_t>
+ static inline T TruncateInteger(T v, size_t W) {
+ return v - (((v % W) + W) % W);
+ }
+};
+
+} // namespace iceberg
diff --git a/test/transform_test.cc b/test/transform_test.cc
index 33149d1..c1efcb5 100644
--- a/test/transform_test.cc
+++ b/test/transform_test.cc
@@ -25,6 +25,7 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
+#include "iceberg/expression/literal.h"
#include "iceberg/type.h"
#include "iceberg/util/formatter.h" // IWYU pragma: keep
#include "matchers.h"
@@ -40,12 +41,6 @@ TEST(TransformTest, Transform) {
auto source_type = iceberg::string();
auto identity_transform = transform->Bind(source_type);
ASSERT_TRUE(identity_transform);
-
- ArrowArray arrow_array;
- auto result = identity_transform.value()->Transform(arrow_array);
- ASSERT_FALSE(result);
- EXPECT_EQ(ErrorKind::kNotImplemented, result.error().kind);
- EXPECT_EQ("IdentityTransform::Transform", result.error().message);
}
TEST(TransformFunctionTest, CreateBucketTransform) {
@@ -136,7 +131,7 @@ TEST(TransformResultTypeTest, PositiveCases) {
.expected_result_type = iceberg::int32()},
{.str = "day",
.source_type = iceberg::timestamp(),
- .expected_result_type = iceberg::date()},
+ .expected_result_type = iceberg::int32()},
{.str = "hour",
.source_type = iceberg::timestamp(),
.expected_result_type = iceberg::int32()},
@@ -160,8 +155,7 @@ TEST(TransformResultTypeTest, PositiveCases) {
ASSERT_TRUE(transformPtr.has_value()) << "Failed to bind: " << c.str;
auto result_type = transformPtr.value()->ResultType();
- ASSERT_TRUE(result_type.has_value()) << "Failed to get result type for: " << c.str;
- EXPECT_EQ(result_type.value()->type_id(), c.expected_result_type->type_id())
+ EXPECT_EQ(result_type->type_id(), c.expected_result_type->type_id())
<< "Unexpected result type for: " << c.str;
}
}
@@ -193,4 +187,373 @@ TEST(TransformResultTypeTest, NegativeCases) {
}
}
+TEST(TransformLiteralTest, IdentityTransform) {
+ struct Case {
+ std::shared_ptr<Type> source_type;
+ Literal source;
+ Literal expected;
+ };
+
+ const std::vector<Case> cases = {
+ {.source_type = iceberg::boolean(),
+ .source = Literal::Boolean(true),
+ .expected = Literal::Boolean(true)},
+ {.source_type = iceberg::int32(),
+ .source = Literal::Int(42),
+ .expected = Literal::Int(42)},
+ {.source_type = iceberg::int32(),
+ .source = Literal::Date(30000),
+ .expected = Literal::Date(30000)},
+ {.source_type = iceberg::int64(),
+ .source = Literal::Long(1234567890),
+ .expected = Literal::Long(1234567890)},
+ {.source_type = iceberg::timestamp(),
+ .source = Literal::Timestamp(1622547800000000),
+ .expected = Literal::Timestamp(1622547800000000)},
+ {.source_type = iceberg::timestamp_tz(),
+ .source = Literal::TimestampTz(1622547800000000),
+ .expected = Literal::TimestampTz(1622547800000000)},
+ {.source_type = iceberg::float32(),
+ .source = Literal::Float(3.14),
+ .expected = Literal::Float(3.14)},
+ {.source_type = iceberg::float64(),
+ .source = Literal::Double(1.23e-5),
+ .expected = Literal::Double(1.23e-5)},
+ {.source_type = iceberg::string(),
+ .source = Literal::String("Hello, World!"),
+ .expected = Literal::String("Hello, World!")},
+ {.source_type = iceberg::binary(),
+ .source = Literal::Binary({0x01, 0x02, 0x03}),
+ .expected = Literal::Binary({0x01, 0x02, 0x03})},
+ };
+
+ for (const auto& c : cases) {
+ auto transform = Transform::Identity();
+ auto transformPtr = transform->Bind(c.source_type);
+ ASSERT_TRUE(transformPtr.has_value()) << "Failed to bind identity transform";
+
+ auto result = transformPtr.value()->Transform(c.source);
+ ASSERT_TRUE(result.has_value())
+ << "Failed to transform literal: " << c.source.ToString();
+
+ EXPECT_EQ(result.value(), c.expected)
+ << "Unexpected result for source: " << c.source.ToString();
+ }
+}
+
+TEST(TransformLiteralTest, BucketTransform) {
+ constexpr int32_t num_buckets = 4;
+ auto transform = Transform::Bucket(num_buckets);
+
+ struct Case {
+ std::shared_ptr<Type> source_type;
+ Literal source;
+ Literal expected;
+ };
+
+ const std::vector<Case> cases = {
+ {.source_type = iceberg::int32(),
+ .source = Literal::Int(42),
+ .expected = Literal::Int(3)},
+ {.source_type = iceberg::date(),
+ .source = Literal::Date(30000),
+ .expected = Literal::Int(2)},
+ {.source_type = iceberg::int64(),
+ .source = Literal::Long(1234567890),
+ .expected = Literal::Int(3)},
+ {.source_type = iceberg::timestamp(),
+ .source = Literal::Timestamp(1622547800000000),
+ .expected = Literal::Int(1)},
+ {.source_type = iceberg::timestamp_tz(),
+ .source = Literal::TimestampTz(1622547800000000),
+ .expected = Literal::Int(1)},
+ {.source_type = iceberg::string(),
+ .source = Literal::String("test"),
+ .expected = Literal::Int(3)},
+ };
+
+ for (const auto& c : cases) {
+ auto transformPtr = transform->Bind(c.source_type);
+ ASSERT_TRUE(transformPtr.has_value()) << "Failed to bind bucket transform";
+ auto result = transformPtr.value()->Transform(c.source);
+ ASSERT_TRUE(result.has_value())
+ << "Failed to transform literal: " << c.source.ToString();
+
+ EXPECT_EQ(result.value(), c.expected)
+ << "Unexpected result for source: " << c.source.ToString();
+ }
+}
+
+TEST(TransformLiteralTest, TruncateTransform) {
+ struct Case {
+ std::shared_ptr<Type> source_type;
+ int32_t width;
+ Literal source;
+ Literal expected;
+ };
+
+ const std::vector<Case> cases = {
+ {.source_type = iceberg::int32(),
+ .width = 5,
+ .source = Literal::Int(123456),
+ .expected = Literal::Int(123455)},
+ {.source_type = iceberg::string(),
+ .width = 5,
+ .source = Literal::String("Hello, World!"),
+ .expected = Literal::String("Hello")},
+ {.source_type = iceberg::string(),
+ .width = 5,
+ .source = Literal::String("😜🧐🤔🤪🥳😵💫😂"),
+ // Truncate to 5 utf-8 code points
+ .expected = Literal::String("😜🧐🤔🤪🥳")},
+ {.source_type = iceberg::string(),
+ .width = 8,
+ .source = Literal::String("a😜b🧐c🤔d🤪e🥳"),
+ .expected = Literal::String("a😜b🧐c🤔d🤪")},
+ {.source_type = iceberg::binary(),
+ .width = 5,
+ .source = Literal::Binary({0x01, 0x02, 0x03, 0x04, 0x05, 0x06}),
+ .expected = Literal::Binary({0x01, 0x02, 0x03, 0x04, 0x05})},
+ };
+
+ for (const auto& c : cases) {
+ auto transform = Transform::Truncate(c.width);
+ auto transformPtr = transform->Bind(c.source_type);
+ ASSERT_TRUE(transformPtr.has_value()) << "Failed to bind truncate transform";
+ auto result = transformPtr.value()->Transform(c.source);
+ ASSERT_TRUE(result.has_value())
+ << "Failed to transform literal: " << c.source.ToString();
+
+ EXPECT_EQ(result.value(), c.expected)
+ << "Unexpected result for source: " << c.source.ToString();
+ }
+}
+
+TEST(TransformLiteralTest, YearTransform) {
+ auto transform = Transform::Year();
+
+ struct Case {
+ std::shared_ptr<Type> source_type;
+ Literal source;
+ Literal expected;
+ };
+
+ const std::vector<Case> cases = {
+ {.source_type = iceberg::timestamp(),
+ // 2021-06-01T11:43:20Z
+ .source = Literal::Timestamp(1622547800000000),
+ .expected = Literal::Int(2021)},
+ {.source_type = iceberg::timestamp_tz(),
+ .source = Literal::TimestampTz(1622547800000000),
+ .expected = Literal::Int(2021)},
+ {.source_type = iceberg::date(),
+ .source = Literal::Date(30000),
+ .expected = Literal::Int(2052)},
+ };
+
+ for (const auto& c : cases) {
+ auto transformPtr = transform->Bind(c.source_type);
+ ASSERT_TRUE(transformPtr.has_value()) << "Failed to bind year transform";
+ auto result = transformPtr.value()->Transform(c.source);
+ ASSERT_TRUE(result.has_value())
+ << "Failed to transform literal: " << c.source.ToString();
+
+ EXPECT_EQ(result.value(), c.expected)
+ << "Unexpected result for source: " << c.source.ToString();
+ }
+}
+
+TEST(TransformLiteralTest, MonthTransform) {
+ auto transform = Transform::Month();
+
+ struct Case {
+ std::shared_ptr<Type> source_type;
+ Literal source;
+ Literal expected;
+ };
+
+ const std::vector<Case> cases = {
+ {.source_type = iceberg::timestamp(),
+ .source = Literal::Timestamp(1622547800000000),
+ .expected = Literal::Int(617)},
+ {.source_type = iceberg::timestamp_tz(),
+ .source = Literal::TimestampTz(1622547800000000),
+ .expected = Literal::Int(617)},
+ {.source_type = iceberg::date(),
+ .source = Literal::Date(30000),
+ .expected = Literal::Int(985)},
+ };
+
+ for (const auto& c : cases) {
+ auto transformPtr = transform->Bind(c.source_type);
+ ASSERT_TRUE(transformPtr.has_value()) << "Failed to bind month transform";
+ auto result = transformPtr.value()->Transform(c.source);
+ ASSERT_TRUE(result.has_value())
+ << "Failed to transform literal: " << c.source.ToString();
+
+ EXPECT_EQ(result.value(), c.expected)
+ << "Unexpected result for source: " << c.source.ToString();
+ }
+}
+
+TEST(TransformFunctionTransformTest, DayTransform) {
+ auto transform = Transform::Day();
+
+ struct Case {
+ std::shared_ptr<Type> source_type;
+ Literal source;
+ Literal expected;
+ };
+
+ const std::vector<Case> cases = {
+ {.source_type = iceberg::timestamp(),
+ .source = Literal::Timestamp(1622547800000000),
+ .expected = Literal::Int(18779)},
+ {.source_type = iceberg::timestamp_tz(),
+ .source = Literal::TimestampTz(1622547800000000),
+ .expected = Literal::Int(18779)},
+ {.source_type = iceberg::date(),
+ .source = Literal::Date(30000),
+ .expected = Literal::Int(30000)},
+ };
+
+ for (const auto& c : cases) {
+ auto transformPtr = transform->Bind(c.source_type);
+ ASSERT_TRUE(transformPtr.has_value()) << "Failed to bind day transform";
+ auto result = transformPtr.value()->Transform(c.source);
+ ASSERT_TRUE(result.has_value())
+ << "Failed to transform literal: " << c.source.ToString();
+
+ EXPECT_EQ(result.value(), c.expected)
+ << "Unexpected result for source: " << c.source.ToString();
+ }
+}
+
+TEST(TransformLiteralTest, HourTransform) {
+ auto transform = Transform::Hour();
+
+ struct Case {
+ std::shared_ptr<Type> source_type;
+ Literal source;
+ Literal expected;
+ };
+
+ const std::vector<Case> cases = {
+ {.source_type = iceberg::timestamp(),
+ .source = Literal::Timestamp(1622547800000000),
+ .expected = Literal::Int(450707)},
+ {.source_type = iceberg::timestamp_tz(),
+ .source = Literal::TimestampTz(1622547800000000),
+ .expected = Literal::Int(450707)},
+ };
+
+ for (const auto& c : cases) {
+ auto transformPtr = transform->Bind(c.source_type);
+ ASSERT_TRUE(transformPtr.has_value()) << "Failed to bind hour transform";
+ auto result = transformPtr.value()->Transform(c.source);
+ ASSERT_TRUE(result.has_value())
+ << "Failed to transform literal: " << c.source.ToString();
+
+ EXPECT_EQ(result.value(), c.expected)
+ << "Unexpected result for source: " << c.source.ToString();
+ }
+}
+
+TEST(TransformLiteralTest, VoidTransform) {
+ auto transform = Transform::Void();
+
+ struct Case {
+ std::shared_ptr<Type> source_type;
+ Literal source;
+ };
+
+ const std::vector<Case> cases = {
+ {.source_type = iceberg::boolean(), .source = Literal::Boolean(true)},
+ {.source_type = iceberg::int32(), .source = Literal::Int(42)},
+ {.source_type = iceberg::date(), .source = Literal::Date(30000)},
+ {.source_type = iceberg::int64(), .source = Literal::Long(1234567890)},
+ {.source_type = iceberg::timestamp(),
+ .source = Literal::Timestamp(1622547800000000)},
+ {.source_type = iceberg::timestamp_tz(),
+ .source = Literal::TimestampTz(1622547800000000)},
+ {.source_type = iceberg::float32(), .source = Literal::Float(3.14)},
+ {.source_type = iceberg::float64(), .source = Literal::Double(1.23e-5)},
+ {.source_type = iceberg::string(), .source = Literal::String("Hello, World!")},
+ {.source_type = iceberg::binary(), .source = Literal::Binary({0x01, 0x02, 0x03})},
+ };
+
+ for (const auto& c : cases) {
+ auto transformPtr = transform->Bind(c.source_type);
+ ASSERT_TRUE(transformPtr.has_value()) << "Failed to bind void transform";
+ auto result = transformPtr.value()->Transform(c.source);
+ EXPECT_TRUE(result->IsNull())
+ << "Expected void transform to return null type for source: "
+ << c.source.ToString();
+ EXPECT_EQ(result->type()->type_id(), c.source_type->type_id())
+ << "Expected void transform to return same type as source for: "
+ << c.source.ToString();
+ }
+}
+
+TEST(TransformLiteralTest, NullLiteral) {
+ struct Case {
+ std::string str;
+ std::shared_ptr<Type> source_type;
+ Literal source;
+ std::shared_ptr<Type> expected_result_type;
+ };
+
+ const std::vector<Case> cases = {
+ {.str = "identity",
+ .source_type = iceberg::string(),
+ .source = Literal::Null(iceberg::string()),
+ .expected_result_type = iceberg::string()},
+ {.str = "year",
+ .source_type = iceberg::timestamp(),
+ .source = Literal::Null(iceberg::timestamp()),
+ .expected_result_type = iceberg::int32()},
+ {.str = "month",
+ .source_type = iceberg::timestamp(),
+ .source = Literal::Null(iceberg::timestamp()),
+ .expected_result_type = iceberg::int32()},
+ {.str = "day",
+ .source_type = iceberg::timestamp(),
+ .source = Literal::Null(iceberg::timestamp()),
+ .expected_result_type = iceberg::int32()},
+ {.str = "hour",
+ .source_type = iceberg::timestamp(),
+ .source = Literal::Null(iceberg::timestamp()),
+ .expected_result_type = iceberg::int32()},
+ {.str = "void",
+ .source_type = iceberg::string(),
+ .source = Literal::Null(iceberg::string()),
+ .expected_result_type = iceberg::string()},
+ {.str = "bucket[16]",
+ .source_type = iceberg::string(),
+ .source = Literal::Null(iceberg::string()),
+ .expected_result_type = iceberg::int32()},
+ {.str = "truncate[32]",
+ .source_type = iceberg::string(),
+ .source = Literal::Null(iceberg::string()),
+ .expected_result_type = iceberg::string()},
+ };
+
+ for (const auto& c : cases) {
+ auto result = TransformFromString(c.str);
+ ASSERT_TRUE(result.has_value()) << "Failed to parse: " << c.str;
+
+ const auto& transform = result.value();
+ const auto transformPtr = transform->Bind(c.source_type);
+ ASSERT_TRUE(transformPtr.has_value()) << "Failed to bind: " << c.str;
+
+ auto transform_result = transformPtr.value()->Transform(c.source);
+ EXPECT_TRUE(transform_result->IsNull())
+ << "Expected void transform to return null type for source: "
+ << c.source.ToString();
+ EXPECT_EQ(transform_result->type()->type_id(), c.expected_result_type->type_id())
+ << "Expected void transform to return same type as source for: "
+ << c.source.ToString();
+ }
+}
+
} // namespace iceberg