Coverage Report

Created: 2026-07-27 03:00

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/format_v2/wal/wal_reader.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_v2/wal/wal_reader.h"
19
20
#include <absl/strings/numbers.h>
21
#include <absl/strings/str_split.h>
22
23
#include <ranges>
24
#include <unordered_set>
25
#include <utility>
26
27
#include "agent/be_exec_version_manager.h"
28
#include "common/cast_set.h"
29
#include "core/block/block.h"
30
#include "core/data_type/data_type_factory.hpp"
31
#include "core/data_type/data_type_nullable.h"
32
#include "format_v2/column_mapper.h"
33
#include "format_v2/materialized_reader_util.h"
34
#include "load/group_commit/wal/wal_file_reader.h"
35
#include "load/group_commit/wal/wal_manager.h"
36
#include "runtime/exec_env.h"
37
#include "runtime/runtime_state.h"
38
39
namespace doris::format::wal {
40
namespace {
41
42
class WalColumnMapper final : public TableColumnMapper {
43
public:
44
    using TableColumnMapper::TableColumnMapper;
45
46
    Status create_mapping(const std::vector<ColumnDefinition>& projected_columns,
47
                          const std::map<std::string, Field>& partition_values,
48
0
                          const std::vector<ColumnDefinition>& file_schema) override {
49
0
        for (const auto& projected : projected_columns) {
50
0
            if (!projected.has_identifier_field_id()) {
51
0
                return Status::InternalError("WAL projected column {} has no unique id",
52
0
                                             projected.name);
53
0
            }
54
0
            const auto found = std::ranges::find_if(file_schema, [&](const auto& file_column) {
55
0
                return file_column.has_identifier_field_id() &&
56
0
                       file_column.get_identifier_field_id() == projected.get_identifier_field_id();
57
0
            });
58
0
            if (found == file_schema.end()) {
59
0
                return Status::InternalError("WAL does not contain column unique id {} ({})",
60
0
                                             projected.get_identifier_field_id(), projected.name);
61
0
            }
62
0
        }
63
0
        return TableColumnMapper::create_mapping(projected_columns, partition_values, file_schema);
64
0
    }
65
66
protected:
67
0
    bool enable_lazy_materialization() const override { return false; }
68
0
    bool force_full_complex_scan_projection() const override { return true; }
69
};
70
71
10
ColumnDefinition build_wal_column_definition(const PColumnMeta& meta, int32_t local_id) {
72
10
    ColumnDefinition field;
73
10
    field.local_id = local_id;
74
10
    field.name = meta.name();
75
10
    field.type = make_nullable(DataTypeFactory::instance().create_data_type(meta));
76
10
    field.children.reserve(meta.children_size());
77
16
    for (int child_idx = 0; child_idx < meta.children_size(); ++child_idx) {
78
6
        field.children.push_back(build_wal_column_definition(meta.children(child_idx), child_idx));
79
6
    }
80
10
    return field;
81
10
}
82
83
} // namespace
84
85
6
Status parse_wal_column_ids(const std::string& encoded, std::vector<int32_t>* column_ids) {
86
6
    DORIS_CHECK(column_ids != nullptr);
87
6
    column_ids->clear();
88
6
    if (encoded.empty()) {
89
1
        return Status::Corruption("WAL header contains no column ids");
90
1
    }
91
92
5
    std::unordered_set<int32_t> seen;
93
14
    for (const absl::string_view token : absl::StrSplit(encoded, ',')) {
94
14
        int32_t column_id = 0;
95
14
        if (token.empty() || !absl::SimpleAtoi(token, &column_id)) {
96
2
            return Status::Corruption("invalid WAL column id '{}'", std::string(token));
97
2
        }
98
12
        if (!seen.emplace(column_id).second) {
99
1
            return Status::Corruption("duplicate WAL column id {}", column_id);
100
1
        }
101
11
        column_ids->push_back(column_id);
102
11
    }
103
2
    return Status::OK();
104
5
}
105
106
WalReader::WalReader(std::shared_ptr<io::FileSystemProperties>& system_properties,
107
                     std::unique_ptr<io::FileDescription>& file_description,
108
                     std::shared_ptr<io::IOContext> io_ctx, RuntimeProfile* profile,
109
                     const std::vector<ColumnDefinition>& projected_columns)
110
2
        : FileReader(system_properties, file_description, std::move(io_ctx), profile),
111
2
          _projected_columns(projected_columns) {}
112
113
2
WalReader::~WalReader() {
114
2
    static_cast<void>(close());
115
2
}
116
117
0
Status WalReader::init(RuntimeState* state) {
118
0
    if (state == nullptr || state->exec_env() == nullptr ||
119
0
        state->exec_env()->wal_mgr() == nullptr) {
120
0
        return Status::InvalidArgument("WAL v2 reader requires a runtime WAL manager");
121
0
    }
122
0
    RETURN_IF_ERROR(state->exec_env()->wal_mgr()->get_wal_path(state->wal_id(), _wal_path));
123
0
    _wal_reader = std::make_shared<doris::WalFileReader>(_wal_path);
124
0
    RETURN_IF_ERROR(_wal_reader->init());
125
126
0
    std::string encoded_column_ids;
127
0
    RETURN_IF_ERROR(_wal_reader->read_header(_version, encoded_column_ids));
128
0
    RETURN_IF_ERROR(parse_wal_column_ids(encoded_column_ids, &_column_ids));
129
0
    _reader_eof = false;
130
0
    _eof = false;
131
0
    return Status::OK();
132
0
}
133
134
1
Status WalReader::get_schema(std::vector<ColumnDefinition>* file_schema) const {
135
1
    if (file_schema == nullptr) {
136
0
        return Status::InvalidArgument("WAL v2 file_schema is null");
137
0
    }
138
1
    RETURN_IF_ERROR(_ensure_schema_loaded());
139
1
    *file_schema = _file_schema;
140
1
    return Status::OK();
141
1
}
142
143
std::unique_ptr<TableColumnMapper> WalReader::create_column_mapper(
144
0
        TableColumnMapperOptions options) const {
145
0
    return std::make_unique<WalColumnMapper>(std::move(options));
146
0
}
147
148
1
Status WalReader::open(std::shared_ptr<FileScanRequest> request) {
149
1
    RETURN_IF_ERROR(FileReader::open(std::move(request)));
150
1
    _first_block_consumed = false;
151
1
    _eof = false;
152
1
    return Status::OK();
153
1
}
154
155
1
Status WalReader::get_block(Block* file_block, size_t* rows, bool* eof) {
156
1
    DORIS_CHECK(file_block != nullptr);
157
1
    DORIS_CHECK(rows != nullptr);
158
1
    DORIS_CHECK(eof != nullptr);
159
1
    if (_request == nullptr) {
160
0
        return Status::InternalError("WAL v2 reader is not open");
161
0
    }
162
163
1
    *rows = 0;
164
1
    *eof = false;
165
1
    if (_reader_eof) {
166
0
        *eof = true;
167
0
        _eof = true;
168
0
        return Status::OK();
169
0
    }
170
171
1
    PBlock pblock;
172
1
    if (_first_block_loaded && !_first_block_consumed) {
173
        // Schema discovery owns the first payload temporarily; transfer it into the read path so
174
        // the reader neither retains a second PBlock nor forces protobuf to clone its buffers.
175
1
        pblock.Swap(&_first_block);
176
1
        _first_block_consumed = true;
177
1
        _first_block_loaded = false;
178
1
    } else {
179
0
        auto status = _wal_reader->read_block(pblock);
180
0
        if (status.is<ErrorCode::END_OF_FILE>()) {
181
0
            _reader_eof = true;
182
0
            *eof = true;
183
0
            _eof = true;
184
0
            return Status::OK();
185
0
        }
186
0
        RETURN_IF_ERROR(status);
187
0
    }
188
1
    RETURN_IF_ERROR(_validate_block_version(pblock));
189
190
1
    Block source_block;
191
1
    size_t uncompressed_size = 0;
192
1
    int64_t decompress_time = 0;
193
1
    RETURN_IF_ERROR(source_block.deserialize(pblock, &uncompressed_size, &decompress_time));
194
1
    if (source_block.columns() != _column_ids.size()) {
195
0
        return Status::Corruption("WAL block has {} columns but header declares {}",
196
0
                                  source_block.columns(), _column_ids.size());
197
0
    }
198
1
    RETURN_IF_ERROR(_materialize_requested_columns(&source_block, file_block));
199
1
    *rows = file_block->rows();
200
1
    _record_scan_rows(cast_set<int64_t>(*rows));
201
1
    RETURN_IF_ERROR(
202
1
            apply_materialized_reader_filters(_request.get(), _io_ctx.get(), file_block, rows));
203
1
    return Status::OK();
204
1
}
205
206
3
Status WalReader::close() {
207
3
    _request.reset();
208
3
    _reader_eof = true;
209
3
    _eof = true;
210
3
    if (_wal_reader == nullptr) {
211
2
        return Status::OK();
212
2
    }
213
1
    auto status = _wal_reader->finalize();
214
1
    if (status.ok()) {
215
1
        _wal_reader.reset();
216
1
    }
217
1
    return status;
218
3
}
219
220
1
Status WalReader::_ensure_schema_loaded() const {
221
1
    if (_schema_inited) {
222
0
        return Status::OK();
223
0
    }
224
225
1
    auto status = _wal_reader->read_block(_first_block);
226
1
    if (status.is<ErrorCode::END_OF_FILE>()) {
227
        // An empty WAL still has a complete unique-id header. Use only matching projected types;
228
        // there is no data block from which unprojected physical types could be inferred.
229
0
        return _init_schema_from_block(nullptr);
230
0
    }
231
1
    RETURN_IF_ERROR(status);
232
1
    RETURN_IF_ERROR(_validate_block_version(_first_block));
233
1
    _first_block_loaded = true;
234
1
    return _init_schema_from_block(&_first_block);
235
1
}
236
237
2
Status WalReader::_validate_block_version(const PBlock& pblock) const {
238
2
    const int version = pblock.has_be_exec_version() ? pblock.be_exec_version() : 0;
239
2
    if (!BeExecVersionManager::check_be_exec_version(version)) {
240
0
        return Status::DataQualityError("unsupported BE execution version {} in WAL", version);
241
0
    }
242
2
    return Status::OK();
243
2
}
244
245
1
Status WalReader::_init_schema_from_block(const PBlock* pblock) const {
246
1
    if (pblock != nullptr && cast_set<size_t>(pblock->column_metas_size()) != _column_ids.size()) {
247
0
        return Status::Corruption("WAL block schema has {} columns but header declares {}",
248
0
                                  pblock->column_metas_size(), _column_ids.size());
249
0
    }
250
251
1
    _file_schema.clear();
252
5
    for (size_t idx = 0; idx < _column_ids.size(); ++idx) {
253
4
        ColumnDefinition field;
254
4
        field.identifier = Field::create_field<TYPE_INT>(_column_ids[idx]);
255
4
        field.local_id = cast_set<int32_t>(idx);
256
4
        if (pblock != nullptr) {
257
4
            const auto& meta = pblock->column_metas(cast_set<int>(idx));
258
            // WAL metadata is the file-local schema; preserve its complete nested shape so the
259
            // mapper can validate ARRAY/MAP/STRUCT projections instead of seeing an empty shell.
260
4
            field = build_wal_column_definition(meta, cast_set<int32_t>(idx));
261
4
            field.identifier = Field::create_field<TYPE_INT>(_column_ids[idx]);
262
4
        } else {
263
0
            const auto projected =
264
0
                    std::ranges::find_if(_projected_columns, [&](const auto& candidate) {
265
0
                        return candidate.has_identifier_field_id() &&
266
0
                               candidate.get_identifier_field_id() == _column_ids[idx];
267
0
                    });
268
0
            if (projected == _projected_columns.end()) {
269
0
                continue;
270
0
            }
271
0
            field.name = projected->name;
272
0
            field.type = projected->type;
273
0
        }
274
4
        _file_schema.push_back(std::move(field));
275
4
    }
276
1
    _schema_inited = true;
277
1
    return Status::OK();
278
1
}
279
280
2
Status WalReader::_materialize_requested_columns(Block* source_block, Block* file_block) const {
281
2
    DORIS_CHECK(source_block != nullptr);
282
3
    for (const auto& [file_column_id, block_position] : _request->local_positions) {
283
3
        const auto source_idx = file_column_id.value();
284
3
        if (source_idx < 0 || cast_set<size_t>(source_idx) >= source_block->columns()) {
285
0
            return Status::Corruption("WAL request refers to invalid local column {}", source_idx);
286
0
        }
287
3
        if (block_position.value() >= file_block->columns()) {
288
0
            return Status::InternalError("WAL request has invalid block position {}",
289
0
                                         block_position.value());
290
0
        }
291
3
        const auto& target = file_block->get_by_position(block_position.value());
292
        // Deserialized WAL columns have a single owner. Move that ownership into the output block
293
        // before mutate() so wide payloads do not trigger copy-on-write deep clones.
294
3
        auto column = std::move(source_block->get_by_position(source_idx).column);
295
3
        column = make_column_nullable_if_needed(std::move(column), target.type);
296
3
        file_block->replace_by_position(block_position.value(), IColumn::mutate(std::move(column)));
297
3
    }
298
2
    return Status::OK();
299
2
}
300
301
} // namespace doris::format::wal