Skip to content

Commit 8db3067

Browse files
committed
feat(parquet): apply column default values when reading missing fields (2/4)
When a column is present in the read schema but missing from a Parquet data file (written before the column existed), fill it with the column's v3 initial-default instead of null. Adds a shared Arrow materializer (arrow/literal_util) that turns a Literal into an Arrow scalar/array, and a kDefault projection branch in the Parquet schema/data projection paths. The materializer wraps the storage array in the extension type for extension types such as `arrow.uuid` (compute::Cast has no storage->extension kernel). Part 2 of the v3 column-default-values work (POC #731), built on the schema support merged in #746.
1 parent 93577b3 commit 8db3067

9 files changed

Lines changed: 565 additions & 1 deletion

src/iceberg/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ if(ICEBERG_BUILD_BUNDLE)
261261
arrow/arrow_io.cc
262262
arrow/s3/arrow_s3_file_io.cc
263263
arrow/arrow_register.cc
264+
arrow/literal_util.cc
264265
arrow/metadata_column_util.cc
265266
avro/avro_data_util.cc
266267
avro/avro_direct_decoder.cc

src/iceberg/arrow/literal_util.cc

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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 <cstring>
21+
#include <string>
22+
#include <utility>
23+
#include <vector>
24+
25+
#include <arrow/array.h>
26+
#include <arrow/array/builder_base.h>
27+
#include <arrow/array/util.h>
28+
#include <arrow/buffer.h>
29+
#include <arrow/compute/api.h>
30+
#include <arrow/extension_type.h>
31+
#include <arrow/scalar.h>
32+
#include <arrow/type.h>
33+
34+
#include "iceberg/arrow/arrow_status_internal.h"
35+
#include "iceberg/arrow/literal_util_internal.h"
36+
#include "iceberg/type.h"
37+
#include "iceberg/util/checked_cast.h"
38+
#include "iceberg/util/formatter.h" // IWYU pragma: keep
39+
#include "iceberg/util/macros.h"
40+
41+
namespace iceberg::arrow {
42+
43+
namespace {
44+
45+
Result<std::shared_ptr<::arrow::DataType>> ToArrowType(const PrimitiveType& type) {
46+
switch (type.type_id()) {
47+
case TypeId::kBoolean:
48+
return ::arrow::boolean();
49+
case TypeId::kInt:
50+
return ::arrow::int32();
51+
case TypeId::kLong:
52+
return ::arrow::int64();
53+
case TypeId::kFloat:
54+
return ::arrow::float32();
55+
case TypeId::kDouble:
56+
return ::arrow::float64();
57+
case TypeId::kDecimal: {
58+
const auto& decimal_type = internal::checked_cast<const DecimalType&>(type);
59+
return ::arrow::decimal128(decimal_type.precision(), decimal_type.scale());
60+
}
61+
case TypeId::kDate:
62+
return ::arrow::date32();
63+
case TypeId::kTime:
64+
return ::arrow::time64(::arrow::TimeUnit::MICRO);
65+
case TypeId::kTimestamp:
66+
return ::arrow::timestamp(::arrow::TimeUnit::MICRO);
67+
case TypeId::kTimestampTz:
68+
return ::arrow::timestamp(::arrow::TimeUnit::MICRO, "UTC");
69+
case TypeId::kTimestampNs:
70+
return ::arrow::timestamp(::arrow::TimeUnit::NANO);
71+
case TypeId::kTimestampTzNs:
72+
return ::arrow::timestamp(::arrow::TimeUnit::NANO, "UTC");
73+
case TypeId::kString:
74+
return ::arrow::utf8();
75+
case TypeId::kBinary:
76+
return ::arrow::binary();
77+
case TypeId::kFixed: {
78+
const auto& fixed_type = internal::checked_cast<const FixedType&>(type);
79+
return ::arrow::fixed_size_binary(static_cast<int32_t>(fixed_type.length()));
80+
}
81+
case TypeId::kUuid:
82+
return ::arrow::fixed_size_binary(16);
83+
default:
84+
return NotSupported("Cannot convert {} to an Arrow type", type);
85+
}
86+
}
87+
88+
Result<std::shared_ptr<::arrow::Buffer>> ToArrowBuffer(
89+
const std::vector<uint8_t>& bytes) {
90+
ICEBERG_ARROW_ASSIGN_OR_RETURN(std::unique_ptr<::arrow::Buffer> buffer,
91+
::arrow::AllocateBuffer(bytes.size()));
92+
std::memcpy(buffer->mutable_data(), bytes.data(), bytes.size());
93+
return std::shared_ptr<::arrow::Buffer>(std::move(buffer));
94+
}
95+
96+
} // namespace
97+
98+
Result<std::shared_ptr<::arrow::Scalar>> ToArrowScalar(const Literal& literal) {
99+
if (literal.type() == nullptr) {
100+
return InvalidArgument("Cannot convert a literal without type to an Arrow scalar");
101+
}
102+
103+
if (literal.IsAboveMax() || literal.IsBelowMin()) {
104+
return NotSupported("Cannot convert {} to an Arrow scalar", literal);
105+
}
106+
107+
ICEBERG_ASSIGN_OR_RAISE(std::shared_ptr<::arrow::DataType> arrow_type,
108+
ToArrowType(*literal.type()));
109+
if (literal.IsNull()) {
110+
return ::arrow::MakeNullScalar(std::move(arrow_type));
111+
}
112+
113+
const Literal::Value& value = literal.value();
114+
switch (literal.type()->type_id()) {
115+
case TypeId::kBoolean:
116+
return std::make_shared<::arrow::BooleanScalar>(std::get<bool>(value));
117+
case TypeId::kInt:
118+
return std::make_shared<::arrow::Int32Scalar>(std::get<int32_t>(value));
119+
case TypeId::kLong:
120+
return std::make_shared<::arrow::Int64Scalar>(std::get<int64_t>(value));
121+
case TypeId::kFloat:
122+
return std::make_shared<::arrow::FloatScalar>(std::get<float>(value));
123+
case TypeId::kDouble:
124+
return std::make_shared<::arrow::DoubleScalar>(std::get<double>(value));
125+
case TypeId::kDecimal: {
126+
const auto& decimal = std::get<Decimal>(value);
127+
::arrow::Decimal128 arrow_decimal(
128+
static_cast<int64_t>(decimal.value() >> 64),
129+
static_cast<uint64_t>(decimal.value() & ~uint64_t{0}));
130+
return std::make_shared<::arrow::Decimal128Scalar>(arrow_decimal,
131+
std::move(arrow_type));
132+
}
133+
case TypeId::kDate:
134+
return std::make_shared<::arrow::Date32Scalar>(std::get<int32_t>(value));
135+
case TypeId::kTime:
136+
return std::make_shared<::arrow::Time64Scalar>(std::get<int64_t>(value),
137+
std::move(arrow_type));
138+
case TypeId::kTimestamp:
139+
case TypeId::kTimestampTz:
140+
case TypeId::kTimestampNs:
141+
case TypeId::kTimestampTzNs:
142+
return std::make_shared<::arrow::TimestampScalar>(std::get<int64_t>(value),
143+
std::move(arrow_type));
144+
case TypeId::kString:
145+
return std::make_shared<::arrow::StringScalar>(std::get<std::string>(value));
146+
case TypeId::kBinary: {
147+
ICEBERG_ASSIGN_OR_RAISE(std::shared_ptr<::arrow::Buffer> buffer,
148+
ToArrowBuffer(std::get<std::vector<uint8_t>>(value)));
149+
return std::make_shared<::arrow::BinaryScalar>(std::move(buffer));
150+
}
151+
case TypeId::kFixed: {
152+
ICEBERG_ASSIGN_OR_RAISE(std::shared_ptr<::arrow::Buffer> buffer,
153+
ToArrowBuffer(std::get<std::vector<uint8_t>>(value)));
154+
return std::make_shared<::arrow::FixedSizeBinaryScalar>(std::move(buffer),
155+
std::move(arrow_type));
156+
}
157+
case TypeId::kUuid: {
158+
const Uuid& uuid = std::get<Uuid>(value);
159+
ICEBERG_ASSIGN_OR_RAISE(
160+
std::shared_ptr<::arrow::Buffer> buffer,
161+
ToArrowBuffer(std::vector<uint8_t>(uuid.bytes().begin(), uuid.bytes().end())));
162+
return std::make_shared<::arrow::FixedSizeBinaryScalar>(std::move(buffer),
163+
std::move(arrow_type));
164+
}
165+
default:
166+
return NotSupported("Cannot convert {} literal to an Arrow scalar",
167+
*literal.type());
168+
}
169+
}
170+
171+
Result<std::shared_ptr<::arrow::Array>> MakeDefaultArray(
172+
const Literal& literal, const std::shared_ptr<::arrow::DataType>& type,
173+
int64_t num_rows, ::arrow::MemoryPool* pool) {
174+
// An extension type (e.g. `arrow.uuid` for an Iceberg UUID) is backed by a storage
175+
// type, and compute::Cast has no kernel that casts a storage array into an extension
176+
// type. Materialize the array as the storage type and wrap it in the extension type.
177+
if (type->id() == ::arrow::Type::EXTENSION) {
178+
const auto& extension_type =
179+
internal::checked_cast<const ::arrow::ExtensionType&>(*type);
180+
ICEBERG_ASSIGN_OR_RAISE(
181+
std::shared_ptr<::arrow::Array> storage,
182+
MakeDefaultArray(literal, extension_type.storage_type(), num_rows, pool));
183+
return ::arrow::ExtensionType::WrapArray(type, storage);
184+
}
185+
186+
ICEBERG_ASSIGN_OR_RAISE(std::shared_ptr<::arrow::Scalar> scalar,
187+
ToArrowScalar(literal));
188+
ICEBERG_ARROW_ASSIGN_OR_RETURN(std::shared_ptr<::arrow::Array> array,
189+
::arrow::MakeArrayFromScalar(*scalar, num_rows, pool));
190+
if (!array->type()->Equals(*type)) {
191+
ICEBERG_ARROW_ASSIGN_OR_RETURN(::arrow::Datum cast_result,
192+
::arrow::compute::Cast(array, type));
193+
return cast_result.make_array();
194+
}
195+
return array;
196+
}
197+
198+
Status AppendDefaultToBuilder(const Literal& literal, ::arrow::ArrayBuilder* builder) {
199+
ICEBERG_ASSIGN_OR_RAISE(std::shared_ptr<::arrow::Scalar> scalar,
200+
ToArrowScalar(literal));
201+
if (!scalar->type->Equals(*builder->type())) {
202+
ICEBERG_ARROW_ASSIGN_OR_RETURN(scalar, scalar->CastTo(builder->type()));
203+
}
204+
ICEBERG_ARROW_RETURN_NOT_OK(builder->AppendScalar(*scalar));
205+
return {};
206+
}
207+
208+
} // 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/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/parquet/parquet_schema_util.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,10 @@ Result<FieldProjection> ProjectStruct(
341341
child_projection, ProjectField(field, parquet_field, iter->second.local_index));
342342
} else if (MetadataColumns::IsMetadataColumn(field_id)) {
343343
child_projection.kind = FieldProjection::Kind::kMetadata;
344+
} else if (field.initial_default() != nullptr) {
345+
// Rows written before the field existed assume its `initial-default` value.
346+
child_projection.kind = FieldProjection::Kind::kDefault;
347+
child_projection.from = *field.initial_default();
344348
} else if (field.optional()) {
345349
child_projection.kind = FieldProjection::Kind::kNull;
346350
} else {

src/iceberg/test/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,8 @@ if(ICEBERG_BUILD_BUNDLE)
253253
data_writer_test.cc
254254
delete_filter_test.cc
255255
delete_loader_test.cc
256-
file_scan_task_reader_test.cc)
256+
file_scan_task_reader_test.cc
257+
literal_util_test.cc)
257258

258259
endif()
259260

0 commit comments

Comments
 (0)