Coverage Report

Created: 2026-10-31 01:00

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/format/table/equality_delete.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 "format/table/equality_delete.h"
19
20
#include <gen_cpp/ExternalTableSchema_types.h>
21
22
#include "core/assert_cast.h"
23
#include "core/column/column_const.h"
24
#include "core/column/column_nullable.h"
25
#include "core/data_type/data_type_factory.hpp"
26
#include "exprs/create_predicate_function.h"
27
#include "format/column_type_convert.h"
28
#include "util/hash_util.hpp"
29
30
namespace doris {
31
32
DataTypePtr equality_delete_key_type(const schema::external::TField* field,
33
71
                                     const DataTypePtr& delete_key_type) {
34
71
    DORIS_CHECK(delete_key_type->is_nullable());
35
    // A plan from an FE that sends no scan schema, or no type for the field, leaves only the delete
36
    // file's own type, which is how keys were compared before.
37
71
    if (field == nullptr || !field->__isset.type || is_equality_delete_byte_type(delete_key_type)) {
38
0
        return delete_key_type;
39
0
    }
40
71
    const auto primitive_type = thrift_to_type(field->type.type);
41
71
    DORIS_CHECK(!is_complex_type(primitive_type));
42
71
    return DataTypeFactory::instance().create_data_type(
43
71
            primitive_type, true, field->type.__isset.precision ? field->type.precision : 0,
44
71
            field->type.__isset.scale ? field->type.scale : 0,
45
71
            field->type.__isset.len ? field->type.len : -1);
46
71
}
47
48
315
bool is_equality_delete_byte_type(const DataTypePtr& type) {
49
315
    const auto primitive_type = remove_nullable(type)->get_primitive_type();
50
315
    return is_string_type(primitive_type) || is_varbinary(primitive_type);
51
315
}
52
53
Status widen_equality_delete_key(ColumnPtr column, const DataTypePtr& column_type,
54
105
                                 const DataTypePtr& key_type, ColumnPtr* result) {
55
105
    DORIS_CHECK(result != nullptr);
56
105
    DORIS_CHECK(column_type->is_nullable());
57
105
    DORIS_CHECK(key_type->is_nullable());
58
105
    if (column_type->equals(*key_type)) {
59
96
        *result = std::move(column);
60
96
        return Status::OK();
61
96
    }
62
    // Byte keys keep the type they were read in and match by their bytes, and the converters below
63
    // would turn VARBINARY into its text form.
64
9
    if (is_equality_delete_byte_type(column_type) || is_equality_delete_byte_type(key_type))
65
0
            [[unlikely]] {
66
0
        return Status::InternalError("Unexpected type change of equality delete key from {} to {}",
67
0
                                     column_type->get_name(), key_type->get_name());
68
0
    }
69
    // The Parquet and ORC readers widen the data column of a promoted field with the same
70
    // converters, so a widened delete key holds exactly the value of the data key it must match.
71
9
    auto converter =
72
9
            converter::ColumnTypeConverter::get_converter(column_type, key_type, converter::COMMON);
73
9
    if (!converter->support()) {
74
0
        return Status::InternalError(
75
0
                "Not support type change of equality delete key from {} to {}: {}",
76
0
                column_type->get_name(), key_type->get_name(), converter->get_error_msg());
77
0
    }
78
9
    if (converter->is_consistent()) {
79
        // The same column layout, for example DATETIMEV2 of another scale.
80
0
        *result = std::move(column);
81
0
        return Status::OK();
82
0
    }
83
9
    MutableColumnPtr widened = key_type->create_column();
84
    // The converters fill only the nested values; as in OrcReader::_orc_column_to_doris_column, the
85
    // null map is copied before converting.
86
9
    const auto& source_null_map = assert_cast<const ColumnNullable&>(*column).get_null_map_data();
87
9
    assert_cast<ColumnNullable&>(*widened).get_null_map_data().insert(source_null_map.begin(),
88
9
                                                                      source_null_map.end());
89
9
    RETURN_IF_ERROR(converter->convert(column, widened));
90
9
    *result = std::move(widened);
91
9
    return Status::OK();
92
9
}
93
94
namespace {
95
96
// The rows of a byte-key column. A const column repeats its single value, which can be NULL: when the FE does
97
// not send Iceberg scan semantics v2, a key column that the data file lacks is filled from
98
// Const(Nullable(String) NULL), and its rows must match a NULL delete key rather than an empty string.
99
class ByteKeyColumn {
100
public:
101
18
    explicit ByteKeyColumn(const IColumn& column) : _values(&column) {
102
18
        if (const auto* const_column = check_and_get_column<ColumnConst>(_values)) {
103
2
            _values = &const_column->get_data_column();
104
2
            _is_const = true;
105
2
        }
106
18
        if (const auto* nullable = check_and_get_column<ColumnNullable>(_values)) {
107
10
            _null_map = &nullable->get_null_map_data();
108
10
            _values = &nullable->get_nested_column();
109
10
        }
110
18
    }
111
112
37
    bool is_null(size_t row) const {
113
37
        return _null_map != nullptr && (*_null_map)[_is_const ? 0 : row] != 0;
114
37
    }
115
116
24
    StringRef value(size_t row) const { return _values->get_data_at(_is_const ? 0 : row); }
117
118
private:
119
    const IColumn* _values;
120
    const NullMap* _null_map = nullptr;
121
    bool _is_const = false;
122
};
123
124
3
void insert_byte_values(const IColumn& column, HybridSetBase* byte_set) {
125
3
    const ByteKeyColumn keys(column);
126
9
    for (size_t row = 0; row < column.size(); ++row) {
127
6
        if (keys.is_null(row)) {
128
3
            byte_set->insert(static_cast<const void*>(nullptr));
129
3
            continue;
130
3
        }
131
3
        const StringRef value = keys.value(row);
132
3
        byte_set->insert(&value);
133
3
    }
134
3
}
135
136
void find_byte_values(const IColumn& column, const HybridSetBase& byte_set,
137
3
                      IColumn::Filter* matches) {
138
3
    const ByteKeyColumn keys(column);
139
12
    for (size_t row = 0; row < column.size(); ++row) {
140
9
        if (keys.is_null(row)) {
141
3
            (*matches)[row] = byte_set.contain_null();
142
3
            continue;
143
3
        }
144
6
        const StringRef value = keys.value(row);
145
6
        (*matches)[row] = byte_set.find(&value);
146
6
    }
147
3
}
148
149
void update_equality_delete_hashes(const ColumnWithTypeAndName& column_and_type,
150
58
                                   std::vector<uint64_t>* hashes) {
151
58
    if (!is_equality_delete_byte_type(column_and_type.type)) {
152
52
        column_and_type.column->update_hashes_with_value(hashes->data(), nullptr);
153
52
        return;
154
52
    }
155
    // Hash the bytes rather than call update_hashes_with_value: VARBINARY does not implement it, and a
156
    // STRING delete key must land on the same hash as the equal VARBINARY data key.
157
6
    const ByteKeyColumn keys(*column_and_type.column);
158
21
    for (size_t row = 0; row < column_and_type.column->size(); ++row) {
159
15
        if (keys.is_null(row)) {
160
4
            (*hashes)[row] = HashUtil::xxHash64NullWithSeed((*hashes)[row]);
161
4
            continue;
162
4
        }
163
11
        const StringRef value = keys.value(row);
164
11
        (*hashes)[row] = HashUtil::xxHash64WithSeed(value.data, value.size, (*hashes)[row]);
165
11
    }
166
6
}
167
168
bool equality_delete_values_equal(const ColumnWithTypeAndName& data_column, size_t data_row,
169
30
                                  const ColumnWithTypeAndName& delete_column, size_t delete_row) {
170
30
    if (!is_equality_delete_byte_type(data_column.type) ||
171
30
        !is_equality_delete_byte_type(delete_column.type)) {
172
27
        return data_column.column->compare_at(data_row, delete_row, *delete_column.column, -1) == 0;
173
27
    }
174
3
    const ByteKeyColumn data_keys(*data_column.column);
175
3
    const ByteKeyColumn delete_keys(*delete_column.column);
176
3
    if (data_keys.is_null(data_row) || delete_keys.is_null(delete_row)) {
177
1
        return data_keys.is_null(data_row) && delete_keys.is_null(delete_row);
178
1
    }
179
2
    return data_keys.value(data_row) == delete_keys.value(delete_row);
180
3
}
181
182
} // namespace
183
184
std::unique_ptr<EqualityDeleteBase> EqualityDeleteBase::get_delete_impl(
185
61
        const Block* delete_block, const std::vector<int>& delete_col_ids) {
186
61
    DCHECK_EQ(delete_block->columns(), delete_col_ids.size());
187
61
    if (delete_block->columns() == 1) {
188
50
        return std::make_unique<SimpleEqualityDelete>(delete_block, delete_col_ids);
189
50
    } else {
190
11
        return std::make_unique<MultiEqualityDelete>(delete_block, delete_col_ids);
191
11
    }
192
61
}
193
194
50
Status SimpleEqualityDelete::_build_set() {
195
50
    COUNTER_UPDATE(num_delete_rows, _delete_block->rows());
196
50
    if (_delete_block->columns() != 1) [[unlikely]] {
197
0
        return Status::InternalError("Simple equality delete can be only applied with one column");
198
0
    }
199
50
    const auto& column_and_type = _delete_block->get_by_position(0);
200
50
    auto delete_column_type = remove_nullable(column_and_type.type)->get_primitive_type();
201
50
    size_t non_null_rows = _delete_block->rows();
202
50
    if (const auto* nullable = check_and_get_column<ColumnNullable>(column_and_type.column.get());
203
50
        nullable != nullptr) {
204
50
        non_null_rows = std::ranges::count(nullable->get_null_map_data(), UInt8(0));
205
50
    }
206
50
    if (is_equality_delete_byte_type(column_and_type.type)) {
207
        // create_set has no VARBINARY set (it throws NOT_IMPLEMENTED), so every byte key goes into a
208
        // STRING set as bytes, whichever of STRING and VARBINARY the delete file was read as.
209
3
        _hybrid_set.reset(create_set(TYPE_STRING, non_null_rows, true));
210
3
        insert_byte_values(*column_and_type.column, _hybrid_set.get());
211
47
    } else {
212
47
        _hybrid_set.reset(create_set(delete_column_type, non_null_rows, true));
213
47
        _hybrid_set->insert_fixed_len(column_and_type.column, 0);
214
47
    }
215
50
    return Status::OK();
216
50
}
217
218
Status SimpleEqualityDelete::filter_data_block(
219
        Block* data_block, const std::unordered_map<std::string, uint32_t>* col_name_to_block_idx,
220
        const std::unordered_map<int, std::string>& id_to_block_column_name,
221
50
        IColumn::Filter& filter) {
222
50
    SCOPED_TIMER(equality_delete_time);
223
50
    DCHECK(_delete_col_ids.size() == 1);
224
50
    auto column_field_id = _delete_col_ids[0];
225
226
50
    const auto& block_col_name = id_to_block_column_name.at(column_field_id);
227
50
    auto block_idx = col_name_to_block_idx->at(block_col_name);
228
229
50
    auto column_and_type = data_block->get_by_position(block_idx);
230
50
    const bool delete_is_byte =
231
50
            is_equality_delete_byte_type(_delete_block->get_by_position(0).type);
232
50
    const bool byte_compatible =
233
50
            delete_is_byte && is_equality_delete_byte_type(column_and_type.type);
234
    // The byte set can only be probed with bytes: an INT data column would be read as StringRef values.
235
50
    if (delete_is_byte && !byte_compatible) [[unlikely]] {
236
0
        return Status::InternalError(
237
0
                "Not support type change in column '{}', src type: {}, target type: {}",
238
0
                column_and_type.name, _delete_block->get_by_position(0).type->get_name(),
239
0
                column_and_type.type->get_name());
240
0
    }
241
242
50
    size_t rows = data_block->rows();
243
    //     _filter: 1 => in _hybrid_set; 0 => not in _hybrid_set
244
50
    if (_single_filter == nullptr) {
245
50
        _single_filter = std::make_unique<IColumn::Filter>(rows, 0);
246
50
    } else {
247
        // reset the array capacity and fill all elements using the 0
248
0
        _single_filter->assign(rows, UInt8(0));
249
0
    }
250
50
    auto column = column_and_type.column->convert_to_full_column_if_const();
251
50
    if (byte_compatible) {
252
3
        find_byte_values(*column, *_hybrid_set, _single_filter.get());
253
47
    } else if (const auto* nullable = check_and_get_column<ColumnNullable>(column.get())) {
254
47
        const NullMap& null_map = nullable->get_null_map_data();
255
47
        _hybrid_set->find_batch_nullable(*remove_nullable(column), rows, null_map, *_single_filter);
256
47
        if (_hybrid_set->contain_null()) {
257
5
            auto* filter_data = _single_filter->data();
258
21
            for (size_t i = 0; i < rows; ++i) {
259
16
                filter_data[i] = filter_data[i] || null_map[i];
260
16
            }
261
5
        }
262
47
    } else {
263
0
        _hybrid_set->find_batch(*column, rows, *_single_filter);
264
0
    }
265
    // should reverse _filter
266
50
    auto* filter_data = filter.data();
267
199
    for (size_t i = 0; i < rows; ++i) {
268
149
        filter_data[i] &= !_single_filter->data()[i];
269
149
    }
270
50
    return Status::OK();
271
50
}
272
273
13
Status MultiEqualityDelete::_build_set() {
274
13
    COUNTER_UPDATE(num_delete_rows, _delete_block->rows());
275
13
    size_t rows = _delete_block->rows();
276
13
    _delete_hashes.clear();
277
13
    _delete_hashes.resize(rows, 0);
278
29
    for (const auto& column : _delete_block->get_columns_with_type_and_name()) {
279
29
        update_equality_delete_hashes(column, &_delete_hashes);
280
29
    }
281
29
    for (size_t i = 0; i < rows; ++i) {
282
16
        _delete_hash_map.insert({_delete_hashes[i], i});
283
16
    }
284
13
    _data_column_index.resize(_delete_block->columns());
285
13
    return Status::OK();
286
13
}
287
288
Status MultiEqualityDelete::filter_data_block(
289
        Block* data_block, const std::unordered_map<std::string, uint32_t>* col_name_to_block_idx,
290
        const std::unordered_map<int, std::string>& id_to_block_column_name,
291
13
        IColumn::Filter& filter) {
292
13
    SCOPED_TIMER(equality_delete_time);
293
13
    DCHECK_EQ(_delete_block->get_columns_with_type_and_name().size(), _delete_col_ids.size());
294
13
    size_t column_index = 0;
295
296
42
    for (size_t idx = 0; idx < _delete_block->get_columns_with_type_and_name().size(); ++idx) {
297
29
        auto delete_col = _delete_block->get_columns_with_type_and_name()[idx];
298
29
        auto delete_col_id = _delete_col_ids[idx];
299
300
29
        DCHECK(id_to_block_column_name.contains(delete_col_id));
301
29
        const auto& block_column_name = id_to_block_column_name.at(delete_col_id);
302
29
        if (!col_name_to_block_idx->contains(block_column_name)) [[unlikely]] {
303
0
            return Status::InternalError("Column '{}' not found in data block: {}",
304
0
                                         block_column_name, data_block->dump_structure());
305
0
        }
306
29
        auto column_and_type =
307
29
                data_block->safe_get_by_position(col_name_to_block_idx->at(block_column_name));
308
        // A STRING delete key may meet a VARBINARY data column for the same Iceberg BINARY field: both are
309
        // hashed and compared as bytes, so only a change to or from a non-byte type is rejected.
310
29
        const bool byte_compatible = is_equality_delete_byte_type(delete_col.type) &&
311
29
                                     is_equality_delete_byte_type(column_and_type.type);
312
29
        if (!delete_col.type->equals(*column_and_type.type) && !byte_compatible) [[unlikely]] {
313
0
            return Status::InternalError(
314
0
                    "Not support type change in column '{}', src type: {}, target type: {}",
315
0
                    block_column_name, delete_col.type->get_name(),
316
0
                    column_and_type.type->get_name());
317
0
        }
318
29
        _data_column_index[column_index++] = col_name_to_block_idx->at(block_column_name);
319
29
    }
320
13
    size_t rows = data_block->rows();
321
13
    _data_hashes.clear();
322
13
    _data_hashes.resize(rows, 0);
323
29
    for (size_t index : _data_column_index) {
324
29
        update_equality_delete_hashes(data_block->get_by_position(index), &_data_hashes);
325
29
    }
326
13
    auto* filter_data = filter.data();
327
47
    for (size_t i = 0; i < rows; ++i) {
328
34
        for (auto beg = _delete_hash_map.lower_bound(_data_hashes[i]),
329
34
                  end = _delete_hash_map.upper_bound(_data_hashes[i]);
330
34
             beg != end; ++beg) {
331
14
            if (filter[i] && _equal(data_block, i, beg->second)) {
332
14
                filter_data[i] = 0;
333
14
                break;
334
14
            }
335
14
        }
336
34
    }
337
338
13
    return Status::OK();
339
13
}
340
341
bool MultiEqualityDelete::_equal(Block* data_block, size_t data_row_index,
342
14
                                 size_t delete_row_index) {
343
44
    for (size_t i = 0; i < _delete_block->columns(); ++i) {
344
30
        if (!equality_delete_values_equal(data_block->get_by_position(_data_column_index[i]),
345
30
                                          data_row_index, _delete_block->get_by_position(i),
346
30
                                          delete_row_index)) {
347
0
            return false;
348
0
        }
349
30
    }
350
14
    return true;
351
14
}
352
353
} // namespace doris