2222#include < format>
2323#include < string_view>
2424#include < utility>
25+ #include < variant>
2526
2627#include " iceberg/expression/literal.h"
2728#include " iceberg/type.h"
29+ #include " iceberg/util/checked_cast.h"
2830#include " iceberg/util/formatter.h" // IWYU pragma: keep
2931#include " iceberg/util/macros.h"
3032
3133namespace iceberg {
3234
3335namespace {
3436
35- // A null default value is modeled as the absence of a default (matching Java), so it is
36- // not stored.
37+ // Treat null defaults as absent.
3738std::shared_ptr<const Literal> DropNullDefault (std::shared_ptr<const Literal> value) {
3839 if (value != nullptr && value->IsNull ()) {
3940 return nullptr ;
@@ -65,6 +66,55 @@ SchemaField SchemaField::MakeRequired(int32_t field_id, std::string_view name,
6566 return {field_id, name, std::move (type), false , doc};
6667}
6768
69+ SchemaField SchemaField::WithName (std::string_view name) const {
70+ return {field_id_, name, type_, optional_, doc_, initial_default_, write_default_};
71+ }
72+
73+ SchemaField SchemaField::WithType (std::shared_ptr<Type> type) const {
74+ return {field_id_, name_, std::move (type), optional_, doc_,
75+ initial_default_, write_default_};
76+ }
77+
78+ SchemaField SchemaField::WithDoc (std::string_view doc) const {
79+ return {field_id_, name_, type_, optional_, doc, initial_default_, write_default_};
80+ }
81+
82+ SchemaField SchemaField::WithInitialDefault (
83+ std::shared_ptr<const Literal> initial_default) const {
84+ return {field_id_, name_, type_, optional_, doc_, std::move (initial_default),
85+ write_default_};
86+ }
87+
88+ SchemaField SchemaField::WithWriteDefault (
89+ std::shared_ptr<const Literal> write_default) const {
90+ return {field_id_,
91+ name_,
92+ type_,
93+ optional_,
94+ doc_,
95+ initial_default_,
96+ std::move (write_default)};
97+ }
98+
99+ Result<Literal> SchemaField::CastDefaultValue (
100+ const Literal& value, const std::shared_ptr<PrimitiveType>& target_type) {
101+ if (target_type->type_id () == TypeId::kDecimal &&
102+ std::holds_alternative<Decimal>(value.value ())) {
103+ const auto & source_type = internal::checked_cast<const DecimalType&>(*value.type ());
104+ const auto & decimal_type = internal::checked_cast<const DecimalType&>(*target_type);
105+ if (source_type.scale () == decimal_type.scale ()) {
106+ const auto & decimal_value = std::get<Decimal>(value.value ());
107+ if (!decimal_value.FitsInPrecision (decimal_type.precision ())) {
108+ return InvalidArgument (" Cannot cast default value to {}: {}" , *target_type,
109+ value);
110+ }
111+ return Literal::Decimal (decimal_value.value (), decimal_type.precision (),
112+ decimal_type.scale ());
113+ }
114+ }
115+ return value.CastTo (target_type);
116+ }
117+
68118int32_t SchemaField::field_id () const { return field_id_; }
69119
70120std::string_view SchemaField::name () const { return name_; }
@@ -87,8 +137,7 @@ namespace {
87137
88138Status ValidateDefault (const SchemaField& field, const Literal& value,
89139 std::string_view kind) {
90- // A null default is modeled as absence and dropped at construction, so it never reaches
91- // here; only the out-of-range cast sentinels need rejecting.
140+ // Null defaults are dropped at construction.
92141 if (value.IsAboveMax () || value.IsBelowMin ()) {
93142 return InvalidSchema (" Invalid {} value for {}: value is out of range" , kind,
94143 field.name ());
@@ -97,8 +146,7 @@ Status ValidateDefault(const SchemaField& field, const Literal& value,
97146 return InvalidSchema (" Invalid {} value for {}: field has no type" , kind,
98147 field.name ());
99148 }
100- // The spec requires unknown/variant/geometry/geography columns to default to null, so a
101- // non-null default on them is invalid (a null default was already dropped as absence).
149+ // These types can only use null defaults.
102150 switch (field.type ()->type_id ()) {
103151 case TypeId::kUnknown :
104152 case TypeId::kVariant :
@@ -109,17 +157,13 @@ Status ValidateDefault(const SchemaField& field, const Literal& value,
109157 default :
110158 break ;
111159 }
112- // Defaults are otherwise only supported on primitive fields. The spec also permits JSON
113- // single-value defaults for struct/list/map (e.g. an empty struct `{}` whose sub-field
114- // defaults live in field metadata); that matches the current Java model's gap and is
115- // left as a follow-up.
160+ // Nested defaults are not supported yet.
116161 if (!field.type ()->is_primitive ()) {
117162 return InvalidSchema (
118163 " Invalid {} value for {}: default values are only supported for primitive types" ,
119164 kind, field.name ());
120165 }
121- // Defaults are stored verbatim (no implicit cast), so a default whose literal type does
122- // not match the field type is invalid.
166+ // Stored defaults must already match the field type.
123167 if (*value.type () != *field.type ()) {
124168 return InvalidSchema (" {} of field {} has type {} but expected {}" , kind, field.name (),
125169 *value.type (), *field.type ());
0 commit comments