Coverage Report

Created: 2026-07-12 08:37

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/format/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/native/native_reader.h"
19
20
#include <gen_cpp/data.pb.h>
21
22
#include <utility>
23
24
#include "core/block/block.h"
25
#include "format/native/native_format.h"
26
#include "io/file_factory.h"
27
#include "io/fs/buffered_reader.h"
28
#include "io/fs/file_reader.h"
29
#include "io/fs/tracing_file_reader.h"
30
#include "runtime/runtime_profile.h"
31
#include "runtime/runtime_state.h"
32
33
namespace doris {
34
35
NativeReader::NativeReader(RuntimeProfile* profile, const TFileScanRangeParams& params,
36
                           const TFileRangeDesc& range, io::IOContext* io_ctx, RuntimeState* state)
37
20
        : _profile(profile),
38
20
          _scan_params(params),
39
20
          _scan_range(range),
40
20
          _io_ctx(io_ctx),
41
20
          _state(state) {}
42
43
NativeReader::NativeReader(RuntimeProfile* profile, const TFileScanRangeParams& params,
44
                           const TFileRangeDesc& range,
45
                           std::shared_ptr<io::IOContext> io_ctx_holder, RuntimeState* state)
46
1
        : _profile(profile),
47
1
          _scan_params(params),
48
1
          _scan_range(range),
49
1
          _io_ctx(io_ctx_holder ? io_ctx_holder.get() : nullptr),
50
1
          _io_ctx_holder(std::move(io_ctx_holder)),
51
1
          _state(state) {}
52
53
21
NativeReader::~NativeReader() {
54
21
    (void)close();
55
21
}
56
57
namespace {
58
59
Status validate_and_consume_header(io::FileReaderSPtr file_reader, const TFileRangeDesc& range,
60
                                   int64_t* file_size, int64_t* current_offset, bool* eof,
61
21
                                   const io::IOContext* io_ctx) {
62
21
    *file_size = file_reader->size();
63
21
    *current_offset = 0;
64
21
    *eof = (*file_size == 0);
65
66
    // Validate and consume Doris Native file header.
67
    // Expected layout:
68
    // [magic bytes "DORISN1\0"][uint32_t format_version][uint64_t block_size]...
69
21
    static constexpr size_t HEADER_SIZE = sizeof(DORIS_NATIVE_MAGIC) + sizeof(uint32_t);
70
21
    if (*eof || *file_size < static_cast<int64_t>(HEADER_SIZE)) {
71
0
        return Status::InternalError(
72
0
                "invalid Doris Native file {}, file size {} is smaller than header size {}",
73
0
                range.path, *file_size, HEADER_SIZE);
74
0
    }
75
76
21
    char header[HEADER_SIZE];
77
21
    Slice header_slice(header, sizeof(header));
78
21
    size_t bytes_read = 0;
79
21
    RETURN_IF_ERROR(file_reader->read_at(0, header_slice, &bytes_read, io_ctx));
80
21
    if (bytes_read != sizeof(header)) {
81
0
        return Status::InternalError(
82
0
                "failed to read Doris Native header from file {}, expect {} bytes, got {} bytes",
83
0
                range.path, sizeof(header), bytes_read);
84
0
    }
85
86
21
    if (memcmp(header, DORIS_NATIVE_MAGIC, sizeof(DORIS_NATIVE_MAGIC)) != 0) {
87
0
        return Status::InternalError("invalid Doris Native magic header in file {}", range.path);
88
0
    }
89
90
21
    uint32_t version = 0;
91
21
    memcpy(&version, header + sizeof(DORIS_NATIVE_MAGIC), sizeof(uint32_t));
92
21
    if (version != DORIS_NATIVE_FORMAT_VERSION) {
93
0
        return Status::InternalError(
94
0
                "unsupported Doris Native format version {} in file {}, expect {}", version,
95
0
                range.path, DORIS_NATIVE_FORMAT_VERSION);
96
0
    }
97
98
21
    *current_offset = sizeof(header);
99
21
    *eof = (*file_size == *current_offset);
100
21
    return Status::OK();
101
21
}
102
103
} // namespace
104
105
192
Status NativeReader::init_reader() {
106
192
    if (_file_reader != nullptr) {
107
171
        return Status::OK();
108
171
    }
109
110
    // Create underlying file reader. For now we always use random access mode.
111
21
    io::FileSystemProperties system_properties;
112
21
    io::FileDescription file_description;
113
21
    file_description.file_size = -1;
114
21
    if (_scan_range.__isset.file_size) {
115
21
        file_description.file_size = _scan_range.file_size;
116
21
    }
117
21
    file_description.path = _scan_range.path;
118
21
    if (_scan_range.__isset.fs_name) {
119
0
        file_description.fs_name = _scan_range.fs_name;
120
0
    }
121
21
    if (_scan_range.__isset.modification_time) {
122
3
        file_description.mtime = _scan_range.modification_time;
123
18
    } else {
124
18
        file_description.mtime = 0;
125
18
    }
126
127
21
    if (_scan_range.__isset.file_type) {
128
        // For compatibility with older FE.
129
20
        system_properties.system_type = _scan_range.file_type;
130
20
    } else {
131
1
        system_properties.system_type = _scan_params.file_type;
132
1
    }
133
21
    system_properties.properties = _scan_params.properties;
134
21
    system_properties.hdfs_params = _scan_params.hdfs_params;
135
21
    if (_scan_params.__isset.broker_addresses) {
136
1
        system_properties.broker_addresses.assign(_scan_params.broker_addresses.begin(),
137
1
                                                  _scan_params.broker_addresses.end());
138
1
    }
139
140
21
    io::FileReaderOptions reader_options = FileFactory::get_reader_options(
141
21
            _state ? _state->query_options() : _default_query_options, file_description);
142
21
    auto reader_res =
143
21
            _io_ctx_holder ? io::DelegateReader::create_file_reader(
144
1
                                     _profile, system_properties, file_description, reader_options,
145
1
                                     io::DelegateReader::AccessMode::RANDOM,
146
1
                                     std::static_pointer_cast<const io::IOContext>(_io_ctx_holder))
147
21
                           : io::DelegateReader::create_file_reader(
148
20
                                     _profile, system_properties, file_description, reader_options,
149
20
                                     io::DelegateReader::AccessMode::RANDOM, _io_ctx);
150
21
    if (!reader_res.has_value()) {
151
0
        return reader_res.error();
152
0
    }
153
21
    _file_reader = reader_res.value();
154
155
21
    if (_io_ctx && _io_ctx->file_reader_stats) {
156
3
        _file_reader =
157
3
                std::make_shared<io::TracingFileReader>(_file_reader, _io_ctx->file_reader_stats);
158
3
    }
159
160
21
    RETURN_IF_ERROR(validate_and_consume_header(_file_reader, _scan_range, &_file_size,
161
21
                                                &_current_offset, &_eof, _io_ctx));
162
21
    return Status::OK();
163
21
}
164
165
193
Status NativeReader::_do_get_next_block(Block* block, size_t* read_rows, bool* eof) {
166
193
    if (_eof) {
167
11
        *read_rows = 0;
168
11
        *eof = true;
169
11
        return Status::OK();
170
11
    }
171
172
182
    RETURN_IF_ERROR(init_reader());
173
174
182
    std::string buff;
175
182
    bool local_eof = false;
176
177
    // If we have already loaded the first block for schema probing, use it first.
178
182
    if (_first_block_loaded && !_first_block_consumed) {
179
1
        buff = _first_block_buf;
180
1
        local_eof = false;
181
181
    } else {
182
181
        RETURN_IF_ERROR(_read_next_pblock(&buff, &local_eof));
183
181
    }
184
185
    // If we reach EOF and also read no data for this call, the whole file is considered finished.
186
182
    if (local_eof && buff.empty()) {
187
2
        *read_rows = 0;
188
2
        *eof = true;
189
2
        _eof = true;
190
2
        return Status::OK();
191
2
    }
192
    // If buffer is empty but we have not reached EOF yet, treat this as an error.
193
180
    if (buff.empty()) {
194
0
        return Status::InternalError("read empty native block from file {}", _scan_range.path);
195
0
    }
196
197
180
    PBlock pblock;
198
180
    if (!pblock.ParseFromArray(buff.data(), static_cast<int>(buff.size()))) {
199
0
        return Status::InternalError("Failed to parse native PBlock from file {}",
200
0
                                     _scan_range.path);
201
0
    }
202
203
    // Initialize schema from first block if not done yet.
204
180
    if (!_schema_inited) {
205
14
        RETURN_IF_ERROR(_init_schema_from_pblock(pblock));
206
14
    }
207
208
180
    size_t uncompressed_bytes = 0;
209
180
    int64_t decompress_time = 0;
210
180
    RETURN_IF_ERROR(block->deserialize(pblock, &uncompressed_bytes, &decompress_time));
211
212
    // For external file scan / TVF scenarios, unify all columns as nullable to match
213
    // GenericReader/SlotDescriptor convention. This ensures schema consistency when
214
    // some writers emit non-nullable columns.
215
1.30k
    for (size_t i = 0; i < block->columns(); ++i) {
216
1.12k
        auto& col_with_type = block->get_by_position(i);
217
1.12k
        if (!col_with_type.type->is_nullable()) {
218
14
            col_with_type.column = make_nullable(col_with_type.column);
219
14
            col_with_type.type = make_nullable(col_with_type.type);
220
14
        }
221
1.12k
    }
222
223
180
    *read_rows = block->rows();
224
180
    *eof = false;
225
226
180
    if (_first_block_loaded && !_first_block_consumed) {
227
1
        _first_block_consumed = true;
228
1
    }
229
230
    // If we reached the physical end of file, mark eof for subsequent calls.
231
180
    if (_current_offset >= _file_size) {
232
15
        _eof = true;
233
15
    }
234
235
180
    return Status::OK();
236
180
}
237
238
3
Status NativeReader::_get_columns_impl(std::unordered_map<std::string, DataTypePtr>* name_to_type) {
239
3
    RETURN_IF_ERROR(init_reader());
240
241
3
    if (!_schema_inited) {
242
        // Load first block lazily to initialize schema.
243
3
        if (!_first_block_loaded) {
244
3
            bool local_eof = false;
245
3
            RETURN_IF_ERROR(_read_next_pblock(&_first_block_buf, &local_eof));
246
            // Treat file as empty only if we reach EOF and there is no block data at all.
247
3
            if (local_eof && _first_block_buf.empty()) {
248
0
                return Status::EndOfFile("empty native file {}", _scan_range.path);
249
0
            }
250
            // Non-EOF but empty buffer means corrupted native file.
251
3
            if (_first_block_buf.empty()) {
252
0
                return Status::InternalError("first native block is empty {}", _scan_range.path);
253
0
            }
254
3
            _first_block_loaded = true;
255
3
        }
256
257
3
        PBlock pblock;
258
3
        if (!pblock.ParseFromArray(_first_block_buf.data(),
259
3
                                   static_cast<int>(_first_block_buf.size()))) {
260
0
            return Status::InternalError("Failed to parse native PBlock for schema from file {}",
261
0
                                         _scan_range.path);
262
0
        }
263
3
        RETURN_IF_ERROR(_init_schema_from_pblock(pblock));
264
3
    }
265
266
17
    for (size_t i = 0; i < _schema_col_names.size(); ++i) {
267
14
        name_to_type->emplace(_schema_col_names[i], _schema_col_types[i]);
268
14
    }
269
3
    return Status::OK();
270
3
}
271
272
2
Status NativeReader::init_schema_reader() {
273
2
    RETURN_IF_ERROR(init_reader());
274
2
    return Status::OK();
275
2
}
276
277
Status NativeReader::get_parsed_schema(std::vector<std::string>* col_names,
278
4
                                       std::vector<DataTypePtr>* col_types) {
279
4
    RETURN_IF_ERROR(init_reader());
280
281
4
    if (!_schema_inited) {
282
2
        if (!_first_block_loaded) {
283
2
            bool local_eof = false;
284
2
            RETURN_IF_ERROR(_read_next_pblock(&_first_block_buf, &local_eof));
285
            // Treat file as empty only if we reach EOF and there is no block data at all.
286
2
            if (local_eof && _first_block_buf.empty()) {
287
0
                return Status::EndOfFile("empty native file {}", _scan_range.path);
288
0
            }
289
            // Non-EOF but empty buffer means corrupted native file.
290
2
            if (_first_block_buf.empty()) {
291
0
                return Status::InternalError("first native block is empty {}", _scan_range.path);
292
0
            }
293
2
            _first_block_loaded = true;
294
2
        }
295
296
2
        PBlock pblock;
297
2
        if (!pblock.ParseFromArray(_first_block_buf.data(),
298
2
                                   static_cast<int>(_first_block_buf.size()))) {
299
0
            return Status::InternalError("Failed to parse native PBlock for schema from file {}",
300
0
                                         _scan_range.path);
301
0
        }
302
2
        RETURN_IF_ERROR(_init_schema_from_pblock(pblock));
303
2
    }
304
305
4
    *col_names = _schema_col_names;
306
4
    *col_types = _schema_col_types;
307
4
    return Status::OK();
308
4
}
309
310
22
Status NativeReader::close() {
311
22
    _file_reader.reset();
312
22
    return Status::OK();
313
22
}
314
315
186
Status NativeReader::_read_next_pblock(std::string* buff, bool* eof) {
316
186
    *eof = false;
317
186
    buff->clear();
318
319
186
    if (_file_reader == nullptr) {
320
0
        RETURN_IF_ERROR(init_reader());
321
0
    }
322
323
186
    if (_current_offset >= _file_size) {
324
2
        *eof = true;
325
2
        return Status::OK();
326
2
    }
327
328
184
    uint64_t len = 0;
329
184
    Slice len_slice(reinterpret_cast<char*>(&len), sizeof(len));
330
184
    size_t bytes_read = 0;
331
184
    RETURN_IF_ERROR(_file_reader->read_at(_current_offset, len_slice, &bytes_read, _io_ctx));
332
184
    if (bytes_read == 0) {
333
0
        *eof = true;
334
0
        return Status::OK();
335
0
    }
336
184
    if (bytes_read != sizeof(len)) {
337
0
        return Status::InternalError(
338
0
                "Failed to read native block length from file {}, expect {}, "
339
0
                "actual {}",
340
0
                _scan_range.path, sizeof(len), bytes_read);
341
0
    }
342
343
184
    _current_offset += sizeof(len);
344
184
    if (len == 0) {
345
        // Empty block, nothing to read.
346
0
        *eof = (_current_offset >= _file_size);
347
0
        return Status::OK();
348
0
    }
349
350
184
    buff->assign(len, '\0');
351
184
    Slice data_slice(buff->data(), len);
352
184
    bytes_read = 0;
353
184
    RETURN_IF_ERROR(_file_reader->read_at(_current_offset, data_slice, &bytes_read, _io_ctx));
354
184
    if (bytes_read != len) {
355
0
        return Status::InternalError(
356
0
                "Failed to read native block body from file {}, expect {}, "
357
0
                "actual {}",
358
0
                _scan_range.path, len, bytes_read);
359
0
    }
360
361
184
    _current_offset += len;
362
184
    *eof = (_current_offset >= _file_size);
363
184
    return Status::OK();
364
184
}
365
366
19
Status NativeReader::_init_schema_from_pblock(const PBlock& pblock) {
367
19
    _schema_col_names.clear();
368
19
    _schema_col_types.clear();
369
370
188
    for (const auto& pcol_meta : pblock.column_metas()) {
371
188
        DataTypePtr type = make_nullable(DataTypeFactory::instance().create_data_type(pcol_meta));
372
188
        VLOG_DEBUG << "init_schema_from_pblock, name=" << pcol_meta.name()
373
0
                   << ", type=" << type->get_name();
374
188
        _schema_col_names.emplace_back(pcol_meta.name());
375
188
        _schema_col_types.emplace_back(type);
376
188
    }
377
19
    _schema_inited = true;
378
19
    return Status::OK();
379
19
}
380
381
} // namespace doris