This is an automated email from the ASF dual-hosted git repository.
gangwu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-cpp.git
The following commit(s) were added to refs/heads/main by this push:
new dc23f76 feat: implement all functions of bound predicates (#280)
dc23f76 is described below
commit dc23f7688f88b844fde7a05c1d243cd1c51b8580
Author: Gang Wu <[email protected]>
AuthorDate: Tue Nov 4 10:12:46 2025 +0800
feat: implement all functions of bound predicates (#280)
- Implemented `Negate`, `ToString` and `Test` functions for bound
predicate subclasses.
- Added hash support to `Literal`.
- Refactored `BoundSetPredicate` to use unordered set for literals.
- Refactored predicate unit test to be better organized.
---
src/iceberg/expression/literal.cc | 47 ++++
src/iceberg/expression/literal.h | 98 +++-----
src/iceberg/expression/predicate.cc | 193 ++++++++++++++--
src/iceberg/expression/predicate.h | 32 ++-
src/iceberg/expression/term.cc | 4 +-
src/iceberg/expression/term.h | 6 +-
src/iceberg/test/literal_test.cc | 31 +++
src/iceberg/test/predicate_test.cc | 437 ++++++++++++++++++++++++++++++++++++
src/iceberg/util/macros.h | 17 ++
9 files changed, 762 insertions(+), 103 deletions(-)
diff --git a/src/iceberg/expression/literal.cc
b/src/iceberg/expression/literal.cc
index 790b59a..4f4a3c3 100644
--- a/src/iceberg/expression/literal.cc
+++ b/src/iceberg/expression/literal.cc
@@ -554,4 +554,51 @@ Result<Literal> LiteralCaster::CastTo(const Literal&
literal,
target_type->ToString());
}
+// LiteralValueHash implementation
+std::size_t LiteralValueHash::operator()(const Literal::Value& value) const
noexcept {
+ return std::visit(
+ [](const auto& v) -> std::size_t {
+ using T = std::decay_t<decltype(v)>;
+
+ constexpr size_t kHashPrime = 0x9e3779b9;
+
+ if constexpr (std::is_same_v<T, std::monostate>) {
+ return 0;
+ } else if constexpr (std::is_same_v<T, Literal::BelowMin>) {
+ return std::numeric_limits<std::size_t>::min();
+ } else if constexpr (std::is_same_v<T, Literal::AboveMax>) {
+ return std::numeric_limits<std::size_t>::max();
+ } else if constexpr (std::is_same_v<T, bool> || std::is_same_v<T,
int32_t> ||
+ std::is_same_v<T, int64_t> || std::is_same_v<T,
float> ||
+ std::is_same_v<T, double> ||
+ std::is_same_v<T, std::string>) {
+ return std::hash<T>{}(v);
+ } else if constexpr (std::is_same_v<T, std::vector<uint8_t>>) {
+ std::size_t hash = 0;
+ for (size_t i = 0; i < v.size(); ++i) {
+ hash ^= std::hash<uint8_t>{}(v[i]) + kHashPrime + (hash << 6) +
(hash >> 2);
+ }
+ return hash;
+ } else if constexpr (std::is_same_v<T, Decimal>) {
+ const int128_t& val = v.value();
+ std::size_t hash = std::hash<uint64_t>{}(static_cast<uint64_t>(val
>> 64));
+ hash ^= std::hash<uint64_t>{}(static_cast<uint64_t>(val)) +
kHashPrime +
+ (hash << 6) + (hash >> 2);
+ return hash;
+ } else if constexpr (std::is_same_v<T, Uuid>) {
+ std::size_t hash = 0;
+ const auto& bytes = v.bytes();
+ for (size_t i = 0; i < bytes.size(); ++i) {
+ hash ^=
+ std::hash<uint8_t>{}(bytes[i]) + kHashPrime + (hash << 6) +
(hash >> 2);
+ }
+ return hash;
+ } else {
+ static_assert(sizeof(T) == 0, "Unhandled variant type in
LiteralValueHash");
+ return 0;
+ }
+ },
+ value);
+}
+
} // namespace iceberg
diff --git a/src/iceberg/expression/literal.h b/src/iceberg/expression/literal.h
index 664b5a9..3ea94d0 100644
--- a/src/iceberg/expression/literal.h
+++ b/src/iceberg/expression/literal.h
@@ -166,79 +166,43 @@ class ICEBERG_EXPORT Literal : public util::Formattable {
std::shared_ptr<PrimitiveType> type_;
};
-template <TypeId type_id>
-struct LiteralTraits {
- using ValueType = void;
-};
-
-template <>
-struct LiteralTraits<TypeId::kBoolean> {
- using ValueType = bool;
-};
-
-template <>
-struct LiteralTraits<TypeId::kInt> {
- using ValueType = int32_t;
-};
-
-template <>
-struct LiteralTraits<TypeId::kDate> {
- using ValueType = int32_t;
-};
-
-template <>
-struct LiteralTraits<TypeId::kLong> {
- using ValueType = int64_t;
-};
-
-template <>
-struct LiteralTraits<TypeId::kTime> {
- using ValueType = int64_t;
-};
-
-template <>
-struct LiteralTraits<TypeId::kTimestamp> {
- using ValueType = int64_t;
+/// \brief Hash function for Literal to facilitate use in unordered containers
+struct ICEBERG_EXPORT LiteralValueHash {
+ std::size_t operator()(const Literal::Value& value) const noexcept;
};
-template <>
-struct LiteralTraits<TypeId::kTimestampTz> {
- using ValueType = int64_t;
-};
-
-template <>
-struct LiteralTraits<TypeId::kFloat> {
- using ValueType = float;
-};
-
-template <>
-struct LiteralTraits<TypeId::kDouble> {
- using ValueType = double;
-};
-
-template <>
-struct LiteralTraits<TypeId::kDecimal> {
- using ValueType = Decimal;
-};
-
-template <>
-struct LiteralTraits<TypeId::kString> {
- using ValueType = std::string;
+struct ICEBERG_EXPORT LiteralHash {
+ std::size_t operator()(const Literal& value) const noexcept {
+ return LiteralValueHash{}(value.value());
+ }
};
-template <>
-struct LiteralTraits<TypeId::kUuid> {
- using ValueType = Uuid;
+template <TypeId type_id>
+struct LiteralTraits {
+ using ValueType = void;
};
-template <>
-struct LiteralTraits<TypeId::kBinary> {
- using ValueType = std::vector<uint8_t>;
-};
+#define DEFINE_LITERAL_TRAIT(TYPE_ID, VALUE_TYPE) \
+ template <> \
+ struct LiteralTraits<TypeId::TYPE_ID> { \
+ using ValueType = VALUE_TYPE; \
+ };
-template <>
-struct LiteralTraits<TypeId::kFixed> {
- using ValueType = std::vector<uint8_t>;
-};
+DEFINE_LITERAL_TRAIT(kBoolean, bool)
+DEFINE_LITERAL_TRAIT(kInt, int32_t)
+DEFINE_LITERAL_TRAIT(kDate, int32_t)
+DEFINE_LITERAL_TRAIT(kLong, int64_t)
+DEFINE_LITERAL_TRAIT(kTime, int64_t)
+DEFINE_LITERAL_TRAIT(kTimestamp, int64_t)
+DEFINE_LITERAL_TRAIT(kTimestampTz, int64_t)
+DEFINE_LITERAL_TRAIT(kFloat, float)
+DEFINE_LITERAL_TRAIT(kDouble, double)
+DEFINE_LITERAL_TRAIT(kDecimal, Decimal)
+DEFINE_LITERAL_TRAIT(kString, std::string)
+DEFINE_LITERAL_TRAIT(kUuid, Uuid)
+DEFINE_LITERAL_TRAIT(kBinary, std::vector<uint8_t>)
+DEFINE_LITERAL_TRAIT(kFixed, std::vector<uint8_t>)
+
+#undef DEFINE_LITERAL_TRAIT
} // namespace iceberg
diff --git a/src/iceberg/expression/predicate.cc
b/src/iceberg/expression/predicate.cc
index 2ce04a1..959443c 100644
--- a/src/iceberg/expression/predicate.cc
+++ b/src/iceberg/expression/predicate.cc
@@ -20,9 +20,9 @@
#include "iceberg/expression/predicate.h"
#include <algorithm>
+#include <cmath>
#include <format>
-#include "iceberg/exception.h"
#include "iceberg/expression/expressions.h"
#include "iceberg/expression/literal.h"
#include "iceberg/result.h"
@@ -143,6 +143,26 @@ bool IsFloatingType(TypeId type) {
return type == TypeId::kFloat || type == TypeId::kDouble;
}
+bool IsNan(const Literal& literal) {
+ const auto& value = literal.value();
+ if (std::holds_alternative<float>(value)) {
+ return std::isnan(std::get<float>(value));
+ } else if (std::holds_alternative<double>(value)) {
+ return std::isnan(std::get<double>(value));
+ }
+ return false;
+}
+
+bool StartsWith(const Literal& lhs, const Literal& rhs) {
+ const auto& lhs_value = lhs.value();
+ const auto& rhs_value = rhs.value();
+ if (std::holds_alternative<std::string>(lhs_value) &&
+ std::holds_alternative<std::string>(rhs_value)) {
+ return
std::get<std::string>(lhs_value).starts_with(std::get<std::string>(rhs_value));
+ }
+ return false;
+}
+
} // namespace
template <typename B>
@@ -287,10 +307,10 @@ BoundPredicate::BoundPredicate(Expression::Operation op,
std::shared_ptr<BoundTe
BoundPredicate::~BoundPredicate() = default;
-Result<Literal::Value> BoundPredicate::Evaluate(const StructLike& data) const {
+Result<Literal> BoundPredicate::Evaluate(const StructLike& data) const {
ICEBERG_ASSIGN_OR_RAISE(auto eval_result, term_->Evaluate(data));
ICEBERG_ASSIGN_OR_RAISE(auto test_result, Test(eval_result));
- return Literal::Value{test_result};
+ return Literal::Boolean(test_result);
}
// BoundUnaryPredicate implementation
@@ -300,12 +320,37 @@
BoundUnaryPredicate::BoundUnaryPredicate(Expression::Operation op,
BoundUnaryPredicate::~BoundUnaryPredicate() = default;
-Result<bool> BoundUnaryPredicate::Test(const Literal::Value& value) const {
- return NotImplemented("BoundUnaryPredicate::Test not implemented");
+Result<bool> BoundUnaryPredicate::Test(const Literal& literal) const {
+ switch (op()) {
+ case Expression::Operation::kIsNull:
+ return literal.IsNull();
+ case Expression::Operation::kNotNull:
+ return !literal.IsNull();
+ case Expression::Operation::kIsNan:
+ return IsNan(literal);
+ case Expression::Operation::kNotNan:
+ return !IsNan(literal);
+ default:
+ return InvalidExpression("Invalid operation for BoundUnaryPredicate:
{}", op());
+ }
+}
+
+Result<std::shared_ptr<Expression>> BoundUnaryPredicate::Negate() const {
+ ICEBERG_ASSIGN_OR_RAISE(auto negated_op, ::iceberg::Negate(op()));
+ return std::make_shared<BoundUnaryPredicate>(negated_op, term_);
}
bool BoundUnaryPredicate::Equals(const Expression& other) const {
- throw IcebergError("BoundUnaryPredicate::Equals not implemented");
+ if (op() != other.op()) {
+ return false;
+ }
+
+ if (const auto* other_pred = dynamic_cast<const
BoundUnaryPredicate*>(&other);
+ other_pred) {
+ return term_->Equals(*other_pred->term());
+ }
+
+ return false;
}
std::string BoundUnaryPredicate::ToString() const {
@@ -331,12 +376,91 @@
BoundLiteralPredicate::BoundLiteralPredicate(Expression::Operation op,
BoundLiteralPredicate::~BoundLiteralPredicate() = default;
-Result<bool> BoundLiteralPredicate::Test(const Literal::Value& value) const {
- return NotImplemented("BoundLiteralPredicate::Test not implemented");
+Result<bool> BoundLiteralPredicate::Test(const Literal& value) const {
+ switch (op()) {
+ case Expression::Operation::kLt:
+ return value < literal_;
+ case Expression::Operation::kLtEq:
+ return value <= literal_;
+ case Expression::Operation::kGt:
+ return value > literal_;
+ case Expression::Operation::kGtEq:
+ return value >= literal_;
+ case Expression::Operation::kEq:
+ return value == literal_;
+ case Expression::Operation::kNotEq:
+ return value != literal_;
+ case Expression::Operation::kStartsWith:
+ return StartsWith(value, literal_);
+ case Expression::Operation::kNotStartsWith:
+ return !StartsWith(value, literal_);
+ default:
+ return InvalidExpression("Invalid operation for BoundLiteralPredicate:
{}", op());
+ }
+}
+
+Result<std::shared_ptr<Expression>> BoundLiteralPredicate::Negate() const {
+ ICEBERG_ASSIGN_OR_RAISE(auto negated_op, ::iceberg::Negate(op()));
+ return std::make_shared<BoundLiteralPredicate>(negated_op, term_, literal_);
}
bool BoundLiteralPredicate::Equals(const Expression& other) const {
- throw IcebergError("BoundLiteralPredicate::Equals not implemented");
+ const auto* other_pred = dynamic_cast<const BoundLiteralPredicate*>(&other);
+ if (!other_pred) {
+ return false;
+ }
+
+ if (op() == other.op()) {
+ if (term_->Equals(*other_pred->term())) {
+ // because the term is equivalent, the literal must have the same type
+ return literal_ == other_pred->literal();
+ }
+ }
+
+ // TODO(gangwu): add TypeId::kTimestampNano
+ static const std::unordered_set<TypeId> kIntegralTypes = {
+ TypeId::kInt, TypeId::kLong, TypeId::kDate,
+ TypeId::kTime, TypeId::kTimestamp, TypeId::kTimestampTz};
+
+ if (kIntegralTypes.contains(term_->type()->type_id()) &&
+ term_->Equals(*other_pred->term())) {
+ auto get_long = [](const Literal& lit) -> std::optional<int64_t> {
+ const auto& val = lit.value();
+ if (std::holds_alternative<int32_t>(val)) {
+ return std::get<int32_t>(val);
+ } else if (std::holds_alternative<int64_t>(val)) {
+ return std::get<int64_t>(val);
+ }
+ return std::nullopt;
+ };
+
+ auto this_val = get_long(literal_);
+ auto other_val = get_long(other_pred->literal());
+ if (this_val && other_val) {
+ switch (op()) {
+ case Expression::Operation::kLt:
+ // < 6 is equivalent to <= 5
+ return other_pred->op() == Expression::Operation::kLtEq &&
+ *this_val == *other_val + 1;
+ case Expression::Operation::kLtEq:
+ // <= 5 is equivalent to < 6
+ return other_pred->op() == Expression::Operation::kLt &&
+ *this_val == *other_val - 1;
+ case Expression::Operation::kGt:
+ // > 5 is equivalent to >= 6
+ return other_pred->op() == Expression::Operation::kGtEq &&
+ *this_val == *other_val - 1;
+ case Expression::Operation::kGtEq:
+ // >= 6 is equivalent to > 5
+ return other_pred->op() == Expression::Operation::kGt &&
+ *this_val == *other_val + 1;
+ default:
+ return false;
+ }
+ }
+ }
+
+ return false;
}
std::string BoundLiteralPredicate::ToString() const {
@@ -370,27 +494,54 @@ std::string BoundLiteralPredicate::ToString() const {
BoundSetPredicate::BoundSetPredicate(Expression::Operation op,
std::shared_ptr<BoundTerm> term,
std::span<const Literal> literals)
- : BoundPredicate(op, std::move(term)) {
- for (const auto& literal : literals) {
- ICEBERG_DCHECK((*literal.type() == *term_->type()),
- "Literal type does not match term type");
- value_set_.push_back(literal.value());
- }
-}
+ : BoundPredicate(op, std::move(term)), value_set_(literals.begin(),
literals.end()) {}
+
+BoundSetPredicate::BoundSetPredicate(Expression::Operation op,
+ std::shared_ptr<BoundTerm> term,
+ LiteralSet value_set)
+ : BoundPredicate(op, std::move(term)), value_set_(std::move(value_set)) {}
BoundSetPredicate::~BoundSetPredicate() = default;
-Result<bool> BoundSetPredicate::Test(const Literal::Value& value) const {
- return NotImplemented("BoundSetPredicate::Test not implemented");
+Result<bool> BoundSetPredicate::Test(const Literal& value) const {
+ switch (op()) {
+ case Expression::Operation::kIn:
+ return value_set_.contains(value);
+ case Expression::Operation::kNotIn:
+ return !value_set_.contains(value);
+ default:
+ return InvalidExpression("Invalid operation for BoundSetPredicate: {}",
op());
+ }
+}
+
+Result<std::shared_ptr<Expression>> BoundSetPredicate::Negate() const {
+ ICEBERG_ASSIGN_OR_RAISE(auto negated_op, ::iceberg::Negate(op()));
+ return std::make_shared<BoundSetPredicate>(negated_op, term_, value_set_);
}
bool BoundSetPredicate::Equals(const Expression& other) const {
- throw IcebergError("BoundSetPredicate::Equals not implemented");
+ if (op() != other.op()) {
+ return false;
+ }
+
+ if (const auto* other_pred = dynamic_cast<const BoundSetPredicate*>(&other);
+ other_pred) {
+ return value_set_ == other_pred->value_set_;
+ }
+
+ return false;
}
std::string BoundSetPredicate::ToString() const {
- // TODO(gangwu): Literal::Value does not have std::format support.
- throw IcebergError("BoundSetPredicate::ToString not implemented");
+ switch (op()) {
+ case Expression::Operation::kIn:
+ return std::format("{} in {}", *term(), FormatRange(value_set_, ", ",
"(", ")"));
+ case Expression::Operation::kNotIn:
+ return std::format("{} not in {}", *term(),
+ FormatRange(value_set_, ", ", "(", ")"));
+ default:
+ return std::format("Invalid set predicate: operation = {}", op());
+ }
}
// Explicit template instantiations
diff --git a/src/iceberg/expression/predicate.h
b/src/iceberg/expression/predicate.h
index 3c40af6..979db12 100644
--- a/src/iceberg/expression/predicate.h
+++ b/src/iceberg/expression/predicate.h
@@ -23,8 +23,10 @@
/// Predicate interface for boolean expressions that test terms.
#include <concepts>
+#include <unordered_set>
#include "iceberg/expression/expression.h"
+#include "iceberg/expression/literal.h"
#include "iceberg/expression/term.h"
namespace iceberg {
@@ -111,13 +113,13 @@ class ICEBERG_EXPORT BoundPredicate : public
Predicate<BoundTerm>, public Bound
std::shared_ptr<BoundReference> reference() override { return
term_->reference(); }
- Result<Literal::Value> Evaluate(const StructLike& data) const override;
+ Result<Literal> Evaluate(const StructLike& data) const override;
/// \brief Test a value against this predicate.
///
- /// \param value The value to test
+ /// \param value The literal value to test
/// \return true if the predicate passes, false otherwise
- virtual Result<bool> Test(const Literal::Value& value) const = 0;
+ virtual Result<bool> Test(const Literal& value) const = 0;
enum class Kind : int8_t {
// A unary predicate (tests for null, not-null, etc.).
@@ -143,12 +145,14 @@ class ICEBERG_EXPORT BoundUnaryPredicate : public
BoundPredicate {
~BoundUnaryPredicate() override;
- Result<bool> Test(const Literal::Value& value) const override;
+ Result<bool> Test(const Literal& value) const override;
Kind kind() const override { return Kind::kUnary; }
std::string ToString() const override;
+ Result<std::shared_ptr<Expression>> Negate() const override;
+
bool Equals(const Expression& other) const override;
};
@@ -168,12 +172,14 @@ class ICEBERG_EXPORT BoundLiteralPredicate : public
BoundPredicate {
/// \brief Returns the literal being compared against.
const Literal& literal() const { return literal_; }
- Result<bool> Test(const Literal::Value& value) const override;
+ Result<bool> Test(const Literal& value) const override;
Kind kind() const override { return Kind::kLiteral; }
std::string ToString() const override;
+ Result<std::shared_ptr<Expression>> Negate() const override;
+
bool Equals(const Expression& other) const override;
private:
@@ -183,6 +189,8 @@ class ICEBERG_EXPORT BoundLiteralPredicate : public
BoundPredicate {
/// \brief Bound set predicate (membership testing against a set of values).
class ICEBERG_EXPORT BoundSetPredicate : public BoundPredicate {
public:
+ using LiteralSet = std::unordered_set<Literal, LiteralHash>;
+
/// \brief Create a bound set predicate.
///
/// \param op The set operation (kIn, kNotIn)
@@ -191,23 +199,27 @@ class ICEBERG_EXPORT BoundSetPredicate : public
BoundPredicate {
BoundSetPredicate(Expression::Operation op, std::shared_ptr<BoundTerm> term,
std::span<const Literal> literals);
+ /// \brief Create a bound set predicate using a set of literals.
+ BoundSetPredicate(Expression::Operation op, std::shared_ptr<BoundTerm> term,
+ LiteralSet value_set);
+
~BoundSetPredicate() override;
/// \brief Returns the set of literals to test against.
- const std::vector<Literal::Value>& literal_set() const { return value_set_; }
+ const LiteralSet& literal_set() const { return value_set_; }
- Result<bool> Test(const Literal::Value& value) const override;
+ Result<bool> Test(const Literal& value) const override;
Kind kind() const override { return Kind::kSet; }
std::string ToString() const override;
+ Result<std::shared_ptr<Expression>> Negate() const override;
+
bool Equals(const Expression& other) const override;
private:
- /// FIXME: Literal::Value does not have hash support. We need to add this
- /// and replace the vector with a unordered_set.
- std::vector<Literal::Value> value_set_;
+ LiteralSet value_set_;
};
} // namespace iceberg
diff --git a/src/iceberg/expression/term.cc b/src/iceberg/expression/term.cc
index 5bb9b71..30bdf8e 100644
--- a/src/iceberg/expression/term.cc
+++ b/src/iceberg/expression/term.cc
@@ -71,7 +71,7 @@ std::string BoundReference::ToString() const {
return std::format("ref(id={}, type={})", field_.field_id(),
field_.type()->ToString());
}
-Result<Literal::Value> BoundReference::Evaluate(const StructLike& data) const {
+Result<Literal> BoundReference::Evaluate(const StructLike& data) const {
return NotImplemented("BoundReference::Evaluate(StructLike) not
implemented");
}
@@ -119,7 +119,7 @@ std::string BoundTransform::ToString() const {
return std::format("{}({})", transform_->ToString(), ref_->ToString());
}
-Result<Literal::Value> BoundTransform::Evaluate(const StructLike& data) const {
+Result<Literal> BoundTransform::Evaluate(const StructLike& data) const {
throw IcebergError("BoundTransform::Evaluate(StructLike) not implemented");
}
diff --git a/src/iceberg/expression/term.h b/src/iceberg/expression/term.h
index 2911dfa..e0a883c 100644
--- a/src/iceberg/expression/term.h
+++ b/src/iceberg/expression/term.h
@@ -79,7 +79,7 @@ class ICEBERG_EXPORT Bound {
virtual ~Bound();
/// \brief Evaluate this expression against a row-based data.
- virtual Result<Literal::Value> Evaluate(const StructLike& data) const = 0;
+ virtual Result<Literal> Evaluate(const StructLike& data) const = 0;
/// \brief Returns the underlying bound reference for this term.
virtual std::shared_ptr<class BoundReference> reference() = 0;
@@ -176,7 +176,7 @@ class ICEBERG_EXPORT BoundReference
std::string ToString() const override;
- Result<Literal::Value> Evaluate(const StructLike& data) const override;
+ Result<Literal> Evaluate(const StructLike& data) const override;
std::shared_ptr<BoundReference> reference() override { return
shared_from_this(); }
@@ -236,7 +236,7 @@ class ICEBERG_EXPORT BoundTransform : public BoundTerm {
std::string ToString() const override;
- Result<Literal::Value> Evaluate(const StructLike& data) const override;
+ Result<Literal> Evaluate(const StructLike& data) const override;
std::shared_ptr<BoundReference> reference() override { return ref_; }
diff --git a/src/iceberg/test/literal_test.cc b/src/iceberg/test/literal_test.cc
index 23703c2..01a7a7c 100644
--- a/src/iceberg/test/literal_test.cc
+++ b/src/iceberg/test/literal_test.cc
@@ -21,6 +21,7 @@
#include <limits>
#include <numbers>
+#include <unordered_set>
#include <vector>
#include <gtest/gtest.h>
@@ -795,4 +796,34 @@ INSTANTIATE_TEST_SUITE_P(
return info.param.test_name;
});
+TEST(LiteralTest, LiteralHash) {
+ LiteralHash hasher;
+
+ EXPECT_EQ(hasher(Literal::Int(42)), hasher(Literal::Int(42)));
+ EXPECT_NE(hasher(Literal::Int(42)), hasher(Literal::Int(43)));
+
+ EXPECT_EQ(hasher(Literal::String("hello")),
hasher(Literal::String("hello")));
+ EXPECT_NE(hasher(Literal::String("hello")),
hasher(Literal::String("world")));
+}
+
+TEST(LiteralTest, LiteralHashUnorderedSet) {
+ std::unordered_set<Literal, LiteralHash> literal_set;
+
+ literal_set.insert(Literal::Int(1));
+ literal_set.insert(Literal::Int(2));
+ literal_set.insert(Literal::Int(1)); // Duplicate
+
+ EXPECT_EQ(literal_set.size(), 2);
+ EXPECT_TRUE(literal_set.contains(Literal::Int(1)));
+ EXPECT_TRUE(literal_set.contains(Literal::Int(2)));
+ EXPECT_FALSE(literal_set.contains(Literal::Int(3)));
+
+ std::unordered_set<Literal, LiteralHash> string_set;
+ string_set.insert(Literal::String("a"));
+ string_set.insert(Literal::String("b"));
+ string_set.insert(Literal::String("a")); // Duplicate
+
+ EXPECT_EQ(string_set.size(), 2);
+}
+
} // namespace iceberg
diff --git a/src/iceberg/test/predicate_test.cc
b/src/iceberg/test/predicate_test.cc
index ca38769..5ab7908 100644
--- a/src/iceberg/test/predicate_test.cc
+++ b/src/iceberg/test/predicate_test.cc
@@ -17,10 +17,16 @@
* under the License.
*/
+#include "iceberg/expression/predicate.h"
+
+#include <limits>
+#include <memory>
+
#include "iceberg/expression/expressions.h"
#include "iceberg/schema.h"
#include "iceberg/test/matchers.h"
#include "iceberg/type.h"
+#include "iceberg/util/macros.h"
namespace iceberg {
@@ -433,4 +439,435 @@ TEST_F(PredicateTest, ComplexExpressionCombinations) {
EXPECT_EQ(nested->op(), Expression::Operation::kAnd);
}
+TEST_F(PredicateTest, BoundUnaryPredicateNegate) {
+ auto is_null_pred = Expressions::IsNull("name");
+ auto bound_null = is_null_pred->Bind(*schema_,
/*case_sensitive=*/true).value();
+
+ auto negated_result = bound_null->Negate();
+ ASSERT_THAT(negated_result, IsOk());
+ auto negated = negated_result.value();
+ EXPECT_EQ(negated->op(), Expression::Operation::kNotNull);
+
+ // Double negation should return the original predicate
+ auto double_neg_result = negated->Negate();
+ ASSERT_THAT(double_neg_result, IsOk());
+ auto double_neg = double_neg_result.value();
+ EXPECT_EQ(double_neg->op(), Expression::Operation::kIsNull);
+}
+
+TEST_F(PredicateTest, BoundUnaryPredicateEquals) {
+ auto is_null_name1 = Expressions::IsNull("name");
+ auto is_null_name2 = Expressions::IsNull("name");
+ auto is_null_age = Expressions::IsNull("age");
+ auto not_null_name = Expressions::NotNull("name");
+
+ auto bound_null1 = is_null_name1->Bind(*schema_, true).value();
+ auto bound_null2 = is_null_name2->Bind(*schema_, true).value();
+ auto bound_null_age = is_null_age->Bind(*schema_, true).value();
+ auto bound_not_null = not_null_name->Bind(*schema_, true).value();
+
+ // Same predicate should be equal
+ EXPECT_TRUE(bound_null1->Equals(*bound_null2));
+ EXPECT_TRUE(bound_null2->Equals(*bound_null1));
+
+ // Different fields should not be equal
+ EXPECT_FALSE(bound_null1->Equals(*bound_null_age));
+
+ // Different operations should not be equal
+ EXPECT_FALSE(bound_null1->Equals(*bound_not_null));
+}
+
+TEST_F(PredicateTest, BoundLiteralPredicateNegate) {
+ auto eq_pred = Expressions::Equal("age", Literal::Int(25));
+ auto bound_eq = eq_pred->Bind(*schema_, true).value();
+
+ auto negated_result = bound_eq->Negate();
+ ASSERT_THAT(negated_result, IsOk());
+
+ auto negated = negated_result.value();
+ EXPECT_EQ(negated->op(), Expression::Operation::kNotEq);
+
+ // Test less than negation
+ auto lt_pred = Expressions::LessThan("age", Literal::Int(30));
+ auto bound_lt = lt_pred->Bind(*schema_, true).value();
+ auto neg_lt_result = bound_lt->Negate();
+ ASSERT_THAT(neg_lt_result, IsOk());
+ EXPECT_EQ(neg_lt_result.value()->op(), Expression::Operation::kGtEq);
+}
+
+TEST_F(PredicateTest, BoundLiteralPredicateEquals) {
+ auto eq1 = Expressions::Equal("age", Literal::Int(25));
+ auto eq2 = Expressions::Equal("age", Literal::Int(25));
+ auto eq3 = Expressions::Equal("age", Literal::Int(30));
+ auto neq = Expressions::NotEqual("age", Literal::Int(25));
+
+ auto bound_eq1 = eq1->Bind(*schema_, true).value();
+ auto bound_eq2 = eq2->Bind(*schema_, true).value();
+ auto bound_eq3 = eq3->Bind(*schema_, true).value();
+ auto bound_neq = neq->Bind(*schema_, true).value();
+
+ // Same predicate should be equal
+ EXPECT_TRUE(bound_eq1->Equals(*bound_eq2));
+
+ // Different literal values should not be equal
+ EXPECT_FALSE(bound_eq1->Equals(*bound_eq3));
+
+ // Different operations should not be equal
+ EXPECT_FALSE(bound_eq1->Equals(*bound_neq));
+}
+
+TEST_F(PredicateTest, BoundLiteralPredicateIntegerEquivalence) {
+ // Test that < 6 is equivalent to <= 5
+ auto lt_6 = Expressions::LessThan("age", Literal::Int(6));
+ auto lte_5 = Expressions::LessThanOrEqual("age", Literal::Int(5));
+ auto bound_lt = lt_6->Bind(*schema_, true).value();
+ auto bound_lte = lte_5->Bind(*schema_, true).value();
+ EXPECT_TRUE(bound_lt->Equals(*bound_lte));
+ EXPECT_TRUE(bound_lte->Equals(*bound_lt));
+
+ // Test that > 5 is equivalent to >= 6
+ auto gt_5 = Expressions::GreaterThan("age", Literal::Int(5));
+ auto gte_6 = Expressions::GreaterThanOrEqual("age", Literal::Int(6));
+ auto bound_gt = gt_5->Bind(*schema_, true).value();
+ auto bound_gte = gte_6->Bind(*schema_, true).value();
+ EXPECT_TRUE(bound_gt->Equals(*bound_gte));
+ EXPECT_TRUE(bound_gte->Equals(*bound_gt));
+
+ // Test that < 6 is not equivalent to <= 6
+ auto lte_6 = Expressions::LessThanOrEqual("age", Literal::Int(6));
+ auto bound_lte_6 = lte_6->Bind(*schema_, true).value();
+ EXPECT_FALSE(bound_lt->Equals(*bound_lte_6));
+}
+
+TEST_F(PredicateTest, BoundSetPredicateToString) {
+ auto in_pred =
+ Expressions::In("age", {Literal::Int(10), Literal::Int(20),
Literal::Int(30)});
+ auto bound_in = in_pred->Bind(*schema_, true).value();
+
+ auto str = bound_in->ToString();
+ // The set order might vary, but should contain the key elements
+ // BoundReference uses field_id in ToString, so check for id=3 (age field)
+ EXPECT_TRUE(str.find("id=3") != std::string::npos);
+ EXPECT_TRUE(str.find("in") != std::string::npos);
+
+ auto not_in_pred =
+ Expressions::NotIn("name", {Literal::String("a"), Literal::String("b")});
+ auto bound_not_in = not_in_pred->Bind(*schema_, true).value();
+
+ auto not_in_str = bound_not_in->ToString();
+ // Check for id=2 (name field)
+ EXPECT_TRUE(not_in_str.find("id=2") != std::string::npos);
+ EXPECT_TRUE(not_in_str.find("not in") != std::string::npos);
+}
+
+TEST_F(PredicateTest, BoundSetPredicateNegate) {
+ auto in_pred = Expressions::In("age", {Literal::Int(10), Literal::Int(20)});
+ auto bound_in = in_pred->Bind(*schema_, true).value();
+
+ auto negated_result = bound_in->Negate();
+ ASSERT_THAT(negated_result, IsOk());
+
+ auto negated = negated_result.value();
+ EXPECT_EQ(negated->op(), Expression::Operation::kNotIn);
+
+ // Test double negation
+ auto double_neg_result = negated->Negate();
+ ASSERT_THAT(double_neg_result, IsOk());
+ EXPECT_EQ(double_neg_result.value()->op(), Expression::Operation::kIn);
+}
+
+TEST_F(PredicateTest, BoundSetPredicateEquals) {
+ auto in1 = Expressions::In("age", {Literal::Int(10), Literal::Int(20)});
+ auto in2 =
+ Expressions::In("age", {Literal::Int(20), Literal::Int(10)}); //
Different order
+ auto in3 =
+ Expressions::In("age", {Literal::Int(10), Literal::Int(30)}); //
Different values
+
+ auto bound_in1 = in1->Bind(*schema_, /*case_sensitive=*/true).value();
+ auto bound_in2 = in2->Bind(*schema_, /*case_sensitive=*/true).value();
+ auto bound_in3 = in3->Bind(*schema_, /*case_sensitive=*/true).value();
+
+ // Same values in different order should be equal (unordered_set)
+ EXPECT_TRUE(bound_in1->Equals(*bound_in2));
+ EXPECT_TRUE(bound_in2->Equals(*bound_in1));
+
+ // Different values should not be equal
+ EXPECT_FALSE(bound_in1->Equals(*bound_in3));
+}
+
+namespace {
+
+std::shared_ptr<BoundPredicate> AssertAndCastToBoundPredicate(
+ std::shared_ptr<Expression> expr) {
+ auto bound_pred = std::dynamic_pointer_cast<BoundPredicate>(expr);
+ EXPECT_NE(bound_pred, nullptr) << "Expected a BoundPredicate, got " <<
expr->ToString();
+ return bound_pred;
+}
+
+} // namespace
+
+TEST_F(PredicateTest, BoundUnaryPredicateTestIsNull) {
+ ICEBERG_ASSIGN_OR_THROW(auto is_null_pred, Expressions::IsNull("name")->Bind(
+ *schema_,
/*case_sensitive=*/true));
+ auto bound_pred = AssertAndCastToBoundPredicate(is_null_pred);
+ EXPECT_THAT(bound_pred->Test(Literal::Null(string())),
HasValue(testing::Eq(true)));
+ EXPECT_THAT(bound_pred->Test(Literal::String("test")),
HasValue(testing::Eq(false)));
+}
+
+TEST_F(PredicateTest, BoundUnaryPredicateTestNotNull) {
+ ICEBERG_ASSIGN_OR_THROW(auto not_null_pred,
Expressions::NotNull("name")->Bind(
+ *schema_,
/*case_sensitive=*/true));
+ auto bound_pred = AssertAndCastToBoundPredicate(not_null_pred);
+ EXPECT_THAT(bound_pred->Test(Literal::String("test")),
HasValue(testing::Eq(true)));
+ EXPECT_THAT(bound_pred->Test(Literal::Null(string())),
HasValue(testing::Eq(false)));
+}
+
+TEST_F(PredicateTest, BoundUnaryPredicateTestIsNaN) {
+ ICEBERG_ASSIGN_OR_THROW(auto is_nan_pred, Expressions::IsNaN("salary")->Bind(
+ *schema_,
/*case_sensitive=*/true));
+ auto bound_pred = AssertAndCastToBoundPredicate(is_nan_pred);
+
+ // Test with NaN values
+
EXPECT_THAT(bound_pred->Test(Literal::Float(std::numeric_limits<float>::quiet_NaN())),
+ HasValue(testing::Eq(true)));
+
EXPECT_THAT(bound_pred->Test(Literal::Double(std::numeric_limits<double>::quiet_NaN())),
+ HasValue(testing::Eq(true)));
+
+ // Test with regular values
+ EXPECT_THAT(bound_pred->Test(Literal::Float(3.14f)),
HasValue(testing::Eq(false)));
+ EXPECT_THAT(bound_pred->Test(Literal::Double(2.718)),
HasValue(testing::Eq(false)));
+
+ // Test with infinity
+
EXPECT_THAT(bound_pred->Test(Literal::Float(std::numeric_limits<float>::infinity())),
+ HasValue(testing::Eq(false)));
+}
+
+TEST_F(PredicateTest, BoundUnaryPredicateTestNotNaN) {
+ ICEBERG_ASSIGN_OR_THROW(auto not_nan_pred,
Expressions::NotNaN("salary")->Bind(
+ *schema_,
/*case_sensitive=*/true));
+ auto bound_pred = AssertAndCastToBoundPredicate(not_nan_pred);
+
+ // Test with regular values
+ EXPECT_THAT(bound_pred->Test(Literal::Double(100.5)),
HasValue(testing::Eq(true)));
+
+ // Test with NaN
+
EXPECT_THAT(bound_pred->Test(Literal::Double(std::numeric_limits<double>::quiet_NaN())),
+ HasValue(testing::Eq(false)));
+
+ // Test with infinity (should be true as infinity is not NaN)
+
EXPECT_THAT(bound_pred->Test(Literal::Double(std::numeric_limits<double>::infinity())),
+ HasValue(testing::Eq(true)));
+}
+
+TEST_F(PredicateTest, BoundLiteralPredicateTestComparison) {
+ // Test less than
+ ICEBERG_ASSIGN_OR_THROW(auto lt_pred, Expressions::LessThan("age",
Literal::Int(30))
+ ->Bind(*schema_,
/*case_sensitive=*/true));
+ auto bound_lt = AssertAndCastToBoundPredicate(lt_pred);
+ EXPECT_THAT(bound_lt->Test(Literal::Int(20)), HasValue(testing::Eq(true)));
+ EXPECT_THAT(bound_lt->Test(Literal::Int(30)), HasValue(testing::Eq(false)));
+ EXPECT_THAT(bound_lt->Test(Literal::Int(40)), HasValue(testing::Eq(false)));
+
+ // Test less than or equal
+ ICEBERG_ASSIGN_OR_THROW(auto lte_pred,
+ Expressions::LessThanOrEqual("age", Literal::Int(30))
+ ->Bind(*schema_, /*case_sensitive=*/true));
+ auto bound_lte = AssertAndCastToBoundPredicate(lte_pred);
+ EXPECT_THAT(bound_lte->Test(Literal::Int(20)), HasValue(testing::Eq(true)));
+ EXPECT_THAT(bound_lte->Test(Literal::Int(30)), HasValue(testing::Eq(true)));
+ EXPECT_THAT(bound_lte->Test(Literal::Int(40)), HasValue(testing::Eq(false)));
+
+ // Test greater than
+ ICEBERG_ASSIGN_OR_THROW(auto gt_pred, Expressions::GreaterThan("age",
Literal::Int(30))
+ ->Bind(*schema_,
/*case_sensitive=*/true));
+ auto bound_gt = AssertAndCastToBoundPredicate(gt_pred);
+ EXPECT_THAT(bound_gt->Test(Literal::Int(20)), HasValue(testing::Eq(false)));
+ EXPECT_THAT(bound_gt->Test(Literal::Int(30)), HasValue(testing::Eq(false)));
+ EXPECT_THAT(bound_gt->Test(Literal::Int(40)), HasValue(testing::Eq(true)));
+
+ // Test greater than or equal
+ ICEBERG_ASSIGN_OR_THROW(auto gte_pred,
+ Expressions::GreaterThanOrEqual("age",
Literal::Int(30))
+ ->Bind(*schema_, /*case_sensitive=*/true));
+ auto bound_gte = AssertAndCastToBoundPredicate(gte_pred);
+ EXPECT_THAT(bound_gte->Test(Literal::Int(20)), HasValue(testing::Eq(false)));
+ EXPECT_THAT(bound_gte->Test(Literal::Int(30)), HasValue(testing::Eq(true)));
+ EXPECT_THAT(bound_gte->Test(Literal::Int(40)), HasValue(testing::Eq(true)));
+}
+
+TEST_F(PredicateTest, BoundLiteralPredicateTestEquality) {
+ // Test equal
+ ICEBERG_ASSIGN_OR_THROW(auto eq_pred, Expressions::Equal("age",
Literal::Int(25))
+ ->Bind(*schema_,
/*case_sensitive=*/true));
+ auto bound_eq = AssertAndCastToBoundPredicate(eq_pred);
+ EXPECT_THAT(bound_eq->Test(Literal::Int(25)), HasValue(testing::Eq(true)));
+ EXPECT_THAT(bound_eq->Test(Literal::Int(26)), HasValue(testing::Eq(false)));
+ EXPECT_THAT(bound_eq->Test(Literal::Int(24)), HasValue(testing::Eq(false)));
+
+ // Test not equal
+ ICEBERG_ASSIGN_OR_THROW(auto neq_pred, Expressions::NotEqual("age",
Literal::Int(25))
+ ->Bind(*schema_,
/*case_sensitive=*/true));
+ auto bound_neq = AssertAndCastToBoundPredicate(neq_pred);
+ EXPECT_THAT(bound_neq->Test(Literal::Int(25)), HasValue(testing::Eq(false)));
+ EXPECT_THAT(bound_neq->Test(Literal::Int(26)), HasValue(testing::Eq(true)));
+ EXPECT_THAT(bound_neq->Test(Literal::Int(24)), HasValue(testing::Eq(true)));
+}
+
+TEST_F(PredicateTest, BoundLiteralPredicateTestWithDifferentTypes) {
+ // Test with double
+ ICEBERG_ASSIGN_OR_THROW(auto gt_pred,
+ Expressions::GreaterThan("salary",
Literal::Double(50000.0))
+ ->Bind(*schema_, /*case_sensitive=*/true));
+ auto bound_double = AssertAndCastToBoundPredicate(gt_pred);
+ EXPECT_THAT(bound_double->Test(Literal::Double(60000.0)),
HasValue(testing::Eq(true)));
+ EXPECT_THAT(bound_double->Test(Literal::Double(40000.0)),
HasValue(testing::Eq(false)));
+ EXPECT_THAT(bound_double->Test(Literal::Double(50000.0)),
HasValue(testing::Eq(false)));
+
+ // Test with string
+ ICEBERG_ASSIGN_OR_THROW(auto str_eq_pred,
+ Expressions::Equal("name", Literal::String("Alice"))
+ ->Bind(*schema_, /*case_sensitive=*/true));
+ auto bound_string = AssertAndCastToBoundPredicate(str_eq_pred);
+ EXPECT_THAT(bound_string->Test(Literal::String("Alice")),
HasValue(testing::Eq(true)));
+ EXPECT_THAT(bound_string->Test(Literal::String("Bob")),
HasValue(testing::Eq(false)));
+ EXPECT_THAT(bound_string->Test(Literal::String("alice")),
+ HasValue(testing::Eq(false))); // Case sensitive
+
+ // Test with boolean
+ ICEBERG_ASSIGN_OR_THROW(auto bool_eq_pred,
+ Expressions::Equal("active", Literal::Boolean(true))
+ ->Bind(*schema_, /*case_sensitive=*/true));
+ auto bound_bool = AssertAndCastToBoundPredicate(bool_eq_pred);
+ EXPECT_THAT(bound_bool->Test(Literal::Boolean(true)),
HasValue(testing::Eq(true)));
+ EXPECT_THAT(bound_bool->Test(Literal::Boolean(false)),
HasValue(testing::Eq(false)));
+}
+
+TEST_F(PredicateTest, BoundLiteralPredicateTestStartsWith) {
+ ICEBERG_ASSIGN_OR_THROW(
+ auto starts_with_pred,
+ Expressions::StartsWith("name", "Jo")->Bind(*schema_,
/*case_sensitive=*/true));
+ auto bound_pred = AssertAndCastToBoundPredicate(starts_with_pred);
+
+ // Test strings that start with "Jo"
+ EXPECT_THAT(bound_pred->Test(Literal::String("John")),
HasValue(testing::Eq(true)));
+ EXPECT_THAT(bound_pred->Test(Literal::String("Joe")),
HasValue(testing::Eq(true)));
+ EXPECT_THAT(bound_pred->Test(Literal::String("Jo")),
HasValue(testing::Eq(true)));
+
+ // Test strings that don't start with "Jo"
+ EXPECT_THAT(bound_pred->Test(Literal::String("Alice")),
HasValue(testing::Eq(false)));
+ EXPECT_THAT(bound_pred->Test(Literal::String("Bob")),
HasValue(testing::Eq(false)));
+ EXPECT_THAT(bound_pred->Test(Literal::String("")),
HasValue(testing::Eq(false)));
+
+ // Test empty prefix
+ ICEBERG_ASSIGN_OR_THROW(
+ auto empty_prefix_pred,
+ Expressions::StartsWith("name", "")->Bind(*schema_,
/*case_sensitive=*/true));
+ auto bound_empty = AssertAndCastToBoundPredicate(empty_prefix_pred);
+
+ // All strings should start with empty prefix
+ EXPECT_THAT(bound_empty->Test(Literal::String("test")),
HasValue(testing::Eq(true)));
+ EXPECT_THAT(bound_empty->Test(Literal::String("")),
HasValue(testing::Eq(true)));
+}
+
+TEST_F(PredicateTest, BoundLiteralPredicateTestNotStartsWith) {
+ ICEBERG_ASSIGN_OR_THROW(
+ auto not_starts_with_pred,
+ Expressions::NotStartsWith("name", "Jo")->Bind(*schema_,
/*case_sensitive=*/true));
+ auto bound_pred = AssertAndCastToBoundPredicate(not_starts_with_pred);
+
+ // Test strings that don't start with "Jo"
+ EXPECT_THAT(bound_pred->Test(Literal::String("Alice")),
HasValue(testing::Eq(true)));
+ EXPECT_THAT(bound_pred->Test(Literal::String("Bob")),
HasValue(testing::Eq(true)));
+ EXPECT_THAT(bound_pred->Test(Literal::String("")),
HasValue(testing::Eq(true)));
+
+ // Test strings that start with "Jo"
+ EXPECT_THAT(bound_pred->Test(Literal::String("John")),
HasValue(testing::Eq(false)));
+ EXPECT_THAT(bound_pred->Test(Literal::String("Joe")),
HasValue(testing::Eq(false)));
+ EXPECT_THAT(bound_pred->Test(Literal::String("Jo")),
HasValue(testing::Eq(false)));
+}
+
+TEST_F(PredicateTest, BoundSetPredicateTestIn) {
+ ICEBERG_ASSIGN_OR_THROW(
+ auto in_pred,
+ Expressions::In("age", {Literal::Int(10), Literal::Int(20),
Literal::Int(30)})
+ ->Bind(*schema_, /*case_sensitive=*/true));
+ auto bound_pred = AssertAndCastToBoundPredicate(in_pred);
+
+ // Test values in the set
+ EXPECT_THAT(bound_pred->Test(Literal::Int(10)), HasValue(testing::Eq(true)));
+ EXPECT_THAT(bound_pred->Test(Literal::Int(20)), HasValue(testing::Eq(true)));
+ EXPECT_THAT(bound_pred->Test(Literal::Int(30)), HasValue(testing::Eq(true)));
+
+ // Test values not in the set
+ EXPECT_THAT(bound_pred->Test(Literal::Int(15)),
HasValue(testing::Eq(false)));
+ EXPECT_THAT(bound_pred->Test(Literal::Int(40)),
HasValue(testing::Eq(false)));
+ EXPECT_THAT(bound_pred->Test(Literal::Int(0)), HasValue(testing::Eq(false)));
+}
+
+TEST_F(PredicateTest, BoundSetPredicateTestNotIn) {
+ ICEBERG_ASSIGN_OR_THROW(
+ auto not_in_pred,
+ Expressions::NotIn("age", {Literal::Int(10), Literal::Int(20),
Literal::Int(30)})
+ ->Bind(*schema_, /*case_sensitive=*/true));
+ auto bound_pred = AssertAndCastToBoundPredicate(not_in_pred);
+
+ // Test values not in the set
+ EXPECT_THAT(bound_pred->Test(Literal::Int(15)), HasValue(testing::Eq(true)));
+ EXPECT_THAT(bound_pred->Test(Literal::Int(40)), HasValue(testing::Eq(true)));
+ EXPECT_THAT(bound_pred->Test(Literal::Int(0)), HasValue(testing::Eq(true)));
+
+ // Test values in the set
+ EXPECT_THAT(bound_pred->Test(Literal::Int(10)),
HasValue(testing::Eq(false)));
+ EXPECT_THAT(bound_pred->Test(Literal::Int(20)),
HasValue(testing::Eq(false)));
+ EXPECT_THAT(bound_pred->Test(Literal::Int(30)),
HasValue(testing::Eq(false)));
+}
+
+TEST_F(PredicateTest, BoundSetPredicateTestWithStrings) {
+ ICEBERG_ASSIGN_OR_THROW(
+ auto in_pred,
+ Expressions::In("name", {Literal::String("Alice"),
Literal::String("Bob"),
+ Literal::String("Charlie")})
+ ->Bind(*schema_, /*case_sensitive=*/true));
+ auto bound_pred = AssertAndCastToBoundPredicate(in_pred);
+
+ // Test strings in the set
+ EXPECT_THAT(bound_pred->Test(Literal::String("Alice")),
HasValue(testing::Eq(true)));
+ EXPECT_THAT(bound_pred->Test(Literal::String("Bob")),
HasValue(testing::Eq(true)));
+ EXPECT_THAT(bound_pred->Test(Literal::String("Charlie")),
HasValue(testing::Eq(true)));
+
+ // Test strings not in the set
+ EXPECT_THAT(bound_pred->Test(Literal::String("David")),
HasValue(testing::Eq(false)));
+ EXPECT_THAT(bound_pred->Test(Literal::String("alice")),
+ HasValue(testing::Eq(false))); // Case sensitive
+ EXPECT_THAT(bound_pred->Test(Literal::String("")),
HasValue(testing::Eq(false)));
+}
+
+TEST_F(PredicateTest, BoundSetPredicateTestWithLongs) {
+ ICEBERG_ASSIGN_OR_THROW(auto in_pred,
+ Expressions::In("id", {Literal::Long(100L),
Literal::Long(200L),
+ Literal::Long(300L)})
+ ->Bind(*schema_, /*case_sensitive=*/true));
+ auto bound_pred = AssertAndCastToBoundPredicate(in_pred);
+
+ // Test longs in the set
+ EXPECT_THAT(bound_pred->Test(Literal::Long(100L)),
HasValue(testing::Eq(true)));
+ EXPECT_THAT(bound_pred->Test(Literal::Long(200L)),
HasValue(testing::Eq(true)));
+ EXPECT_THAT(bound_pred->Test(Literal::Long(300L)),
HasValue(testing::Eq(true)));
+
+ // Test longs not in the set
+ EXPECT_THAT(bound_pred->Test(Literal::Long(150L)),
HasValue(testing::Eq(false)));
+ EXPECT_THAT(bound_pred->Test(Literal::Long(400L)),
HasValue(testing::Eq(false)));
+}
+
+TEST_F(PredicateTest, BoundSetPredicateTestSingleLiteral) {
+ ICEBERG_ASSIGN_OR_THROW(auto in_pred, Expressions::In("age",
{Literal::Int(42)})
+ ->Bind(*schema_,
/*case_sensitive=*/true));
+
+ // Single element IN becomes Equal
+ EXPECT_EQ(in_pred->op(), Expression::Operation::kEq);
+ auto bound_literal = AssertAndCastToBoundPredicate(in_pred);
+ EXPECT_THAT(bound_literal->Test(Literal::Int(42)),
HasValue(testing::Eq(true)));
+ EXPECT_THAT(bound_literal->Test(Literal::Int(41)),
HasValue(testing::Eq(false)));
+}
+
} // namespace iceberg
diff --git a/src/iceberg/util/macros.h b/src/iceberg/util/macros.h
index 278035d..733b07f 100644
--- a/src/iceberg/util/macros.h
+++ b/src/iceberg/util/macros.h
@@ -21,6 +21,9 @@
#include <cassert>
+#include "iceberg/exception.h"
+#include "iceberg/result.h"
+
#define ICEBERG_RETURN_UNEXPECTED(result) \
if (auto&& result_name = result; !result_name) [[unlikely]] { \
return std::unexpected<Error>(result_name.error()); \
@@ -40,3 +43,17 @@
rexpr)
#define ICEBERG_DCHECK(expr, message) assert((expr) && (message))
+
+#define ICEBERG_THROW_NOT_OK(result) \
+ if (auto&& result_name = result; !result_name) [[unlikely]] { \
+ throw iceberg::IcebergError(result_name.error().message); \
+ }
+
+#define ICEBERG_ASSIGN_OR_THROW_IMPL(result_name, lhs, rexpr) \
+ auto&& result_name = (rexpr); \
+ ICEBERG_THROW_NOT_OK(result_name); \
+ lhs = std::move(result_name.value());
+
+#define ICEBERG_ASSIGN_OR_THROW(lhs, rexpr) \
+ ICEBERG_ASSIGN_OR_THROW_IMPL( \
+ ICEBERG_ASSIGN_OR_RAISE_NAME(_error_or_value, __COUNTER__), lhs, rexpr);