diff --git a/src/iceberg/schema_field.cc b/src/iceberg/schema_field.cc index 80ea61bce..d09852a04 100644 --- a/src/iceberg/schema_field.cc +++ b/src/iceberg/schema_field.cc @@ -19,12 +19,14 @@ #include "iceberg/schema_field.h" +#include #include #include #include #include #include "iceberg/expression/literal.h" +#include "iceberg/result.h" #include "iceberg/type.h" #include "iceberg/util/checked_cast.h" #include "iceberg/util/formatter.h" // IWYU pragma: keep @@ -168,6 +170,21 @@ Status ValidateDefault(const SchemaField& field, const Literal& value, return InvalidSchema("{} of field {} has type {} but expected {}", kind, field.name(), *value.type(), *field.type()); } + // A non-finite float/double default cannot be represented in JSON: the serializer emits + // it as `null`, which reads back as an absent default. Reject it so the default is not + // silently lost when the metadata round-trips. The literal type already matches the + // field type here, so this only inspects the value for actual floating fields and other + // types pay nothing. + if (field.type()->type_id() == TypeId::kFloat && + !std::isfinite(std::get(value.value()))) { + return InvalidSchema("Invalid {} value for {}: value must be finite", kind, + field.name()); + } + if (field.type()->type_id() == TypeId::kDouble && + !std::isfinite(std::get(value.value()))) { + return InvalidSchema("Invalid {} value for {}: value must be finite", kind, + field.name()); + } return {}; } diff --git a/src/iceberg/test/schema_field_test.cc b/src/iceberg/test/schema_field_test.cc index d757cb0b2..434742ab6 100644 --- a/src/iceberg/test/schema_field_test.cc +++ b/src/iceberg/test/schema_field_test.cc @@ -20,11 +20,14 @@ #include "iceberg/schema_field.h" #include +#include #include #include #include "iceberg/expression/literal.h" +#include "iceberg/result.h" +#include "iceberg/test/matchers.h" #include "iceberg/type.h" #include "iceberg/util/formatter.h" // IWYU pragma: keep @@ -170,4 +173,34 @@ TEST(SchemaFieldTest, CastDefaultValue) { } } +TEST(SchemaFieldTest, ValidateRejectsNonFiniteFloatingDefault) { + // NaN / infinity cannot be represented in JSON (the serializer emits `null`, which + // reads back as an absent default), so a non-finite floating default must be rejected. + SchemaField nan_field(/*field_id=*/1, /*name=*/"f", float32(), + /*optional=*/true, /*doc=*/"", + std::make_shared( + Literal::Float(std::numeric_limits::quiet_NaN()))); + EXPECT_THAT(nan_field.Validate(), IsError(ErrorKind::kInvalidSchema)); + EXPECT_THAT(nan_field.Validate(), HasErrorMessage("must be finite")); + + SchemaField inf_field(/*field_id=*/2, /*name=*/"d", float64(), + /*optional=*/true, /*doc=*/"", + std::make_shared( + Literal::Double(std::numeric_limits::infinity()))); + EXPECT_THAT(inf_field.Validate(), IsError(ErrorKind::kInvalidSchema)); + + SchemaField neg_inf_field(/*field_id=*/3, /*name=*/"d2", float64(), + /*optional=*/true, /*doc=*/"", + std::make_shared(Literal::Double( + -std::numeric_limits::infinity()))); + EXPECT_THAT(neg_inf_field.Validate(), IsError(ErrorKind::kInvalidSchema)); +} + +TEST(SchemaFieldTest, ValidateAcceptsFiniteFloatingDefault) { + SchemaField field(/*field_id=*/1, /*name=*/"f", float32(), + /*optional=*/true, /*doc=*/"", + std::make_shared(Literal::Float(1.5f))); + EXPECT_THAT(field.Validate(), IsOk()); +} + } // namespace iceberg