Skip to content

Commit e524d3e

Browse files
committed
polish design
1 parent 77ed430 commit e524d3e

9 files changed

Lines changed: 268 additions & 300 deletions

File tree

src/iceberg/schema_field.cc

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,19 @@
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

3133
namespace iceberg {
3234

3335
namespace {
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.
3738
std::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+
68118
int32_t SchemaField::field_id() const { return field_id_; }
69119

70120
std::string_view SchemaField::name() const { return name_; }
@@ -87,8 +137,7 @@ namespace {
87137

88138
Status 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());

src/iceberg/schema_field.h

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,8 @@ class ICEBERG_EXPORT SchemaField : public iceberg::util::Formattable {
4646
/// \param[in] type The field type.
4747
/// \param[in] optional Whether values of this field are required or nullable.
4848
/// \param[in] doc Optional documentation string for the field.
49-
/// \param[in] initial_default The v3 `initial-default` value, or null if absent. The
50-
/// field shares ownership of the (immutable) value.
51-
/// \param[in] write_default The v3 `write-default` value, or null if absent. The field
52-
/// shares ownership of the (immutable) value.
49+
/// \param[in] initial_default The v3 `initial-default`, or null if absent.
50+
/// \param[in] write_default The v3 `write-default`, or null if absent.
5351
SchemaField(int32_t field_id, std::string_view name, std::shared_ptr<Type> type,
5452
bool optional, std::string_view doc = {},
5553
std::shared_ptr<const Literal> initial_default = nullptr,
@@ -62,30 +60,47 @@ class ICEBERG_EXPORT SchemaField : public iceberg::util::Formattable {
6260
static SchemaField MakeRequired(int32_t field_id, std::string_view name,
6361
std::shared_ptr<Type> type, std::string_view doc = {});
6462

63+
/// \brief Return a copy with a new name.
64+
SchemaField WithName(std::string_view name) const;
65+
66+
/// \brief Return a copy with a new type.
67+
SchemaField WithType(std::shared_ptr<Type> type) const;
68+
69+
/// \brief Return a copy with new documentation.
70+
SchemaField WithDoc(std::string_view doc) const;
71+
72+
/// \brief Return a copy with a new `initial-default`.
73+
SchemaField WithInitialDefault(std::shared_ptr<const Literal> initial_default) const;
74+
75+
/// \brief Return a copy with a new `write-default`.
76+
SchemaField WithWriteDefault(std::shared_ptr<const Literal> write_default) const;
77+
78+
/// \brief Cast a default literal to a field type.
79+
static Result<Literal> CastDefaultValue(
80+
const Literal& value, const std::shared_ptr<PrimitiveType>& target_type);
81+
6582
/// \brief Get the field ID.
66-
[[nodiscard]] int32_t field_id() const;
83+
int32_t field_id() const;
6784

6885
/// \brief Get the field name.
69-
[[nodiscard]] std::string_view name() const;
86+
std::string_view name() const;
7087

7188
/// \brief Get the field type.
72-
[[nodiscard]] const std::shared_ptr<Type>& type() const;
89+
const std::shared_ptr<Type>& type() const;
7390

7491
/// \brief Get whether the field is optional.
7592
[[nodiscard]] bool optional() const;
7693

7794
/// \brief Get the field documentation.
7895
std::string_view doc() const;
7996

80-
/// \brief Get the owning pointer to the default value for this field used when reading
81-
/// rows written before the field existed (v3 `initial-default`), or null if absent.
97+
/// \brief Get the v3 `initial-default`, or null if absent.
8298
const std::shared_ptr<const Literal>& initial_default() const;
8399

84-
/// \brief Get the owning pointer to the default value for this field used when a writer
85-
/// does not supply a value (v3 `write-default`), or null if absent.
100+
/// \brief Get the v3 `write-default`, or null if absent.
86101
const std::shared_ptr<const Literal>& write_default() const;
87102

88-
[[nodiscard]] std::string ToString() const override;
103+
std::string ToString() const override;
89104

90105
Status Validate() const;
91106

@@ -107,14 +122,13 @@ class ICEBERG_EXPORT SchemaField : public iceberg::util::Formattable {
107122

108123
private:
109124
/// \brief Compare two fields for equality.
110-
[[nodiscard]] bool Equals(const SchemaField& other) const;
125+
bool Equals(const SchemaField& other) const;
111126

112127
int32_t field_id_;
113128
std::string name_;
114129
std::shared_ptr<Type> type_;
115130
bool optional_;
116131
std::string doc_;
117-
// Immutable default values, shared (not deep-copied) across field copies, like `type_`.
118132
std::shared_ptr<const Literal> initial_default_;
119133
std::shared_ptr<const Literal> write_default_;
120134
};

src/iceberg/test/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ iceberg_tests = {
5959
'table_requirements_test.cc',
6060
'table_test.cc',
6161
'table_update_test.cc',
62+
'update_schema_test.cc',
6263
),
6364
},
6465
'logging_test': {

src/iceberg/test/resources/TableMetadataV3Valid.json

Lines changed: 0 additions & 123 deletions
This file was deleted.

0 commit comments

Comments
 (0)