Coverage Report

Created: 2026-07-27 20:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/exec/sink/viceberg_merge_sink.cpp
Line
Count
Source
1
// Licensed to the Apache Software Foundation (ASF) under one
2
// or more contributor license agreements.  See the NOTICE file
3
// distributed with this work for additional information
4
// regarding copyright ownership.  The ASF licenses this file
5
// to you under the Apache License, Version 2.0 (the
6
// "License"); you may not use this file except in compliance
7
// with the License.  You may obtain a copy of the License at
8
//
9
//   http://www.apache.org/licenses/LICENSE-2.0
10
//
11
// Unless required by applicable law or agreed to in writing,
12
// software distributed under the License is distributed on an
13
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14
// KIND, either express or implied.  See the License for the
15
// specific language governing permissions and limitations
16
// under the License.
17
18
#include "exec/sink/viceberg_merge_sink.h"
19
20
#include <fmt/format.h>
21
22
#include "common/consts.h"
23
#include "common/exception.h"
24
#include "common/logging.h"
25
#include "core/block/block.h"
26
#include "core/column/column_nullable.h"
27
#include "core/column/column_string.h"
28
#include "core/column/column_struct.h"
29
#include "core/column/column_vector.h"
30
#include "core/data_type/data_type_nullable.h"
31
#include "core/data_type/data_type_struct.h"
32
#include "exec/sink/sink_common.h"
33
#include "exec/sink/viceberg_delete_sink.h"
34
#include "exec/sink/writer/iceberg/viceberg_table_writer.h"
35
#include "exprs/vexpr_context.h"
36
#include "format/table/iceberg/schema.h"
37
#include "format/table/iceberg/schema_parser.h"
38
#include "runtime/runtime_state.h"
39
#include "util/string_util.h"
40
41
namespace doris {
42
43
namespace {} // namespace
44
45
VIcebergMergeSink::VIcebergMergeSink(const TDataSink& t_sink, const VExprContextSPtrs& output_exprs,
46
                                     std::shared_ptr<Dependency> dep,
47
                                     std::shared_ptr<Dependency> fin_dep)
48
7
        : AsyncResultWriter(output_exprs, dep, fin_dep), _t_sink(t_sink) {
49
7
    DCHECK(_t_sink.__isset.iceberg_merge_sink);
50
7
}
51
52
7
VIcebergMergeSink::~VIcebergMergeSink() = default;
53
54
7
Status VIcebergMergeSink::init_properties(ObjectPool* pool, const RowDescriptor& row_desc) {
55
7
    RETURN_IF_ERROR(_build_inner_sinks());
56
57
7
    _table_writer = std::make_unique<VIcebergTableWriter>(_table_sink, _table_output_expr_ctxs,
58
7
                                                          nullptr, nullptr);
59
7
    _delete_writer = std::make_unique<VIcebergDeleteSink>(_delete_sink, _delete_output_expr_ctxs,
60
7
                                                          nullptr, nullptr);
61
7
    RETURN_IF_ERROR(_table_writer->init_properties(pool, row_desc));
62
7
    RETURN_IF_ERROR(_delete_writer->init_properties(pool));
63
7
    return Status::OK();
64
7
}
65
66
7
Status VIcebergMergeSink::open(RuntimeState* state, RuntimeProfile* profile) {
67
7
    _state = state;
68
69
7
    _written_rows_counter = ADD_COUNTER(profile, "RowsWritten", TUnit::UNIT);
70
7
    _insert_rows_counter = ADD_COUNTER(profile, "InsertRows", TUnit::UNIT);
71
7
    _delete_rows_counter = ADD_COUNTER(profile, "DeleteRows", TUnit::UNIT);
72
7
    _send_data_timer = ADD_TIMER(profile, "SendDataTime");
73
7
    _open_timer = ADD_TIMER(profile, "OpenTime");
74
7
    _close_timer = ADD_TIMER(profile, "CloseTime");
75
76
7
    SCOPED_TIMER(_open_timer);
77
78
7
    RETURN_IF_ERROR(_prepare_output_layout());
79
80
5
    RuntimeProfile* table_profile = profile->create_child("IcebergMergeTableWriter", true, true);
81
5
    RuntimeProfile* delete_profile = profile->create_child("IcebergMergeDeleteWriter", true, true);
82
83
5
    RETURN_IF_ERROR(_table_writer->open(state, table_profile));
84
4
    RETURN_IF_ERROR(_delete_writer->open(state, delete_profile));
85
86
4
    return Status::OK();
87
4
}
88
89
5
Status VIcebergMergeSink::write(RuntimeState* state, Block& block) {
90
5
    SCOPED_TIMER(_send_data_timer);
91
5
    if (block.rows() == 0) {
92
0
        return Status::OK();
93
0
    }
94
95
5
    Block output_block;
96
5
    RETURN_IF_ERROR(_projection_block(block, &output_block));
97
5
    if (output_block.rows() == 0) {
98
0
        return Status::OK();
99
0
    }
100
101
5
    if (_operation_idx < 0 || _row_id_idx < 0) {
102
0
        return Status::InternalError("Iceberg merge sink missing operation/row_id columns");
103
0
    }
104
105
5
    const auto& op_column = output_block.get_by_position(_operation_idx).column;
106
5
    const auto* op_data = remove_nullable(op_column).get();
107
108
5
    IColumn::Filter delete_filter(output_block.rows(), 0);
109
5
    IColumn::Filter insert_filter(output_block.rows(), 0);
110
5
    bool has_delete = false;
111
5
    bool has_insert = false;
112
5
    size_t delete_rows = 0;
113
5
    size_t insert_rows = 0;
114
115
12
    for (size_t i = 0; i < output_block.rows(); ++i) {
116
8
        int8_t op = static_cast<int8_t>(op_data->get_int(i));
117
8
        bool delete_op = is_delete_op(op);
118
8
        bool insert_op = is_insert_op(op);
119
8
        if (!delete_op && !insert_op) {
120
1
            return Status::InternalError("Unknown Iceberg merge operation {}", op);
121
1
        }
122
7
        if (delete_op) {
123
5
            delete_filter[i] = 1;
124
5
            has_delete = true;
125
5
            ++delete_rows;
126
5
        }
127
7
        if (insert_op) {
128
5
            insert_filter[i] = 1;
129
5
            has_insert = true;
130
5
            ++insert_rows;
131
5
        }
132
7
    }
133
134
    // The physical sink hashes matched rows by row_id, so identical targets reach the same sink.
135
    // An exact set retained across blocks therefore enforces MERGE cardinality for the whole query.
136
4
    RETURN_IF_ERROR(_validate_matched_row_ids(output_block, delete_filter.data()));
137
3
    _row_count += output_block.rows();
138
3
    _delete_row_count += delete_rows;
139
3
    _insert_row_count += insert_rows;
140
141
3
    bool skip_io = false;
142
3
#ifdef BE_TEST
143
3
    skip_io = _skip_io;
144
3
#endif
145
146
3
    if (has_delete && !skip_io) {
147
0
        Block delete_block = output_block;
148
0
        std::vector<int> delete_indices {_row_id_idx};
149
0
        delete_block.erase_not_in(delete_indices);
150
0
        Block::filter_block_internal(&delete_block, delete_filter);
151
0
        RETURN_IF_ERROR(_delete_writer->write(state, delete_block));
152
0
    }
153
154
3
    if (has_insert && !skip_io) {
155
0
        if (_data_column_indices.empty()) {
156
0
            return Status::InternalError("Iceberg merge sink has no data columns for insert");
157
0
        }
158
0
        Block insert_block = output_block;
159
0
        insert_block.erase_not_in(_data_column_indices);
160
0
        Block::filter_block_internal(&insert_block, insert_filter);
161
0
        RETURN_IF_ERROR(_table_writer->write_prepared_block(insert_block));
162
0
    }
163
164
3
    if (_written_rows_counter != nullptr) {
165
3
        COUNTER_UPDATE(_written_rows_counter, output_block.rows());
166
3
    }
167
3
    if (_insert_rows_counter != nullptr) {
168
3
        COUNTER_UPDATE(_insert_rows_counter, insert_rows);
169
3
    }
170
3
    if (_delete_rows_counter != nullptr) {
171
3
        COUNTER_UPDATE(_delete_rows_counter, delete_rows);
172
3
    }
173
174
3
    return Status::OK();
175
3
}
176
177
Status VIcebergMergeSink::_validate_matched_row_ids(const Block& block,
178
4
                                                    const uint8_t* delete_filter) {
179
4
    const auto& row_id = block.get_by_position(_row_id_idx);
180
4
    const IColumn* row_id_data = row_id.column.get();
181
4
    const IDataType* row_id_type = row_id.type.get();
182
4
    const auto* nullable_row_id = check_and_get_column<ColumnNullable>(row_id_data);
183
4
    if (nullable_row_id != nullptr) {
184
0
        row_id_data = nullable_row_id->get_nested_column_ptr().get();
185
0
    }
186
4
    if (const auto* nullable_type = check_and_get_data_type<DataTypeNullable>(row_id_type)) {
187
0
        row_id_type = nullable_type->get_nested_type().get();
188
0
    }
189
190
4
    const auto* struct_column = check_and_get_column<ColumnStruct>(row_id_data);
191
4
    const auto* struct_type = check_and_get_data_type<DataTypeStruct>(row_id_type);
192
4
    if (struct_column == nullptr || struct_type == nullptr) {
193
0
        return Status::InternalError("Iceberg merge row_id column is not a struct");
194
0
    }
195
196
4
    int file_path_idx = -1;
197
4
    int row_position_idx = -1;
198
4
    const auto& field_names = struct_type->get_element_names();
199
12
    for (size_t i = 0; i < field_names.size(); ++i) {
200
8
        std::string field_name = doris::to_lower(field_names[i]);
201
8
        if (field_name == "file_path") {
202
4
            file_path_idx = static_cast<int>(i);
203
4
        } else if (field_name == "row_position") {
204
4
            row_position_idx = static_cast<int>(i);
205
4
        }
206
8
    }
207
4
    if (file_path_idx < 0 || row_position_idx < 0) {
208
0
        return Status::InternalError(
209
0
                "Iceberg merge row_id must contain file_path and row_position fields");
210
0
    }
211
212
4
    const auto& file_path_column = struct_column->get_column_ptr(file_path_idx);
213
4
    const auto& row_position_column = struct_column->get_column_ptr(row_position_idx);
214
4
    const auto* nullable_file_path = check_and_get_column<ColumnNullable>(file_path_column.get());
215
4
    const auto* nullable_row_position =
216
4
            check_and_get_column<ColumnNullable>(row_position_column.get());
217
4
    const auto* file_paths =
218
4
            check_and_get_column<ColumnString>(remove_nullable(file_path_column).get());
219
4
    const auto* row_positions = check_and_get_column<ColumnVector<TYPE_BIGINT>>(
220
4
            remove_nullable(row_position_column).get());
221
4
    if (file_paths == nullptr || row_positions == nullptr) {
222
0
        return Status::InternalError("Iceberg merge row_id fields have incorrect types");
223
0
    }
224
225
10
    for (size_t i = 0; i < block.rows(); ++i) {
226
7
        if (delete_filter[i] == 0) {
227
2
            continue;
228
2
        }
229
5
        if ((nullable_row_id != nullptr && nullable_row_id->is_null_at(i)) ||
230
5
            (nullable_file_path != nullptr && nullable_file_path->is_null_at(i)) ||
231
5
            (nullable_row_position != nullptr && nullable_row_position->is_null_at(i))) {
232
0
            return Status::InternalError("Iceberg merge matched row_id cannot be null");
233
0
        }
234
235
5
        int64_t row_position = row_positions->get_element(i);
236
5
        if (row_position < 0) {
237
0
            return Status::InternalError("Invalid row_position {} in Iceberg merge row_id",
238
0
                                         row_position);
239
0
        }
240
5
        std::string exact_row_id = file_paths->get_data_at(i).to_string();
241
5
        exact_row_id.append(reinterpret_cast<const char*>(&row_position), sizeof(row_position));
242
5
        if (!_matched_row_ids.emplace(std::move(exact_row_id)).second) {
243
1
            return Status::InvalidArgument(
244
1
                    "Iceberg MERGE failed because multiple source rows matched the same target "
245
1
                    "row");
246
1
        }
247
5
    }
248
3
    return Status::OK();
249
4
}
250
251
2
Status VIcebergMergeSink::close(Status close_status) {
252
2
    SCOPED_TIMER(_close_timer);
253
254
2
    if (!close_status.ok()) {
255
0
        LOG(WARNING) << fmt::format("VIcebergMergeSink close with error: {}",
256
0
                                    close_status.to_string());
257
0
        if (_table_writer) {
258
0
            static_cast<void>(_table_writer->close(close_status));
259
0
        }
260
0
        if (_delete_writer) {
261
0
            static_cast<void>(_delete_writer->close(close_status));
262
0
        }
263
0
        return close_status;
264
0
    }
265
266
2
    Status table_status = Status::OK();
267
2
    Status delete_status = Status::OK();
268
2
    if (_table_writer) {
269
2
        table_status = _table_writer->close(close_status);
270
2
    }
271
2
    if (_delete_writer) {
272
2
        delete_status = _delete_writer->close(close_status);
273
2
    }
274
275
2
    if (_written_rows_counter != nullptr) {
276
2
        COUNTER_SET(_written_rows_counter, static_cast<int64_t>(_row_count));
277
2
    }
278
2
    if (_insert_rows_counter != nullptr) {
279
2
        COUNTER_SET(_insert_rows_counter, static_cast<int64_t>(_insert_row_count));
280
2
    }
281
2
    if (_delete_rows_counter != nullptr) {
282
2
        COUNTER_SET(_delete_rows_counter, static_cast<int64_t>(_delete_row_count));
283
2
    }
284
285
2
    if (!table_status.ok()) {
286
0
        return table_status;
287
0
    }
288
2
    return delete_status;
289
2
}
290
291
7
Status VIcebergMergeSink::_build_inner_sinks() {
292
7
    if (!_t_sink.__isset.iceberg_merge_sink) {
293
0
        return Status::InternalError("Missing iceberg merge sink config");
294
0
    }
295
296
7
    const auto& merge_sink = _t_sink.iceberg_merge_sink;
297
298
7
    TIcebergTableSink table_sink;
299
7
    if (merge_sink.__isset.db_name) {
300
7
        table_sink.__set_db_name(merge_sink.db_name);
301
7
    }
302
7
    if (merge_sink.__isset.tb_name) {
303
7
        table_sink.__set_tb_name(merge_sink.tb_name);
304
7
    }
305
7
    if (merge_sink.__isset.schema_json) {
306
7
        table_sink.__set_schema_json(merge_sink.schema_json);
307
7
    }
308
7
    if (merge_sink.__isset.partition_specs_json) {
309
0
        table_sink.__set_partition_specs_json(merge_sink.partition_specs_json);
310
0
    }
311
7
    if (merge_sink.__isset.partition_spec_id) {
312
7
        table_sink.__set_partition_spec_id(merge_sink.partition_spec_id);
313
7
    }
314
7
    if (merge_sink.__isset.sort_fields) {
315
0
        table_sink.__set_sort_fields(merge_sink.sort_fields);
316
0
    }
317
7
    if (merge_sink.__isset.file_format) {
318
7
        table_sink.__set_file_format(merge_sink.file_format);
319
7
    }
320
7
    if (merge_sink.__isset.compression_type) {
321
7
        table_sink.__set_compression_type(merge_sink.compression_type);
322
7
    }
323
7
    if (merge_sink.__isset.output_path) {
324
7
        table_sink.__set_output_path(merge_sink.output_path);
325
7
    }
326
7
    if (merge_sink.__isset.original_output_path) {
327
7
        table_sink.__set_original_output_path(merge_sink.original_output_path);
328
7
    }
329
7
    if (merge_sink.__isset.hadoop_config) {
330
0
        table_sink.__set_hadoop_config(merge_sink.hadoop_config);
331
0
    }
332
7
    if (merge_sink.__isset.file_type) {
333
7
        table_sink.__set_file_type(merge_sink.file_type);
334
7
    }
335
7
    if (merge_sink.__isset.broker_addresses) {
336
0
        table_sink.__set_broker_addresses(merge_sink.broker_addresses);
337
0
    }
338
7
    if (merge_sink.__isset.collect_column_stats) {
339
0
        table_sink.__set_collect_column_stats(merge_sink.collect_column_stats);
340
0
    }
341
7
    _table_sink.__set_type(TDataSinkType::ICEBERG_TABLE_SINK);
342
7
    _table_sink.__set_iceberg_table_sink(table_sink);
343
344
7
    TIcebergDeleteSink delete_sink;
345
7
    if (merge_sink.__isset.db_name) {
346
7
        delete_sink.__set_db_name(merge_sink.db_name);
347
7
    }
348
7
    if (merge_sink.__isset.tb_name) {
349
7
        delete_sink.__set_tb_name(merge_sink.tb_name);
350
7
    }
351
7
    if (merge_sink.__isset.delete_type) {
352
7
        delete_sink.__set_delete_type(merge_sink.delete_type);
353
7
    }
354
7
    if (merge_sink.__isset.file_format) {
355
7
        delete_sink.__set_file_format(merge_sink.file_format);
356
7
    }
357
7
    if (merge_sink.__isset.compression_type) {
358
7
        delete_sink.__set_compress_type(merge_sink.compression_type);
359
7
    }
360
7
    if (merge_sink.__isset.output_path) {
361
7
        delete_sink.__set_output_path(merge_sink.output_path);
362
7
    }
363
7
    if (merge_sink.__isset.table_location) {
364
7
        delete_sink.__set_table_location(merge_sink.table_location);
365
7
    }
366
7
    if (merge_sink.__isset.hadoop_config) {
367
0
        delete_sink.__set_hadoop_config(merge_sink.hadoop_config);
368
0
    }
369
7
    if (merge_sink.__isset.file_type) {
370
7
        delete_sink.__set_file_type(merge_sink.file_type);
371
7
    }
372
7
    if (merge_sink.__isset.partition_spec_id_for_delete) {
373
7
        delete_sink.__set_partition_spec_id(merge_sink.partition_spec_id_for_delete);
374
7
    }
375
7
    if (merge_sink.__isset.partition_data_json_for_delete) {
376
0
        delete_sink.__set_partition_data_json(merge_sink.partition_data_json_for_delete);
377
0
    }
378
7
    if (merge_sink.__isset.broker_addresses) {
379
0
        delete_sink.__set_broker_addresses(merge_sink.broker_addresses);
380
0
    }
381
7
    if (merge_sink.__isset.format_version) {
382
0
        delete_sink.__set_format_version(merge_sink.format_version);
383
0
    }
384
7
    if (merge_sink.__isset.rewritable_delete_file_sets) {
385
0
        delete_sink.__set_rewritable_delete_file_sets(merge_sink.rewritable_delete_file_sets);
386
0
    }
387
7
    _delete_sink.__set_type(TDataSinkType::ICEBERG_DELETE_SINK);
388
7
    _delete_sink.__set_iceberg_delete_sink(delete_sink);
389
390
7
    return Status::OK();
391
7
}
392
393
7
Status VIcebergMergeSink::_prepare_output_layout() {
394
7
    if (_vec_output_expr_ctxs.empty()) {
395
0
        return Status::InternalError("Iceberg merge sink has empty output expressions");
396
0
    }
397
398
7
    std::string row_id_name = doris::to_lower(BeConsts::ICEBERG_ROWID_COL);
399
7
    std::string op_name = doris::to_lower(kOperationColumnName);
400
401
7
    _operation_idx = -1;
402
7
    _row_id_idx = -1;
403
33
    for (size_t i = 0; i < _vec_output_expr_ctxs.size(); ++i) {
404
26
        std::string expr_name = doris::to_lower(_vec_output_expr_ctxs[i]->expr_name());
405
26
        if (_operation_idx < 0 && expr_name == op_name) {
406
6
            _operation_idx = static_cast<int>(i);
407
20
        } else if (_row_id_idx < 0 && expr_name == row_id_name) {
408
6
            _row_id_idx = static_cast<int>(i);
409
6
        }
410
26
    }
411
412
7
    if (_operation_idx < 0) {
413
1
        return Status::InternalError("Iceberg merge sink missing operation column");
414
1
    }
415
6
    if (_row_id_idx < 0) {
416
1
        return Status::InternalError("Iceberg merge sink missing row_id column");
417
1
    }
418
419
5
    _data_column_indices.clear();
420
5
    _table_output_expr_ctxs.clear();
421
25
    for (size_t i = 0; i < _vec_output_expr_ctxs.size(); ++i) {
422
20
        if (static_cast<int>(i) == _operation_idx || static_cast<int>(i) == _row_id_idx) {
423
10
            continue;
424
10
        }
425
10
        _data_column_indices.push_back(static_cast<int>(i));
426
10
        _table_output_expr_ctxs.emplace_back(_vec_output_expr_ctxs[i]);
427
10
    }
428
429
5
    return Status::OK();
430
6
}
431
432
} // namespace doris