Skip to content

Commit 9dc30a7

Browse files
committed
feat(schema): represent, serialize and validate v3 column default values
First of a multi-part split of column default value support (apache#730) — the schema foundation the read and evolution paths build on. Purely additive; no read/write behavior change on its own. - SchemaField carries `initial-default` / `write-default` (immutable std::shared_ptr<const Literal>) with copy-preserving WithInitialDefault / WithWriteDefault modifiers; getters return optional<reference_wrapper>. - JSON serde reads/writes `initial-default` / `write-default` via the existing single-value serialization. - Schema::Validate rejects default values below format v3 and validates they are non-null primitive literals matching the field type. - Generic schema projection maps a column missing from a data file with an initial-default to FieldProjection::Kind::kDefault. Read-path application (Parquet/Avro) and schema evolution follow in separate PRs. See apache#731 for the full end-to-end proof-of-concept.
1 parent c0c6b01 commit 9dc30a7

9 files changed

Lines changed: 437 additions & 7 deletions

File tree

src/iceberg/json_serde.cc

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
#include <nlohmann/json.hpp>
2828

2929
#include "iceberg/constants.h"
30+
#include "iceberg/expression/json_serde_internal.h"
31+
#include "iceberg/expression/literal.h"
3032
#include "iceberg/json_serde_internal.h"
3133
#include "iceberg/name_mapping.h"
3234
#include "iceberg/partition_field.h"
@@ -298,6 +300,15 @@ nlohmann::json ToJson(const SchemaField& field) {
298300
if (!field.doc().empty()) {
299301
json[kDoc] = field.doc();
300302
}
303+
// Defaults are validated to be primitive literals matching the field type, so
304+
// single-value serialization cannot fail here.
305+
if (field.initial_default().has_value()) {
306+
ICEBERG_ASSIGN_OR_THROW(json[kInitialDefault],
307+
ToJson(field.initial_default()->get()));
308+
}
309+
if (field.write_default().has_value()) {
310+
ICEBERG_ASSIGN_OR_THROW(json[kWriteDefault], ToJson(field.write_default()->get()));
311+
}
301312
return json;
302313
}
303314

@@ -310,7 +321,6 @@ nlohmann::json ToJson(const Type& type) {
310321
nlohmann::json fields_json = nlohmann::json::array();
311322
for (const auto& field : struct_type.fields()) {
312323
fields_json.push_back(ToJson(field));
313-
// TODO(gangwu): add default values
314324
}
315325
json[kFields] = fields_json;
316326
return json;
@@ -552,9 +562,23 @@ Result<std::unique_ptr<SchemaField>> FieldFromJson(const nlohmann::json& json) {
552562
ICEBERG_ASSIGN_OR_RAISE(auto name, GetJsonValue<std::string>(json, kName));
553563
ICEBERG_ASSIGN_OR_RAISE(auto required, GetJsonValue<bool>(json, kRequired));
554564
ICEBERG_ASSIGN_OR_RAISE(auto doc, GetJsonValueOrDefault<std::string>(json, kDoc));
555-
556-
return std::make_unique<SchemaField>(field_id, std::move(name), std::move(type),
557-
!required, doc);
565+
ICEBERG_ASSIGN_OR_RAISE(std::optional<nlohmann::json> initial_default_json,
566+
GetJsonValueOptional<nlohmann::json>(json, kInitialDefault));
567+
ICEBERG_ASSIGN_OR_RAISE(std::optional<nlohmann::json> write_default_json,
568+
GetJsonValueOptional<nlohmann::json>(json, kWriteDefault));
569+
570+
SchemaField field(field_id, std::move(name), std::move(type), !required, doc);
571+
if (initial_default_json.has_value()) {
572+
ICEBERG_ASSIGN_OR_RAISE(Literal literal,
573+
LiteralFromJson(*initial_default_json, field.type().get()));
574+
field = field.WithInitialDefault(std::move(literal));
575+
}
576+
if (write_default_json.has_value()) {
577+
ICEBERG_ASSIGN_OR_RAISE(Literal literal,
578+
LiteralFromJson(*write_default_json, field.type().get()));
579+
field = field.WithWriteDefault(std::move(literal));
580+
}
581+
return std::make_unique<SchemaField>(std::move(field));
558582
}
559583

560584
Result<std::unique_ptr<Schema>> SchemaFromJson(const nlohmann::json& json) {

src/iceberg/schema.cc

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,15 @@ Status Schema::Validate(int32_t format_version) const {
447447
}
448448
}
449449

450-
// TODO(GuoTao.yu): Check default values when they are supported
450+
// Column default values (both initial-default and write-default) require v3+.
451+
if (field.initial_default().has_value() || field.write_default().has_value()) {
452+
if (format_version < TableMetadata::kMinFormatVersionDefaultValues) {
453+
return InvalidSchema(
454+
"Invalid default value for {}: default values are not supported until v{}",
455+
field.name(), TableMetadata::kMinFormatVersionDefaultValues);
456+
}
457+
ICEBERG_RETURN_UNEXPECTED(field.Validate());
458+
}
451459
}
452460

453461
return {};

src/iceberg/schema_field.cc

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@
2121

2222
#include <format>
2323
#include <string_view>
24+
#include <utility>
2425

26+
#include "iceberg/expression/literal.h"
2527
#include "iceberg/type.h"
2628
#include "iceberg/util/formatter.h" // IWYU pragma: keep
29+
#include "iceberg/util/macros.h"
2730

2831
namespace iceberg {
2932

@@ -55,13 +58,86 @@ bool SchemaField::optional() const { return optional_; }
5558

5659
std::string_view SchemaField::doc() const { return doc_; }
5760

61+
std::optional<std::reference_wrapper<const Literal>> SchemaField::initial_default()
62+
const {
63+
if (initial_default_ == nullptr) {
64+
return std::nullopt;
65+
}
66+
return std::cref(*initial_default_);
67+
}
68+
69+
std::optional<std::reference_wrapper<const Literal>> SchemaField::write_default() const {
70+
if (write_default_ == nullptr) {
71+
return std::nullopt;
72+
}
73+
return std::cref(*write_default_);
74+
}
75+
76+
SchemaField SchemaField::WithInitialDefault(Literal initial_default) const {
77+
SchemaField copy = *this;
78+
copy.initial_default_ = std::make_shared<const Literal>(std::move(initial_default));
79+
return copy;
80+
}
81+
82+
SchemaField SchemaField::WithInitialDefault(
83+
std::optional<std::reference_wrapper<const Literal>> initial_default) const {
84+
SchemaField copy = *this;
85+
copy.initial_default_ = initial_default.has_value()
86+
? std::make_shared<const Literal>(initial_default->get())
87+
: nullptr;
88+
return copy;
89+
}
90+
91+
SchemaField SchemaField::WithWriteDefault(Literal write_default) const {
92+
SchemaField copy = *this;
93+
copy.write_default_ = std::make_shared<const Literal>(std::move(write_default));
94+
return copy;
95+
}
96+
97+
SchemaField SchemaField::WithWriteDefault(
98+
std::optional<std::reference_wrapper<const Literal>> write_default) const {
99+
SchemaField copy = *this;
100+
copy.write_default_ = write_default.has_value()
101+
? std::make_shared<const Literal>(write_default->get())
102+
: nullptr;
103+
return copy;
104+
}
105+
106+
namespace {
107+
108+
Status ValidateDefault(const SchemaField& field, const Literal& value,
109+
std::string_view kind) {
110+
if (value.IsNull() || value.IsAboveMax() || value.IsBelowMin()) {
111+
return InvalidSchema("Invalid {} value for {}: must be a non-null value", kind,
112+
field.name());
113+
}
114+
if (field.type() == nullptr || !field.type()->is_primitive()) {
115+
return InvalidSchema("Invalid {} value for {}: {} (must be null)", kind, field.name(),
116+
value);
117+
}
118+
if (*value.type() != *field.type()) {
119+
return InvalidSchema("{} of field {} has type {} but expected {}", kind, field.name(),
120+
*value.type(), *field.type());
121+
}
122+
return {};
123+
}
124+
125+
} // namespace
126+
58127
Status SchemaField::Validate() const {
59128
if (name_.empty()) [[unlikely]] {
60129
return InvalidSchema("SchemaField cannot have empty name");
61130
}
62131
if (type_ == nullptr) [[unlikely]] {
63132
return InvalidSchema("SchemaField cannot have null type");
64133
}
134+
if (initial_default_ != nullptr) {
135+
ICEBERG_RETURN_UNEXPECTED(
136+
ValidateDefault(*this, *initial_default_, "initial-default"));
137+
}
138+
if (write_default_ != nullptr) {
139+
ICEBERG_RETURN_UNEXPECTED(ValidateDefault(*this, *write_default_, "write-default"));
140+
}
65141
return {};
66142
}
67143

@@ -72,9 +148,23 @@ std::string SchemaField::ToString() const {
72148
return result;
73149
}
74150

151+
namespace {
152+
153+
bool DefaultEquals(const std::shared_ptr<const Literal>& lhs,
154+
const std::shared_ptr<const Literal>& rhs) {
155+
if (lhs == nullptr || rhs == nullptr) {
156+
return lhs == rhs;
157+
}
158+
return *lhs == *rhs;
159+
}
160+
161+
} // namespace
162+
75163
bool SchemaField::Equals(const SchemaField& other) const {
76164
return field_id_ == other.field_id_ && name_ == other.name_ && *type_ == *other.type_ &&
77-
optional_ == other.optional_;
165+
optional_ == other.optional_ &&
166+
DefaultEquals(initial_default_, other.initial_default_) &&
167+
DefaultEquals(write_default_, other.write_default_);
78168
}
79169

80170
} // namespace iceberg

src/iceberg/schema_field.h

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
/// type (e.g. a struct).
2525

2626
#include <cstdint>
27+
#include <functional>
2728
#include <memory>
29+
#include <optional>
2830
#include <string>
2931
#include <string_view>
3032

@@ -71,6 +73,22 @@ class ICEBERG_EXPORT SchemaField : public iceberg::util::Formattable {
7173
/// \brief Get the field documentation.
7274
std::string_view doc() const;
7375

76+
/// \brief Get the default value for this field used when reading rows written
77+
/// before the field existed (v3 `initial-default`). Empty if absent.
78+
///
79+
/// The returned reference is a non-owning view into a value owned by this field;
80+
/// it remains valid for the lifetime of this SchemaField.
81+
[[nodiscard]] std::optional<std::reference_wrapper<const Literal>> initial_default()
82+
const;
83+
84+
/// \brief Get the default value for this field used when a writer does not
85+
/// supply a value (v3 `write-default`). Empty if absent.
86+
///
87+
/// The returned reference is a non-owning view into a value owned by this field;
88+
/// it remains valid for the lifetime of this SchemaField.
89+
[[nodiscard]] std::optional<std::reference_wrapper<const Literal>> write_default()
90+
const;
91+
7492
[[nodiscard]] std::string ToString() const override;
7593

7694
Status Validate() const;
@@ -91,6 +109,30 @@ class ICEBERG_EXPORT SchemaField : public iceberg::util::Formattable {
91109
return copy;
92110
}
93111

112+
/// \brief Return a copy of this field with the given `initial-default` value.
113+
///
114+
/// The returned field takes ownership of the value.
115+
[[nodiscard]] SchemaField WithInitialDefault(Literal initial_default) const;
116+
117+
/// \brief Return a copy of this field with the `initial-default` value copied from
118+
/// `initial_default`, or without one if it is empty.
119+
///
120+
/// The referenced value is copied; no reference to it is retained.
121+
[[nodiscard]] SchemaField WithInitialDefault(
122+
std::optional<std::reference_wrapper<const Literal>> initial_default) const;
123+
124+
/// \brief Return a copy of this field with the given `write-default` value.
125+
///
126+
/// The returned field takes ownership of the value.
127+
[[nodiscard]] SchemaField WithWriteDefault(Literal write_default) const;
128+
129+
/// \brief Return a copy of this field with the `write-default` value copied from
130+
/// `write_default`, or without one if it is empty.
131+
///
132+
/// The referenced value is copied; no reference to it is retained.
133+
[[nodiscard]] SchemaField WithWriteDefault(
134+
std::optional<std::reference_wrapper<const Literal>> write_default) const;
135+
94136
private:
95137
/// \brief Compare two fields for equality.
96138
[[nodiscard]] bool Equals(const SchemaField& other) const;
@@ -100,6 +142,11 @@ class ICEBERG_EXPORT SchemaField : public iceberg::util::Formattable {
100142
std::shared_ptr<Type> type_;
101143
bool optional_;
102144
std::string doc_;
145+
// Default values are owned by this field and never mutated after being set; copies
146+
// of the field share the same payload (reference-counted) instead of deep-copying,
147+
// like `type_` above. Sharing is unobservable because the payload is immutable.
148+
std::shared_ptr<const Literal> initial_default_;
149+
std::shared_ptr<const Literal> write_default_;
103150
};
104151

105152
} // namespace iceberg

src/iceberg/schema_util.cc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,14 @@ Result<FieldProjection> ProjectNested(const Type& expected_type, const Type& sou
172172
iter->second.local_index, prune_source));
173173
} else if (MetadataColumns::IsMetadataColumn(field_id)) {
174174
child_projection.kind = FieldProjection::Kind::kMetadata;
175+
} else if (expected_field.initial_default().has_value()) {
176+
// Rows written before the field existed assume its `initial-default` value.
177+
child_projection.kind = FieldProjection::Kind::kDefault;
178+
child_projection.from = expected_field.initial_default()->get();
175179
} else if (expected_field.optional()) {
176180
child_projection.kind = FieldProjection::Kind::kNull;
177181
} else {
178-
// TODO(gangwu): support default value for v3 and constant value
182+
// TODO(gangwu): support constant value
179183
return InvalidSchema("Missing required field: {}", expected_field.ToString());
180184
}
181185
result.children.emplace_back(std::move(child_projection));

0 commit comments

Comments
 (0)