diff --git a/src/iceberg/expression/json_serde.cc b/src/iceberg/expression/json_serde.cc index 0fd7dd01d..e59f60be8 100644 --- a/src/iceberg/expression/json_serde.cc +++ b/src/iceberg/expression/json_serde.cc @@ -17,22 +17,33 @@ * under the License. */ -#include #include #include -#include #include #include #include "iceberg/expression/json_serde_internal.h" #include "iceberg/expression/literal.h" +#include "iceberg/expression/predicate.h" +#include "iceberg/expression/term.h" +#include "iceberg/transform.h" #include "iceberg/util/checked_cast.h" #include "iceberg/util/json_util_internal.h" #include "iceberg/util/macros.h" +#include "iceberg/util/transform_util.h" namespace iceberg { namespace { +// JSON field names +constexpr std::string_view kType = "type"; +constexpr std::string_view kTerm = "term"; +constexpr std::string_view kTransform = "transform"; +constexpr std::string_view kValue = "value"; +constexpr std::string_view kValues = "values"; +constexpr std::string_view kLeft = "left"; +constexpr std::string_view kRight = "right"; +constexpr std::string_view kChild = "child"; // Expression type strings constexpr std::string_view kTypeTrue = "true"; constexpr std::string_view kTypeFalse = "false"; @@ -58,6 +69,53 @@ constexpr std::string_view kTypeCountNull = "count-null"; constexpr std::string_view kTypeCountStar = "count-star"; constexpr std::string_view kTypeMin = "min"; constexpr std::string_view kTypeMax = "max"; +constexpr std::string_view kTypeLiteral = "literal"; +constexpr std::string_view kTypeReference = "reference"; + +/// Helper to check if a JSON term represents a transform +bool IsTransformTerm(const nlohmann::json& json) { + return json.is_object() && json.contains(kType) && + json[kType].get() == kTransform && json.contains(kTerm); +} + +/// Template helper to create predicates from JSON with the appropriate term type +template +Result> MakePredicateFromJson( + Expression::Operation op, std::shared_ptr> term, + const nlohmann::json& json) { + if (IsUnaryOperation(op)) { + if (json.contains(kValue)) [[unlikely]] { + return JsonParseError("Unary predicate has invalid 'value' field: {}", + SafeDumpJson(json)); + } + if (json.contains(kValues)) [[unlikely]] { + return JsonParseError("Unary predicate has invalid 'values' field: {}", + SafeDumpJson(json)); + } + return UnboundPredicateImpl::Make(op, std::move(term)); + } + + if (IsSetOperation(op)) { + std::vector literals; + if (!json.contains(kValues) || !json[kValues].is_array()) [[unlikely]] { + return JsonParseError("Missing or invalid 'values' field for set operation: {}", + SafeDumpJson(json)); + } + for (const auto& val : json[kValues]) { + ICEBERG_ASSIGN_OR_RAISE(auto lit, LiteralFromJson(val)); + literals.push_back(std::move(lit)); + } + return UnboundPredicateImpl::Make(op, std::move(term), std::move(literals)); + } + + // Literal predicate + if (!json.contains(kValue)) [[unlikely]] { + return JsonParseError("Missing 'value' field for literal predicate: {}", + SafeDumpJson(json)); + } + ICEBERG_ASSIGN_OR_RAISE(auto literal, LiteralFromJson(json[kValue])); + return UnboundPredicateImpl::Make(op, std::move(term), std::move(literal)); +} } // namespace bool IsUnaryOperation(Expression::Operation op) { @@ -83,7 +141,7 @@ bool IsSetOperation(Expression::Operation op) { } Result OperationTypeFromJson(const nlohmann::json& json) { - if (!json.is_string()) { + if (!json.is_string()) [[unlikely]] { return JsonParseError("Unable to create operation. Json value is not a string"); } auto typeStr = json.get(); @@ -123,27 +181,252 @@ nlohmann::json ToJson(Expression::Operation op) { return json; } +nlohmann::json ToJson(const NamedReference& ref) { return ref.name(); } + +Result> NamedReferenceFromJson( + const nlohmann::json& json) { + if (json.is_object() && json.contains(kType) && + json[kType].get() == kTypeReference && json.contains(kTerm)) { + return NamedReference::Make(json[kTerm].get()); + } + if (!json.is_string()) [[unlikely]] { + return JsonParseError("Expected string for named reference"); + } + return NamedReference::Make(json.get()); +} + +nlohmann::json ToJson(const UnboundTransform& transform) { + auto& mutable_transform = const_cast(transform); + nlohmann::json json; + json[kType] = kTransform; + json[kTransform] = transform.transform()->ToString(); + json[kTerm] = mutable_transform.reference()->name(); + return json; +} + +Result> UnboundTransformFromJson( + const nlohmann::json& json) { + if (IsTransformTerm(json)) { + ICEBERG_ASSIGN_OR_RAISE(auto transform_str, + GetJsonValue(json, kTransform)); + ICEBERG_ASSIGN_OR_RAISE(auto transform, TransformFromString(transform_str)); + ICEBERG_ASSIGN_OR_RAISE(auto ref, NamedReferenceFromJson(json[kTerm])); + return UnboundTransform::Make(std::move(ref), std::move(transform)); + } + return JsonParseError("Invalid unbound transform json: {}", SafeDumpJson(json)); +} + +nlohmann::json ToJson(const Literal& literal) { + if (literal.IsNull()) { + return nullptr; + } + + const auto type_id = literal.type()->type_id(); + const auto& value = literal.value(); + + switch (type_id) { + case TypeId::kBoolean: + return std::get(value); + case TypeId::kInt: + return std::get(value); + case TypeId::kDate: + return TransformUtil::HumanDay(std::get(value)); + case TypeId::kLong: + return std::get(value); + case TypeId::kTime: + return TransformUtil::HumanTime(std::get(value)); + case TypeId::kTimestamp: + return TransformUtil::HumanTimestamp(std::get(value)); + case TypeId::kTimestampTz: + return TransformUtil::HumanTimestampWithZone(std::get(value)); + case TypeId::kFloat: + return std::get(value); + case TypeId::kDouble: + return std::get(value); + case TypeId::kString: + return std::get(value); + case TypeId::kBinary: + case TypeId::kFixed: { + const auto& bytes = std::get>(value); + std::string hex; + hex.reserve(bytes.size() * 2); + for (uint8_t byte : bytes) { + hex += std::format("{:02X}", byte); + } + return hex; + } + case TypeId::kDecimal: { + return literal.ToString(); + } + case TypeId::kUuid: + return std::get(value).ToString(); + default: + nlohmann::json json; + return json; + } +} + +Result LiteralFromJson(const nlohmann::json& json) { + // Unwrap {"type": "literal", "value": } wrapper + if (json.is_object() && json.contains(kType) && + json[kType].get() == kTypeLiteral && json.contains(kValue)) { + return LiteralFromJson(json[kValue]); + } + if (json.is_null()) { + return Literal::Null(nullptr); + } + if (json.is_boolean()) { + return Literal::Boolean(json.get()); + } + if (json.is_number_integer()) { + return Literal::Long(json.get()); + } + if (json.is_number_float()) { + return Literal::Double(json.get()); + } + if (json.is_string()) { + // All strings are returned as String literals. + // Conversion to binary/date/time/etc. happens during binding + // when schema type information is available. + return Literal::String(json.get()); + } + return JsonParseError("Unsupported literal JSON type"); +} + +nlohmann::json TermToJson(const Term& term) { + switch (term.kind()) { + case Term::Kind::kReference: + return ToJson(static_cast(term)); + case Term::Kind::kTransform: + return ToJson(static_cast(term)); + default: + return nullptr; + } +} + +nlohmann::json ToJson(const UnboundPredicate& pred) { + nlohmann::json json; + json[kType] = ToJson(pred.op()); + + // Get term and literals by casting to the appropriate impl type + std::span literals; + + if (auto* ref_pred = dynamic_cast*>(&pred)) { + json[kTerm] = TermToJson(*ref_pred->term()); + literals = ref_pred->literals(); + } else if (auto* transform_pred = + dynamic_cast*>(&pred)) { + json[kTerm] = TermToJson(*transform_pred->term()); + literals = transform_pred->literals(); + } + + if (!IsUnaryOperation(pred.op())) { + if (IsSetOperation(pred.op())) { + nlohmann::json values = nlohmann::json::array(); + for (const auto& lit : literals) { + values.push_back(ToJson(lit)); + } + json[kValues] = std::move(values); + } else if (!literals.empty()) { + json[kValue] = ToJson(literals[0]); + } + } + return json; +} + +Result> UnboundPredicateFromJson( + const nlohmann::json& json) { + ICEBERG_ASSIGN_OR_RAISE(auto op, OperationTypeFromJson(json[kType])); + + const auto& term_json = json[kTerm]; + + if (IsTransformTerm(term_json)) { + ICEBERG_ASSIGN_OR_RAISE(auto term, UnboundTransformFromJson(term_json)); + return MakePredicateFromJson(op, std::move(term), json); + } + + ICEBERG_ASSIGN_OR_RAISE(auto term, NamedReferenceFromJson(term_json)); + return MakePredicateFromJson(op, std::move(term), json); +} + Result> ExpressionFromJson(const nlohmann::json& json) { - // Handle boolean + // Handle boolean constants if (json.is_boolean()) { return json.get() ? internal::checked_pointer_cast(True::Instance()) : internal::checked_pointer_cast(False::Instance()); } - return JsonParseError("Only booleans are currently supported."); + + if (!json.is_object()) [[unlikely]] { + return JsonParseError("Expression must be boolean or object"); + } + + ICEBERG_ASSIGN_OR_RAISE(auto op, OperationTypeFromJson(json[kType])); + + switch (op) { + case Expression::Operation::kAnd: { + if (!json.contains(kLeft) || !json.contains(kRight)) [[unlikely]] { + return JsonParseError("AND expression missing 'left' or 'right' field"); + } + ICEBERG_ASSIGN_OR_RAISE(auto left, ExpressionFromJson(json[kLeft])); + ICEBERG_ASSIGN_OR_RAISE(auto right, ExpressionFromJson(json[kRight])); + ICEBERG_ASSIGN_OR_RAISE(auto result, And::Make(std::move(left), std::move(right))); + return std::shared_ptr(std::move(result)); + } + case Expression::Operation::kOr: { + if (!json.contains(kLeft) || !json.contains(kRight)) [[unlikely]] { + return JsonParseError("OR expression missing 'left' or 'right' field"); + } + ICEBERG_ASSIGN_OR_RAISE(auto left, ExpressionFromJson(json[kLeft])); + ICEBERG_ASSIGN_OR_RAISE(auto right, ExpressionFromJson(json[kRight])); + ICEBERG_ASSIGN_OR_RAISE(auto result, Or::Make(std::move(left), std::move(right))); + return std::shared_ptr(std::move(result)); + } + case Expression::Operation::kNot: { + if (!json.contains(kChild)) [[unlikely]] { + return JsonParseError("NOT expression missing 'child' field"); + } + ICEBERG_ASSIGN_OR_RAISE(auto child, ExpressionFromJson(json[kChild])); + ICEBERG_ASSIGN_OR_RAISE(auto result, Not::Make(std::move(child))); + return std::shared_ptr(std::move(result)); + } + default: + // All other operations are predicates + return UnboundPredicateFromJson(json); + } } nlohmann::json ToJson(const Expression& expr) { switch (expr.op()) { case Expression::Operation::kTrue: return true; - case Expression::Operation::kFalse: return false; + case Expression::Operation::kAnd: { + const auto& and_expr = static_cast(expr); + nlohmann::json json; + json[kType] = ToJson(expr.op()); + json[kLeft] = ToJson(*and_expr.left()); + json[kRight] = ToJson(*and_expr.right()); + return json; + } + case Expression::Operation::kOr: { + const auto& or_expr = static_cast(expr); + nlohmann::json json; + json[kType] = ToJson(expr.op()); + json[kLeft] = ToJson(*or_expr.left()); + json[kRight] = ToJson(*or_expr.right()); + return json; + } + case Expression::Operation::kNot: { + const auto& not_expr = static_cast(expr); + nlohmann::json json; + json[kType] = ToJson(expr.op()); + json[kChild] = ToJson(*not_expr.child()); + return json; + } default: - // TODO(evindj): This code will be removed as we implemented the full expression - // serialization. - ICEBERG_CHECK_OR_DIE(false, "Only booleans are currently supported."); + return ToJson(dynamic_cast(expr)); } } diff --git a/src/iceberg/expression/json_serde_internal.h b/src/iceberg/expression/json_serde_internal.h index e44234d39..98233d542 100644 --- a/src/iceberg/expression/json_serde_internal.h +++ b/src/iceberg/expression/json_serde_internal.h @@ -19,6 +19,8 @@ #pragma once +#include + #include #include "iceberg/expression/expression.h" @@ -57,6 +59,63 @@ ICEBERG_EXPORT Result> ExpressionFromJson( /// \return A JSON object representing the expression ICEBERG_EXPORT nlohmann::json ToJson(const Expression& expr); +/// \brief Deserializes a JSON object into a NamedReference. +/// +/// \param json A JSON object representing a named reference +/// \return A shared pointer to the deserialized NamedReference or an error +ICEBERG_EXPORT Result> NamedReferenceFromJson( + const nlohmann::json& json); + +/// \brief Serializes a NamedReference into its JSON representation. +/// +/// \param ref The named reference to serialize +/// \return A JSON object representing the named reference +ICEBERG_EXPORT nlohmann::json ToJson(const NamedReference& ref); + +/// \brief Serializes an UnboundTransform into its JSON representation. +/// +/// \param transform The unbound transform to serialize +/// \return A JSON object representing the unbound transform +ICEBERG_EXPORT nlohmann::json ToJson(const UnboundTransform& transform); + +/// \brief Deserializes a JSON object into an UnboundTransform. +/// +/// \param json A JSON object representing an unbound transform +/// \return A shared pointer to the deserialized UnboundTransform or an error +ICEBERG_EXPORT Result> UnboundTransformFromJson( + const nlohmann::json& json); + +/// \brief Serializes a Literal into its JSON representation. +/// +/// \param literal The literal to serialize +/// \return A JSON value representing the literal +ICEBERG_EXPORT nlohmann::json ToJson(const Literal& literal); + +/// \brief Deserializes a JSON value into a Literal. +/// +/// \param json A JSON value representing a literal +/// \return The deserialized Literal or an error +ICEBERG_EXPORT Result LiteralFromJson(const nlohmann::json& json); + +/// \brief Serializes an UnboundPredicate into its JSON representation. +/// +/// \param pred The unbound predicate to serialize +/// \return A JSON object representing the predicate +ICEBERG_EXPORT nlohmann::json ToJson(const UnboundPredicate& pred); + +/// \brief Deserializes a JSON object into an UnboundPredicate. +/// +/// \param json A JSON object representing an unbound predicate +/// \return A shared pointer to the deserialized UnboundPredicate or an error +ICEBERG_EXPORT Result> UnboundPredicateFromJson( + const nlohmann::json& json); + +/// \brief Serializes a Term into its JSON representation. +/// +/// \param term The term to serialize (NamedReference or UnboundTransform) +/// \return A JSON value representing the term +ICEBERG_EXPORT nlohmann::json TermToJson(const Term& term); + /// Check if an operation is a unary predicate ICEBERG_EXPORT bool IsUnaryOperation(Expression::Operation op); diff --git a/src/iceberg/test/expression_json_test.cc b/src/iceberg/test/expression_json_test.cc index dd3ac5e3e..1851946d3 100644 --- a/src/iceberg/test/expression_json_test.cc +++ b/src/iceberg/test/expression_json_test.cc @@ -18,6 +18,7 @@ */ #include +#include #include #include @@ -25,42 +26,210 @@ #include #include "iceberg/expression/expression.h" -#include "iceberg/expression/expressions.h" #include "iceberg/expression/json_serde_internal.h" #include "iceberg/expression/literal.h" -#include "iceberg/expression/predicate.h" #include "iceberg/expression/term.h" #include "iceberg/test/matchers.h" +#include "iceberg/transform.h" +#include "iceberg/util/uuid.h" namespace iceberg { -// Test boolean constant expressions -TEST(ExpressionJsonTest, CheckBooleanExpression) { - auto checkBoolean = [](std::shared_ptr expr, bool value) { - auto json = ToJson(*expr); - EXPECT_TRUE(json.is_boolean()); - EXPECT_EQ(json.get(), value); - - auto result = ExpressionFromJson(json); - ASSERT_THAT(result, IsOk()); - if (value) { - EXPECT_EQ(result.value()->op(), Expression::Operation::kTrue); - } else { - EXPECT_EQ(result.value()->op(), Expression::Operation::kFalse); - } - }; - checkBoolean(True::Instance(), true); - checkBoolean(False::Instance(), false); +struct ExpressionJsonRoundTripParam { + std::string name; + nlohmann::json json; + Expression::Operation expected_op; +}; + +class ExpressionJsonRoundTripTest + : public ::testing::TestWithParam {}; + +TEST_P(ExpressionJsonRoundTripTest, RoundTrip) { + const auto& param = GetParam(); + ICEBERG_UNWRAP_OR_FAIL(auto expr, ExpressionFromJson(param.json)); + EXPECT_EQ(expr->op(), param.expected_op); + auto round_trip = ToJson(*expr); + EXPECT_EQ(round_trip, param.json); } -TEST(ExpressionJsonTest, OperationTypeTests) { - EXPECT_EQ(OperationTypeFromJson("true"), Expression::Operation::kTrue); - EXPECT_EQ("true", ToJson(Expression::Operation::kTrue)); - EXPECT_TRUE(IsSetOperation(Expression::Operation::kIn)); - EXPECT_FALSE(IsSetOperation(Expression::Operation::kTrue)); +INSTANTIATE_TEST_SUITE_P( + ExpressionJsonTest, ExpressionJsonRoundTripTest, + ::testing::Values( + ExpressionJsonRoundTripParam{"BooleanTrue", true, Expression::Operation::kTrue}, + ExpressionJsonRoundTripParam{"BooleanFalse", false, + Expression::Operation::kFalse}, + ExpressionJsonRoundTripParam{"UnaryIsNull", + {{"type", "is-null"}, {"term", "col"}}, + Expression::Operation::kIsNull}, + ExpressionJsonRoundTripParam{"LiteralGt", + {{"type", "gt"}, {"term", "age"}, {"value", 21}}, + Expression::Operation::kGt}, + ExpressionJsonRoundTripParam{ + "SetIn", + {{"type", "in"}, + {"term", "status"}, + {"values", nlohmann::json::array({"active", "pending"})}}, + Expression::Operation::kIn}, + ExpressionJsonRoundTripParam{ + "AndExpression", + {{"type", "and"}, + {"left", {{"type", "gt"}, {"term", "age"}, {"value", 18}}}, + {"right", {{"type", "lt"}, {"term", "age"}, {"value", 65}}}}, + Expression::Operation::kAnd}, + ExpressionJsonRoundTripParam{ + "NotExpression", + {{"type", "not"}, {"child", {{"type", "is-null"}, {"term", "name"}}}}, + Expression::Operation::kNot}, + ExpressionJsonRoundTripParam{ + "TransformDay", + {{"type", "eq"}, + {"term", {{"type", "transform"}, {"transform", "day"}, {"term", "ts"}}}, + {"value", 19738}}, + Expression::Operation::kEq}, + ExpressionJsonRoundTripParam{ + "TransformYear", + {{"type", "gt"}, + {"term", + {{"type", "transform"}, {"transform", "year"}, {"term", "timestamp_col"}}}, + {"value", 2020}}, + Expression::Operation::kGt}, + ExpressionJsonRoundTripParam{ + "TransformTruncate", + {{"type", "lt"}, + {"term", + {{"type", "transform"}, {"transform", "truncate[4]"}, {"term", "col"}}}, + {"value", 100}}, + Expression::Operation::kLt}, + ExpressionJsonRoundTripParam{ + "LiteralNotEq", + {{"type", "not-eq"}, {"term", "status"}, {"value", "closed"}}, + Expression::Operation::kNotEq}, + ExpressionJsonRoundTripParam{ + "LiteralLtEq", + {{"type", "lt-eq"}, {"term", "price"}, {"value", 100}}, + Expression::Operation::kLtEq}, + ExpressionJsonRoundTripParam{ + "LiteralGtEq", + {{"type", "gt-eq"}, {"term", "quantity"}, {"value", 1}}, + Expression::Operation::kGtEq}, + ExpressionJsonRoundTripParam{ + "SetNotIn", + {{"type", "not-in"}, + {"term", "category"}, + {"values", nlohmann::json::array({"archived", "deleted"})}}, + Expression::Operation::kNotIn}, + ExpressionJsonRoundTripParam{"UnaryNotNan", + {{"type", "not-nan"}, {"term", "score"}}, + Expression::Operation::kNotNan}, + ExpressionJsonRoundTripParam{ + "LiteralStartsWith", + {{"type", "starts-with"}, {"term", "name"}, {"value", "prefix"}}, + Expression::Operation::kStartsWith}, + ExpressionJsonRoundTripParam{ + "LiteralNotStartsWith", + {{"type", "not-starts-with"}, {"term", "name"}, {"value", "bad"}}, + Expression::Operation::kNotStartsWith}, + ExpressionJsonRoundTripParam{ + "OrExpression", + {{"type", "or"}, + {"left", {{"type", "lt"}, {"term", "price"}, {"value", 50}}}, + {"right", {{"type", "not-null"}, {"term", "discount"}}}}, + Expression::Operation::kOr}, + ExpressionJsonRoundTripParam{ + "NestedWithDecimals", + {{"type", "or"}, + {"left", + {{"type", "and"}, + {"left", + {{"type", "in"}, + {"term", "price"}, + {"values", nlohmann::json::array({3.14, 2.72})}}}, + {"right", {{"type", "eq"}, {"term", "currency"}, {"value", "USD"}}}}}, + {"right", {{"type", "is-nan"}, {"term", "discount"}}}}, + Expression::Operation::kOr}, + ExpressionJsonRoundTripParam{ + "FixedBinaryInPredicate", + {{"type", "eq"}, {"term", "col"}, {"value", "010203"}}, + Expression::Operation::kEq}, + ExpressionJsonRoundTripParam{"ScaleDecimalInSet", + {{"type", "in"}, + {"term", "amount"}, + {"values", nlohmann::json::array({"3.14E+4"})}}, + Expression::Operation::kIn}), + [](const ::testing::TestParamInfo& info) { + return info.param.name; + }); + +// -- Object wrapper normalization tests -- - EXPECT_TRUE(IsUnaryOperation(Expression::Operation::kIsNull)); - EXPECT_FALSE(IsUnaryOperation(Expression::Operation::kTrue)); +TEST(ExpressionJsonTest, PredicateWithObjectLiteral) { + nlohmann::json input = {{"type", "lt-eq"}, + {"term", "col"}, + {"value", {{"type", "literal"}, {"value", 50}}}}; + nlohmann::json expected = {{"type", "lt-eq"}, {"term", "col"}, {"value", 50}}; + ICEBERG_UNWRAP_OR_FAIL(auto expr, ExpressionFromJson(input)); + EXPECT_EQ(ToJson(*expr), expected); } +TEST(ExpressionJsonTest, PredicateWithObjectReference) { + nlohmann::json input = {{"type", "lt-eq"}, + {"term", {{"type", "reference"}, {"term", "col"}}}, + {"value", 50}}; + nlohmann::json expected = {{"type", "lt-eq"}, {"term", "col"}, {"value", 50}}; + ICEBERG_UNWRAP_OR_FAIL(auto expr, ExpressionFromJson(input)); + EXPECT_EQ(ToJson(*expr), expected); +} + +// -- Parameterized invalid expression tests -- + +struct InvalidExpressionParam { + std::string name; + nlohmann::json json; + std::string expected_error_substr; +}; + +class InvalidExpressionTest : public ::testing::TestWithParam {}; + +TEST_P(InvalidExpressionTest, ReturnsError) { + const auto& param = GetParam(); + auto result = ExpressionFromJson(param.json); + EXPECT_THAT(result, HasErrorMessage(param.expected_error_substr)); +} + +INSTANTIATE_TEST_SUITE_P( + ExpressionJsonTest, InvalidExpressionTest, + ::testing::Values( + InvalidExpressionParam{"NotBooleanOrObject", 42, "boolean or object"}, + InvalidExpressionParam{"UnknownOperationType", + {{"type", "illegal"}, {"term", "col"}}, + "Unknown expression type"}, + InvalidExpressionParam{ + "AndMissingLeft", + {{"type", "and"}, {"right", {{"type", "is-null"}, {"term", "col"}}}}, + "missing 'left' or 'right'"}, + InvalidExpressionParam{ + "OrMissingRight", + {{"type", "or"}, {"left", {{"type", "is-null"}, {"term", "col"}}}}, + "missing 'left' or 'right'"}, + InvalidExpressionParam{"NotMissingChild", {{"type", "not"}}, "missing 'child'"}, + InvalidExpressionParam{"UnaryWithSpuriousValue", + {{"type", "not-nan"}, {"term", "col"}, {"value", 42}}, + "invalid 'value' field"}, + InvalidExpressionParam{"UnaryWithSpuriousValues", + {{"type", "is-nan"}, + {"term", "col"}, + {"values", nlohmann::json::array({1, 2})}}, + "invalid 'values' field"}, + InvalidExpressionParam{"NumericTerm", + {{"type", "lt"}, {"term", 23}, {"value", 10}}, + "Expected string for named reference"}, + InvalidExpressionParam{"SetMissingValues", + {{"type", "in"}, {"term", "col"}, {"value", 42}}, + "values"}, + InvalidExpressionParam{ + "LiteralMissingValue", {{"type", "gt"}, {"term", "col"}}, "value"}), + [](const ::testing::TestParamInfo& info) { + return info.param.name; + }); + } // namespace iceberg diff --git a/src/iceberg/type_fwd.h b/src/iceberg/type_fwd.h index e97de0ac5..491775ee3 100644 --- a/src/iceberg/type_fwd.h +++ b/src/iceberg/type_fwd.h @@ -129,8 +129,11 @@ class BoundReference; class BoundTransform; class Expression; class Literal; +class NamedReference; class Term; +class Transform; class UnboundPredicate; +class UnboundTransform; /// \brief Evaluator. class Evaluator;