Skip to content

Commit 2f73f33

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 2f73f33

9 files changed

Lines changed: 562 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: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
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/util.h>
27+
#include <arrow/buffer.h>
28+
#include <arrow/compute/api.h>
29+
#include <arrow/extension_type.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(const std::vector<uint8_t>& bytes,
88+
::arrow::MemoryPool* pool) {
89+
ICEBERG_ARROW_ASSIGN_OR_RETURN(std::unique_ptr<::arrow::Buffer> buffer,
90+
::arrow::AllocateBuffer(bytes.size(), pool));
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+
::arrow::MemoryPool* pool) {
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), pool));
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), pool));
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+
pool));
163+
return std::make_shared<::arrow::FixedSizeBinaryScalar>(std::move(buffer),
164+
std::move(arrow_type));
165+
}
166+
default:
167+
return NotSupported("Cannot convert {} literal to an Arrow scalar",
168+
*literal.type());
169+
}
170+
}
171+
172+
Result<std::shared_ptr<::arrow::Array>> MakeDefaultArray(
173+
const Literal& literal, const std::shared_ptr<::arrow::DataType>& type,
174+
int64_t num_rows, ::arrow::MemoryPool* pool) {
175+
// An extension type (e.g. `arrow.uuid` for an Iceberg UUID) is backed by a storage
176+
// type, and compute::Cast has no kernel that casts a storage array into an extension
177+
// type. Materialize the array as the storage type and wrap it in the extension type.
178+
if (type->id() == ::arrow::Type::EXTENSION) {
179+
const auto& extension_type =
180+
internal::checked_cast<const ::arrow::ExtensionType&>(*type);
181+
ICEBERG_ASSIGN_OR_RAISE(
182+
std::shared_ptr<::arrow::Array> storage,
183+
MakeDefaultArray(literal, extension_type.storage_type(), num_rows, pool));
184+
return ::arrow::ExtensionType::WrapArray(type, storage);
185+
}
186+
187+
ICEBERG_ASSIGN_OR_RAISE(std::shared_ptr<::arrow::Scalar> scalar,
188+
ToArrowScalar(literal, pool));
189+
ICEBERG_ARROW_ASSIGN_OR_RETURN(std::shared_ptr<::arrow::Array> array,
190+
::arrow::MakeArrayFromScalar(*scalar, num_rows, pool));
191+
if (!array->type()->Equals(*type)) {
192+
ICEBERG_ARROW_ASSIGN_OR_RETURN(::arrow::Datum cast_result,
193+
::arrow::compute::Cast(array, type));
194+
return cast_result.make_array();
195+
}
196+
return array;
197+
}
198+
199+
} // namespace iceberg::arrow
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
} // 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)