Skip to content

Commit bb5f2d0

Browse files
committed
feat: support column default values (initial-default / write-default)
Implements Iceberg v3 column default values (#730, item 2 of #637): - Schema model: SchemaField carries optional `initial-default` and `write-default` literals, with validation that defaults are primitive and match the field type. - JSON serde: parse and write the two fields using single-value serialization (resolves the TODO in struct field serialization). - Read path: Project() maps a missing column with an initial-default to FieldProjection::Kind::kDefault carrying the literal (for both required and optional columns, per spec), and the Parquet and Avro readers materialize it as a constant column via a new Literal-to-Arrow helper. This resolves the default-value TODO in schema projection. - Schema evolution: Add*Column accept an optional default value (used as both initial-default and write-default); a required column with a default no longer needs AllowIncompatibleChanges(); RequireColumn() accepts columns added with a default in the same update (resolves the defaulted-add TODO); UpdateColumnDefault() updates the write-default of an existing column; doc/rename/type-promotion updates preserve defaults (promotion casts them to the new type). - Format version gating: Schema::Validate() rejects schemas with default values below v3, using the existing kMinFormatVersionDefaultValues. Writers consume complete Arrow arrays, so applying write-default to omitted columns remains the engine's responsibility (as in Java); the library stores, validates, and serializes it.
1 parent c0c6b01 commit bb5f2d0

20 files changed

Lines changed: 1085 additions & 45 deletions

src/iceberg/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ if(ICEBERG_BUILD_BUNDLE)
244244
arrow/arrow_io.cc
245245
arrow/s3/arrow_s3_file_io.cc
246246
arrow/arrow_register.cc
247+
arrow/literal_util.cc
247248
arrow/metadata_column_util.cc
248249
avro/avro_data_util.cc
249250
avro/avro_direct_decoder.cc

src/iceberg/arrow/literal_util.cc

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
#include <string>
21+
#include <utility>
22+
#include <vector>
23+
24+
#include <arrow/array.h>
25+
#include <arrow/array/builder_base.h>
26+
#include <arrow/array/util.h>
27+
#include <arrow/buffer.h>
28+
#include <arrow/compute/api.h>
29+
#include <arrow/scalar.h>
30+
#include <arrow/type.h>
31+
32+
#include "iceberg/arrow/arrow_status_internal.h"
33+
#include "iceberg/arrow/literal_util_internal.h"
34+
#include "iceberg/type.h"
35+
#include "iceberg/util/checked_cast.h"
36+
#include "iceberg/util/formatter.h" // IWYU pragma: keep
37+
#include "iceberg/util/macros.h"
38+
39+
namespace iceberg::arrow {
40+
41+
namespace {
42+
43+
Result<std::shared_ptr<::arrow::DataType>> ToArrowType(const PrimitiveType& type) {
44+
switch (type.type_id()) {
45+
case TypeId::kBoolean:
46+
return ::arrow::boolean();
47+
case TypeId::kInt:
48+
return ::arrow::int32();
49+
case TypeId::kLong:
50+
return ::arrow::int64();
51+
case TypeId::kFloat:
52+
return ::arrow::float32();
53+
case TypeId::kDouble:
54+
return ::arrow::float64();
55+
case TypeId::kDecimal: {
56+
const auto& decimal_type = internal::checked_cast<const DecimalType&>(type);
57+
return ::arrow::decimal128(decimal_type.precision(), decimal_type.scale());
58+
}
59+
case TypeId::kDate:
60+
return ::arrow::date32();
61+
case TypeId::kTime:
62+
return ::arrow::time64(::arrow::TimeUnit::MICRO);
63+
case TypeId::kTimestamp:
64+
return ::arrow::timestamp(::arrow::TimeUnit::MICRO);
65+
case TypeId::kTimestampTz:
66+
return ::arrow::timestamp(::arrow::TimeUnit::MICRO, "UTC");
67+
case TypeId::kTimestampNs:
68+
return ::arrow::timestamp(::arrow::TimeUnit::NANO);
69+
case TypeId::kTimestampTzNs:
70+
return ::arrow::timestamp(::arrow::TimeUnit::NANO, "UTC");
71+
case TypeId::kString:
72+
return ::arrow::utf8();
73+
case TypeId::kBinary:
74+
return ::arrow::binary();
75+
case TypeId::kFixed: {
76+
const auto& fixed_type = internal::checked_cast<const FixedType&>(type);
77+
return ::arrow::fixed_size_binary(static_cast<int32_t>(fixed_type.length()));
78+
}
79+
case TypeId::kUuid:
80+
return ::arrow::fixed_size_binary(16);
81+
default:
82+
return NotSupported("Cannot convert {} to an Arrow type", type);
83+
}
84+
}
85+
86+
Result<std::shared_ptr<::arrow::Buffer>> ToArrowBuffer(
87+
const std::vector<uint8_t>& bytes) {
88+
ICEBERG_ARROW_ASSIGN_OR_RETURN(auto buffer, ::arrow::AllocateBuffer(bytes.size()));
89+
std::memcpy(buffer->mutable_data(), bytes.data(), bytes.size());
90+
return std::shared_ptr<::arrow::Buffer>(std::move(buffer));
91+
}
92+
93+
} // namespace
94+
95+
Result<std::shared_ptr<::arrow::Scalar>> ToArrowScalar(const Literal& literal) {
96+
if (literal.type() == nullptr) {
97+
return InvalidArgument("Cannot convert a literal without type to an Arrow scalar");
98+
}
99+
100+
ICEBERG_ASSIGN_OR_RAISE(auto arrow_type, ToArrowType(*literal.type()));
101+
if (literal.IsNull()) {
102+
return ::arrow::MakeNullScalar(std::move(arrow_type));
103+
}
104+
105+
const auto& value = literal.value();
106+
switch (literal.type()->type_id()) {
107+
case TypeId::kBoolean:
108+
return std::make_shared<::arrow::BooleanScalar>(std::get<bool>(value));
109+
case TypeId::kInt:
110+
return std::make_shared<::arrow::Int32Scalar>(std::get<int32_t>(value));
111+
case TypeId::kLong:
112+
return std::make_shared<::arrow::Int64Scalar>(std::get<int64_t>(value));
113+
case TypeId::kFloat:
114+
return std::make_shared<::arrow::FloatScalar>(std::get<float>(value));
115+
case TypeId::kDouble:
116+
return std::make_shared<::arrow::DoubleScalar>(std::get<double>(value));
117+
case TypeId::kDecimal: {
118+
const auto& decimal = std::get<Decimal>(value);
119+
::arrow::Decimal128 arrow_decimal(
120+
static_cast<int64_t>(decimal.value() >> 64),
121+
static_cast<uint64_t>(decimal.value() & ~uint64_t{0}));
122+
return std::make_shared<::arrow::Decimal128Scalar>(arrow_decimal,
123+
std::move(arrow_type));
124+
}
125+
case TypeId::kDate:
126+
return std::make_shared<::arrow::Date32Scalar>(std::get<int32_t>(value));
127+
case TypeId::kTime:
128+
return std::make_shared<::arrow::Time64Scalar>(std::get<int64_t>(value),
129+
std::move(arrow_type));
130+
case TypeId::kTimestamp:
131+
case TypeId::kTimestampTz:
132+
case TypeId::kTimestampNs:
133+
case TypeId::kTimestampTzNs:
134+
return std::make_shared<::arrow::TimestampScalar>(std::get<int64_t>(value),
135+
std::move(arrow_type));
136+
case TypeId::kString:
137+
return std::make_shared<::arrow::StringScalar>(std::get<std::string>(value));
138+
case TypeId::kBinary: {
139+
ICEBERG_ASSIGN_OR_RAISE(auto buffer,
140+
ToArrowBuffer(std::get<std::vector<uint8_t>>(value)));
141+
return std::make_shared<::arrow::BinaryScalar>(std::move(buffer));
142+
}
143+
case TypeId::kFixed: {
144+
ICEBERG_ASSIGN_OR_RAISE(auto buffer,
145+
ToArrowBuffer(std::get<std::vector<uint8_t>>(value)));
146+
return std::make_shared<::arrow::FixedSizeBinaryScalar>(std::move(buffer),
147+
std::move(arrow_type));
148+
}
149+
case TypeId::kUuid: {
150+
const auto& uuid = std::get<Uuid>(value);
151+
ICEBERG_ASSIGN_OR_RAISE(
152+
auto buffer,
153+
ToArrowBuffer(std::vector<uint8_t>(uuid.bytes().begin(), uuid.bytes().end())));
154+
return std::make_shared<::arrow::FixedSizeBinaryScalar>(std::move(buffer),
155+
std::move(arrow_type));
156+
}
157+
default:
158+
return NotSupported("Cannot convert {} literal to an Arrow scalar",
159+
*literal.type());
160+
}
161+
}
162+
163+
Result<std::shared_ptr<::arrow::Array>> MakeDefaultArray(
164+
const Literal& literal, const std::shared_ptr<::arrow::DataType>& type,
165+
int64_t num_rows, ::arrow::MemoryPool* pool) {
166+
ICEBERG_ASSIGN_OR_RAISE(auto scalar, ToArrowScalar(literal));
167+
ICEBERG_ARROW_ASSIGN_OR_RETURN(auto array,
168+
::arrow::MakeArrayFromScalar(*scalar, num_rows, pool));
169+
if (!array->type()->Equals(*type)) {
170+
ICEBERG_ARROW_ASSIGN_OR_RETURN(auto cast_result, ::arrow::compute::Cast(array, type));
171+
return cast_result.make_array();
172+
}
173+
return array;
174+
}
175+
176+
Status AppendDefaultToBuilder(const Literal& literal, ::arrow::ArrayBuilder* builder) {
177+
ICEBERG_ASSIGN_OR_RAISE(auto scalar, ToArrowScalar(literal));
178+
if (!scalar->type->Equals(*builder->type())) {
179+
ICEBERG_ARROW_ASSIGN_OR_RETURN(scalar, scalar->CastTo(builder->type()));
180+
}
181+
ICEBERG_ARROW_RETURN_NOT_OK(builder->AppendScalar(*scalar));
182+
return {};
183+
}
184+
185+
} // namespace iceberg::arrow
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
#pragma once
21+
22+
#include <cstdint>
23+
#include <memory>
24+
25+
#include <arrow/type_fwd.h>
26+
27+
#include "iceberg/expression/literal.h"
28+
#include "iceberg/result.h"
29+
30+
namespace iceberg::arrow {
31+
32+
/// \brief Convert a primitive literal to an Arrow scalar of its canonical Arrow type.
33+
///
34+
/// A null literal converts to a null scalar of the corresponding Arrow type.
35+
Result<std::shared_ptr<::arrow::Scalar>> ToArrowScalar(const Literal& literal);
36+
37+
/// \brief Create an Arrow array of `num_rows` rows where every row holds the literal
38+
/// value, e.g. to materialize a missing column with a default value.
39+
///
40+
/// The array is cast to `type` when the literal's canonical Arrow type differs.
41+
Result<std::shared_ptr<::arrow::Array>> MakeDefaultArray(
42+
const Literal& literal, const std::shared_ptr<::arrow::DataType>& type,
43+
int64_t num_rows, ::arrow::MemoryPool* pool);
44+
45+
/// \brief Append the literal value once to `builder`, e.g. to materialize a missing
46+
/// field with a default value while building rows.
47+
Status AppendDefaultToBuilder(const Literal& literal, ::arrow::ArrayBuilder* builder);
48+
49+
} // namespace iceberg::arrow

src/iceberg/avro/avro_data_util.cc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include <avro/Types.hh>
3232

3333
#include "iceberg/arrow/arrow_status_internal.h"
34+
#include "iceberg/arrow/literal_util_internal.h"
3435
#include "iceberg/avro/avro_data_util_internal.h"
3536
#include "iceberg/avro/avro_schema_util_internal.h"
3637
#include "iceberg/metadata_columns.h"
@@ -87,6 +88,9 @@ Status AppendStructToBuilder(const ::avro::NodePtr& avro_node,
8788
metadata_context, field_builder));
8889
} else if (field_projection.kind == FieldProjection::Kind::kNull) {
8990
ICEBERG_ARROW_RETURN_NOT_OK(field_builder->AppendNull());
91+
} else if (field_projection.kind == FieldProjection::Kind::kDefault) {
92+
ICEBERG_RETURN_UNEXPECTED(arrow::AppendDefaultToBuilder(
93+
std::get<Literal>(field_projection.from), field_builder));
9094
} else if (field_projection.kind == FieldProjection::Kind::kMetadata) {
9195
int32_t field_id = expected_field.field_id();
9296
if (field_id == MetadataColumns::kFilePathColumnId) {
@@ -462,6 +466,11 @@ Status AppendFieldToBuilder(const ::avro::NodePtr& avro_node,
462466
return {};
463467
}
464468

469+
if (projection.kind == FieldProjection::Kind::kDefault) {
470+
return arrow::AppendDefaultToBuilder(std::get<Literal>(projection.from),
471+
array_builder);
472+
}
473+
465474
if (avro_node->type() == ::avro::AVRO_UNION) {
466475
size_t branch = avro_datum.unionBranch();
467476
if (avro_node->leafAt(branch)->type() == ::avro::AVRO_NULL) {

src/iceberg/avro/avro_schema_util.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,10 @@ Result<FieldProjection> ProjectStruct(const StructType& struct_type,
730730
iter->second.local_index, prune_source));
731731
} else if (MetadataColumns::IsMetadataColumn(field_id)) {
732732
child_projection.kind = FieldProjection::Kind::kMetadata;
733+
} else if (expected_field.initial_default().has_value()) {
734+
// Rows written before the field existed assume its `initial-default` value.
735+
child_projection.kind = FieldProjection::Kind::kDefault;
736+
child_projection.from = expected_field.initial_default()->get();
733737
} else if (expected_field.optional()) {
734738
child_projection.kind = FieldProjection::Kind::kNull;
735739
} else {

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(auto initial_default_json,
566+
GetJsonValueOptional<nlohmann::json>(json, kInitialDefault));
567+
ICEBERG_ASSIGN_OR_RAISE(auto 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(auto 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(auto 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/parquet/parquet_data_util.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include <arrow/type.h>
2525

2626
#include "iceberg/arrow/arrow_status_internal.h"
27+
#include "iceberg/arrow/literal_util_internal.h"
2728
#include "iceberg/metadata_columns.h"
2829
#include "iceberg/parquet/parquet_data_util_internal.h"
2930
#include "iceberg/schema.h"
@@ -119,6 +120,11 @@ Result<std::shared_ptr<::arrow::Array>> ProjectStructArray(
119120
ICEBERG_ASSIGN_OR_RAISE(
120121
projected_array,
121122
MakeNullArray(output_arrow_type, struct_array->length(), pool));
123+
} else if (field_projection.kind == FieldProjection::Kind::kDefault) {
124+
ICEBERG_ASSIGN_OR_RAISE(
125+
projected_array,
126+
arrow::MakeDefaultArray(std::get<Literal>(field_projection.from),
127+
output_arrow_type, struct_array->length(), pool));
122128
} else if (field_projection.kind == FieldProjection::Kind::kMetadata) {
123129
int32_t field_id = projected_field.field_id();
124130
if (field_id == MetadataColumns::kFilePathColumnId) {

src/iceberg/schema.cc

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,18 @@ 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 require v3+.
451+
if (field.initial_default().has_value() &&
452+
format_version < TableMetadata::kMinFormatVersionDefaultValues) {
453+
return InvalidSchema(
454+
"Invalid initial default for {}: non-null default ({}) is not supported "
455+
"until v{}",
456+
field.name(), field.initial_default()->get(),
457+
TableMetadata::kMinFormatVersionDefaultValues);
458+
}
459+
if (field.initial_default().has_value() || field.write_default().has_value()) {
460+
ICEBERG_RETURN_UNEXPECTED(field.Validate());
461+
}
451462
}
452463

453464
return {};

0 commit comments

Comments
 (0)