Coverage Report

Created: 2026-07-27 08:46

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/storage/row_ttl.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 "storage/row_ttl.h"
19
20
#include <utility>
21
22
#include "common/check.h"
23
#include "core/assert_cast.h"
24
#include "core/column/column_nullable.h"
25
#include "core/column/column_vector.h"
26
#include "core/data_type/data_type_factory.hpp"
27
#include "core/data_type/primitive_type.h"
28
#include "core/data_type_serde/data_type_serde.h"
29
#include "core/value/timestamptz_value.h"
30
#include "core/value/vdatetime_value.h"
31
#include "storage/tablet/tablet_schema.h"
32
#include "storage/utils.h"
33
#include "util/timezone_utils.h"
34
35
namespace doris {
36
namespace {
37
38
24
Result<int64_t> checked_multiply(int64_t value, int64_t multiplier) {
39
24
    int64_t result = 0;
40
24
    if (__builtin_mul_overflow(value, multiplier, &result)) {
41
1
        return ResultError(Status::InvalidArgument("row ttl value overflows microseconds"));
42
1
    }
43
23
    return result;
44
24
}
45
46
34
Result<int64_t> checked_add(int64_t left, int64_t right) {
47
34
    int64_t result = 0;
48
34
    if (__builtin_add_overflow(left, right, &result)) {
49
2
        return ResultError(Status::InvalidArgument("row ttl expiration time overflows int64"));
50
2
    }
51
32
    return result;
52
34
}
53
54
template <typename ColumnType, typename ValueType>
55
void extract_epoch_time(const IColumn& source, size_t row, const cctz::time_zone& time_zone,
56
12
                        int64_t* epoch_seconds, int64_t* microsecond) {
57
12
    const auto& value = assert_cast<const ColumnType&>(source).get_data()[row];
58
12
    const auto& date_time = reinterpret_cast<const ValueType&>(value);
59
12
    date_time.unix_timestamp(epoch_seconds, time_zone);
60
12
    *microsecond = date_time.microsecond();
61
12
}
row_ttl.cpp:_ZN5doris12_GLOBAL__N_118extract_epoch_timeINS_12ColumnVectorILNS_13PrimitiveTypeE25EEENS_11DateV2ValueINS_15DateV2ValueTypeEEEEEvRKNS_7IColumnEmRKN4cctz9time_zoneEPlSF_
Line
Count
Source
56
1
                        int64_t* epoch_seconds, int64_t* microsecond) {
57
1
    const auto& value = assert_cast<const ColumnType&>(source).get_data()[row];
58
1
    const auto& date_time = reinterpret_cast<const ValueType&>(value);
59
1
    date_time.unix_timestamp(epoch_seconds, time_zone);
60
1
    *microsecond = date_time.microsecond();
61
1
}
row_ttl.cpp:_ZN5doris12_GLOBAL__N_118extract_epoch_timeINS_12ColumnVectorILNS_13PrimitiveTypeE26EEENS_11DateV2ValueINS_19DateTimeV2ValueTypeEEEEEvRKNS_7IColumnEmRKN4cctz9time_zoneEPlSF_
Line
Count
Source
56
10
                        int64_t* epoch_seconds, int64_t* microsecond) {
57
10
    const auto& value = assert_cast<const ColumnType&>(source).get_data()[row];
58
10
    const auto& date_time = reinterpret_cast<const ValueType&>(value);
59
10
    date_time.unix_timestamp(epoch_seconds, time_zone);
60
10
    *microsecond = date_time.microsecond();
61
10
}
row_ttl.cpp:_ZN5doris12_GLOBAL__N_118extract_epoch_timeINS_12ColumnVectorILNS_13PrimitiveTypeE42EEENS_16TimestampTzValueEEEvRKNS_7IColumnEmRKN4cctz9time_zoneEPlSD_
Line
Count
Source
56
1
                        int64_t* epoch_seconds, int64_t* microsecond) {
57
1
    const auto& value = assert_cast<const ColumnType&>(source).get_data()[row];
58
1
    const auto& date_time = reinterpret_cast<const ValueType&>(value);
59
1
    date_time.unix_timestamp(epoch_seconds, time_zone);
60
1
    *microsecond = date_time.microsecond();
61
1
}
62
63
} // namespace
64
65
11
bool row_ttl_uses_source_time(const TabletSchema& tablet_schema) {
66
11
    DORIS_CHECK(tablet_schema.has_ttl_col());
67
11
    return tablet_schema.column(tablet_schema.ttl_col_idx()).type() !=
68
11
           FieldType::OLAP_FIELD_TYPE_BIGINT;
69
11
}
70
71
Status calculate_row_ttl_expiration_us(const IColumn& source, FieldType source_type, size_t row,
72
                                       const cctz::time_zone& time_zone, int64_t duration_us,
73
15
                                       int64_t* expiration_us) {
74
15
    int64_t epoch_seconds = 0;
75
15
    int64_t microsecond = 0;
76
15
    switch (source_type) {
77
1
    case FieldType::OLAP_FIELD_TYPE_DATE: {
78
1
        const auto& date_time = assert_cast<const ColumnDate&>(source).get_data()[row];
79
1
        date_time.unix_timestamp(&epoch_seconds, time_zone);
80
1
        break;
81
0
    }
82
1
    case FieldType::OLAP_FIELD_TYPE_DATETIME: {
83
1
        const auto& date_time = assert_cast<const ColumnDateTime&>(source).get_data()[row];
84
1
        date_time.unix_timestamp(&epoch_seconds, time_zone);
85
1
        break;
86
0
    }
87
1
    case FieldType::OLAP_FIELD_TYPE_DATEV2:
88
1
        extract_epoch_time<ColumnDateV2, DateV2Value<DateV2ValueType>>(
89
1
                source, row, time_zone, &epoch_seconds, &microsecond);
90
1
        break;
91
10
    case FieldType::OLAP_FIELD_TYPE_DATETIMEV2:
92
10
        extract_epoch_time<ColumnDateTimeV2, DateV2Value<DateTimeV2ValueType>>(
93
10
                source, row, time_zone, &epoch_seconds, &microsecond);
94
10
        break;
95
1
    case FieldType::OLAP_FIELD_TYPE_TIMESTAMPTZ:
96
1
        extract_epoch_time<ColumnTimeStampTz, TimestampTzValue>(source, row, cctz::utc_time_zone(),
97
1
                                                                &epoch_seconds, &microsecond);
98
1
        break;
99
1
    default:
100
1
        return Status::InvalidArgument("row ttl source column must be DATE or DATETIME");
101
15
    }
102
103
14
    auto epoch_base = checked_multiply(epoch_seconds, 1'000'000);
104
14
    if (!epoch_base) {
105
0
        return epoch_base.error();
106
0
    }
107
14
    auto epoch_micros = checked_add(*epoch_base, microsecond);
108
14
    if (!epoch_micros) {
109
0
        return epoch_micros.error();
110
0
    }
111
14
    auto expiration = checked_add(*epoch_micros, duration_us);
112
14
    if (!expiration) {
113
1
        return expiration.error();
114
1
    }
115
13
    *expiration_us = *expiration;
116
13
    return Status::OK();
117
14
}
118
119
Result<std::optional<int64_t>> convert_row_ttl_time_to_epoch_us(
120
        const TabletColumn& source_column, const std::string& source_value,
121
8
        const std::string& timezone) {
122
8
    cctz::time_zone time_zone;
123
8
    if (!TimezoneUtils::find_cctz_time_zone(timezone, time_zone)) {
124
1
        return ResultError(Status::InvalidArgument("invalid time zone for row ttl: {}", timezone));
125
1
    }
126
127
7
    DataTypePtr data_type = DataTypeFactory::instance().create_data_type(source_column);
128
7
    MutableColumnPtr source = data_type->create_column();
129
7
    StringRef value(source_value);
130
7
    Slice slice = value.to_slice();
131
7
    DataTypeSerDe::FormatOptions options;
132
7
    options.converted_from_string = true;
133
7
    options.timezone = &time_zone;
134
7
    Status status = data_type->get_serde()->deserialize_one_cell_from_json(*source, slice, options);
135
7
    if (!status.ok()) {
136
0
        return ResultError(status);
137
0
    }
138
139
7
    const IColumn* source_data = source.get();
140
7
    if (const auto* nullable = check_and_get_column<ColumnNullable>(source_data)) {
141
7
        if (nullable->is_null_at(0)) {
142
1
            return std::optional<int64_t> {};
143
1
        }
144
6
        source_data = &nullable->get_nested_column();
145
6
    }
146
6
    int64_t expiration_us = 0;
147
6
    status = calculate_row_ttl_expiration_us(*source_data, source_column.type(), 0, time_zone, 0,
148
6
                                             &expiration_us);
149
6
    if (!status.ok()) {
150
1
        return ResultError(status);
151
1
    }
152
5
    return std::optional<int64_t> {expiration_us};
153
6
}
154
155
Result<std::optional<int64_t>> calculate_row_ttl_expiration_us(RowTtlOperation operation,
156
                                                               std::optional<int64_t> value,
157
13
                                                               int64_t now_us) {
158
13
    if (operation == RowTtlOperation::PERSIST) {
159
2
        return std::optional<int64_t> {};
160
2
    }
161
11
    if (!value.has_value()) {
162
1
        return ResultError(Status::InvalidArgument("row ttl operation requires a value"));
163
1
    }
164
165
10
    int64_t multiplier = 0;
166
10
    bool relative = false;
167
10
    switch (operation) {
168
2
    case RowTtlOperation::EXPIRE:
169
2
        multiplier = 1'000'000;
170
2
        relative = true;
171
2
        break;
172
4
    case RowTtlOperation::PEXPIRE:
173
4
        multiplier = 1'000;
174
4
        relative = true;
175
4
        break;
176
3
    case RowTtlOperation::EXPIREAT:
177
3
        multiplier = 1'000'000;
178
3
        break;
179
1
    case RowTtlOperation::PEXPIREAT:
180
1
        multiplier = 1'000;
181
1
        break;
182
0
    case RowTtlOperation::PERSIST:
183
0
        __builtin_unreachable();
184
10
    }
185
186
10
    auto converted = checked_multiply(*value, multiplier);
187
10
    if (!converted) {
188
1
        return ResultError(converted.error());
189
1
    }
190
9
    if (!relative) {
191
3
        return std::optional<int64_t> {*converted};
192
3
    }
193
6
    auto expiration = checked_add(now_us, *converted);
194
6
    if (!expiration) {
195
1
        return ResultError(expiration.error());
196
1
    }
197
5
    return std::optional<int64_t> {*expiration};
198
6
}
199
200
Status apply_row_ttl_update(MutableColumnPtr& ttl_column, size_t row, RowTtlOperation operation,
201
5
                            std::optional<int64_t> value, int64_t now_us) {
202
5
    if (row >= ttl_column->size()) {
203
1
        return Status::InvalidArgument("row ttl target row {} is out of range {}", row,
204
1
                                       ttl_column->size());
205
1
    }
206
4
    auto* nullable = typeid_cast<ColumnNullable*>(ttl_column.get());
207
4
    if (nullable == nullptr) {
208
1
        return Status::InvalidArgument("row ttl column must be nullable");
209
1
    }
210
3
    auto* values = typeid_cast<ColumnInt64*>(&nullable->get_nested_column());
211
3
    if (values == nullptr) {
212
1
        return Status::InvalidArgument("row ttl column must have Int64 nested values");
213
1
    }
214
215
2
    auto expiration = calculate_row_ttl_expiration_us(operation, value, now_us);
216
2
    if (!expiration) {
217
0
        return expiration.error();
218
0
    }
219
2
    auto& null_map = nullable->get_null_map_data();
220
2
    if (!expiration->has_value()) {
221
1
        null_map[row] = 1;
222
1
        values->get_data()[row] = 0;
223
1
        return Status::OK();
224
1
    }
225
1
    null_map[row] = 0;
226
1
    values->get_data()[row] = **expiration;
227
1
    return Status::OK();
228
2
}
229
230
Status apply_row_ttl_update(const RowLocation& location, RowTtlOperation operation,
231
                            std::optional<int64_t> value, int64_t now_us,
232
1
                            const RowTtlLocationWriter& writer) {
233
1
    auto expiration = calculate_row_ttl_expiration_us(operation, value, now_us);
234
1
    if (!expiration) {
235
0
        return expiration.error();
236
0
    }
237
1
    return writer(location, *expiration);
238
1
}
239
240
Status build_row_visibility_filter(const Block& block, const TabletSchema& tablet_schema,
241
                                   bool apply_delete_sign, bool apply_row_ttl, int64_t now_us,
242
508
                                   RowVisibilityFilter* filter) {
243
508
    filter->selection.resize_fill(block.rows(), 1);
244
508
    filter->rows_deleted = 0;
245
246
508
    if (apply_delete_sign) {
247
503
        const int delete_sign_position = block.get_position_by_name(DELETE_SIGN);
248
503
        DORIS_CHECK_GE(delete_sign_position, 0);
249
503
        const auto* delete_sign = check_and_get_column<ColumnInt8>(
250
503
                block.get_by_position(delete_sign_position).column.get());
251
503
        DORIS_CHECK(delete_sign != nullptr);
252
503
        const auto& delete_sign_data = delete_sign->get_data();
253
981k
        for (size_t row = 0; row < block.rows(); ++row) {
254
980k
            if (delete_sign_data[row] != 0) {
255
143
                filter->selection[row] = 0;
256
143
                ++filter->rows_deleted;
257
143
            }
258
980k
        }
259
503
    }
260
261
508
    if (!apply_row_ttl) {
262
503
        return Status::OK();
263
503
    }
264
265
5
    const int ttl_position = block.get_position_by_name(TTL_COL);
266
5
    DORIS_CHECK_GE(ttl_position, 0);
267
5
    const auto* nullable =
268
5
            check_and_get_column<ColumnNullable>(block.get_by_position(ttl_position).column.get());
269
5
    DORIS_CHECK(nullable != nullptr);
270
5
    const auto& null_map = nullable->get_null_map_data();
271
5
    const bool source_time = row_ttl_uses_source_time(tablet_schema);
272
5
    const int64_t duration_us = tablet_schema.row_ttl_duration_us();
273
5
    if (source_time && duration_us < 0) {
274
1
        return Status::InvalidArgument("row ttl duration is missing from temporal tablet schema");
275
1
    }
276
4
    const auto* direct_expiration = source_time
277
4
                                            ? nullptr
278
4
                                            : check_and_get_column<ColumnInt64>(
279
3
                                                      &nullable->get_nested_column());
280
4
    DORIS_CHECK(source_time || direct_expiration != nullptr);
281
4
    const cctz::time_zone local_time_zone = cctz::local_time_zone();
282
18
    for (size_t row = 0; row < block.rows(); ++row) {
283
14
        if (!filter->selection[row] || null_map[row]) {
284
5
            continue;
285
5
        }
286
9
        int64_t expiration_us = 0;
287
9
        if (source_time) {
288
2
            RETURN_IF_ERROR(calculate_row_ttl_expiration_us(
289
2
                    nullable->get_nested_column(),
290
2
                    tablet_schema.column(tablet_schema.ttl_col_idx()).type(), row,
291
2
                    local_time_zone, duration_us, &expiration_us));
292
7
        } else {
293
7
            expiration_us = direct_expiration->get_data()[row];
294
7
        }
295
9
        if (expiration_us <= now_us) {
296
5
            filter->selection[row] = 0;
297
5
            ++filter->rows_deleted;
298
5
        }
299
9
    }
300
4
    return Status::OK();
301
4
}
302
303
504
Status filter_block_by_row_visibility(Block* block, const IColumn::Filter& filter) {
304
504
    if (filter.size() != block->rows()) {
305
1
        return Status::InvalidArgument("row visibility filter size {} does not match block rows {}",
306
1
                                       filter.size(), block->rows());
307
1
    }
308
503
    RETURN_IF_CATCH_EXCEPTION(Block::filter_block_internal(block, filter));
309
503
    return Status::OK();
310
503
}
311
312
Status copy_row_ttl_source(Block* block, const TabletSchema& tablet_schema, int32_t source_cid,
313
1
                           const std::vector<bool>& rows_to_copy, size_t row_pos) {
314
1
    DORIS_CHECK(tablet_schema.has_ttl_col());
315
1
    DORIS_CHECK(source_cid >= 0);
316
1
    DORIS_CHECK(row_pos + rows_to_copy.size() <= block->rows());
317
318
1
    const ColumnWithTypeAndName& source_entry = block->get_by_position(source_cid);
319
1
    const auto* nullable_source = check_and_get_column<ColumnNullable>(source_entry.column.get());
320
1
    const NullMap* source_null_map =
321
1
            nullable_source == nullptr ? nullptr : &nullable_source->get_null_map_data();
322
1
    const ColumnPtr source = nullable_source == nullptr ? source_entry.column
323
1
                                                        : nullable_source->get_nested_column_ptr();
324
325
1
    const int32_t ttl_cid = tablet_schema.ttl_col_idx();
326
1
    MutableColumnPtr mutable_ttl = IColumn::mutate(block->get_by_position(ttl_cid).column);
327
1
    auto& ttl = assert_cast<ColumnNullable&>(*mutable_ttl);
328
1
    auto& ttl_data = ttl.get_nested_column();
329
1
    auto& ttl_null_map = ttl.get_null_map_data();
330
331
3
    for (size_t mask_row = 0; mask_row < rows_to_copy.size(); ++mask_row) {
332
2
        if (!rows_to_copy[mask_row]) {
333
0
            continue;
334
0
        }
335
2
        const size_t row = row_pos + mask_row;
336
2
        if (source_null_map != nullptr && (*source_null_map)[row]) {
337
1
            ttl_null_map[row] = 1;
338
1
            ttl_data.replace_column_data(*source, row, row);
339
1
            continue;
340
1
        }
341
1
        ttl_data.replace_column_data(*source, row, row);
342
1
        ttl_null_map[row] = 0;
343
1
    }
344
1
    block->replace_by_position(ttl_cid, std::move(mutable_ttl));
345
1
    return Status::OK();
346
1
}
347
348
bool should_gc_row_ttl(const TabletSchema& tablet_schema, bool enable_unique_key_merge_on_write,
349
1.30M
                       ReaderType reader_type, const Version& version) {
350
1.30M
    if (!tablet_schema.has_ttl_col() || tablet_schema.keys_type() == KeysType::AGG_KEYS) {
351
1.30M
        return false;
352
1.30M
    }
353
18.4E
    if (reader_type == ReaderType::READER_BINLOG_COMPACTION ||
354
18.4E
        reader_type == ReaderType::READER_COLD_DATA_COMPACTION) {
355
2
        return false;
356
2
    }
357
358
18.4E
    const bool full_coverage =
359
18.4E
            reader_type == ReaderType::READER_FULL_COMPACTION ||
360
18.4E
            (reader_type == ReaderType::READER_BASE_COMPACTION && version.first == 0);
361
18.4E
    if (tablet_schema.keys_type() == KeysType::UNIQUE_KEYS && !enable_unique_key_merge_on_write) {
362
5
        return full_coverage;
363
5
    }
364
18.4E
    return reader_type == ReaderType::READER_CUMULATIVE_COMPACTION ||
365
18.4E
           reader_type == ReaderType::READER_BASE_COMPACTION ||
366
18.4E
           reader_type == ReaderType::READER_FULL_COMPACTION ||
367
18.4E
           reader_type == ReaderType::READER_SEGMENT_COMPACTION;
368
18.4E
}
369
370
} // namespace doris