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 |