blob: 5ff5a0a1f2815093eaa1b29a6b2ea982f3fc45d3 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#include "exec/sink/viceberg_merge_sink.h"
#include <fmt/format.h>
#include "agent/be_exec_version_manager.h"
#include "common/consts.h"
#include "common/exception.h"
#include "common/logging.h"
#include "core/block/block.h"
#include "core/column/column_nullable.h"
#include "core/column/column_string.h"
#include "core/column/column_struct.h"
#include "core/column/column_vector.h"
#include "core/data_type/data_type_nullable.h"
#include "core/data_type/data_type_struct.h"
#include "exec/sink/sink_common.h"
#include "exec/sink/viceberg_delete_sink.h"
#include "exec/sink/writer/iceberg/viceberg_table_writer.h"
#include "exprs/vexpr_context.h"
#include "format/table/iceberg/schema.h"
#include "format/table/iceberg/schema_parser.h"
#include "runtime/runtime_state.h"
#include "util/string_util.h"
namespace doris {
namespace {} // namespace
VIcebergMergeSink::VIcebergMergeSink(const TDataSink& t_sink, const VExprContextSPtrs& output_exprs,
std::shared_ptr<Dependency> dep,
std::shared_ptr<Dependency> fin_dep)
: AsyncResultWriter(output_exprs, dep, fin_dep), _t_sink(t_sink) {
DCHECK(_t_sink.__isset.iceberg_merge_sink);
}
VIcebergMergeSink::~VIcebergMergeSink() = default;
Status VIcebergMergeSink::init_properties(ObjectPool* pool, const RowDescriptor& row_desc) {
RETURN_IF_ERROR(_build_inner_sinks());
_table_writer = std::make_unique<VIcebergTableWriter>(_table_sink, _table_output_expr_ctxs,
nullptr, nullptr);
_table_writer->defer_file_cleanup_until_outer_close();
_delete_writer = std::make_unique<VIcebergDeleteSink>(_delete_sink, _delete_output_expr_ctxs,
nullptr, nullptr);
_delete_writer->defer_file_cleanup_until_outer_close();
RETURN_IF_ERROR(_table_writer->init_properties(pool, row_desc));
RETURN_IF_ERROR(_delete_writer->init_properties(pool));
return Status::OK();
}
Status VIcebergMergeSink::open(RuntimeState* state, RuntimeProfile* profile) {
_state = state;
_written_rows_counter = ADD_COUNTER(profile, "RowsWritten", TUnit::UNIT);
_insert_rows_counter = ADD_COUNTER(profile, "InsertRows", TUnit::UNIT);
_delete_rows_counter = ADD_COUNTER(profile, "DeleteRows", TUnit::UNIT);
// The query-wide version keeps validation all-or-nothing during a rolling BE upgrade.
_require_merge_cardinality_check =
_require_merge_cardinality_check &&
state->be_exec_version() >= SUPPORT_ICEBERG_MERGE_CARDINALITY_VERSION;
if (_require_merge_cardinality_check) {
_matched_row_id_state_bytes_counter =
ADD_COUNTER(profile, "MatchedRowIdStateBytes", TUnit::BYTES);
}
_send_data_timer = ADD_TIMER(profile, "SendDataTime");
_open_timer = ADD_TIMER(profile, "OpenTime");
_close_timer = ADD_TIMER(profile, "CloseTime");
SCOPED_TIMER(_open_timer);
RETURN_IF_ERROR(_prepare_output_layout());
RuntimeProfile* table_profile = profile->create_child("IcebergMergeTableWriter", true, true);
RuntimeProfile* delete_profile = profile->create_child("IcebergMergeDeleteWriter", true, true);
RETURN_IF_ERROR(_table_writer->open(state, table_profile));
RETURN_IF_ERROR(_delete_writer->open(state, delete_profile));
return Status::OK();
}
Status VIcebergMergeSink::write(RuntimeState* state, Block& block) {
SCOPED_TIMER(_send_data_timer);
if (block.rows() == 0) {
return Status::OK();
}
Block output_block;
RETURN_IF_ERROR(_projection_block(block, &output_block));
if (output_block.rows() == 0) {
return Status::OK();
}
if (_operation_idx < 0 || _row_id_idx < 0) {
return Status::InternalError("Iceberg merge sink missing operation/row_id columns");
}
const auto& op_column = output_block.get_by_position(_operation_idx).column;
const auto* op_data = remove_nullable(op_column).get();
IColumn::Filter delete_filter(output_block.rows(), 0);
IColumn::Filter insert_filter(output_block.rows(), 0);
bool has_delete = false;
bool has_insert = false;
size_t delete_rows = 0;
size_t insert_rows = 0;
for (size_t i = 0; i < output_block.rows(); ++i) {
int8_t op = static_cast<int8_t>(op_data->get_int(i));
bool delete_op = is_delete_op(op);
bool insert_op = is_insert_op(op);
if (!delete_op && !insert_op) {
return Status::InternalError("Unknown Iceberg merge operation {}", op);
}
if (delete_op) {
delete_filter[i] = 1;
has_delete = true;
++delete_rows;
}
if (insert_op) {
insert_filter[i] = 1;
has_insert = true;
++insert_rows;
}
}
if (_require_merge_cardinality_check) {
// The physical sink hashes matched rows by row_id, so exact state retained across blocks
// enforces SQL MERGE cardinality for the whole query without changing UPDATE semantics.
RETURN_IF_ERROR(_validate_matched_row_ids(output_block, delete_filter.data()));
COUNTER_SET(_matched_row_id_state_bytes_counter,
static_cast<int64_t>(_matched_row_id_state_size));
}
_row_count += output_block.rows();
_delete_row_count += delete_rows;
_insert_row_count += insert_rows;
bool skip_io = false;
#ifdef BE_TEST
skip_io = _skip_io;
#endif
if (has_delete && !skip_io) {
Block delete_block = output_block;
std::vector<int> delete_indices {_row_id_idx};
delete_block.erase_not_in(delete_indices);
Block::filter_block_internal(&delete_block, delete_filter);
RETURN_IF_ERROR(_delete_writer->write(state, delete_block));
}
if (has_insert && !skip_io) {
if (_data_column_indices.empty()) {
return Status::InternalError("Iceberg merge sink has no data columns for insert");
}
Block insert_block = output_block;
insert_block.erase_not_in(_data_column_indices);
Block::filter_block_internal(&insert_block, insert_filter);
RETURN_IF_ERROR(_table_writer->write_prepared_block(insert_block));
}
if (_written_rows_counter != nullptr) {
COUNTER_UPDATE(_written_rows_counter, output_block.rows());
}
if (_insert_rows_counter != nullptr) {
COUNTER_UPDATE(_insert_rows_counter, insert_rows);
}
if (_delete_rows_counter != nullptr) {
COUNTER_UPDATE(_delete_rows_counter, delete_rows);
}
return Status::OK();
}
Status VIcebergMergeSink::_validate_matched_row_ids(const Block& block,
const uint8_t* delete_filter) {
const auto& row_id = block.get_by_position(_row_id_idx);
const IColumn* row_id_data = row_id.column.get();
const IDataType* row_id_type = row_id.type.get();
const auto* nullable_row_id = check_and_get_column<ColumnNullable>(row_id_data);
if (nullable_row_id != nullptr) {
row_id_data = nullable_row_id->get_nested_column_ptr().get();
}
if (const auto* nullable_type = check_and_get_data_type<DataTypeNullable>(row_id_type)) {
row_id_type = nullable_type->get_nested_type().get();
}
const auto* struct_column = check_and_get_column<ColumnStruct>(row_id_data);
const auto* struct_type = check_and_get_data_type<DataTypeStruct>(row_id_type);
if (struct_column == nullptr || struct_type == nullptr) {
return Status::InternalError("Iceberg merge row_id column is not a struct");
}
int file_path_idx = -1;
int row_position_idx = -1;
const auto& field_names = struct_type->get_element_names();
for (size_t i = 0; i < field_names.size(); ++i) {
std::string field_name = doris::to_lower(field_names[i]);
if (field_name == "file_path") {
file_path_idx = static_cast<int>(i);
} else if (field_name == "row_position") {
row_position_idx = static_cast<int>(i);
}
}
if (file_path_idx < 0 || row_position_idx < 0) {
return Status::InternalError(
"Iceberg merge row_id must contain file_path and row_position fields");
}
const auto& file_path_column = struct_column->get_column_ptr(file_path_idx);
const auto& row_position_column = struct_column->get_column_ptr(row_position_idx);
const auto* nullable_file_path = check_and_get_column<ColumnNullable>(file_path_column.get());
const auto* nullable_row_position =
check_and_get_column<ColumnNullable>(row_position_column.get());
const auto* file_paths =
check_and_get_column<ColumnString>(remove_nullable(file_path_column).get());
const auto* row_positions = check_and_get_column<ColumnVector<TYPE_BIGINT>>(
remove_nullable(row_position_column).get());
if (file_paths == nullptr || row_positions == nullptr) {
return Status::InternalError("Iceberg merge row_id fields have incorrect types");
}
std::map<roaring::Roaring64Map*, size_t> touched_bitmap_sizes;
for (size_t i = 0; i < block.rows(); ++i) {
if (delete_filter[i] == 0) {
continue;
}
if ((nullable_row_id != nullptr && nullable_row_id->is_null_at(i)) ||
(nullable_file_path != nullptr && nullable_file_path->is_null_at(i)) ||
(nullable_row_position != nullptr && nullable_row_position->is_null_at(i))) {
return Status::InternalError("Iceberg merge matched row_id cannot be null");
}
int64_t row_position = row_positions->get_element(i);
if (row_position < 0) {
return Status::InternalError("Invalid row_position {} in Iceberg merge row_id",
row_position);
}
// Intern each file path once and keep exact positions in a compressed bitmap; retaining a
// full path string per matched row makes MERGE memory grow with path_length * row_count.
auto [file_it, inserted] =
_matched_row_positions.try_emplace(file_paths->get_data_at(i).to_string());
auto* positions = &file_it->second;
auto touched_it = touched_bitmap_sizes.find(positions);
if (touched_it == touched_bitmap_sizes.end()) {
touched_it = touched_bitmap_sizes.emplace(positions, positions->getSizeInBytes()).first;
}
if (inserted) {
_matched_row_id_state_size +=
sizeof(std::pair<const std::string, roaring::Roaring64Map>);
_matched_row_id_state_size += file_it->first.capacity();
_matched_row_id_state_size += touched_it->second;
}
if (!positions->addChecked(static_cast<uint64_t>(row_position))) {
return Status::InvalidArgument(
"Iceberg MERGE failed because multiple source rows matched the same target "
"row");
}
}
// Measure only bitmaps touched by this block; rescanning all retained files on every write
// makes a many-file MERGE quadratic in the number of input blocks.
for (const auto& [positions, previous_size] : touched_bitmap_sizes) {
size_t current_size = positions->getSizeInBytes();
if (current_size >= previous_size) {
_matched_row_id_state_size += current_size - previous_size;
} else {
_matched_row_id_state_size -= previous_size - current_size;
}
}
return Status::OK();
}
Status VIcebergMergeSink::close(Status close_status) {
SCOPED_TIMER(_close_timer);
if (!close_status.ok()) {
LOG(WARNING) << fmt::format("VIcebergMergeSink close with error: {}",
close_status.to_string());
if (_table_writer) {
static_cast<void>(_table_writer->close(close_status));
}
if (_delete_writer) {
static_cast<void>(_delete_writer->close(close_status));
}
return close_status;
}
Status table_status = Status::OK();
Status delete_status = Status::OK();
if (_table_writer) {
table_status = _table_writer->close(close_status);
}
if (_delete_writer) {
delete_status = _delete_writer->close(close_status);
}
if (_written_rows_counter != nullptr) {
COUNTER_SET(_written_rows_counter, static_cast<int64_t>(_row_count));
}
if (_insert_rows_counter != nullptr) {
COUNTER_SET(_insert_rows_counter, static_cast<int64_t>(_insert_row_count));
}
if (_delete_rows_counter != nullptr) {
COUNTER_SET(_delete_rows_counter, static_cast<int64_t>(_delete_row_count));
}
Status result_status = table_status.ok() ? delete_status : table_status;
if (_table_writer) {
_table_writer->finish_deferred_file_cleanup(result_status);
}
if (_delete_writer) {
_delete_writer->finish_deferred_file_cleanup(result_status);
}
return result_status;
}
Status VIcebergMergeSink::_build_inner_sinks() {
if (!_t_sink.__isset.iceberg_merge_sink) {
return Status::InternalError("Missing iceberg merge sink config");
}
const auto& merge_sink = _t_sink.iceberg_merge_sink;
// Missing means an old FE plan, which predates SQL MERGE cardinality validation.
_require_merge_cardinality_check = merge_sink.__isset.require_merge_cardinality_check &&
merge_sink.require_merge_cardinality_check;
TIcebergTableSink table_sink;
if (merge_sink.__isset.db_name) {
table_sink.__set_db_name(merge_sink.db_name);
}
if (merge_sink.__isset.tb_name) {
table_sink.__set_tb_name(merge_sink.tb_name);
}
if (merge_sink.__isset.schema_json) {
table_sink.__set_schema_json(merge_sink.schema_json);
}
if (merge_sink.__isset.partition_specs_json) {
table_sink.__set_partition_specs_json(merge_sink.partition_specs_json);
}
if (merge_sink.__isset.partition_spec_id) {
table_sink.__set_partition_spec_id(merge_sink.partition_spec_id);
}
if (merge_sink.__isset.sort_fields) {
table_sink.__set_sort_fields(merge_sink.sort_fields);
}
if (merge_sink.__isset.file_format) {
table_sink.__set_file_format(merge_sink.file_format);
}
if (merge_sink.__isset.compression_type) {
table_sink.__set_compression_type(merge_sink.compression_type);
}
if (merge_sink.__isset.output_path) {
table_sink.__set_output_path(merge_sink.output_path);
}
if (merge_sink.__isset.original_output_path) {
table_sink.__set_original_output_path(merge_sink.original_output_path);
}
if (merge_sink.__isset.hadoop_config) {
table_sink.__set_hadoop_config(merge_sink.hadoop_config);
}
if (merge_sink.__isset.file_type) {
table_sink.__set_file_type(merge_sink.file_type);
}
if (merge_sink.__isset.broker_addresses) {
table_sink.__set_broker_addresses(merge_sink.broker_addresses);
}
if (merge_sink.__isset.collect_column_stats) {
table_sink.__set_collect_column_stats(merge_sink.collect_column_stats);
}
_table_sink.__set_type(TDataSinkType::ICEBERG_TABLE_SINK);
_table_sink.__set_iceberg_table_sink(table_sink);
TIcebergDeleteSink delete_sink;
if (merge_sink.__isset.db_name) {
delete_sink.__set_db_name(merge_sink.db_name);
}
if (merge_sink.__isset.tb_name) {
delete_sink.__set_tb_name(merge_sink.tb_name);
}
if (merge_sink.__isset.delete_type) {
delete_sink.__set_delete_type(merge_sink.delete_type);
}
if (merge_sink.__isset.file_format) {
delete_sink.__set_file_format(merge_sink.file_format);
}
if (merge_sink.__isset.compression_type) {
delete_sink.__set_compress_type(merge_sink.compression_type);
}
if (merge_sink.__isset.output_path) {
delete_sink.__set_output_path(merge_sink.output_path);
}
if (merge_sink.__isset.table_location) {
delete_sink.__set_table_location(merge_sink.table_location);
}
if (merge_sink.__isset.hadoop_config) {
delete_sink.__set_hadoop_config(merge_sink.hadoop_config);
}
if (merge_sink.__isset.file_type) {
delete_sink.__set_file_type(merge_sink.file_type);
}
if (merge_sink.__isset.partition_spec_id_for_delete) {
delete_sink.__set_partition_spec_id(merge_sink.partition_spec_id_for_delete);
}
if (merge_sink.__isset.partition_data_json_for_delete) {
delete_sink.__set_partition_data_json(merge_sink.partition_data_json_for_delete);
}
if (merge_sink.__isset.broker_addresses) {
delete_sink.__set_broker_addresses(merge_sink.broker_addresses);
}
if (merge_sink.__isset.format_version) {
delete_sink.__set_format_version(merge_sink.format_version);
}
if (merge_sink.__isset.rewritable_delete_file_sets) {
delete_sink.__set_rewritable_delete_file_sets(merge_sink.rewritable_delete_file_sets);
}
_delete_sink.__set_type(TDataSinkType::ICEBERG_DELETE_SINK);
_delete_sink.__set_iceberg_delete_sink(delete_sink);
return Status::OK();
}
Status VIcebergMergeSink::_prepare_output_layout() {
if (_vec_output_expr_ctxs.empty()) {
return Status::InternalError("Iceberg merge sink has empty output expressions");
}
std::string row_id_name = doris::to_lower(BeConsts::ICEBERG_ROWID_COL);
std::string op_name = doris::to_lower(kOperationColumnName);
_operation_idx = -1;
_row_id_idx = -1;
for (size_t i = 0; i < _vec_output_expr_ctxs.size(); ++i) {
std::string expr_name = doris::to_lower(_vec_output_expr_ctxs[i]->expr_name());
if (_operation_idx < 0 && expr_name == op_name) {
_operation_idx = static_cast<int>(i);
} else if (_row_id_idx < 0 && expr_name == row_id_name) {
_row_id_idx = static_cast<int>(i);
}
}
if (_operation_idx < 0) {
return Status::InternalError("Iceberg merge sink missing operation column");
}
if (_row_id_idx < 0) {
return Status::InternalError("Iceberg merge sink missing row_id column");
}
_data_column_indices.clear();
_table_output_expr_ctxs.clear();
for (size_t i = 0; i < _vec_output_expr_ctxs.size(); ++i) {
if (static_cast<int>(i) == _operation_idx || static_cast<int>(i) == _row_id_idx) {
continue;
}
_data_column_indices.push_back(static_cast<int>(i));
_table_output_expr_ctxs.emplace_back(_vec_output_expr_ctxs[i]);
}
return Status::OK();
}
} // namespace doris