be/src/format_v2/native/native_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/native/native_reader.h" |
19 | | |
20 | | #include <cstring> |
21 | | #include <utility> |
22 | | |
23 | | #include "common/cast_set.h" |
24 | | #include "core/block/block.h" |
25 | | #include "core/data_type/data_type_factory.hpp" |
26 | | #include "core/data_type/data_type_nullable.h" |
27 | | #include "format/native/native_format.h" |
28 | | #include "format_v2/column_mapper.h" |
29 | | #include "format_v2/materialized_reader_util.h" |
30 | | #include "io/file_factory.h" |
31 | | #include "io/fs/tracing_file_reader.h" |
32 | | #include "runtime/file_scan_profile.h" |
33 | | #include "runtime/runtime_state.h" |
34 | | #include "util/slice.h" |
35 | | |
36 | | namespace doris::format::native { |
37 | | namespace { |
38 | | |
39 | 7 | Status parse_native_pblock(const std::string& buffer, const std::string& path, PBlock* pblock) { |
40 | 7 | DORIS_CHECK(pblock != nullptr); |
41 | 7 | if (!pblock->ParseFromArray(buffer.data(), cast_set<int>(buffer.size()))) { |
42 | 1 | return Status::InternalError("Failed to parse native PBlock from file {}", path); |
43 | 1 | } |
44 | 6 | return Status::OK(); |
45 | 7 | } |
46 | | |
47 | | } // namespace |
48 | | |
49 | | NativeReader::NativeReader(std::shared_ptr<io::FileSystemProperties>& system_properties, |
50 | | std::unique_ptr<io::FileDescription>& file_description, |
51 | | std::shared_ptr<io::IOContext> io_ctx, RuntimeProfile* profile) |
52 | 17 | : FileReader(system_properties, file_description, std::move(io_ctx), profile) {} |
53 | | |
54 | 17 | NativeReader::~NativeReader() { |
55 | 17 | static_cast<void>(close()); |
56 | 17 | } |
57 | | |
58 | 34 | void NativeReader::_init_profile() { |
59 | 34 | if (_profile == nullptr) { |
60 | 0 | return; |
61 | 0 | } |
62 | 34 | file_scan_profile::ensure_hierarchy(_profile); |
63 | 34 | static const char* native_profile = "NativeReader"; |
64 | 34 | _total_time = |
65 | 34 | ADD_CHILD_TIMER_WITH_LEVEL(_profile, native_profile, file_scan_profile::FILE_READER, 1); |
66 | 34 | _read_block_time = |
67 | 34 | ADD_CHILD_TIMER_WITH_LEVEL(_profile, "NativeReadBlockTime", native_profile, 1); |
68 | 34 | _deserialize_time = |
69 | 34 | ADD_CHILD_TIMER_WITH_LEVEL(_profile, "NativeDeserializeTime", native_profile, 1); |
70 | 34 | _materialize_time = |
71 | 34 | ADD_CHILD_TIMER_WITH_LEVEL(_profile, "NativeMaterializeTime", native_profile, 1); |
72 | 34 | _filter_time = ADD_CHILD_TIMER_WITH_LEVEL(_profile, "NativeFilterTime", native_profile, 1); |
73 | 34 | } |
74 | | |
75 | 17 | Status NativeReader::init(RuntimeState* state) { |
76 | 17 | _init_profile(); |
77 | 17 | SCOPED_TIMER(_total_time); |
78 | 17 | _runtime_state = state; |
79 | 17 | if (_file_description == nullptr) { |
80 | 0 | return Status::InvalidArgument("Native v2 reader requires file description"); |
81 | 0 | } |
82 | 17 | RETURN_IF_ERROR(FileReader::init(state)); |
83 | 17 | RETURN_IF_ERROR(_validate_and_consume_header()); |
84 | 14 | return Status::OK(); |
85 | 17 | } |
86 | | |
87 | 14 | Status NativeReader::get_schema(std::vector<ColumnDefinition>* file_schema) const { |
88 | 14 | SCOPED_TIMER(_total_time); |
89 | 14 | if (file_schema == nullptr) { |
90 | 0 | return Status::InvalidArgument("Native v2 file_schema is null"); |
91 | 0 | } |
92 | 14 | RETURN_IF_ERROR(_ensure_schema_loaded()); |
93 | 3 | *file_schema = _file_schema; |
94 | 3 | return Status::OK(); |
95 | 14 | } |
96 | | |
97 | | std::unique_ptr<TableColumnMapper> NativeReader::create_column_mapper( |
98 | 0 | TableColumnMapperOptions options) const { |
99 | 0 | return std::make_unique<MaterializedColumnMapper>(std::move(options)); |
100 | 0 | } |
101 | | |
102 | 3 | Status NativeReader::open(std::shared_ptr<FileScanRequest> request) { |
103 | 3 | SCOPED_TIMER(_total_time); |
104 | 3 | RETURN_IF_ERROR(FileReader::open(std::move(request))); |
105 | 3 | DORIS_CHECK(_request != nullptr); |
106 | 3 | _first_block_consumed = false; |
107 | 3 | _reader_eof = false; |
108 | 3 | _eof = false; |
109 | 3 | return Status::OK(); |
110 | 3 | } |
111 | | |
112 | 4 | Status NativeReader::get_block(Block* file_block, size_t* rows, bool* eof) { |
113 | 4 | SCOPED_TIMER(_total_time); |
114 | 4 | DORIS_CHECK(file_block != nullptr); |
115 | 4 | DORIS_CHECK(rows != nullptr); |
116 | 4 | DORIS_CHECK(eof != nullptr); |
117 | 4 | if (_request == nullptr) { |
118 | 0 | return Status::InternalError("Native v2 reader is not open"); |
119 | 0 | } |
120 | | |
121 | 4 | *rows = 0; |
122 | 4 | *eof = false; |
123 | 4 | if (_reader_eof) { |
124 | 1 | *eof = true; |
125 | 1 | _eof = true; |
126 | 1 | return Status::OK(); |
127 | 1 | } |
128 | | |
129 | 3 | std::string buffer; |
130 | 3 | bool local_eof = false; |
131 | 3 | if (_first_block_loaded && !_first_block_consumed) { |
132 | 3 | buffer = _first_block_buffer; |
133 | 3 | } else { |
134 | 0 | SCOPED_TIMER(_read_block_time); |
135 | 0 | RETURN_IF_ERROR(_read_next_pblock(&buffer, &local_eof)); |
136 | 0 | } |
137 | | |
138 | 3 | if (local_eof && buffer.empty()) { |
139 | 0 | _reader_eof = true; |
140 | 0 | *eof = true; |
141 | 0 | _eof = true; |
142 | 0 | return Status::OK(); |
143 | 0 | } |
144 | 3 | if (buffer.empty()) { |
145 | 0 | return Status::InternalError("read empty native block from file {}", |
146 | 0 | _file_description->path); |
147 | 0 | } |
148 | | |
149 | 3 | PBlock pblock; |
150 | 3 | RETURN_IF_ERROR(parse_native_pblock(buffer, _file_description->path, &pblock)); |
151 | 3 | if (!_schema_inited) { |
152 | 0 | RETURN_IF_ERROR(_init_schema_from_pblock(pblock)); |
153 | 0 | } |
154 | | |
155 | 3 | Block source_block; |
156 | 3 | size_t uncompressed_bytes = 0; |
157 | 3 | int64_t decompress_time = 0; |
158 | 3 | { |
159 | 3 | SCOPED_TIMER(_deserialize_time); |
160 | 3 | RETURN_IF_ERROR(source_block.deserialize(pblock, &uncompressed_bytes, &decompress_time)); |
161 | 3 | } |
162 | 3 | { |
163 | 3 | SCOPED_TIMER(_materialize_time); |
164 | 3 | RETURN_IF_ERROR(_materialize_requested_columns(source_block, file_block)); |
165 | 3 | } |
166 | 2 | *rows = file_block->rows(); |
167 | 2 | _record_scan_rows(cast_set<int64_t>(*rows)); |
168 | 2 | { |
169 | 2 | SCOPED_TIMER(_filter_time); |
170 | 2 | RETURN_IF_ERROR(_apply_filters(file_block, rows)); |
171 | 2 | } |
172 | | |
173 | 2 | if (_first_block_loaded && !_first_block_consumed) { |
174 | 2 | _first_block_consumed = true; |
175 | 2 | } |
176 | 2 | if (_current_offset >= _file_size) { |
177 | 2 | _reader_eof = true; |
178 | 2 | } |
179 | 2 | *eof = _reader_eof && *rows == 0; |
180 | 2 | _eof = *eof; |
181 | 2 | return Status::OK(); |
182 | 2 | } |
183 | | |
184 | 19 | Status NativeReader::close() { |
185 | 19 | SCOPED_TIMER(_total_time); |
186 | 19 | _file_reader.reset(); |
187 | 19 | _tracing_file_reader.reset(); |
188 | 19 | _request.reset(); |
189 | 19 | _reader_eof = true; |
190 | 19 | _eof = true; |
191 | 19 | return Status::OK(); |
192 | 19 | } |
193 | | |
194 | 17 | Status NativeReader::_validate_and_consume_header() { |
195 | 17 | DORIS_CHECK(_tracing_file_reader != nullptr); |
196 | 17 | _file_size = _tracing_file_reader->size(); |
197 | 17 | _current_offset = 0; |
198 | 17 | _reader_eof = (_file_size == 0); |
199 | | |
200 | 17 | static constexpr size_t HEADER_SIZE = sizeof(DORIS_NATIVE_MAGIC) + sizeof(uint32_t); |
201 | 17 | if (_reader_eof || _file_size < cast_set<int64_t>(HEADER_SIZE)) { |
202 | 1 | return Status::InternalError( |
203 | 1 | "invalid Doris Native file {}, file size {} is smaller than header size {}", |
204 | 1 | _file_description->path, _file_size, HEADER_SIZE); |
205 | 1 | } |
206 | | |
207 | 16 | char header[HEADER_SIZE]; |
208 | 16 | Slice header_slice(header, sizeof(header)); |
209 | 16 | size_t bytes_read = 0; |
210 | 16 | RETURN_IF_ERROR(_tracing_file_reader->read_at(0, header_slice, &bytes_read, _io_ctx.get())); |
211 | 16 | if (bytes_read != sizeof(header)) { |
212 | 0 | return Status::InternalError( |
213 | 0 | "failed to read Doris Native header from file {}, expect {} bytes, got {} bytes", |
214 | 0 | _file_description->path, sizeof(header), bytes_read); |
215 | 0 | } |
216 | 16 | if (std::memcmp(header, DORIS_NATIVE_MAGIC, sizeof(DORIS_NATIVE_MAGIC)) != 0) { |
217 | 1 | return Status::InternalError("invalid Doris Native magic header in file {}", |
218 | 1 | _file_description->path); |
219 | 1 | } |
220 | | |
221 | 15 | uint32_t version = 0; |
222 | 15 | std::memcpy(&version, header + sizeof(DORIS_NATIVE_MAGIC), sizeof(uint32_t)); |
223 | 15 | if (version != DORIS_NATIVE_FORMAT_VERSION) { |
224 | 1 | return Status::InternalError( |
225 | 1 | "unsupported Doris Native format version {} in file {}, expect {}", version, |
226 | 1 | _file_description->path, DORIS_NATIVE_FORMAT_VERSION); |
227 | 1 | } |
228 | | |
229 | 14 | _current_offset = sizeof(header); |
230 | 14 | _reader_eof = (_file_size == _current_offset); |
231 | 14 | return Status::OK(); |
232 | 15 | } |
233 | | |
234 | 14 | Status NativeReader::_ensure_schema_loaded() const { |
235 | 14 | if (_schema_inited) { |
236 | 0 | return Status::OK(); |
237 | 0 | } |
238 | 14 | if (!_first_block_loaded) { |
239 | 14 | bool local_eof = false; |
240 | 14 | RETURN_IF_ERROR(_read_next_pblock(&_first_block_buffer, &local_eof)); |
241 | 5 | if (local_eof && _first_block_buffer.empty()) { |
242 | 1 | return Status::EndOfFile("empty native file {}", _file_description->path); |
243 | 1 | } |
244 | 4 | if (_first_block_buffer.empty()) { |
245 | 0 | return Status::InternalError("first native block is empty {}", _file_description->path); |
246 | 0 | } |
247 | 4 | _first_block_loaded = true; |
248 | 4 | } |
249 | | |
250 | 4 | PBlock pblock; |
251 | 4 | RETURN_IF_ERROR(parse_native_pblock(_first_block_buffer, _file_description->path, &pblock)); |
252 | 3 | RETURN_IF_ERROR(_init_schema_from_pblock(pblock)); |
253 | 3 | return Status::OK(); |
254 | 3 | } |
255 | | |
256 | 14 | Status NativeReader::_read_next_pblock(std::string* buffer, bool* eof) const { |
257 | 14 | DORIS_CHECK(buffer != nullptr); |
258 | 14 | DORIS_CHECK(eof != nullptr); |
259 | 14 | DORIS_CHECK(_tracing_file_reader != nullptr); |
260 | 14 | buffer->clear(); |
261 | 14 | *eof = false; |
262 | | |
263 | 14 | if (_current_offset >= _file_size) { |
264 | 1 | *eof = true; |
265 | 1 | return Status::OK(); |
266 | 1 | } |
267 | | |
268 | 13 | const auto remaining_prefix_bytes = _file_size - _current_offset; |
269 | 13 | if (remaining_prefix_bytes < sizeof(uint64_t)) { |
270 | | // Header-only is the one valid empty-file representation and returned above. Once any |
271 | | // prefix byte exists, all eight bytes are mandatory. For example, `header + 4 bytes` is a |
272 | | // truncated Native file, not EOF that FileScannerV2 may skip as an empty split. |
273 | 7 | return Status::InternalError( |
274 | 7 | "truncated native block length in file {} at offset {}, expect {}, remaining {}", |
275 | 7 | _file_description->path, _current_offset, sizeof(uint64_t), remaining_prefix_bytes); |
276 | 7 | } |
277 | | |
278 | 6 | uint64_t block_len = 0; |
279 | 6 | Slice len_slice(reinterpret_cast<char*>(&block_len), sizeof(block_len)); |
280 | 6 | size_t bytes_read = 0; |
281 | 6 | RETURN_IF_ERROR( |
282 | 6 | _tracing_file_reader->read_at(_current_offset, len_slice, &bytes_read, _io_ctx.get())); |
283 | 6 | if (bytes_read == 0) { |
284 | 0 | *eof = true; |
285 | 0 | return Status::OK(); |
286 | 0 | } |
287 | 6 | if (bytes_read != sizeof(block_len)) { |
288 | 0 | return Status::InternalError( |
289 | 0 | "Failed to read native block length from file {}, expect {}, actual {}", |
290 | 0 | _file_description->path, sizeof(block_len), bytes_read); |
291 | 0 | } |
292 | 6 | _current_offset += sizeof(block_len); |
293 | 6 | if (block_len == 0) { |
294 | | // A header-only file reaches the `_current_offset >= _file_size` branch above and is a |
295 | | // valid empty Native file. Once an explicit length prefix is present, however, zero is not |
296 | | // an EOF marker in the Native format. For example, `header + uint64(0)` is truncated input, |
297 | | // not a zero-row split, and must not escape as EOF for FileScannerV2 to count as empty. |
298 | 1 | return Status::InternalError("zero-length native block in file {} at offset {}", |
299 | 1 | _file_description->path, _current_offset - sizeof(block_len)); |
300 | 1 | } |
301 | | |
302 | 5 | const auto remaining_block_bytes = cast_set<uint64_t>(_file_size - _current_offset); |
303 | 5 | if (block_len > remaining_block_bytes) { |
304 | | // Validate before allocating `block_len` bytes. Besides reporting a precise corruption |
305 | | // error, this prevents a truncated file with a damaged length prefix from requesting an |
306 | | // allocation much larger than the physical file. |
307 | 1 | return Status::InternalError( |
308 | 1 | "truncated native block body in file {} at offset {}, expect {}, remaining {}", |
309 | 1 | _file_description->path, _current_offset, block_len, remaining_block_bytes); |
310 | 1 | } |
311 | | |
312 | 4 | buffer->assign(block_len, '\0'); |
313 | 4 | Slice data_slice(buffer->data(), block_len); |
314 | 4 | bytes_read = 0; |
315 | 4 | RETURN_IF_ERROR( |
316 | 4 | _tracing_file_reader->read_at(_current_offset, data_slice, &bytes_read, _io_ctx.get())); |
317 | 4 | if (bytes_read != block_len) { |
318 | 0 | return Status::InternalError( |
319 | 0 | "Failed to read native block body from file {}, expect {}, actual {}", |
320 | 0 | _file_description->path, block_len, bytes_read); |
321 | 0 | } |
322 | 4 | _current_offset += block_len; |
323 | 4 | *eof = (_current_offset >= _file_size); |
324 | 4 | return Status::OK(); |
325 | 4 | } |
326 | | |
327 | 3 | Status NativeReader::_init_schema_from_pblock(const PBlock& pblock) const { |
328 | 3 | _file_schema.clear(); |
329 | 3 | _file_schema.reserve(pblock.column_metas_size()); |
330 | 9 | for (int idx = 0; idx < pblock.column_metas_size(); ++idx) { |
331 | 6 | const auto& meta = pblock.column_metas(idx); |
332 | 6 | ColumnDefinition field; |
333 | 6 | field.identifier = Field::create_field<TYPE_STRING>(meta.name()); |
334 | 6 | field.local_id = idx; |
335 | 6 | field.name = meta.name(); |
336 | 6 | field.type = make_nullable(DataTypeFactory::instance().create_data_type(meta)); |
337 | 6 | _file_schema.push_back(std::move(field)); |
338 | 6 | } |
339 | 3 | _schema_inited = true; |
340 | 3 | return Status::OK(); |
341 | 3 | } |
342 | | |
343 | | Status NativeReader::_materialize_requested_columns(const Block& source_block, |
344 | 3 | Block* file_block) const { |
345 | 3 | DORIS_CHECK(file_block != nullptr); |
346 | 3 | DORIS_CHECK(_request != nullptr); |
347 | 5 | for (const auto& [file_column_id, block_position] : _request->local_positions) { |
348 | 5 | const auto source_idx = file_column_id.value(); |
349 | 5 | if (source_idx < 0 || cast_set<size_t>(source_idx) >= source_block.columns()) { |
350 | 1 | return Status::InternalError("native file {} does not contain local column id {}", |
351 | 1 | _file_description->path, source_idx); |
352 | 1 | } |
353 | 4 | if (block_position.value() >= file_block->columns()) { |
354 | 0 | return Status::InternalError("native v2 request has invalid block position {}", |
355 | 0 | block_position.value()); |
356 | 0 | } |
357 | 4 | const auto& target = file_block->get_by_position(block_position.value()); |
358 | 4 | auto column = source_block.get_by_position(source_idx).column; |
359 | 4 | column = make_column_nullable_if_needed(std::move(column), target.type); |
360 | 4 | file_block->replace_by_position(block_position.value(), IColumn::mutate(std::move(column))); |
361 | 4 | } |
362 | 2 | return Status::OK(); |
363 | 3 | } |
364 | | |
365 | 2 | Status NativeReader::_apply_filters(Block* file_block, size_t* rows) const { |
366 | 2 | return apply_materialized_reader_filters(_request.get(), _io_ctx.get(), file_block, rows); |
367 | 2 | } |
368 | | |
369 | | } // namespace doris::format::native |