| /* | |
| * 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/transform.h" | |
| #include <format> | |
| #include <memory> | |
| #include <string> | |
| #include <vector> | |
| #include <gmock/gmock.h> | |
| #include <gtest/gtest.h> | |
| #include "iceberg/expression/expressions.h" | |
| #include "iceberg/expression/literal.h" | |
| #include "iceberg/expression/predicate.h" | |
| #include "iceberg/schema.h" | |
| #include "iceberg/schema_field.h" | |
| #include "iceberg/test/matchers.h" | |
| #include "iceberg/test/temporal_test_helper.h" | |
| #include "iceberg/transform_function.h" | |
| #include "iceberg/type.h" | |
| #include "iceberg/util/checked_cast.h" | |
| #include "iceberg/util/formatter.h" // IWYU pragma: keep | |
| namespace iceberg { | |
| TEST(TransformTest, Transform) { | |
| auto transform = Transform::Identity(); | |
| EXPECT_EQ(TransformType::kIdentity, transform->transform_type()); | |
| EXPECT_EQ("identity", transform->ToString()); | |
| EXPECT_EQ("identity", std::format("{}", *transform)); | |
| auto source_type = iceberg::string(); | |
| auto identity_transform = transform->Bind(source_type); | |
| ASSERT_TRUE(identity_transform); | |
| } | |
| TEST(TransformTest, IdentityDoesNotSupportV3Types) { | |
| const auto transform = Transform::Identity(); | |
| const std::vector<std::shared_ptr<Type>> unsupported_types = { | |
| iceberg::variant(), iceberg::geometry(), iceberg::geography()}; | |
| for (const auto& type : unsupported_types) { | |
| EXPECT_FALSE(transform->CanTransform(*type)); | |
| EXPECT_THAT(transform->Bind(type), | |
| HasErrorMessage("is not a valid input type for identity transform")); | |
| } | |
| } | |
| TEST(TransformFunctionTest, CreateBucketTransform) { | |
| constexpr int32_t bucket_count = 8; | |
| auto transform = Transform::Bucket(bucket_count); | |
| EXPECT_EQ("bucket[8]", transform->ToString()); | |
| EXPECT_EQ("bucket[8]", std::format("{}", *transform)); | |
| const auto transformPtr = transform->Bind(iceberg::string()); | |
| ASSERT_TRUE(transformPtr); | |
| EXPECT_EQ(transformPtr.value()->transform_type(), TransformType::kBucket); | |
| BucketTransform same_bucket(string(), bucket_count); | |
| BucketTransform different_bucket(string(), 16); | |
| BucketTransform different_source(int32(), bucket_count); | |
| EXPECT_EQ(*transformPtr.value(), same_bucket); | |
| EXPECT_NE(*transformPtr.value(), different_bucket); | |
| EXPECT_NE(*transformPtr.value(), different_source); | |
| } | |
| TEST(TransformFunctionTest, CreateTruncateTransform) { | |
| constexpr int32_t width = 16; | |
| auto transform = Transform::Truncate(width); | |
| EXPECT_EQ("truncate[16]", transform->ToString()); | |
| EXPECT_EQ("truncate[16]", std::format("{}", *transform)); | |
| auto transformPtr = transform->Bind(iceberg::string()); | |
| ASSERT_TRUE(transformPtr); | |
| EXPECT_EQ(transformPtr.value()->transform_type(), TransformType::kTruncate); | |
| TruncateTransform same_truncate(string(), width); | |
| TruncateTransform different_truncate(string(), 32); | |
| TruncateTransform different_source(int32(), width); | |
| EXPECT_EQ(*transformPtr.value(), same_truncate); | |
| EXPECT_NE(*transformPtr.value(), different_truncate); | |
| EXPECT_NE(*transformPtr.value(), different_source); | |
| } | |
| TEST(TransformFunctionTest, CreateYearTransform) { | |
| auto transform = Transform::Year(); | |
| EXPECT_EQ("year", transform->ToString()); | |
| EXPECT_EQ("year", std::format("{}", *transform)); | |
| auto transformPtr = transform->Bind(iceberg::timestamp()); | |
| ASSERT_TRUE(transformPtr); | |
| EXPECT_EQ(transformPtr.value()->transform_type(), TransformType::kYear); | |
| } | |
| TEST(TransformFunctionTest, CreateMonthTransform) { | |
| auto transform = Transform::Month(); | |
| EXPECT_EQ("month", transform->ToString()); | |
| EXPECT_EQ("month", std::format("{}", *transform)); | |
| auto transformPtr = transform->Bind(iceberg::timestamp()); | |
| ASSERT_TRUE(transformPtr); | |
| EXPECT_EQ(transformPtr.value()->transform_type(), TransformType::kMonth); | |
| } | |
| TEST(TransformFunctionTest, CreateDayTransform) { | |
| auto transform = Transform::Day(); | |
| EXPECT_EQ("day", transform->ToString()); | |
| EXPECT_EQ("day", std::format("{}", *transform)); | |
| auto transformPtr = transform->Bind(iceberg::timestamp_tz()); | |
| ASSERT_TRUE(transformPtr); | |
| EXPECT_EQ(transformPtr.value()->transform_type(), TransformType::kDay); | |
| } | |
| TEST(TransformFunctionTest, CreateHourTransform) { | |
| auto transform = Transform::Hour(); | |
| EXPECT_EQ("hour", transform->ToString()); | |
| EXPECT_EQ("hour", std::format("{}", *transform)); | |
| auto transformPtr = transform->Bind(iceberg::timestamp()); | |
| ASSERT_TRUE(transformPtr); | |
| EXPECT_EQ(transformPtr.value()->transform_type(), TransformType::kHour); | |
| } | |
| TEST(TransformFunctionTest, CreateVoidTransform) { | |
| auto transform = Transform::Void(); | |
| EXPECT_EQ("void", transform->ToString()); | |
| EXPECT_EQ("void", std::format("{}", *transform)); | |
| auto transformPtr = transform->Bind(iceberg::int32()); | |
| ASSERT_TRUE(transformPtr); | |
| EXPECT_EQ(transformPtr.value()->transform_type(), TransformType::kVoid); | |
| } | |
| TEST(TransformFromStringTest, PositiveCases) { | |
| struct Case { | |
| std::string str; | |
| TransformType type; | |
| std::optional<int32_t> param; | |
| }; | |
| const std::vector<Case> cases = { | |
| {.str = "identity", .type = TransformType::kIdentity, .param = std::nullopt}, | |
| {.str = "year", .type = TransformType::kYear, .param = std::nullopt}, | |
| {.str = "month", .type = TransformType::kMonth, .param = std::nullopt}, | |
| {.str = "day", .type = TransformType::kDay, .param = std::nullopt}, | |
| {.str = "hour", .type = TransformType::kHour, .param = std::nullopt}, | |
| {.str = "void", .type = TransformType::kVoid, .param = std::nullopt}, | |
| {.str = "bucket[16]", .type = TransformType::kBucket, .param = 16}, | |
| {.str = "truncate[32]", .type = TransformType::kTruncate, .param = 32}, | |
| }; | |
| 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(); | |
| EXPECT_EQ(transform->transform_type(), c.type); | |
| if (c.param.has_value()) { | |
| EXPECT_EQ(transform->ToString(), | |
| std::format("{}[{}]", TransformTypeToString(c.type), *c.param)); | |
| } else { | |
| EXPECT_EQ(transform->ToString(), TransformTypeToString(c.type)); | |
| } | |
| } | |
| } | |
| TEST(TransformFromStringTest, NegativeCases) { | |
| constexpr std::array<std::string_view, 8> invalid_cases = { | |
| "bucket", // missing param | |
| "bucket[]", // empty param | |
| "bucket[abc]", // invalid number | |
| "bucket[0]", // invalid number of buckets | |
| "truncate[0]", // invalid width | |
| "unknown", // unsupported transform | |
| "bucket[16", // missing closing bracket | |
| "truncate[1]extra" // extra characters | |
| }; | |
| for (const auto& str : invalid_cases) { | |
| auto result = TransformFromString(str); | |
| EXPECT_FALSE(result.has_value()) << "Unexpected success for: " << str; | |
| EXPECT_THAT(result, IsError(ErrorKind::kInvalidArgument)); | |
| } | |
| } | |
| TEST(TransformResultTypeTest, PositiveCases) { | |
| struct Case { | |
| std::string str; | |
| std::shared_ptr<Type> source_type; | |
| std::shared_ptr<Type> expected_result_type; | |
| }; | |
| const std::vector<Case> cases = { | |
| {.str = "identity", | |
| .source_type = iceberg::string(), | |
| .expected_result_type = iceberg::string()}, | |
| {.str = "year", | |
| .source_type = iceberg::timestamp(), | |
| .expected_result_type = iceberg::int32()}, | |
| {.str = "month", | |
| .source_type = iceberg::timestamp(), | |
| .expected_result_type = iceberg::int32()}, | |
| {.str = "day", | |
| .source_type = iceberg::timestamp(), | |
| .expected_result_type = iceberg::date()}, | |
| {.str = "hour", | |
| .source_type = iceberg::timestamp(), | |
| .expected_result_type = iceberg::int32()}, | |
| {.str = "void", | |
| .source_type = iceberg::string(), | |
| .expected_result_type = iceberg::string()}, | |
| {.str = "bucket[16]", | |
| .source_type = iceberg::string(), | |
| .expected_result_type = iceberg::int32()}, | |
| {.str = "truncate[32]", | |
| .source_type = 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(); | |
| auto result_type = transform->ResultType(c.source_type); | |
| ASSERT_NE(result_type, nullptr) << "Missing result type for: " << c.str; | |
| EXPECT_EQ(result_type->type_id(), c.expected_result_type->type_id()) | |
| << "Unexpected result type for: " << c.str; | |
| const auto transform_func = transform->Bind(c.source_type); | |
| ASSERT_TRUE(transform_func.has_value()) << "Failed to bind: " << c.str; | |
| EXPECT_EQ(transform_func.value()->ResultType()->type_id(), | |
| c.expected_result_type->type_id()) | |
| << "Unexpected bound result type for: " << c.str; | |
| } | |
| } | |
| TEST(TransformResultTypeTest, NegativeCases) { | |
| struct Case { | |
| std::string str; | |
| std::shared_ptr<Type> source_type; | |
| }; | |
| const std::vector<Case> cases = { | |
| {.str = "identity", .source_type = nullptr}, | |
| {.str = "year", .source_type = iceberg::string()}, | |
| {.str = "month", .source_type = iceberg::string()}, | |
| {.str = "day", .source_type = iceberg::string()}, | |
| {.str = "hour", .source_type = iceberg::string()}, | |
| {.str = "void", .source_type = nullptr}, | |
| {.str = "bucket[16]", .source_type = iceberg::float32()}, | |
| {.str = "truncate[32]", .source_type = iceberg::float64()}}; | |
| 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(); | |
| auto transformPtr = transform->Bind(c.source_type); | |
| ASSERT_THAT(transformPtr, IsError(ErrorKind::kNotSupported)); | |
| } | |
| } | |
| // Parameterized tests for transform functions | |
| struct TransformParam { | |
| std::string str; | |
| // The integer parameter associated with the transform. | |
| int32_t param; | |
| std::shared_ptr<Type> source_type; | |
| Literal source; | |
| Literal expected; | |
| }; | |
| class TransformLiteralTest : public ::testing::TestWithParam<TransformParam> {}; | |
| TEST_P(TransformLiteralTest, IdentityTransform) { | |
| const auto& param = GetParam(); | |
| auto transform = Transform::Identity(); | |
| auto transformPtr = transform->Bind(param.source_type); | |
| ASSERT_TRUE(transformPtr.has_value()) << "Failed to bind identity transform"; | |
| auto result = transformPtr.value()->Transform(param.source); | |
| ASSERT_TRUE(result.has_value()) | |
| << "Failed to transform literal: " << param.source.ToString(); | |
| EXPECT_EQ(result.value(), param.expected) | |
| << "Unexpected result for source: " << param.source.ToString(); | |
| } | |
| INSTANTIATE_TEST_SUITE_P( | |
| IdentityTransformTests, TransformLiteralTest, | |
| ::testing::Values( | |
| TransformParam{.str = "BooleanTrue", | |
| .source_type = iceberg::boolean(), | |
| .source = Literal::Boolean(true), | |
| .expected = Literal::Boolean(true)}, | |
| TransformParam{.str = "BooleanFalse", | |
| .source_type = iceberg::boolean(), | |
| .source = Literal::Boolean(false), | |
| .expected = Literal::Boolean(false)}, | |
| TransformParam{.str = "Int32", | |
| .source_type = iceberg::int32(), | |
| .source = Literal::Int(42), | |
| .expected = Literal::Int(42)}, | |
| TransformParam{.str = "Date", | |
| .source_type = iceberg::int32(), | |
| .source = Literal::Date(30000), | |
| .expected = Literal::Date(30000)}, | |
| TransformParam{.str = "Int64", | |
| .source_type = iceberg::int64(), | |
| .source = Literal::Long(1234567890), | |
| .expected = Literal::Long(1234567890)}, | |
| TransformParam{.str = "Timestamp", | |
| .source_type = iceberg::timestamp(), | |
| .source = Literal::Timestamp(1622547800000000), | |
| .expected = Literal::Timestamp(1622547800000000)}, | |
| TransformParam{.str = "TimestampTz", | |
| .source_type = iceberg::timestamp_tz(), | |
| .source = Literal::TimestampTz(1622547800000000), | |
| .expected = Literal::TimestampTz(1622547800000000)}, | |
| TransformParam{.str = "Float", | |
| .source_type = iceberg::float32(), | |
| .source = Literal::Float(3.14), | |
| .expected = Literal::Float(3.14)}, | |
| TransformParam{.str = "Double", | |
| .source_type = iceberg::float64(), | |
| .source = Literal::Double(1.23e-5), | |
| .expected = Literal::Double(1.23e-5)}, | |
| TransformParam{.str = "Decimal", | |
| .source_type = iceberg::decimal(10, 2), | |
| .source = Literal::Decimal(123456, 10, 2), | |
| .expected = Literal::Decimal(123456, 10, 2)}, | |
| TransformParam{.str = "String", | |
| .source_type = iceberg::string(), | |
| .source = Literal::String("Hello, World!"), | |
| .expected = Literal::String("Hello, World!")}, | |
| TransformParam{ | |
| .str = "Uuid", | |
| .source_type = iceberg::uuid(), | |
| .source = Literal::UUID( | |
| Uuid::FromString("123e4567-e89b-12d3-a456-426614174000").value()), | |
| .expected = Literal::UUID( | |
| Uuid::FromString("123e4567-e89b-12d3-a456-426614174000").value())}, | |
| TransformParam{.str = "Binary", | |
| .source_type = iceberg::binary(), | |
| .source = Literal::Binary({0x01, 0x02, 0x03}), | |
| .expected = Literal::Binary({0x01, 0x02, 0x03})}, | |
| TransformParam{.str = "Fixed", | |
| .source_type = iceberg::fixed(3), | |
| .source = Literal::Fixed({0x01, 0x02, 0x03}), | |
| .expected = Literal::Fixed({0x01, 0x02, 0x03})}), | |
| [](const ::testing::TestParamInfo<TransformParam>& info) { return info.param.str; }); | |
| class BucketTransformTest : public ::testing::TestWithParam<TransformParam> {}; | |
| TEST_P(BucketTransformTest, BucketTransform) { | |
| constexpr int32_t num_buckets = 4; | |
| auto transform = Transform::Bucket(num_buckets); | |
| const auto& param = GetParam(); | |
| auto transformPtr = transform->Bind(param.source_type); | |
| ASSERT_TRUE(transformPtr.has_value()) << "Failed to bind bucket transform"; | |
| auto result = transformPtr.value()->Transform(param.source); | |
| ASSERT_TRUE(result.has_value()) | |
| << "Failed to transform literal: " << param.source.ToString(); | |
| EXPECT_EQ(result.value(), param.expected) | |
| << "Unexpected result for source: " << param.source.ToString(); | |
| } | |
| INSTANTIATE_TEST_SUITE_P( | |
| BucketTransformTests, BucketTransformTest, | |
| ::testing::Values( | |
| TransformParam{.str = "Int32", | |
| .source_type = iceberg::int32(), | |
| .source = Literal::Int(34), | |
| .expected = Literal::Int(3)}, | |
| TransformParam{.str = "Int64", | |
| .source_type = iceberg::int64(), | |
| .source = Literal::Long(34), | |
| .expected = Literal::Int(3)}, | |
| TransformParam{.str = "Decimal", | |
| // 14.20 | |
| .source_type = iceberg::decimal(4, 2), | |
| .source = Literal::Decimal(1420, 4, 2), | |
| .expected = Literal::Int(3)}, | |
| TransformParam{.str = "Date", | |
| .source_type = iceberg::date(), | |
| .source = Literal::Date(TemporalTestHelper::CreateDate( | |
| {.year = 2017, .month = 11, .day = 16})), | |
| .expected = Literal::Int(2)}, | |
| TransformParam{.str = "Time", | |
| .source_type = iceberg::time(), | |
| .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( | |
| TemporalTestHelper::CreateTimestamp({.year = 2017, | |
| .month = 11, | |
| .day = 16, | |
| .hour = 22, | |
| .minute = 31, | |
| .second = 8})), | |
| .expected = Literal::Int(3)}, | |
| 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"), | |
| .expected = Literal::Int(1)}, | |
| TransformParam{ | |
| .str = "Uuid", | |
| .source_type = iceberg::uuid(), | |
| .source = Literal::UUID( | |
| Uuid::FromString("f79c3e09-677c-4bbd-a479-3f349cb785e7").value()), | |
| .expected = Literal::Int(0)}, | |
| TransformParam{.str = "Fixed", | |
| .source_type = iceberg::fixed(4), | |
| .source = Literal::Fixed({0, 1, 2, 3}), | |
| .expected = Literal::Int(1)}, | |
| TransformParam{.str = "Binary", | |
| .source_type = iceberg::binary(), | |
| .source = Literal::Binary({0, 1, 2, 3}), | |
| .expected = Literal::Int(1)}), | |
| [](const ::testing::TestParamInfo<TransformParam>& info) { return info.param.str; }); | |
| class TruncateTransformTest : public ::testing::TestWithParam<TransformParam> {}; | |
| TEST_P(TruncateTransformTest, TruncateTransform) { | |
| const auto& param = GetParam(); | |
| auto transform = Transform::Truncate(param.param); | |
| auto transformPtr = transform->Bind(param.source_type); | |
| ASSERT_TRUE(transformPtr.has_value()) << "Failed to bind truncate transform"; | |
| auto result = transformPtr.value()->Transform(param.source); | |
| ASSERT_TRUE(result.has_value()) | |
| << "Failed to transform literal: " << param.source.ToString(); | |
| EXPECT_EQ(result.value(), param.expected) | |
| << "Unexpected result for source: " << param.source.ToString(); | |
| } | |
| INSTANTIATE_TEST_SUITE_P( | |
| TruncateTransformTests, TruncateTransformTest, | |
| ::testing::Values( | |
| TransformParam{.str = "Int32", | |
| .param = 5, | |
| .source_type = iceberg::int32(), | |
| .source = Literal::Int(123456), | |
| .expected = Literal::Int(123455)}, | |
| TransformParam{.str = "Int64", | |
| .param = 10, | |
| .source_type = iceberg::int64(), | |
| .source = Literal::Long(-1), | |
| .expected = Literal::Long(-10)}, | |
| TransformParam{.str = "Decimal", | |
| .param = 50, | |
| .source_type = iceberg::decimal(5, 2), | |
| .source = Literal::Decimal(12345, 5, 2), | |
| .expected = Literal::Decimal(12300, 5, 2)}, | |
| TransformParam{.str = "StringShort", | |
| .param = 5, | |
| .source_type = iceberg::string(), | |
| .source = Literal::String("Hello, World!"), | |
| .expected = Literal::String("Hello")}, | |
| TransformParam{.str = "StringEmoji", | |
| .param = 5, | |
| .source_type = iceberg::string(), | |
| .source = Literal::String("😜🧐🤔🤪🥳😵💫😂"), | |
| .expected = Literal::String("😜🧐🤔🤪🥳")}, | |
| TransformParam{.str = "StringMixed", | |
| .param = 8, | |
| .source_type = iceberg::string(), | |
| .source = Literal::String("a😜b🧐c🤔d🤪e🥳"), | |
| .expected = Literal::String("a😜b🧐c🤔d🤪")}, | |
| TransformParam{.str = "Binary", | |
| .param = 5, | |
| .source_type = iceberg::binary(), | |
| .source = Literal::Binary({0x01, 0x02, 0x03, 0x04, 0x05, 0x06}), | |
| .expected = Literal::Binary({0x01, 0x02, 0x03, 0x04, 0x05})}), | |
| [](const ::testing::TestParamInfo<TransformParam>& info) { return info.param.str; }); | |
| class YearTransformTest : public ::testing::TestWithParam<TransformParam> {}; | |
| TEST_P(YearTransformTest, YearTransform) { | |
| auto transform = Transform::Year(); | |
| const auto& param = GetParam(); | |
| auto transformPtr = transform->Bind(param.source_type); | |
| ASSERT_TRUE(transformPtr.has_value()) << "Failed to bind year transform"; | |
| auto result = transformPtr.value()->Transform(param.source); | |
| ASSERT_TRUE(result.has_value()) | |
| << "Failed to transform literal: " << param.source.ToString(); | |
| EXPECT_EQ(result.value(), param.expected) | |
| << "Unexpected result for source: " << param.source.ToString(); | |
| } | |
| INSTANTIATE_TEST_SUITE_P( | |
| YearTransformTests, YearTransformTest, | |
| ::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 - 1970)}, | |
| 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 - 1970)}, | |
| TransformParam{.str = "Date", | |
| .source_type = iceberg::date(), | |
| .source = Literal::Date(TemporalTestHelper::CreateDate( | |
| {.year = 2052, .month = 2, .day = 20})), | |
| .expected = Literal::Int(2052 - 1970)}), | |
| [](const ::testing::TestParamInfo<TransformParam>& info) { return info.param.str; }); | |
| class MonthTransformTest : public ::testing::TestWithParam<TransformParam> {}; | |
| TEST_P(MonthTransformTest, MonthTransform) { | |
| auto transform = Transform::Month(); | |
| const auto& param = GetParam(); | |
| auto transformPtr = transform->Bind(param.source_type); | |
| ASSERT_TRUE(transformPtr.has_value()) << "Failed to bind month transform"; | |
| auto result = transformPtr.value()->Transform(param.source); | |
| ASSERT_TRUE(result.has_value()) | |
| << "Failed to transform literal: " << param.source.ToString(); | |
| EXPECT_EQ(result.value(), param.expected) | |
| << "Unexpected result for source: " << param.source.ToString(); | |
| } | |
| INSTANTIATE_TEST_SUITE_P( | |
| MonthTransformTests, MonthTransformTest, | |
| ::testing::Values(TransformParam{.str = "Timestamp", | |
| .source_type = iceberg::timestamp(), | |
| .source = Literal::Timestamp(1622547800000000), | |
| .expected = Literal::Int(617)}, | |
| TransformParam{.str = "TimestampTz", | |
| .source_type = iceberg::timestamp_tz(), | |
| .source = Literal::TimestampTz(1622547800000000), | |
| .expected = Literal::Int(617)}, | |
| TransformParam{.str = "Date", | |
| .source_type = iceberg::date(), | |
| .source = Literal::Date(30000), | |
| .expected = Literal::Int(985)}), | |
| [](const ::testing::TestParamInfo<TransformParam>& info) { return info.param.str; }); | |
| class DayTransformTest : public ::testing::TestWithParam<TransformParam> {}; | |
| TEST_P(DayTransformTest, DayTransform) { | |
| auto transform = Transform::Day(); | |
| const auto& param = GetParam(); | |
| auto transformPtr = transform->Bind(param.source_type); | |
| ASSERT_TRUE(transformPtr.has_value()) << "Failed to bind day transform"; | |
| auto result = transformPtr.value()->Transform(param.source); | |
| ASSERT_TRUE(result.has_value()) | |
| << "Failed to transform literal: " << param.source.ToString(); | |
| EXPECT_EQ(result.value(), param.expected) | |
| << "Unexpected result for source: " << param.source.ToString(); | |
| } | |
| INSTANTIATE_TEST_SUITE_P( | |
| DayTransformTests, DayTransformTest, | |
| ::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<TransformParam>& info) { return info.param.str; }); | |
| class HourTransformTest : public ::testing::TestWithParam<TransformParam> {}; | |
| TEST_P(HourTransformTest, HourTransform) { | |
| auto transform = Transform::Hour(); | |
| const auto& param = GetParam(); | |
| auto transformPtr = transform->Bind(param.source_type); | |
| ASSERT_TRUE(transformPtr.has_value()) << "Failed to bind hour transform"; | |
| auto result = transformPtr.value()->Transform(param.source); | |
| ASSERT_TRUE(result.has_value()) | |
| << "Failed to transform literal: " << param.source.ToString(); | |
| EXPECT_EQ(result.value(), param.expected) | |
| << "Unexpected result for source: " << param.source.ToString(); | |
| } | |
| INSTANTIATE_TEST_SUITE_P( | |
| HourTransformTests, HourTransformTest, | |
| ::testing::Values(TransformParam{.str = "Timestamp", | |
| .source_type = iceberg::timestamp(), | |
| .source = Literal::Timestamp(1622547800000000), | |
| .expected = Literal::Int(450707)}, | |
| TransformParam{.str = "TimestampTz", | |
| .source_type = iceberg::timestamp_tz(), | |
| .source = Literal::TimestampTz(1622547800000000), | |
| .expected = Literal::Int(450707)}), | |
| [](const ::testing::TestParamInfo<TransformParam>& info) { return info.param.str; }); | |
| class VoidTransformTest : public ::testing::TestWithParam<TransformParam> {}; | |
| TEST_P(VoidTransformTest, VoidTransform) { | |
| auto transform = Transform::Void(); | |
| const auto& param = GetParam(); | |
| auto transformPtr = transform->Bind(param.source_type); | |
| ASSERT_TRUE(transformPtr.has_value()) << "Failed to bind void transform"; | |
| auto result = transformPtr.value()->Transform(param.source); | |
| EXPECT_TRUE(result->IsNull()) | |
| << "Expected void transform to return null type for source: " | |
| << param.source.ToString(); | |
| EXPECT_EQ(result->type()->type_id(), param.source_type->type_id()) | |
| << "Expected void transform to return same type as source for: " | |
| << param.source.ToString(); | |
| EXPECT_EQ(result->ToString(), param.expected.ToString()) | |
| << "Unexpected result for source: " << param.source.ToString(); | |
| } | |
| INSTANTIATE_TEST_SUITE_P( | |
| VoidTransformTests, VoidTransformTest, | |
| ::testing::Values( | |
| TransformParam{.str = "Boolean", | |
| .source_type = iceberg::boolean(), | |
| .source = Literal::Boolean(true), | |
| .expected = Literal::Null(iceberg::boolean())}, | |
| TransformParam{.str = "Int32", | |
| .source_type = iceberg::int32(), | |
| .source = Literal::Int(42), | |
| .expected = Literal::Null(iceberg::int32())}, | |
| TransformParam{.str = "Date", | |
| .source_type = iceberg::date(), | |
| .source = Literal::Date(30000), | |
| .expected = Literal::Null(iceberg::date())}, | |
| TransformParam{.str = "Int64", | |
| .source_type = iceberg::int64(), | |
| .source = Literal::Long(1234567890), | |
| .expected = Literal::Null(iceberg::int64())}, | |
| TransformParam{.str = "Timestamp", | |
| .source_type = iceberg::timestamp(), | |
| .source = Literal::Timestamp(1622547800000000), | |
| .expected = Literal::Null(iceberg::timestamp())}, | |
| TransformParam{.str = "TimestampTz", | |
| .source_type = iceberg::timestamp_tz(), | |
| .source = Literal::TimestampTz(1622547800000000), | |
| .expected = Literal::Null(iceberg::timestamp_tz())}, | |
| TransformParam{.str = "Float", | |
| .source_type = iceberg::float32(), | |
| .source = Literal::Float(3.14), | |
| .expected = Literal::Null(iceberg::float32())}, | |
| TransformParam{.str = "Double", | |
| .source_type = iceberg::float64(), | |
| .source = Literal::Double(1.23e-5), | |
| .expected = Literal::Null(iceberg::float64())}, | |
| TransformParam{.str = "Decimal", | |
| .source_type = iceberg::decimal(10, 2), | |
| .source = Literal::Decimal(123456, 10, 2), | |
| .expected = Literal::Null(iceberg::decimal(10, 2))}, | |
| TransformParam{.str = "String", | |
| .source_type = iceberg::string(), | |
| .source = Literal::String("Hello, World!"), | |
| .expected = Literal::Null(iceberg::string())}, | |
| TransformParam{ | |
| .str = "Uuid", | |
| .source_type = iceberg::uuid(), | |
| .source = Literal::UUID( | |
| Uuid::FromString("123e4567-e89b-12d3-a456-426614174000").value()), | |
| .expected = Literal::Null(iceberg::uuid())}, | |
| TransformParam{.str = "Binary", | |
| .source_type = iceberg::binary(), | |
| .source = Literal::Binary({0x01, 0x02, 0x03}), | |
| .expected = Literal::Null(iceberg::binary())}, | |
| TransformParam{.str = "Fixed", | |
| .source_type = iceberg::fixed(3), | |
| .source = Literal::Fixed({0x01, 0x02, 0x03}), | |
| .expected = Literal::Null(iceberg::fixed(3))}), | |
| [](const ::testing::TestParamInfo<TransformParam>& info) { return info.param.str; }); | |
| class NullLiteralTransformTest : public ::testing::TestWithParam<TransformParam> {}; | |
| TEST_P(NullLiteralTransformTest, NullLiteralTransform) { | |
| const auto& param = GetParam(); | |
| auto result = TransformFromString(param.str); | |
| ASSERT_TRUE(result.has_value()) << "Failed to parse: " << param.str; | |
| const auto& transform = result.value(); | |
| const auto transformPtr = transform->Bind(param.source_type); | |
| ASSERT_TRUE(transformPtr.has_value()) << "Failed to bind: " << param.str; | |
| auto transform_result = transformPtr.value()->Transform(param.source); | |
| EXPECT_TRUE(transform_result->IsNull()) | |
| << "Expected transform to return null type for source: " << param.source.ToString(); | |
| EXPECT_EQ(transform_result->ToString(), param.expected.ToString()) | |
| << "Unexpected result for source: " << param.source.ToString(); | |
| } | |
| INSTANTIATE_TEST_SUITE_P( | |
| NullLiteralTransformTests, NullLiteralTransformTest, | |
| ::testing::Values(TransformParam{.str = "identity", | |
| .source_type = iceberg::string(), | |
| .source = Literal::Null(iceberg::string()), | |
| .expected = Literal::Null(iceberg::string())}, | |
| TransformParam{.str = "year", | |
| .source_type = iceberg::timestamp(), | |
| .source = Literal::Null(iceberg::timestamp()), | |
| .expected = Literal::Null(iceberg::int32())}, | |
| TransformParam{.str = "month", | |
| .source_type = iceberg::timestamp(), | |
| .source = Literal::Null(iceberg::timestamp()), | |
| .expected = Literal::Null(iceberg::int32())}, | |
| TransformParam{.str = "day", | |
| .source_type = iceberg::timestamp(), | |
| .source = Literal::Null(iceberg::timestamp()), | |
| .expected = Literal::Null(iceberg::int32())}, | |
| TransformParam{.str = "hour", | |
| .source_type = iceberg::timestamp(), | |
| .source = Literal::Null(iceberg::timestamp()), | |
| .expected = Literal::Null(iceberg::int32())}, | |
| TransformParam{.str = "void", | |
| .source_type = iceberg::string(), | |
| .source = Literal::Null(iceberg::string()), | |
| .expected = Literal::Null(iceberg::string())}, | |
| TransformParam{.str = "bucket[16]", | |
| .source_type = iceberg::string(), | |
| .source = Literal::Null(iceberg::string()), | |
| .expected = Literal::Null(iceberg::int32())}, | |
| TransformParam{.str = "truncate[32]", | |
| .source_type = iceberg::string(), | |
| .source = Literal::Null(iceberg::string()), | |
| .expected = Literal::Null(iceberg::string())})); | |
| TEST(TransformPreservesOrderTest, PreservesOrder) { | |
| struct Case { | |
| std::string transform_str; | |
| bool expected; | |
| }; | |
| const std::vector<Case> cases = { | |
| {.transform_str = "identity", .expected = true}, | |
| {.transform_str = "year", .expected = true}, | |
| {.transform_str = "month", .expected = true}, | |
| {.transform_str = "day", .expected = true}, | |
| {.transform_str = "hour", .expected = true}, | |
| {.transform_str = "void", .expected = false}, | |
| {.transform_str = "bucket[16]", .expected = false}, | |
| {.transform_str = "truncate[32]", .expected = true}, | |
| }; | |
| for (const auto& c : cases) { | |
| auto transform = TransformFromString(c.transform_str); | |
| ASSERT_TRUE(transform.has_value()) << "Failed to parse: " << c.transform_str; | |
| EXPECT_EQ(transform.value()->PreservesOrder(), c.expected) | |
| << "Unexpected result for transform: " << c.transform_str; | |
| } | |
| } | |
| TEST(TransformSatisfiesOrderOfTest, SatisfiesOrderOf) { | |
| struct Case { | |
| std::string transform_str; | |
| std::string other_transform_str; | |
| bool expected; | |
| }; | |
| const std::vector<Case> cases = { | |
| // Identity satisfies all order-preserving transforms | |
| {.transform_str = "identity", .other_transform_str = "identity", .expected = true}, | |
| {.transform_str = "identity", .other_transform_str = "year", .expected = true}, | |
| {.transform_str = "identity", .other_transform_str = "month", .expected = true}, | |
| {.transform_str = "identity", .other_transform_str = "day", .expected = true}, | |
| {.transform_str = "identity", .other_transform_str = "hour", .expected = true}, | |
| {.transform_str = "identity", | |
| .other_transform_str = "truncate[32]", | |
| .expected = true}, | |
| {.transform_str = "identity", | |
| .other_transform_str = "bucket[16]", | |
| .expected = false}, | |
| // Truncate satisfies Truncate with smaller width | |
| {.transform_str = "truncate[32]", | |
| .other_transform_str = "truncate[16]", | |
| .expected = true}, | |
| {.transform_str = "truncate[16]", | |
| .other_transform_str = "truncate[16]", | |
| .expected = true}, | |
| {.transform_str = "truncate[16]", | |
| .other_transform_str = "truncate[32]", | |
| .expected = false}, | |
| {.transform_str = "truncate[16]", | |
| .other_transform_str = "bucket[32]", | |
| .expected = false}, | |
| // Hour satisfies hour, day, month, and year | |
| {.transform_str = "hour", .other_transform_str = "hour", .expected = true}, | |
| {.transform_str = "hour", .other_transform_str = "day", .expected = true}, | |
| {.transform_str = "hour", .other_transform_str = "month", .expected = true}, | |
| {.transform_str = "hour", .other_transform_str = "year", .expected = true}, | |
| {.transform_str = "hour", .other_transform_str = "identity", .expected = false}, | |
| {.transform_str = "hour", .other_transform_str = "bucket[16]", .expected = false}, | |
| // Day satisfies day, month, and year | |
| {.transform_str = "day", .other_transform_str = "day", .expected = true}, | |
| {.transform_str = "day", .other_transform_str = "month", .expected = true}, | |
| {.transform_str = "day", .other_transform_str = "year", .expected = true}, | |
| {.transform_str = "day", .other_transform_str = "hour", .expected = false}, | |
| {.transform_str = "day", .other_transform_str = "identity", .expected = false}, | |
| // Month satisfies month and year | |
| {.transform_str = "month", .other_transform_str = "month", .expected = true}, | |
| {.transform_str = "month", .other_transform_str = "year", .expected = true}, | |
| {.transform_str = "month", .other_transform_str = "day", .expected = false}, | |
| {.transform_str = "month", .other_transform_str = "hour", .expected = false}, | |
| // Year satisfies only year | |
| {.transform_str = "year", .other_transform_str = "year", .expected = true}, | |
| {.transform_str = "year", .other_transform_str = "month", .expected = false}, | |
| {.transform_str = "year", .other_transform_str = "day", .expected = false}, | |
| {.transform_str = "year", .other_transform_str = "hour", .expected = false}, | |
| // Void satisfies no order-preserving transforms | |
| {.transform_str = "void", .other_transform_str = "identity", .expected = false}, | |
| {.transform_str = "void", .other_transform_str = "year", .expected = false}, | |
| {.transform_str = "void", .other_transform_str = "month", .expected = false}, | |
| {.transform_str = "void", .other_transform_str = "day", .expected = false}, | |
| {.transform_str = "void", .other_transform_str = "hour", .expected = false}, | |
| // Bucket satisfies only itself | |
| {.transform_str = "bucket[16]", | |
| .other_transform_str = "bucket[16]", | |
| .expected = true}, | |
| {.transform_str = "bucket[16]", | |
| .other_transform_str = "bucket[32]", | |
| .expected = false}, | |
| {.transform_str = "bucket[16]", | |
| .other_transform_str = "identity", | |
| .expected = false}, | |
| }; | |
| for (const auto& c : cases) { | |
| auto transform = TransformFromString(c.transform_str); | |
| auto other_transform = TransformFromString(c.other_transform_str); | |
| ASSERT_TRUE(transform.has_value()) << "Failed to parse: " << c.transform_str; | |
| ASSERT_TRUE(other_transform.has_value()) | |
| << "Failed to parse: " << c.other_transform_str; | |
| EXPECT_EQ(transform.value()->SatisfiesOrderOf(*other_transform.value()), c.expected) | |
| << "Unexpected result for transform: " << c.transform_str | |
| << " and other transform: " << c.other_transform_str; | |
| } | |
| } | |
| TEST(TransformCanTransformTest, CanTransform) { | |
| struct Case { | |
| std::string transform_str; | |
| std::shared_ptr<Type> source_type; | |
| bool expected; | |
| }; | |
| const std::vector<Case> cases = { | |
| // Identity can transform all primitive types | |
| {.transform_str = "identity", .source_type = int32(), .expected = true}, | |
| {.transform_str = "identity", .source_type = string(), .expected = true}, | |
| {.transform_str = "identity", .source_type = boolean(), .expected = true}, | |
| {.transform_str = "identity", | |
| .source_type = list(SchemaField(123, "element", int32(), false)), | |
| .expected = false}, | |
| // Void can transform any type | |
| {.transform_str = "void", .source_type = iceberg::int32(), .expected = true}, | |
| {.transform_str = "void", | |
| .source_type = iceberg::map(SchemaField(123, "key", iceberg::string(), false), | |
| SchemaField(124, "value", iceberg::int32(), true)), | |
| .expected = true}, | |
| // Bucket can transform specific types | |
| {.transform_str = "bucket[16]", .source_type = iceberg::int32(), .expected = true}, | |
| {.transform_str = "bucket[16]", .source_type = iceberg::string(), .expected = true}, | |
| {.transform_str = "bucket[16]", | |
| .source_type = iceberg::float32(), | |
| .expected = false}, | |
| // Truncate can transform specific types | |
| {.transform_str = "truncate[32]", | |
| .source_type = iceberg::int32(), | |
| .expected = true}, | |
| {.transform_str = "truncate[32]", | |
| .source_type = iceberg::string(), | |
| .expected = true}, | |
| {.transform_str = "truncate[32]", | |
| .source_type = iceberg::boolean(), | |
| .expected = false}, | |
| // Year can transform date and timestamp types | |
| {.transform_str = "year", .source_type = iceberg::date(), .expected = true}, | |
| {.transform_str = "year", .source_type = iceberg::timestamp(), .expected = true}, | |
| {.transform_str = "year", .source_type = iceberg::string(), .expected = false}, | |
| // Month can transform date and timestamp types | |
| {.transform_str = "month", .source_type = iceberg::date(), .expected = true}, | |
| {.transform_str = "month", .source_type = iceberg::timestamp(), .expected = true}, | |
| {.transform_str = "month", .source_type = iceberg::binary(), .expected = false}, | |
| // Day can transform date and timestamp types | |
| {.transform_str = "day", .source_type = iceberg::date(), .expected = true}, | |
| {.transform_str = "day", .source_type = iceberg::timestamp(), .expected = true}, | |
| {.transform_str = "day", .source_type = iceberg::uuid(), .expected = false}, | |
| // Hour can transform timestamp types | |
| {.transform_str = "hour", .source_type = iceberg::timestamp(), .expected = true}, | |
| {.transform_str = "hour", .source_type = iceberg::timestamp_tz(), .expected = true}, | |
| {.transform_str = "hour", .source_type = iceberg::int32(), .expected = false}, | |
| }; | |
| for (const auto& c : cases) { | |
| auto transform = TransformFromString(c.transform_str); | |
| ASSERT_TRUE(transform.has_value()) << "Failed to parse: " << c.transform_str; | |
| EXPECT_EQ(transform.value()->CanTransform(*c.source_type), c.expected) | |
| << "Unexpected result for transform: " << c.transform_str | |
| << " and source type: " << c.source_type->ToString(); | |
| } | |
| } | |
| // Test fixture for Transform::Project tests | |
| class TransformProjectTest : public ::testing::Test { | |
| protected: | |
| void SetUp() override { | |
| // Create test schemas for different source types | |
| int_schema_ = std::make_shared<Schema>( | |
| std::vector<SchemaField>{SchemaField::MakeRequired(1, "key", int32()), | |
| SchemaField::MakeOptional(2, "value", int32())}, | |
| /*schema_id=*/0); | |
| long_schema_ = std::make_shared<Schema>( | |
| std::vector<SchemaField>{SchemaField::MakeRequired(1, "value", int64())}, | |
| /*schema_id=*/0); | |
| string_schema_ = std::make_shared<Schema>( | |
| std::vector<SchemaField>{SchemaField::MakeRequired(1, "value", string())}, | |
| /*schema_id=*/0); | |
| date_schema_ = std::make_shared<Schema>( | |
| std::vector<SchemaField>{SchemaField::MakeRequired(1, "value", date())}, | |
| /*schema_id=*/0); | |
| timestamp_schema_ = std::make_shared<Schema>( | |
| std::vector<SchemaField>{SchemaField::MakeRequired(1, "value", timestamp())}, | |
| /*schema_id=*/0); | |
| } | |
| std::shared_ptr<Schema> int_schema_; | |
| std::shared_ptr<Schema> long_schema_; | |
| std::shared_ptr<Schema> string_schema_; | |
| std::shared_ptr<Schema> date_schema_; | |
| std::shared_ptr<Schema> timestamp_schema_; | |
| }; | |
| TEST_F(TransformProjectTest, IdentityProjectEquality) { | |
| auto transform = Transform::Identity(); | |
| // Test equality predicate | |
| auto unbound = Expressions::Equal("value", Literal::Int(100)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->Project("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kEq); | |
| auto unbound_projected = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected)); | |
| ASSERT_NE(unbound_projected, nullptr); | |
| EXPECT_EQ(unbound_projected->op(), Expression::Operation::kEq); | |
| EXPECT_EQ(unbound_projected->literals().size(), 1); | |
| EXPECT_EQ(std::get<int32_t>(unbound_projected->literals().front().value()), 100); | |
| } | |
| TEST_F(TransformProjectTest, IdentityProjectComparison) { | |
| auto transform = Transform::Identity(); | |
| // Test less than predicate | |
| auto unbound_lt = Expressions::LessThan("value", Literal::Int(50)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound_lt, | |
| unbound_lt->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_lt = std::dynamic_pointer_cast<BoundPredicate>(bound_lt); | |
| ASSERT_NE(bound_pred_lt, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_lt, transform->Project("part", bound_pred_lt)); | |
| ASSERT_NE(projected_lt, nullptr); | |
| EXPECT_EQ(projected_lt->op(), Expression::Operation::kLt); | |
| // Test greater than or equal predicate | |
| auto unbound_gte = Expressions::GreaterThanOrEqual("value", Literal::Int(100)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound_gte, | |
| unbound_gte->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_gte = std::dynamic_pointer_cast<BoundPredicate>(bound_gte); | |
| ASSERT_NE(bound_pred_gte, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_gte, transform->Project("part", bound_pred_gte)); | |
| ASSERT_NE(projected_gte, nullptr); | |
| EXPECT_EQ(projected_gte->op(), Expression::Operation::kGtEq); | |
| } | |
| TEST_F(TransformProjectTest, IdentityProjectUnary) { | |
| auto transform = Transform::Identity(); | |
| // Test IsNull predicate | |
| auto unbound_null = Expressions::IsNull("value"); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound_null, | |
| unbound_null->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_null = std::dynamic_pointer_cast<BoundPredicate>(bound_null); | |
| ASSERT_NE(bound_pred_null, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_null, | |
| transform->Project("part", bound_pred_null)); | |
| ASSERT_NE(projected_null, nullptr); | |
| EXPECT_EQ(projected_null->op(), Expression::Operation::kIsNull); | |
| } | |
| TEST_F(TransformProjectTest, IdentityProjectSet) { | |
| auto transform = Transform::Identity(); | |
| // Test IN predicate | |
| auto unbound_in = | |
| Expressions::In("value", {Literal::Int(1), Literal::Int(2), Literal::Int(3)}); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound_in, | |
| unbound_in->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_in = std::dynamic_pointer_cast<BoundPredicate>(bound_in); | |
| ASSERT_NE(bound_pred_in, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_in, transform->Project("part", bound_pred_in)); | |
| ASSERT_NE(projected_in, nullptr); | |
| EXPECT_EQ(projected_in->op(), Expression::Operation::kIn); | |
| auto unbound_projected = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected_in)); | |
| ASSERT_NE(unbound_projected, nullptr); | |
| EXPECT_EQ(unbound_projected->op(), Expression::Operation::kIn); | |
| EXPECT_EQ(unbound_projected->literals().size(), 3); | |
| std::vector<int32_t> values; | |
| for (const auto& lit : unbound_projected->literals()) { | |
| values.push_back(std::get<int32_t>(lit.value())); | |
| } | |
| EXPECT_THAT(values, testing::UnorderedElementsAre(1, 2, 3)); | |
| } | |
| TEST_F(TransformProjectTest, BucketProjectEquality) { | |
| auto transform = Transform::Bucket(4); | |
| // Bucket can project equality predicates | |
| auto unbound = Expressions::Equal("value", Literal::Int(34)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->Project("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kEq); | |
| auto unbound_projected = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected)); | |
| ASSERT_NE(unbound_projected, nullptr); | |
| EXPECT_EQ(unbound_projected->op(), Expression::Operation::kEq); | |
| EXPECT_EQ(unbound_projected->literals().size(), 1); | |
| EXPECT_EQ(std::get<int32_t>(unbound_projected->literals().front().value()), 3); | |
| } | |
| TEST_F(TransformProjectTest, BucketProjectWithMatchingTransformedChild) { | |
| auto partition_transform = Transform::Bucket(16); | |
| // Create a predicate like: bucket(value, 16) = 5 | |
| auto bucket_term = Expressions::Bucket("value", 16); | |
| auto unbound = Expressions::Equal<BoundTransform>(bucket_term, Literal::Int(5)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| // The predicate's term should be a transform | |
| EXPECT_EQ(bound_pred->term()->kind(), Term::Kind::kTransform); | |
| // When the transform matches, Project should use RemoveTransform and return the | |
| // predicate | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, | |
| partition_transform->Project("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kEq); | |
| auto unbound_projected = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected)); | |
| ASSERT_NE(unbound_projected, nullptr); | |
| EXPECT_EQ(unbound_projected->op(), Expression::Operation::kEq); | |
| EXPECT_EQ(unbound_projected->literals().size(), 1); | |
| EXPECT_EQ(std::get<int32_t>(unbound_projected->literals().front().value()), 5); | |
| } | |
| TEST_F(TransformProjectTest, BucketProjectComparisonReturnsNull) { | |
| auto transform = Transform::Bucket(16); | |
| // Bucket cannot project comparison predicates (they return null) | |
| auto unbound_lt = Expressions::LessThan("value", Literal::Int(50)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound_lt, | |
| unbound_lt->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_lt = std::dynamic_pointer_cast<BoundPredicate>(bound_lt); | |
| ASSERT_NE(bound_pred_lt, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_lt, transform->Project("part", bound_pred_lt)); | |
| EXPECT_EQ(projected_lt, nullptr); | |
| } | |
| TEST_F(TransformProjectTest, BucketProjectInSet) { | |
| auto transform = Transform::Bucket(16); | |
| // Bucket can project IN predicates | |
| auto unbound_in = | |
| Expressions::In("value", {Literal::Int(1), Literal::Int(2), Literal::Int(3)}); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound_in, | |
| unbound_in->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_in = std::dynamic_pointer_cast<BoundPredicate>(bound_in); | |
| ASSERT_NE(bound_pred_in, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_in, transform->Project("part", bound_pred_in)); | |
| ASSERT_NE(projected_in, nullptr); | |
| EXPECT_EQ(projected_in->op(), Expression::Operation::kIn); | |
| } | |
| TEST_F(TransformProjectTest, BucketProjectNotInReturnsNull) { | |
| auto transform = Transform::Bucket(16); | |
| // Bucket cannot project NOT IN predicates | |
| auto unbound_not_in = | |
| Expressions::NotIn("value", {Literal::Int(1), Literal::Int(2), Literal::Int(3)}); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound_not_in, | |
| unbound_not_in->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_not_in = std::dynamic_pointer_cast<BoundPredicate>(bound_not_in); | |
| ASSERT_NE(bound_pred_not_in, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_not_in, | |
| transform->Project("part", bound_pred_not_in)); | |
| EXPECT_EQ(projected_not_in, nullptr); | |
| } | |
| TEST_F(TransformProjectTest, TruncateProjectIntEquality) { | |
| auto transform = Transform::Truncate(10); | |
| // Truncate can project equality predicates | |
| auto unbound = Expressions::Equal("value", Literal::Int(123)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->Project("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kEq); | |
| auto unbound_projected = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected)); | |
| ASSERT_NE(unbound_projected, nullptr); | |
| EXPECT_EQ(unbound_projected->op(), Expression::Operation::kEq); | |
| EXPECT_EQ(unbound_projected->literals().size(), 1); | |
| EXPECT_EQ(std::get<int32_t>(unbound_projected->literals().front().value()), 120); | |
| } | |
| TEST_F(TransformProjectTest, TruncateProjectIntLessThan) { | |
| auto transform = Transform::Truncate(10); | |
| // Truncate projects LT as LTE | |
| auto unbound = Expressions::LessThan("value", Literal::Int(25)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->Project("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kLtEq); | |
| } | |
| TEST_F(TransformProjectTest, TruncateProjectIntGreaterThan) { | |
| auto transform = Transform::Truncate(10); | |
| // Truncate projects GT as GTE | |
| auto unbound = Expressions::GreaterThan("value", Literal::Int(25)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->Project("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kGtEq); | |
| auto unbound_projected = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected)); | |
| ASSERT_NE(unbound_projected, nullptr); | |
| EXPECT_EQ(unbound_projected->op(), Expression::Operation::kGtEq); | |
| EXPECT_EQ(unbound_projected->literals().size(), 1); | |
| EXPECT_EQ(std::get<int32_t>(unbound_projected->literals().front().value()), 20); | |
| } | |
| TEST_F(TransformProjectTest, TruncateProjectStringEquality) { | |
| auto transform = Transform::Truncate(5); | |
| auto unbound = Expressions::Equal("value", Literal::String("Hello, World!")); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*string_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->Project("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kEq); | |
| auto unbound_projected = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected)); | |
| ASSERT_NE(unbound_projected, nullptr); | |
| EXPECT_EQ(unbound_projected->op(), Expression::Operation::kEq); | |
| EXPECT_EQ(unbound_projected->literals().size(), 1); | |
| EXPECT_EQ(std::get<std::string>(unbound_projected->literals().front().value()), | |
| "Hello"); | |
| } | |
| TEST_F(TransformProjectTest, TruncateProjectStringStartsWith) { | |
| auto transform = Transform::Truncate(5); | |
| // StartsWith with shorter string than width | |
| auto unbound_short = Expressions::StartsWith("value", "Hi"); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound_short, | |
| unbound_short->Bind(*string_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_short = std::dynamic_pointer_cast<BoundPredicate>(bound_short); | |
| ASSERT_NE(bound_pred_short, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_short, | |
| transform->Project("part", bound_pred_short)); | |
| ASSERT_NE(projected_short, nullptr); | |
| EXPECT_EQ(projected_short->op(), Expression::Operation::kStartsWith); | |
| auto unbound_projected_short = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected_short)); | |
| ASSERT_NE(unbound_projected_short, nullptr); | |
| EXPECT_EQ(unbound_projected_short->op(), Expression::Operation::kStartsWith); | |
| EXPECT_EQ(unbound_projected_short->literals().size(), 1); | |
| EXPECT_EQ(std::get<std::string>(unbound_projected_short->literals().front().value()), | |
| "Hi"); | |
| // StartsWith with string equal to width | |
| auto unbound_equal = Expressions::StartsWith("value", "Hello"); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound_equal, | |
| unbound_equal->Bind(*string_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_equal = std::dynamic_pointer_cast<BoundPredicate>(bound_equal); | |
| ASSERT_NE(bound_pred_equal, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_equal, | |
| transform->Project("part", bound_pred_equal)); | |
| ASSERT_NE(projected_equal, nullptr); | |
| EXPECT_EQ(projected_equal->op(), Expression::Operation::kEq); | |
| auto unbound_projected_equal = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected_equal)); | |
| ASSERT_NE(unbound_projected_equal, nullptr); | |
| EXPECT_EQ(unbound_projected_equal->op(), Expression::Operation::kEq); | |
| EXPECT_EQ(unbound_projected_equal->literals().size(), 1); | |
| EXPECT_EQ(std::get<std::string>(unbound_projected_equal->literals().front().value()), | |
| "Hello"); | |
| } | |
| TEST_F(TransformProjectTest, TruncateProjectStringStartsWithCodePointCountLessThanWidth) { | |
| auto transform = Transform::Truncate(5); | |
| // Code point count < width (multi-byte UTF-8 characters) | |
| // "😜🧐" has 2 code points, width is 5 | |
| auto unbound_emoji_short = Expressions::StartsWith("value", "😜🧐"); | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto bound_emoji_short, | |
| unbound_emoji_short->Bind(*string_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_emoji_short = | |
| std::dynamic_pointer_cast<BoundPredicate>(bound_emoji_short); | |
| ASSERT_NE(bound_pred_emoji_short, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_emoji_short, | |
| transform->Project("part", bound_pred_emoji_short)); | |
| ASSERT_NE(projected_emoji_short, nullptr); | |
| EXPECT_EQ(projected_emoji_short->op(), Expression::Operation::kStartsWith); | |
| auto unbound_projected_emoji_short = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected_emoji_short)); | |
| ASSERT_NE(unbound_projected_emoji_short, nullptr); | |
| EXPECT_EQ(unbound_projected_emoji_short->op(), Expression::Operation::kStartsWith); | |
| EXPECT_EQ(unbound_projected_emoji_short->literals().size(), 1); | |
| EXPECT_EQ( | |
| std::get<std::string>(unbound_projected_emoji_short->literals().front().value()), | |
| "😜🧐"); | |
| } | |
| TEST_F(TransformProjectTest, TruncateProjectStringStartsWithCodePointCountEqualToWidth) { | |
| auto transform = Transform::Truncate(5); | |
| // Code point count == width (exactly 5 code points) | |
| // "😜🧐🤔🤪🥳" has exactly 5 code points | |
| auto unbound_emoji_equal = Expressions::StartsWith("value", "😜🧐🤔🤪🥳"); | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto bound_emoji_equal, | |
| unbound_emoji_equal->Bind(*string_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_emoji_equal = | |
| std::dynamic_pointer_cast<BoundPredicate>(bound_emoji_equal); | |
| ASSERT_NE(bound_pred_emoji_equal, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_emoji_equal, | |
| transform->Project("part", bound_pred_emoji_equal)); | |
| ASSERT_NE(projected_emoji_equal, nullptr); | |
| EXPECT_EQ(projected_emoji_equal->op(), Expression::Operation::kEq); | |
| auto unbound_projected_emoji_equal = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected_emoji_equal)); | |
| ASSERT_NE(unbound_projected_emoji_equal, nullptr); | |
| EXPECT_EQ(unbound_projected_emoji_equal->op(), Expression::Operation::kEq); | |
| EXPECT_EQ(unbound_projected_emoji_equal->literals().size(), 1); | |
| EXPECT_EQ( | |
| std::get<std::string>(unbound_projected_emoji_equal->literals().front().value()), | |
| "😜🧐🤔🤪🥳"); | |
| } | |
| TEST_F(TransformProjectTest, | |
| TruncateProjectStringStartsWithCodePointCountGreaterThanWidth) { | |
| auto transform = Transform::Truncate(5); | |
| // Code point count > width (truncate to 5 code points) | |
| // "😜🧐🤔🤪🥳😵💫😂" has 7 code points, should truncate to 5 | |
| auto unbound_emoji_long = | |
| Expressions::StartsWith("value", "😜🧐🤔🤪🥳😵💫😂"); | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto bound_emoji_long, | |
| unbound_emoji_long->Bind(*string_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_emoji_long = | |
| std::dynamic_pointer_cast<BoundPredicate>(bound_emoji_long); | |
| ASSERT_NE(bound_pred_emoji_long, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_emoji_long, | |
| transform->Project("part", bound_pred_emoji_long)); | |
| ASSERT_NE(projected_emoji_long, nullptr); | |
| EXPECT_EQ(projected_emoji_long->op(), Expression::Operation::kStartsWith); | |
| auto unbound_projected_emoji_long = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected_emoji_long)); | |
| ASSERT_NE(unbound_projected_emoji_long, nullptr); | |
| EXPECT_EQ(unbound_projected_emoji_long->op(), Expression::Operation::kStartsWith); | |
| EXPECT_EQ(unbound_projected_emoji_long->literals().size(), 1); | |
| EXPECT_EQ( | |
| std::get<std::string>(unbound_projected_emoji_long->literals().front().value()), | |
| "😜🧐🤔🤪🥳"); | |
| } | |
| TEST_F(TransformProjectTest, TruncateProjectStringStartsWithMixedAsciiAndMultiByte) { | |
| auto transform = Transform::Truncate(5); | |
| // Mixed ASCII and multi-byte UTF-8 characters | |
| // "a😜b🧐c" has 5 code points (3 ASCII + 2 emojis) | |
| auto unbound_mixed_equal = Expressions::StartsWith("value", "a😜b🧐c"); | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto bound_mixed_equal, | |
| unbound_mixed_equal->Bind(*string_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_mixed_equal = | |
| std::dynamic_pointer_cast<BoundPredicate>(bound_mixed_equal); | |
| ASSERT_NE(bound_pred_mixed_equal, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_mixed_equal, | |
| transform->Project("part", bound_pred_mixed_equal)); | |
| ASSERT_NE(projected_mixed_equal, nullptr); | |
| EXPECT_EQ(projected_mixed_equal->op(), Expression::Operation::kEq); | |
| auto unbound_projected_mixed_equal = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected_mixed_equal)); | |
| ASSERT_NE(unbound_projected_mixed_equal, nullptr); | |
| EXPECT_EQ(unbound_projected_mixed_equal->op(), Expression::Operation::kEq); | |
| EXPECT_EQ(unbound_projected_mixed_equal->literals().size(), 1); | |
| EXPECT_EQ( | |
| std::get<std::string>(unbound_projected_mixed_equal->literals().front().value()), | |
| "a😜b🧐c"); | |
| } | |
| TEST_F(TransformProjectTest, TruncateProjectStringStartsWithChineseCharactersShort) { | |
| auto transform = Transform::Truncate(5); | |
| // Chinese characters (3-byte UTF-8) | |
| // "你好世界" has 4 code points, width is 5 | |
| auto unbound_chinese_short = Expressions::StartsWith("value", "你好世界"); | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto bound_chinese_short, | |
| unbound_chinese_short->Bind(*string_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_chinese_short = | |
| std::dynamic_pointer_cast<BoundPredicate>(bound_chinese_short); | |
| ASSERT_NE(bound_pred_chinese_short, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_chinese_short, | |
| transform->Project("part", bound_pred_chinese_short)); | |
| ASSERT_NE(projected_chinese_short, nullptr); | |
| EXPECT_EQ(projected_chinese_short->op(), Expression::Operation::kStartsWith); | |
| auto unbound_projected_chinese_short = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected_chinese_short)); | |
| ASSERT_NE(unbound_projected_chinese_short, nullptr); | |
| EXPECT_EQ(unbound_projected_chinese_short->op(), Expression::Operation::kStartsWith); | |
| EXPECT_EQ(unbound_projected_chinese_short->literals().size(), 1); | |
| EXPECT_EQ( | |
| std::get<std::string>(unbound_projected_chinese_short->literals().front().value()), | |
| "你好世界"); | |
| } | |
| TEST_F(TransformProjectTest, TruncateProjectStringStartsWithChineseCharactersEqualWidth) { | |
| auto transform = Transform::Truncate(5); | |
| // Chinese characters exactly matching width | |
| // "你好世界好" has exactly 5 code points | |
| auto unbound_chinese_equal = Expressions::StartsWith("value", "你好世界好"); | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto bound_chinese_equal, | |
| unbound_chinese_equal->Bind(*string_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_chinese_equal = | |
| std::dynamic_pointer_cast<BoundPredicate>(bound_chinese_equal); | |
| ASSERT_NE(bound_pred_chinese_equal, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_chinese_equal, | |
| transform->Project("part", bound_pred_chinese_equal)); | |
| ASSERT_NE(projected_chinese_equal, nullptr); | |
| EXPECT_EQ(projected_chinese_equal->op(), Expression::Operation::kEq); | |
| auto unbound_projected_chinese_equal = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected_chinese_equal)); | |
| ASSERT_NE(unbound_projected_chinese_equal, nullptr); | |
| EXPECT_EQ(unbound_projected_chinese_equal->op(), Expression::Operation::kEq); | |
| EXPECT_EQ(unbound_projected_chinese_equal->literals().size(), 1); | |
| EXPECT_EQ( | |
| std::get<std::string>(unbound_projected_chinese_equal->literals().front().value()), | |
| "你好世界好"); | |
| } | |
| TEST_F(TransformProjectTest, | |
| TruncateProjectStringNotStartsWithCodePointCountEqualToWidth) { | |
| auto transform = Transform::Truncate(5); | |
| // NotStartsWith with code point count == width | |
| // Should convert to NotEq | |
| auto unbound_not_starts_equal = Expressions::NotStartsWith("value", "😜🧐🤔🤪🥳"); | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto bound_not_starts_equal, | |
| unbound_not_starts_equal->Bind(*string_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_not_starts_equal = | |
| std::dynamic_pointer_cast<BoundPredicate>(bound_not_starts_equal); | |
| ASSERT_NE(bound_pred_not_starts_equal, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_not_starts_equal, | |
| transform->Project("part", bound_pred_not_starts_equal)); | |
| ASSERT_NE(projected_not_starts_equal, nullptr); | |
| EXPECT_EQ(projected_not_starts_equal->op(), Expression::Operation::kNotEq); | |
| auto unbound_projected_not_starts_equal = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected_not_starts_equal)); | |
| ASSERT_NE(unbound_projected_not_starts_equal, nullptr); | |
| EXPECT_EQ(unbound_projected_not_starts_equal->op(), Expression::Operation::kNotEq); | |
| EXPECT_EQ(unbound_projected_not_starts_equal->literals().size(), 1); | |
| EXPECT_EQ(std::get<std::string>( | |
| unbound_projected_not_starts_equal->literals().front().value()), | |
| "😜🧐🤔🤪🥳"); | |
| } | |
| TEST_F(TransformProjectTest, | |
| TruncateProjectStringNotStartsWithCodePointCountLessThanWidth) { | |
| auto transform = Transform::Truncate(5); | |
| // NotStartsWith with code point count < width | |
| // Should remain NotStartsWith | |
| auto unbound_not_starts_short = Expressions::NotStartsWith("value", "😜🧐"); | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto bound_not_starts_short, | |
| unbound_not_starts_short->Bind(*string_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_not_starts_short = | |
| std::dynamic_pointer_cast<BoundPredicate>(bound_not_starts_short); | |
| ASSERT_NE(bound_pred_not_starts_short, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_not_starts_short, | |
| transform->Project("part", bound_pred_not_starts_short)); | |
| ASSERT_NE(projected_not_starts_short, nullptr); | |
| EXPECT_EQ(projected_not_starts_short->op(), Expression::Operation::kNotStartsWith); | |
| auto unbound_projected_not_starts_short = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected_not_starts_short)); | |
| ASSERT_NE(unbound_projected_not_starts_short, nullptr); | |
| EXPECT_EQ(unbound_projected_not_starts_short->op(), | |
| Expression::Operation::kNotStartsWith); | |
| EXPECT_EQ(unbound_projected_not_starts_short->literals().size(), 1); | |
| EXPECT_EQ(std::get<std::string>( | |
| unbound_projected_not_starts_short->literals().front().value()), | |
| "😜🧐"); | |
| } | |
| TEST_F(TransformProjectTest, | |
| TruncateProjectStringNotStartsWithCodePointCountGreaterThanWidth) { | |
| auto transform = Transform::Truncate(5); | |
| // NotStartsWith with code point count > width | |
| // Should return nullptr (cannot project) | |
| auto unbound_not_starts_long = | |
| Expressions::NotStartsWith("value", "😜🧐🤔🤪🥳😵💫😂"); | |
| ICEBERG_UNWRAP_OR_FAIL( | |
| auto bound_not_starts_long, | |
| unbound_not_starts_long->Bind(*string_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_not_starts_long = | |
| std::dynamic_pointer_cast<BoundPredicate>(bound_not_starts_long); | |
| ASSERT_NE(bound_pred_not_starts_long, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_not_starts_long, | |
| transform->Project("part", bound_pred_not_starts_long)); | |
| EXPECT_EQ(projected_not_starts_long, nullptr); | |
| } | |
| TEST_F(TransformProjectTest, YearProjectEquality) { | |
| auto transform = Transform::Year(); | |
| // 2021-06-01 as days from epoch | |
| int32_t date_value = | |
| TemporalTestHelper::CreateDate({.year = 2021, .month = 6, .day = 1}); | |
| auto unbound = Expressions::Equal("value", Literal::Date(date_value)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*date_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->Project("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kEq); | |
| } | |
| TEST_F(TransformProjectTest, YearProjectComparison) { | |
| auto transform = Transform::Year(); | |
| int32_t date_value = | |
| TemporalTestHelper::CreateDate({.year = 2021, .month = 6, .day = 1}); | |
| // LT projects to LTE | |
| auto unbound_lt = Expressions::LessThan("value", Literal::Date(date_value)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound_lt, | |
| unbound_lt->Bind(*date_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_lt = std::dynamic_pointer_cast<BoundPredicate>(bound_lt); | |
| ASSERT_NE(bound_pred_lt, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_lt, transform->Project("part", bound_pred_lt)); | |
| ASSERT_NE(projected_lt, nullptr); | |
| EXPECT_EQ(projected_lt->op(), Expression::Operation::kLtEq); | |
| // GT projects to GTE | |
| auto unbound_gt = Expressions::GreaterThan("value", Literal::Date(date_value)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound_gt, | |
| unbound_gt->Bind(*date_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_gt = std::dynamic_pointer_cast<BoundPredicate>(bound_gt); | |
| ASSERT_NE(bound_pred_gt, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_gt, transform->Project("part", bound_pred_gt)); | |
| ASSERT_NE(projected_gt, nullptr); | |
| EXPECT_EQ(projected_gt->op(), Expression::Operation::kGtEq); | |
| } | |
| TEST_F(TransformProjectTest, MonthProjectEquality) { | |
| auto transform = Transform::Month(); | |
| int64_t ts_value = | |
| TemporalTestHelper::CreateTimestamp({.year = 2021, .month = 6, .day = 1}); | |
| auto unbound = Expressions::Equal("value", Literal::Timestamp(ts_value)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*timestamp_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->Project("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kEq); | |
| } | |
| TEST_F(TransformProjectTest, DayProjectEquality) { | |
| auto transform = Transform::Day(); | |
| int32_t date_value = | |
| TemporalTestHelper::CreateDate({.year = 2021, .month = 6, .day = 15}); | |
| auto unbound = Expressions::Equal("value", Literal::Date(date_value)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*date_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->Project("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kEq); | |
| } | |
| TEST_F(TransformProjectTest, HourProjectEquality) { | |
| auto transform = Transform::Hour(); | |
| int64_t ts_value = TemporalTestHelper::CreateTimestamp( | |
| {.year = 2021, .month = 6, .day = 1, .hour = 14, .minute = 30}); | |
| auto unbound = Expressions::Equal("value", Literal::Timestamp(ts_value)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*timestamp_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->Project("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kEq); | |
| } | |
| TEST_F(TransformProjectTest, VoidProjectReturnsNull) { | |
| auto transform = Transform::Void(); | |
| auto unbound = Expressions::Equal("value", Literal::Int(100)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| // Void transform always returns null (no projection possible) | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->Project("part", bound_pred)); | |
| EXPECT_EQ(projected, nullptr); | |
| } | |
| TEST_F(TransformProjectTest, TemporalProjectInSet) { | |
| auto transform = Transform::Year(); | |
| int32_t date1 = TemporalTestHelper::CreateDate({.year = 2020, .month = 1, .day = 1}); | |
| int32_t date2 = TemporalTestHelper::CreateDate({.year = 2021, .month = 6, .day = 15}); | |
| int32_t date3 = TemporalTestHelper::CreateDate({.year = 2022, .month = 12, .day = 31}); | |
| auto unbound_in = Expressions::In( | |
| "value", {Literal::Date(date1), Literal::Date(date2), Literal::Date(date3)}); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound_in, | |
| unbound_in->Bind(*date_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_in = std::dynamic_pointer_cast<BoundPredicate>(bound_in); | |
| ASSERT_NE(bound_pred_in, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_in, transform->Project("part", bound_pred_in)); | |
| ASSERT_NE(projected_in, nullptr); | |
| EXPECT_EQ(projected_in->op(), Expression::Operation::kIn); | |
| } | |
| TEST_F(TransformProjectTest, DayTimestampProjectionFix) { | |
| auto transform = Transform::Day(); | |
| // Predicate: value < 1970-01-01 00:00:00 (0) | |
| // This implies value <= -1 micros. | |
| // day(-1 micros) = -1 day (1969-12-31). | |
| // If we don't fix, we project to day <= -1. | |
| // If we fix (for buggy writers), we project to day <= 0. | |
| auto unbound = Expressions::LessThan("value", Literal::Timestamp(0)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*timestamp_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->Project("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| auto unbound_projected = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected)); | |
| EXPECT_EQ(unbound_projected->op(), Expression::Operation::kLtEq); | |
| ASSERT_EQ(unbound_projected->literals().size(), 1); | |
| int32_t val = std::get<int32_t>(unbound_projected->literals().front().value()); | |
| EXPECT_EQ(val, 0) << "Expected projected value to be 0 (fix applied), but got " << val; | |
| } | |
| // Test fixture for Transform::ProjectStrict tests | |
| class TransformProjectStrictTest : public ::testing::Test { | |
| protected: | |
| void SetUp() override { | |
| // Create test schemas for different source types | |
| int_schema_ = std::make_shared<Schema>( | |
| std::vector<SchemaField>{SchemaField::MakeRequired(1, "value", int32())}, | |
| /*schema_id=*/0); | |
| long_schema_ = std::make_shared<Schema>( | |
| std::vector<SchemaField>{SchemaField::MakeRequired(1, "value", int64())}, | |
| /*schema_id=*/0); | |
| string_schema_ = std::make_shared<Schema>( | |
| std::vector<SchemaField>{SchemaField::MakeRequired(1, "value", string())}, | |
| /*schema_id=*/0); | |
| date_schema_ = std::make_shared<Schema>( | |
| std::vector<SchemaField>{SchemaField::MakeRequired(1, "value", date())}, | |
| /*schema_id=*/0); | |
| timestamp_schema_ = std::make_shared<Schema>( | |
| std::vector<SchemaField>{SchemaField::MakeRequired(1, "value", timestamp())}, | |
| /*schema_id=*/0); | |
| decimal_schema_ = std::make_shared<Schema>( | |
| std::vector<SchemaField>{SchemaField::MakeRequired(1, "value", decimal(9, 2))}, | |
| /*schema_id=*/0); | |
| } | |
| std::shared_ptr<Schema> int_schema_; | |
| std::shared_ptr<Schema> long_schema_; | |
| std::shared_ptr<Schema> string_schema_; | |
| std::shared_ptr<Schema> date_schema_; | |
| std::shared_ptr<Schema> timestamp_schema_; | |
| std::shared_ptr<Schema> decimal_schema_; | |
| }; | |
| TEST_F(TransformProjectStrictTest, IdentityStrictProjection) { | |
| auto transform = Transform::Identity(); | |
| // Identity strict projection should behave the same as inclusive projection | |
| auto unbound = Expressions::Equal("value", Literal::Int(100)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kEq); | |
| auto unbound_projected = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected)); | |
| EXPECT_EQ(unbound_projected->op(), Expression::Operation::kEq); | |
| EXPECT_EQ(unbound_projected->literals().size(), 1); | |
| EXPECT_EQ(std::get<int32_t>(unbound_projected->literals().front().value()), 100); | |
| } | |
| TEST_F(TransformProjectStrictTest, BucketStrictEqualityReturnsFalse) { | |
| auto transform = Transform::Bucket(10); | |
| // Bucket strict projection: equality should return FALSE (cannot guarantee equality) | |
| auto unbound = Expressions::Equal("value", Literal::Int(100)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); | |
| EXPECT_EQ(projected, nullptr); | |
| } | |
| TEST_F(TransformProjectStrictTest, BucketStrictNotEqual) { | |
| auto transform = Transform::Bucket(10); | |
| // Bucket strict projection: notEqual can be projected | |
| auto unbound = Expressions::NotEqual("value", Literal::Int(100)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kNotEq); | |
| auto unbound_projected = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected)); | |
| EXPECT_EQ(unbound_projected->op(), Expression::Operation::kNotEq); | |
| EXPECT_EQ(unbound_projected->literals().size(), 1); | |
| // bucket(100, 10) = 6 | |
| EXPECT_EQ(std::get<int32_t>(unbound_projected->literals().front().value()), 6); | |
| } | |
| TEST_F(TransformProjectStrictTest, BucketStrictComparisonReturnsNull) { | |
| auto transform = Transform::Bucket(10); | |
| // Bucket strict projection: comparison predicates return null | |
| auto unbound_lt = Expressions::LessThan("value", Literal::Int(100)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound_lt, | |
| unbound_lt->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_lt = std::dynamic_pointer_cast<BoundPredicate>(bound_lt); | |
| ASSERT_NE(bound_pred_lt, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_lt, | |
| transform->ProjectStrict("part", bound_pred_lt)); | |
| EXPECT_EQ(projected_lt, nullptr); | |
| } | |
| TEST_F(TransformProjectStrictTest, BucketStrictNotIn) { | |
| auto transform = Transform::Bucket(10); | |
| // Bucket strict projection: NOT_IN can be projected | |
| auto unbound_not_in = Expressions::NotIn( | |
| "value", {Literal::Int(99), Literal::Int(100), Literal::Int(101)}); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound_not_in, | |
| unbound_not_in->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_not_in = std::dynamic_pointer_cast<BoundPredicate>(bound_not_in); | |
| ASSERT_NE(bound_pred_not_in, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_not_in, | |
| transform->ProjectStrict("part", bound_pred_not_in)); | |
| ASSERT_NE(projected_not_in, nullptr); | |
| EXPECT_EQ(projected_not_in->op(), Expression::Operation::kNotIn); | |
| } | |
| TEST_F(TransformProjectStrictTest, BucketStrictInReturnsNull) { | |
| auto transform = Transform::Bucket(10); | |
| // Bucket strict projection: IN returns null (cannot guarantee) | |
| auto unbound_in = | |
| Expressions::In("value", {Literal::Int(99), Literal::Int(100), Literal::Int(101)}); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound_in, | |
| unbound_in->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_in = std::dynamic_pointer_cast<BoundPredicate>(bound_in); | |
| ASSERT_NE(bound_pred_in, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_in, | |
| transform->ProjectStrict("part", bound_pred_in)); | |
| EXPECT_EQ(projected_in, nullptr); | |
| } | |
| TEST_F(TransformProjectStrictTest, BucketStrictString) { | |
| auto transform = Transform::Bucket(10); | |
| // Bucket strict projection for string | |
| auto unbound_not_eq = Expressions::NotEqual("value", Literal::String("abcdefg")); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound_not_eq, | |
| unbound_not_eq->Bind(*string_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_not_eq = std::dynamic_pointer_cast<BoundPredicate>(bound_not_eq); | |
| ASSERT_NE(bound_pred_not_eq, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_not_eq, | |
| transform->ProjectStrict("part", bound_pred_not_eq)); | |
| ASSERT_NE(projected_not_eq, nullptr); | |
| EXPECT_EQ(projected_not_eq->op(), Expression::Operation::kNotEq); | |
| } | |
| TEST_F(TransformProjectStrictTest, TruncateStrictIntEqualityReturnsNull) { | |
| auto transform = Transform::Truncate(10); | |
| // Truncate strict projection: equality returns null (cannot guarantee) | |
| auto unbound = Expressions::Equal("value", Literal::Int(123)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); | |
| EXPECT_EQ(projected, nullptr); | |
| } | |
| TEST_F(TransformProjectStrictTest, TruncateStrictIntLessThan) { | |
| auto transform = Transform::Truncate(10); | |
| // Truncate strict projection: LT projects to LT | |
| auto unbound = Expressions::LessThan("value", Literal::Int(100)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kLt); | |
| auto unbound_projected = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected)); | |
| EXPECT_EQ(unbound_projected->op(), Expression::Operation::kLt); | |
| EXPECT_EQ(unbound_projected->literals().size(), 1); | |
| EXPECT_EQ(std::get<int32_t>(unbound_projected->literals().front().value()), 100); | |
| } | |
| TEST_F(TransformProjectStrictTest, TruncateStrictIntLessThanOrEqual) { | |
| auto transform = Transform::Truncate(10); | |
| // Truncate strict projection: LTE projects to LT | |
| auto unbound = Expressions::LessThanOrEqual("value", Literal::Int(100)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kLt); | |
| auto unbound_projected = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected)); | |
| EXPECT_EQ(unbound_projected->op(), Expression::Operation::kLt); | |
| EXPECT_EQ(unbound_projected->literals().size(), 1); | |
| EXPECT_EQ(std::get<int32_t>(unbound_projected->literals().front().value()), 100); | |
| } | |
| TEST_F(TransformProjectStrictTest, TruncateStrictIntGreaterThan) { | |
| auto transform = Transform::Truncate(10); | |
| // Truncate strict projection: GT projects to GT | |
| auto unbound = Expressions::GreaterThan("value", Literal::Int(100)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kGt); | |
| auto unbound_projected = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected)); | |
| EXPECT_EQ(unbound_projected->op(), Expression::Operation::kGt); | |
| EXPECT_EQ(unbound_projected->literals().size(), 1); | |
| EXPECT_EQ(std::get<int32_t>(unbound_projected->literals().front().value()), 100); | |
| } | |
| TEST_F(TransformProjectStrictTest, TruncateStrictIntGreaterThanOrEqualLowerBound) { | |
| auto transform = Transform::Truncate(10); | |
| // Truncate strict projection: GTE projects to GT (lower bound, value = 100) | |
| auto unbound = Expressions::GreaterThanOrEqual("value", Literal::Int(100)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kGt); | |
| auto unbound_projected = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected)); | |
| EXPECT_EQ(unbound_projected->op(), Expression::Operation::kGt); | |
| EXPECT_EQ(unbound_projected->literals().size(), 1); | |
| // For GTE with value 100 and width 10, truncate(100) = 100, so GT should be 90 | |
| EXPECT_EQ(std::get<int32_t>(unbound_projected->literals().front().value()), 90); | |
| } | |
| TEST_F(TransformProjectStrictTest, TruncateStrictIntGreaterThanOrEqualUpperBound) { | |
| auto transform = Transform::Truncate(10); | |
| // Truncate strict projection: GTE projects to GT (upper bound, value = 99) | |
| auto unbound = Expressions::GreaterThanOrEqual("value", Literal::Int(99)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kGt); | |
| auto unbound_projected = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected)); | |
| EXPECT_EQ(unbound_projected->op(), Expression::Operation::kGt); | |
| EXPECT_EQ(unbound_projected->literals().size(), 1); | |
| // For GTE with value 99 and width 10, truncate(99) = 90, so GT should be 90 | |
| EXPECT_EQ(std::get<int32_t>(unbound_projected->literals().front().value()), 90); | |
| } | |
| TEST_F(TransformProjectStrictTest, TruncateStrictIntNotEqual) { | |
| auto transform = Transform::Truncate(10); | |
| // Truncate strict projection: notEqual can be projected | |
| auto unbound = Expressions::NotEqual("value", Literal::Int(100)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kNotEq); | |
| auto unbound_projected = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected)); | |
| EXPECT_EQ(unbound_projected->op(), Expression::Operation::kNotEq); | |
| EXPECT_EQ(unbound_projected->literals().size(), 1); | |
| EXPECT_EQ(std::get<int32_t>(unbound_projected->literals().front().value()), 100); | |
| } | |
| TEST_F(TransformProjectStrictTest, TruncateStrictIntNotIn) { | |
| auto transform = Transform::Truncate(10); | |
| // Truncate strict projection: NOT_IN can be projected | |
| auto unbound_not_in = Expressions::NotIn( | |
| "value", {Literal::Int(99), Literal::Int(100), Literal::Int(101)}); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound_not_in, | |
| unbound_not_in->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_not_in = std::dynamic_pointer_cast<BoundPredicate>(bound_not_in); | |
| ASSERT_NE(bound_pred_not_in, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_not_in, | |
| transform->ProjectStrict("part", bound_pred_not_in)); | |
| ASSERT_NE(projected_not_in, nullptr); | |
| EXPECT_EQ(projected_not_in->op(), Expression::Operation::kNotIn); | |
| } | |
| TEST_F(TransformProjectStrictTest, TruncateStrictString) { | |
| auto transform = Transform::Truncate(5); | |
| // Truncate strict projection for string | |
| auto unbound_lt = Expressions::LessThan("value", Literal::String("abcdefg")); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound_lt, | |
| unbound_lt->Bind(*string_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred_lt = std::dynamic_pointer_cast<BoundPredicate>(bound_lt); | |
| ASSERT_NE(bound_pred_lt, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected_lt, | |
| transform->ProjectStrict("part", bound_pred_lt)); | |
| ASSERT_NE(projected_lt, nullptr); | |
| EXPECT_EQ(projected_lt->op(), Expression::Operation::kLt); | |
| auto unbound_projected_lt = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected_lt)); | |
| EXPECT_EQ(unbound_projected_lt->op(), Expression::Operation::kLt); | |
| EXPECT_EQ(unbound_projected_lt->literals().size(), 1); | |
| EXPECT_EQ(std::get<std::string>(unbound_projected_lt->literals().front().value()), | |
| "abcde"); | |
| } | |
| TEST_F(TransformProjectStrictTest, YearStrictEqualityReturnsNull) { | |
| auto transform = Transform::Year(); | |
| // Year strict projection: equality returns null (cannot guarantee) | |
| int32_t date_value = | |
| TemporalTestHelper::CreateDate({.year = 2021, .month = 6, .day = 1}); | |
| auto unbound = Expressions::Equal("value", Literal::Date(date_value)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*date_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); | |
| EXPECT_EQ(projected, nullptr); | |
| } | |
| TEST_F(TransformProjectStrictTest, YearStrictLessThan) { | |
| auto transform = Transform::Year(); | |
| // Year strict projection: LT projects to LT | |
| int32_t date_value = | |
| TemporalTestHelper::CreateDate({.year = 2021, .month = 1, .day = 1}); | |
| auto unbound = Expressions::LessThan("value", Literal::Date(date_value)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*date_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kLt); | |
| auto unbound_projected = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected)); | |
| EXPECT_EQ(unbound_projected->op(), Expression::Operation::kLt); | |
| EXPECT_EQ(unbound_projected->literals().size(), 1); | |
| EXPECT_EQ(std::get<int32_t>(unbound_projected->literals().front().value()), | |
| 2021 - 1970); | |
| } | |
| TEST_F(TransformProjectStrictTest, YearStrictGreaterThanOrEqual) { | |
| auto transform = Transform::Year(); | |
| // Year strict projection: GTE projects to GT (lower bound) | |
| int32_t date_value = | |
| TemporalTestHelper::CreateDate({.year = 2021, .month = 1, .day = 1}); | |
| auto unbound = Expressions::GreaterThanOrEqual("value", Literal::Date(date_value)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*date_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kGt); | |
| auto unbound_projected = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected)); | |
| EXPECT_EQ(unbound_projected->op(), Expression::Operation::kGt); | |
| EXPECT_EQ(unbound_projected->literals().size(), 1); | |
| EXPECT_EQ(std::get<int32_t>(unbound_projected->literals().front().value()), | |
| 2020 - 1970); | |
| } | |
| TEST_F(TransformProjectStrictTest, YearStrictNotEqual) { | |
| auto transform = Transform::Year(); | |
| // Year strict projection: notEqual can be projected | |
| int32_t date_value = | |
| TemporalTestHelper::CreateDate({.year = 2021, .month = 1, .day = 1}); | |
| auto unbound = Expressions::NotEqual("value", Literal::Date(date_value)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*date_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kNotEq); | |
| auto unbound_projected = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected)); | |
| EXPECT_EQ(unbound_projected->op(), Expression::Operation::kNotEq); | |
| EXPECT_EQ(unbound_projected->literals().size(), 1); | |
| EXPECT_EQ(std::get<int32_t>(unbound_projected->literals().front().value()), | |
| 2021 - 1970); | |
| } | |
| TEST_F(TransformProjectStrictTest, MonthStrictLessThan) { | |
| auto transform = Transform::Month(); | |
| // Month strict projection: LT projects to LT | |
| int64_t ts_value = | |
| TemporalTestHelper::CreateTimestamp({.year = 2017, .month = 12, .day = 1}); | |
| auto unbound = Expressions::LessThan("value", Literal::Timestamp(ts_value)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*timestamp_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kLt); | |
| } | |
| TEST_F(TransformProjectStrictTest, DayStrictLessThan) { | |
| auto transform = Transform::Day(); | |
| // Day strict projection: LT projects to LT | |
| int64_t ts_value = | |
| TemporalTestHelper::CreateTimestamp({.year = 2017, .month = 12, .day = 1}); | |
| auto unbound = Expressions::LessThan("value", Literal::Timestamp(ts_value)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*timestamp_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kLt); | |
| } | |
| TEST_F(TransformProjectStrictTest, HourStrictLessThan) { | |
| auto transform = Transform::Hour(); | |
| // Hour strict projection: LT projects to LT | |
| int64_t ts_value = TemporalTestHelper::CreateTimestamp( | |
| {.year = 2017, .month = 12, .day = 1, .hour = 10, .minute = 0}); | |
| auto unbound = Expressions::LessThan("value", Literal::Timestamp(ts_value)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*timestamp_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kLt); | |
| } | |
| TEST_F(TransformProjectStrictTest, DayStrictEpoch) { | |
| auto transform = Transform::Day(); | |
| // Day strict projection at epoch: LT projects to LT | |
| auto unbound = Expressions::LessThan("value", Literal::Timestamp(0)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*timestamp_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kLt); | |
| } | |
| TEST_F(TransformProjectStrictTest, MonthStrictNotEqualNegative) { | |
| auto transform = Transform::Month(); | |
| // Month strict projection: notEqual with negative dates may convert to NOT_IN | |
| int64_t ts_value = | |
| TemporalTestHelper::CreateTimestamp({.year = 1969, .month = 1, .day = 1}); | |
| auto unbound = Expressions::NotEqual("value", Literal::Timestamp(ts_value)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*timestamp_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| // For negative dates, NOT_EQ may convert to NOT_IN | |
| EXPECT_TRUE(projected->op() == Expression::Operation::kNotEq || | |
| projected->op() == Expression::Operation::kNotIn); | |
| } | |
| TEST_F(TransformProjectStrictTest, YearStrictUpperBound) { | |
| auto transform = Transform::Year(); | |
| // Year strict projection: upper bound (end of year) | |
| int32_t date_value = | |
| TemporalTestHelper::CreateDate({.year = 2017, .month = 12, .day = 31}); | |
| auto unbound = Expressions::LessThanOrEqual("value", Literal::Date(date_value)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*date_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); | |
| ASSERT_NE(projected, nullptr); | |
| EXPECT_EQ(projected->op(), Expression::Operation::kLt); | |
| auto unbound_projected = | |
| internal::checked_pointer_cast<UnboundPredicateImpl<BoundReference>>( | |
| std::move(projected)); | |
| EXPECT_EQ(unbound_projected->op(), Expression::Operation::kLt); | |
| EXPECT_EQ(unbound_projected->literals().size(), 1); | |
| EXPECT_EQ(std::get<int32_t>(unbound_projected->literals().front().value()), | |
| 2018 - 1970); | |
| } | |
| TEST_F(TransformProjectStrictTest, VoidStrictReturnsNull) { | |
| auto transform = Transform::Void(); | |
| // Void transform always returns null for strict projection | |
| auto unbound = Expressions::Equal("value", Literal::Int(100)); | |
| ICEBERG_UNWRAP_OR_FAIL(auto bound, | |
| unbound->Bind(*int_schema_, /*case_sensitive=*/true)); | |
| auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(bound); | |
| ASSERT_NE(bound_pred, nullptr); | |
| ICEBERG_UNWRAP_OR_FAIL(auto projected, transform->ProjectStrict("part", bound_pred)); | |
| EXPECT_EQ(projected, nullptr); | |
| } | |
| } // namespace iceberg |