| /* | |
| * 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/schema_field.h" | |
| #include <cmath> | |
| #include <format> | |
| #include <string_view> | |
| #include <utility> | |
| #include <variant> | |
| #include "iceberg/expression/literal.h" | |
| #include "iceberg/result.h" | |
| #include "iceberg/type.h" | |
| #include "iceberg/util/checked_cast.h" | |
| #include "iceberg/util/decimal.h" | |
| #include "iceberg/util/formatter.h" // IWYU pragma: keep | |
| #include "iceberg/util/macros.h" | |
| namespace iceberg { | |
| namespace { | |
| // Treat null defaults as absent. | |
| std::shared_ptr<const Literal> DropNullDefault(std::shared_ptr<const Literal> value) { | |
| if (value != nullptr && value->IsNull()) { | |
| return nullptr; | |
| } | |
| return value; | |
| } | |
| } // namespace | |
| SchemaField::SchemaField(int32_t field_id, std::string_view name, | |
| std::shared_ptr<Type> type, bool optional, std::string_view doc, | |
| std::shared_ptr<const Literal> initial_default, | |
| std::shared_ptr<const Literal> write_default) | |
| : field_id_(field_id), | |
| name_(name), | |
| type_(std::move(type)), | |
| optional_(optional), | |
| doc_(doc), | |
| initial_default_(DropNullDefault(std::move(initial_default))), | |
| write_default_(DropNullDefault(std::move(write_default))) {} | |
| SchemaField SchemaField::MakeOptional(int32_t field_id, std::string_view name, | |
| std::shared_ptr<Type> type, std::string_view doc) { | |
| return {field_id, name, std::move(type), true, doc}; | |
| } | |
| SchemaField SchemaField::MakeRequired(int32_t field_id, std::string_view name, | |
| std::shared_ptr<Type> type, std::string_view doc) { | |
| return {field_id, name, std::move(type), false, doc}; | |
| } | |
| SchemaField SchemaField::WithName(std::string_view name) const { | |
| return {field_id_, name, type_, optional_, doc_, initial_default_, write_default_}; | |
| } | |
| SchemaField SchemaField::WithType(std::shared_ptr<Type> type) const { | |
| return {field_id_, name_, std::move(type), optional_, doc_, | |
| initial_default_, write_default_}; | |
| } | |
| SchemaField SchemaField::WithDoc(std::string_view doc) const { | |
| return {field_id_, name_, type_, optional_, doc, initial_default_, write_default_}; | |
| } | |
| SchemaField SchemaField::WithInitialDefault( | |
| std::shared_ptr<const Literal> initial_default) const { | |
| return {field_id_, name_, type_, optional_, doc_, std::move(initial_default), | |
| write_default_}; | |
| } | |
| SchemaField SchemaField::WithWriteDefault( | |
| std::shared_ptr<const Literal> write_default) const { | |
| return {field_id_, | |
| name_, | |
| type_, | |
| optional_, | |
| doc_, | |
| initial_default_, | |
| std::move(write_default)}; | |
| } | |
| Result<Literal> SchemaField::CastDefaultValue( | |
| const Literal& value, const std::shared_ptr<PrimitiveType>& target_type) { | |
| if (target_type->type_id() == TypeId::kDecimal && | |
| std::holds_alternative<Decimal>(value.value())) { | |
| const auto& source_type = internal::checked_cast<const DecimalType&>(*value.type()); | |
| const auto& decimal_type = internal::checked_cast<const DecimalType&>(*target_type); | |
| if (source_type.scale() == decimal_type.scale()) { | |
| const auto& decimal_value = std::get<Decimal>(value.value()); | |
| if (!decimal_value.FitsInPrecision(decimal_type.precision())) { | |
| return InvalidArgument("Cannot cast default value to {}: {}", *target_type, | |
| value); | |
| } | |
| return Literal::Decimal(decimal_value.value(), decimal_type.precision(), | |
| decimal_type.scale()); | |
| } | |
| } | |
| return value.CastTo(target_type); | |
| } | |
| int32_t SchemaField::field_id() const { return field_id_; } | |
| std::string_view SchemaField::name() const { return name_; } | |
| const std::shared_ptr<Type>& SchemaField::type() const { return type_; } | |
| bool SchemaField::optional() const { return optional_; } | |
| std::string_view SchemaField::doc() const { return doc_; } | |
| const std::shared_ptr<const Literal>& SchemaField::initial_default() const { | |
| return initial_default_; | |
| } | |
| const std::shared_ptr<const Literal>& SchemaField::write_default() const { | |
| return write_default_; | |
| } | |
| namespace { | |
| Status ValidateDefault(const SchemaField& field, const Literal& value, | |
| std::string_view kind) { | |
| // Null defaults are dropped at construction. | |
| if (value.IsAboveMax() || value.IsBelowMin()) { | |
| return InvalidSchema("Invalid {} value for {}: value is out of range", kind, | |
| field.name()); | |
| } | |
| if (field.type() == nullptr) { | |
| return InvalidSchema("Invalid {} value for {}: field has no type", kind, | |
| field.name()); | |
| } | |
| // These types can only use null defaults. | |
| switch (field.type()->type_id()) { | |
| case TypeId::kUnknown: | |
| case TypeId::kVariant: | |
| case TypeId::kGeometry: | |
| case TypeId::kGeography: | |
| return InvalidSchema("Invalid {} value for {}: type {} cannot have a default value", | |
| kind, field.name(), *field.type()); | |
| default: | |
| break; | |
| } | |
| // Nested defaults are not supported yet. | |
| if (!field.type()->is_primitive()) { | |
| return InvalidSchema( | |
| "Invalid {} value for {}: default values are only supported for primitive types", | |
| kind, field.name()); | |
| } | |
| // Stored defaults must already match the field type. | |
| if (*value.type() != *field.type()) { | |
| 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<float>(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<double>(value.value()))) { | |
| return InvalidSchema("Invalid {} value for {}: value must be finite", kind, | |
| field.name()); | |
| } | |
| // A decimal literal carries a precision in its type but stores only an unscaled value, | |
| // so a value that does not fit the field precision would pass the type check above yet | |
| // be rejected when the metadata is parsed back. Reject it here to keep the default | |
| // consistent with what the reader accepts. | |
| if (field.type()->type_id() == TypeId::kDecimal) { | |
| if (const auto* dec = std::get_if<Decimal>(&value.value())) { | |
| const auto& decimal_type = | |
| internal::checked_cast<const DecimalType&>(*field.type()); | |
| if (!dec->FitsInPrecision(decimal_type.precision())) { | |
| return InvalidSchema("{} of field {} does not fit precision {}", kind, | |
| field.name(), decimal_type.precision()); | |
| } | |
| } | |
| } | |
| return {}; | |
| } | |
| } // namespace | |
| Status SchemaField::Validate() const { | |
| if (name_.empty()) [[unlikely]] { | |
| return InvalidSchema("SchemaField cannot have empty name"); | |
| } | |
| if (type_ == nullptr) [[unlikely]] { | |
| return InvalidSchema("SchemaField cannot have null type"); | |
| } | |
| if (initial_default_ != nullptr) { | |
| ICEBERG_RETURN_UNEXPECTED( | |
| ValidateDefault(*this, *initial_default_, "initial-default")); | |
| } | |
| if (write_default_ != nullptr) { | |
| ICEBERG_RETURN_UNEXPECTED(ValidateDefault(*this, *write_default_, "write-default")); | |
| } | |
| return {}; | |
| } | |
| std::string SchemaField::ToString() const { | |
| std::string result = std::format("{} ({}): {} ({}){}", name_, field_id_, *type_, | |
| optional_ ? "optional" : "required", | |
| !doc_.empty() ? std::format(" - {}", doc_) : ""); | |
| return result; | |
| } | |
| namespace { | |
| bool DefaultEquals(const std::shared_ptr<const Literal>& lhs, | |
| const std::shared_ptr<const Literal>& rhs) { | |
| if (lhs == nullptr || rhs == nullptr) { | |
| return lhs == rhs; | |
| } | |
| return *lhs == *rhs; | |
| } | |
| } // namespace | |
| bool SchemaField::Equals(const SchemaField& other) const { | |
| return field_id_ == other.field_id_ && name_ == other.name_ && *type_ == *other.type_ && | |
| optional_ == other.optional_ && | |
| DefaultEquals(initial_default_, other.initial_default_) && | |
| DefaultEquals(write_default_, other.write_default_); | |
| } | |
| } // namespace iceberg |