Skip to content

Commit 3046194

Browse files
committed
[feature](iceberg) Support Iceberg V3 default values
Backport #65851 to branch-4.1. Preserve missing Iceberg default columns across TopN row-id fetch batches and keep the branch-4.1 table reader layout.
1 parent 4c2c191 commit 3046194

98 files changed

Lines changed: 16569 additions & 2146 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

be/src/exec/scan/access_path_parser.cpp

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,10 @@ void inherit_schema_metadata(format::ColumnDefinition* column,
9090
// The presence bit is part of the mapping contract: an explicit empty mapping must remain
9191
// authoritative after access-path pruning instead of enabling current-name fallback.
9292
column->has_name_mapping = schema_column->has_name_mapping;
93-
// Initial defaults describe the logical value of fields absent from older files. Nested
94-
// access-path pruning must retain them just like it retains rename metadata.
9593
column->initial_default_value = schema_column->initial_default_value;
9694
column->initial_default_value_is_base64 = schema_column->initial_default_value_is_base64;
95+
column->is_optional = schema_column->is_optional;
96+
column->default_expr = schema_column->default_expr;
9797
}
9898

9999
const format::ColumnDefinition* find_schema_child_by_path(
@@ -152,8 +152,7 @@ int32_t schema_field_id_or(const format::ColumnDefinition* schema_column, int32_
152152

153153
std::string schema_field_name_or(const format::ColumnDefinition* schema_column,
154154
std::string fallback) {
155-
return schema_column == nullptr || schema_column->name.empty() ? std::move(fallback)
156-
: schema_column->name;
155+
return schema_column == nullptr || schema_column->name.empty() ? fallback : schema_column->name;
157156
}
158157

159158
struct AccessPathNode {
@@ -253,7 +252,7 @@ Status build_all_nested_children_from_schema(format::ColumnDefinition* column,
253252
case TYPE_ARRAY: {
254253
const auto& array_type = assert_cast<const DataTypeArray&>(*nested_type);
255254
const auto* element_schema = schema_column != nullptr && !schema_column->children.empty()
256-
? &schema_column->children[0]
255+
? schema_column->children.data()
257256
: nullptr;
258257
auto* child = find_or_add_child(column, schema_field_id_or(element_schema, 0), "element",
259258
array_type.get_nested_type());
@@ -264,7 +263,7 @@ Status build_all_nested_children_from_schema(format::ColumnDefinition* column,
264263
case TYPE_MAP: {
265264
const auto& map_type = assert_cast<const DataTypeMap&>(*nested_type);
266265
const auto* key_schema = schema_column != nullptr && !schema_column->children.empty()
267-
? &schema_column->children[0]
266+
? schema_column->children.data()
268267
: nullptr;
269268
const auto* value_schema = schema_column != nullptr && schema_column->children.size() > 1
270269
? &schema_column->children[1]
@@ -362,15 +361,7 @@ Status build_map_children_from_access_node(format::ColumnDefinition* column,
362361
merge_access_path_node(&key_node, child_node);
363362
continue;
364363
}
365-
if (child_path == "VALUES") {
366-
need_key = true;
367-
key_node.project_all = true;
368-
key_node.children.clear();
369-
need_value = true;
370-
merge_access_path_node(&value_node, child_node);
371-
continue;
372-
}
373-
if (child_path == "*") {
364+
if (child_path == "VALUES" || child_path == "*") {
374365
need_key = true;
375366
key_node.project_all = true;
376367
key_node.children.clear();
@@ -406,7 +397,7 @@ Status build_map_children_from_access_node(format::ColumnDefinition* column,
406397
}
407398

408399
const auto* key_schema = schema_column != nullptr && !schema_column->children.empty()
409-
? &schema_column->children[0]
400+
? schema_column->children.data()
410401
: nullptr;
411402
const auto* value_schema = schema_column != nullptr && schema_column->children.size() > 1
412403
? &schema_column->children[1]
@@ -455,7 +446,7 @@ Status build_nested_children_from_access_node(format::ColumnDefinition* column,
455446
}
456447
const auto& array_type = assert_cast<const DataTypeArray&>(*nested_type);
457448
const auto* element_schema = schema_column != nullptr && !schema_column->children.empty()
458-
? &schema_column->children[0]
449+
? schema_column->children.data()
459450
: nullptr;
460451
auto* child = find_or_add_child(column, schema_field_id_or(element_schema, 0), "element",
461452
array_type.get_nested_type());

be/src/exec/sink/writer/iceberg/viceberg_table_writer.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
#include "exec/sink/writer/iceberg/viceberg_table_writer.h"
1919

20+
#include "common/exception.h"
2021
#include "core/block/block.h"
2122
#include "core/block/column_with_type_and_name.h"
2223
#include "core/block/materialize_block.h"
@@ -127,7 +128,14 @@ VIcebergTableWriter::_to_iceberg_partition_columns() {
127128
id_to_column_idx[_schema->columns()[i].field_id()] = i;
128129
}
129130
for (const auto& partition_field : _partition_spec->fields()) {
130-
int column_idx = id_to_column_idx[partition_field.source_id()];
131+
auto column_idx_it = id_to_column_idx.find(partition_field.source_id());
132+
if (column_idx_it == id_to_column_idx.end()) {
133+
throw Exception(
134+
ErrorCode::INTERNAL_ERROR,
135+
"Iceberg partition field {} references source field {} outside writer schema",
136+
partition_field.field_id(), partition_field.source_id());
137+
}
138+
int column_idx = column_idx_it->second;
131139
std::unique_ptr<PartitionColumnTransform> partition_column_transform =
132140
PartitionColumnTransforms::create(
133141
partition_field, _vec_output_expr_ctxs[column_idx]->root()->data_type());

be/src/exec/sink/writer/iceberg/viceberg_table_writer.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#pragma once
1919

2020
#include <gen_cpp/DataSinks_types.h>
21+
#include <gtest/gtest_prod.h>
2122

2223
#include "common/atomic_shared_ptr.h"
2324
#include "core/block/block.h"
@@ -81,6 +82,8 @@ class VIcebergTableWriter final : public AsyncResultWriter {
8182
std::shared_ptr<IPartitionWriterBase> current_writer() const { return _current_writer.load(); }
8283

8384
private:
85+
FRIEND_TEST(VIcebergTableWriterTest, RejectMissingPartitionSource);
86+
8487
// The currently active partition writer (may be VIcebergPartitionWriter or VIcebergSortWriter).
8588
// Updated during write() to track which writer received the most recent data.
8689
// Wrapped in atomic_shared_ptr because revoke_memory / get_revocable_mem_size run on

be/src/format/arrow/arrow_block_convertor.cpp

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
#include <utility>
4040
#include <vector>
4141

42+
#include "common/cast_set.h"
4243
#include "common/status.h"
4344
#include "core/block/column_with_type_and_name.h"
4445
#include "core/column/column.h"
@@ -84,7 +85,9 @@ int hex_value(char c) {
8485
return -1;
8586
}
8687

87-
Status parse_uuid_to_bytes(StringRef uuid, std::array<uint8_t, 16>* bytes) {
88+
} // namespace
89+
90+
Status parse_iceberg_uuid_to_bytes(StringRef uuid, std::array<uint8_t, 16>* bytes) {
8891
if (uuid.size == 16) {
8992
std::memcpy(bytes->data(), uuid.data, bytes->size());
9093
return Status::OK();
@@ -126,6 +129,8 @@ Status parse_uuid_to_bytes(StringRef uuid, std::array<uint8_t, 16>* bytes) {
126129
return Status::OK();
127130
}
128131

132+
namespace {
133+
129134
Status write_iceberg_uuid_string_column_to_arrow(const IColumn& column, const DataTypePtr& type,
130135
arrow::ArrayBuilder* array_builder, int64_t start,
131136
int64_t end) {
@@ -153,13 +158,15 @@ Status write_iceberg_uuid_string_column_to_arrow(const IColumn& column, const Da
153158
}
154159

155160
const auto& string_column = assert_cast<const ColumnString&>(*data_column);
156-
for (size_t row = start; row < end; ++row) {
161+
const auto begin_row = cast_set<size_t>(start);
162+
const auto end_row = cast_set<size_t>(end);
163+
for (size_t row = begin_row; row < end_row; ++row) {
157164
if (null_map != nullptr && (*null_map)[row]) {
158165
RETURN_IF_ERROR(checkArrowStatus(builder.AppendNull(), column, builder));
159166
continue;
160167
}
161168
std::array<uint8_t, 16> bytes;
162-
RETURN_IF_ERROR(parse_uuid_to_bytes(string_column.get_data_at(row), &bytes));
169+
RETURN_IF_ERROR(parse_iceberg_uuid_to_bytes(string_column.get_data_at(row), &bytes));
163170
RETURN_IF_ERROR(checkArrowStatus(builder.Append(bytes.data()), column, builder));
164171
}
165172
return Status::OK();

be/src/format/arrow/arrow_block_convertor.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@
1919

2020
#include <cctz/time_zone.h>
2121

22+
#include <array>
2223
#include <cstdint>
2324
#include <memory>
2425

2526
#include "common/status.h"
2627
#include "core/block/block.h"
2728
#include "core/column/column.h"
2829
#include "core/data_type/data_type.h"
30+
#include "core/string_ref.h"
2931

3032
// This file will convert Doris Block to/from Arrow's RecordBatch
3133
// Block is used by Doris query engine to exchange data between
@@ -41,6 +43,8 @@ class Schema;
4143

4244
namespace doris {
4345

46+
Status parse_iceberg_uuid_to_bytes(StringRef uuid, std::array<uint8_t, 16>* bytes);
47+
4448
class FromBlockToRecordBatchConverter {
4549
public:
4650
FromBlockToRecordBatchConverter(const Block& block,

be/src/format/orc/vorc_reader.cpp

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
#include "exprs/vin_predicate.h"
8282
#include "exprs/vruntimefilter_wrapper.h"
8383
#include "format/orc/orc_file_reader.h"
84+
#include "format/table/iceberg_default_value.h"
8485
#include "format/table/iceberg_reader.h"
8586
#include "format/table/partition_column_filler.h"
8687
#include "format/table/transactional_hive_common.h"
@@ -559,6 +560,7 @@ Status OrcReader::init_reader(
559560

560561
RETURN_IF_ERROR(_create_file_reader());
561562
RETURN_IF_ERROR(_init_read_columns());
563+
_nested_initial_default_values.clear();
562564
return Status::OK();
563565
}
564566

@@ -2290,7 +2292,8 @@ Status OrcReader::_fill_doris_data_column(const std::string& col_name,
22902292
const auto* orc_struct = dynamic_cast<const orc::StructVectorBatch*>(cvb);
22912293
auto& doris_struct = static_cast<ColumnStruct&>(*data_column);
22922294
std::map<int, int> read_fields;
2293-
std::set<int> missing_fields;
2295+
std::set<int> schema_missing_fields;
2296+
std::set<int> projected_out_fields;
22942297
const auto* doris_struct_type =
22952298
assert_cast<const DataTypeStruct*>(remove_nullable(data_type).get());
22962299

@@ -2305,7 +2308,7 @@ Status OrcReader::_fill_doris_data_column(const std::string& col_name,
23052308
for (int i = 0; i < doris_struct.tuple_size(); ++i) {
23062309
const auto& table_column_name = doris_struct_type->get_name_by_position(i);
23072310
if (!root_node->children_column_exists(table_column_name)) {
2308-
missing_fields.insert(i);
2311+
schema_missing_fields.insert(i);
23092312
continue;
23102313
}
23112314
const auto& file_column_name = root_node->children_file_column_name(table_column_name);
@@ -2321,27 +2324,47 @@ Status OrcReader::_fill_doris_data_column(const std::string& col_name,
23212324
<< "], table_column: " << table_column_name
23222325
<< ", file_column: " << file_column_name_lower;
23232326
} else {
2324-
missing_fields.insert(i);
2325-
VLOG_DEBUG << "[OrcReader] Missing field: doris_field[" << i
2327+
projected_out_fields.insert(i);
2328+
VLOG_DEBUG << "[OrcReader] Projected-out field: doris_field[" << i
23262329
<< "], table_column: " << table_column_name
23272330
<< ", file_column: " << file_column_name_lower
2328-
<< " (not found in ORC file)";
2331+
<< " (not found in projected ORC type)";
23292332
}
23302333
}
23312334

2332-
for (int missing_field : missing_fields) {
2333-
ColumnPtr& doris_field = doris_struct.get_column_ptr(missing_field);
2334-
if (!doris_field->is_nullable()) {
2335-
return Status::InternalError(
2336-
"Child field of '{}' is not nullable, but is missing in orc file",
2337-
col_name);
2338-
}
2335+
// The selected ORC type can omit physical struct children that were not requested. They
2336+
// still exist in the file schema, so they must not be treated as Iceberg schema-evolution
2337+
// misses. Append placeholders only to keep every ColumnStruct child at the same size; the
2338+
// projected-out values are never exposed to the query.
2339+
for (int projected_out_field : projected_out_fields) {
2340+
ColumnPtr& doris_field = doris_struct.get_column_ptr(projected_out_field);
23392341
auto mutable_field = IColumn::mutate(std::move(doris_field));
2340-
reinterpret_cast<ColumnNullable*>(mutable_field.get())
2341-
->insert_many_defaults(num_values);
2342+
mutable_field->insert_many_defaults(num_values);
23422343
doris_field = std::move(mutable_field);
23432344
}
23442345

2346+
for (int missing_field : schema_missing_fields) {
2347+
ColumnPtr& doris_field = doris_struct.get_column_ptr(missing_field);
2348+
const auto& doris_name = doris_struct_type->get_name_by_position(missing_field);
2349+
const auto& doris_type = doris_struct_type->get_element(missing_field);
2350+
const auto* iceberg_field = root_node->get_missing_column_field(doris_name);
2351+
if (iceberg_field != nullptr) {
2352+
RETURN_IF_ERROR(iceberg::append_initial_default(
2353+
*iceberg_field, doris_type, num_values, &_nested_initial_default_values,
2354+
&doris_field));
2355+
} else {
2356+
if (!doris_field->is_nullable()) {
2357+
return Status::InternalError(
2358+
"Child field of '{}' is not nullable, but is missing in orc file",
2359+
col_name);
2360+
}
2361+
auto mutable_field = IColumn::mutate(std::move(doris_field));
2362+
reinterpret_cast<ColumnNullable*>(mutable_field.get())
2363+
->insert_many_defaults(num_values);
2364+
doris_field = std::move(mutable_field);
2365+
}
2366+
}
2367+
23452368
for (auto read_field : read_fields) {
23462369
orc::ColumnVectorBatch* orc_field = orc_struct->fields[read_field.second];
23472370
const orc::Type* orc_type = orc_column_type->getSubtype(read_field.second);
@@ -2353,7 +2376,7 @@ Status OrcReader::_fill_doris_data_column(const std::string& col_name,
23532376
field_name, doris_field, doris_type,
23542377
root_node->get_children_node(
23552378
doris_struct_type->get_name_by_position(read_field.first)),
2356-
orc_type, orc_field, num_values));
2379+
orc_type, orc_field, num_values, orc_struct));
23572380
}
23582381
return Status::OK();
23592382
}
@@ -2368,7 +2391,8 @@ template <bool is_filter>
23682391
Status OrcReader::_orc_column_to_doris_column(
23692392
const std::string& col_name, ColumnPtr& doris_column, const DataTypePtr& data_type,
23702393
std::shared_ptr<TableSchemaChangeHelper::Node> root_node, const orc::Type* orc_column_type,
2371-
const orc::ColumnVectorBatch* cvb, size_t num_values) {
2394+
const orc::ColumnVectorBatch* cvb, size_t num_values,
2395+
const orc::ColumnVectorBatch* parent_cvb) {
23722396
DataTypePtr resolved_type;
23732397
ColumnPtr resolved_column;
23742398
MutableColumnPtr data_column;
@@ -2422,8 +2446,18 @@ Status OrcReader::_orc_column_to_doris_column(
24222446
fill_orc_null_map(nullable_column, cvb, num_values);
24232447
} else {
24242448
if (cvb->hasNulls) {
2425-
return Status::InternalError("Not nullable column {} has null values in orc file",
2426-
col_name);
2449+
if (parent_cvb == nullptr || !parent_cvb->hasNulls) {
2450+
return Status::InternalError(
2451+
"Not nullable column {} has null values in orc file", col_name);
2452+
}
2453+
DORIS_CHECK_GE(parent_cvb->capacity, num_values);
2454+
DORIS_CHECK_GE(cvb->capacity, num_values);
2455+
for (size_t i = 0; i < num_values; ++i) {
2456+
if (!cvb->notNull[i] && parent_cvb->notNull[i]) {
2457+
return Status::InternalError(
2458+
"Not nullable column {} has null values in orc file", col_name);
2459+
}
2460+
}
24272461
}
24282462
data_column = std::move(mutable_resolved_column);
24292463
}

be/src/format/orc/vorc_reader.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,11 @@ class OrcReader : public GenericReader {
195195
Status get_parsed_schema(std::vector<std::string>* col_names,
196196
std::vector<DataTypePtr>* col_types) override;
197197

198+
const orc::Type* get_file_root_type() const {
199+
DORIS_CHECK(_reader != nullptr);
200+
return &_reader->getType();
201+
}
202+
198203
void set_position_delete_rowids(const std::vector<int64_t>* delete_rows) {
199204
_position_delete_ordered_rowids = delete_rows;
200205
}
@@ -369,7 +374,8 @@ class OrcReader : public GenericReader {
369374
const DataTypePtr& data_type,
370375
std::shared_ptr<TableSchemaChangeHelper::Node> root_node,
371376
const orc::Type* orc_column_type,
372-
const orc::ColumnVectorBatch* cvb, size_t num_values);
377+
const orc::ColumnVectorBatch* cvb, size_t num_values,
378+
const orc::ColumnVectorBatch* parent_cvb = nullptr);
373379

374380
template <PrimitiveType PType, typename OrcColumnType>
375381
Status _decode_flat_column(const std::string& col_name, const MutableColumnPtr& data_column,
@@ -772,6 +778,9 @@ class OrcReader : public GenericReader {
772778
// Through this node, you can find the file column based on the table column.
773779
std::shared_ptr<TableSchemaChangeHelper::Node> _table_info_node_ptr =
774780
TableSchemaChangeHelper::ConstNode::get_instance();
781+
// Hold the resolved type with the one-row value so equivalent complex types reconstructed for
782+
// later Blocks reuse the same Iceberg field-ID entry outside the batch conversion path.
783+
std::unordered_map<int32_t, std::pair<DataTypePtr, ColumnPtr>> _nested_initial_default_values;
775784

776785
std::set<uint64_t> _column_ids;
777786
std::set<uint64_t> _filter_column_ids;

0 commit comments

Comments
 (0)