Skip to content

Commit 378150c

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. Part 2 of the v3 column-default-values work (POC #731), built on the schema support merged in #746.
1 parent 93577b3 commit 378150c

9 files changed

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