Skip to content

Commit 8932b97

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 apache#731), built on the schema support merged in apache#746.
1 parent 93577b3 commit 8932b97

9 files changed

Lines changed: 584 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: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
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(const std::vector<uint8_t>& bytes,
89+
::arrow::MemoryPool* pool) {
90+
ICEBERG_ARROW_ASSIGN_OR_RETURN(std::unique_ptr<::arrow::Buffer> buffer,
91+
::arrow::AllocateBuffer(bytes.size(), pool));
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+
::arrow::MemoryPool* pool) {
100+
if (literal.type() == nullptr) {
101+
return InvalidArgument("Cannot convert a literal without type to an Arrow scalar");
102+
}
103+
104+
if (literal.IsAboveMax() || literal.IsBelowMin()) {
105+
return NotSupported("Cannot convert {} to an Arrow scalar", literal);
106+
}
107+
108+
ICEBERG_ASSIGN_OR_RAISE(std::shared_ptr<::arrow::DataType> arrow_type,
109+
ToArrowType(*literal.type()));
110+
if (literal.IsNull()) {
111+
return ::arrow::MakeNullScalar(std::move(arrow_type));
112+
}
113+
114+
const Literal::Value& value = literal.value();
115+
switch (literal.type()->type_id()) {
116+
case TypeId::kBoolean:
117+
return std::make_shared<::arrow::BooleanScalar>(std::get<bool>(value));
118+
case TypeId::kInt:
119+
return std::make_shared<::arrow::Int32Scalar>(std::get<int32_t>(value));
120+
case TypeId::kLong:
121+
return std::make_shared<::arrow::Int64Scalar>(std::get<int64_t>(value));
122+
case TypeId::kFloat:
123+
return std::make_shared<::arrow::FloatScalar>(std::get<float>(value));
124+
case TypeId::kDouble:
125+
return std::make_shared<::arrow::DoubleScalar>(std::get<double>(value));
126+
case TypeId::kDecimal: {
127+
const auto& decimal = std::get<Decimal>(value);
128+
::arrow::Decimal128 arrow_decimal(
129+
static_cast<int64_t>(decimal.value() >> 64),
130+
static_cast<uint64_t>(decimal.value() & ~uint64_t{0}));
131+
return std::make_shared<::arrow::Decimal128Scalar>(arrow_decimal,
132+
std::move(arrow_type));
133+
}
134+
case TypeId::kDate:
135+
return std::make_shared<::arrow::Date32Scalar>(std::get<int32_t>(value));
136+
case TypeId::kTime:
137+
return std::make_shared<::arrow::Time64Scalar>(std::get<int64_t>(value),
138+
std::move(arrow_type));
139+
case TypeId::kTimestamp:
140+
case TypeId::kTimestampTz:
141+
case TypeId::kTimestampNs:
142+
case TypeId::kTimestampTzNs:
143+
return std::make_shared<::arrow::TimestampScalar>(std::get<int64_t>(value),
144+
std::move(arrow_type));
145+
case TypeId::kString:
146+
return std::make_shared<::arrow::StringScalar>(std::get<std::string>(value));
147+
case TypeId::kBinary: {
148+
ICEBERG_ASSIGN_OR_RAISE(std::shared_ptr<::arrow::Buffer> buffer,
149+
ToArrowBuffer(std::get<std::vector<uint8_t>>(value), pool));
150+
return std::make_shared<::arrow::BinaryScalar>(std::move(buffer));
151+
}
152+
case TypeId::kFixed: {
153+
ICEBERG_ASSIGN_OR_RAISE(std::shared_ptr<::arrow::Buffer> buffer,
154+
ToArrowBuffer(std::get<std::vector<uint8_t>>(value), pool));
155+
return std::make_shared<::arrow::FixedSizeBinaryScalar>(std::move(buffer),
156+
std::move(arrow_type));
157+
}
158+
case TypeId::kUuid: {
159+
const Uuid& uuid = std::get<Uuid>(value);
160+
ICEBERG_ASSIGN_OR_RAISE(
161+
std::shared_ptr<::arrow::Buffer> buffer,
162+
ToArrowBuffer(std::vector<uint8_t>(uuid.bytes().begin(), uuid.bytes().end()),
163+
pool));
164+
return std::make_shared<::arrow::FixedSizeBinaryScalar>(std::move(buffer),
165+
std::move(arrow_type));
166+
}
167+
default:
168+
return NotSupported("Cannot convert {} literal to an Arrow scalar",
169+
*literal.type());
170+
}
171+
}
172+
173+
Result<std::shared_ptr<::arrow::Array>> MakeDefaultArray(
174+
const Literal& literal, const std::shared_ptr<::arrow::DataType>& type,
175+
int64_t num_rows, ::arrow::MemoryPool* pool) {
176+
// An extension type (e.g. `arrow.uuid` for an Iceberg UUID) is backed by a storage
177+
// type, and compute::Cast has no kernel that casts a storage array into an extension
178+
// type. Materialize the array as the storage type and wrap it in the extension type.
179+
if (type->id() == ::arrow::Type::EXTENSION) {
180+
const auto& extension_type =
181+
internal::checked_cast<const ::arrow::ExtensionType&>(*type);
182+
ICEBERG_ASSIGN_OR_RAISE(
183+
std::shared_ptr<::arrow::Array> storage,
184+
MakeDefaultArray(literal, extension_type.storage_type(), num_rows, pool));
185+
return ::arrow::ExtensionType::WrapArray(type, storage);
186+
}
187+
188+
ICEBERG_ASSIGN_OR_RAISE(std::shared_ptr<::arrow::Scalar> scalar,
189+
ToArrowScalar(literal, pool));
190+
ICEBERG_ARROW_ASSIGN_OR_RETURN(std::shared_ptr<::arrow::Array> array,
191+
::arrow::MakeArrayFromScalar(*scalar, num_rows, pool));
192+
if (!array->type()->Equals(*type)) {
193+
ICEBERG_ARROW_ASSIGN_OR_RETURN(::arrow::Datum cast_result,
194+
::arrow::compute::Cast(array, type));
195+
return cast_result.make_array();
196+
}
197+
return array;
198+
}
199+
200+
Status AppendDefaultToBuilder(const Literal& literal, ::arrow::ArrayBuilder* builder) {
201+
// The builder's own memory pool is not exposed, so the small scalar buffer uses the
202+
// default pool.
203+
ICEBERG_ASSIGN_OR_RAISE(std::shared_ptr<::arrow::Scalar> scalar,
204+
ToArrowScalar(literal));
205+
206+
// Target the storage type for an extension builder: ToArrowScalar yields the storage
207+
// scalar (e.g. fixed_size_binary(16) for `arrow.uuid`), and Scalar::CastTo has no
208+
// kernel that targets an extension type. This mirrors MakeDefaultArray's extension
209+
// handling. It is currently unreachable because arrow::MakeBuilder cannot construct a
210+
// builder for an extension type (see the UUID FIXME in avro_data_test.cc), but it is
211+
// the forward-compatible companion to that path.
212+
std::shared_ptr<::arrow::DataType> target_type = builder->type();
213+
if (target_type->id() == ::arrow::Type::EXTENSION) {
214+
target_type = internal::checked_cast<const ::arrow::ExtensionType&>(*target_type)
215+
.storage_type();
216+
}
217+
218+
if (!scalar->type->Equals(*target_type)) {
219+
ICEBERG_ARROW_ASSIGN_OR_RETURN(scalar, scalar->CastTo(target_type));
220+
}
221+
ICEBERG_ARROW_RETURN_NOT_OK(builder->AppendScalar(*scalar));
222+
return {};
223+
}
224+
225+
} // namespace iceberg::arrow
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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. `pool` is
35+
/// used for the backing buffer of binary/fixed/uuid scalars.
36+
Result<std::shared_ptr<::arrow::Scalar>> ToArrowScalar(
37+
const Literal& literal, ::arrow::MemoryPool* pool = ::arrow::default_memory_pool());
38+
39+
/// \brief Create an Arrow array of `num_rows` rows where every row holds the literal
40+
/// value, e.g. to materialize a missing column with a default value.
41+
///
42+
/// The array is cast to `type` when the literal's canonical Arrow type differs.
43+
Result<std::shared_ptr<::arrow::Array>> MakeDefaultArray(
44+
const Literal& literal, const std::shared_ptr<::arrow::DataType>& type,
45+
int64_t num_rows, ::arrow::MemoryPool* pool);
46+
47+
/// \brief Append the literal value once to `builder`, e.g. to materialize a missing
48+
/// field with a default value while building rows.
49+
Status AppendDefaultToBuilder(const Literal& literal, ::arrow::ArrayBuilder* builder);
50+
51+
} // 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)