Coverage Report

Created: 2026-11-04 23:49

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/format/transformer/vparquet_writer.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/transformer/vparquet_writer.h"
19
20
#include <arrow/io/type_fwd.h>
21
#include <arrow/table.h>
22
#include <glog/logging.h>
23
#include <parquet/api/reader.h>
24
#include <parquet/column_writer.h>
25
#include <parquet/platform.h>
26
#include <parquet/schema.h>
27
#include <parquet/type_fwd.h>
28
#include <parquet/types.h>
29
30
#include <algorithm>
31
#include <ctime>
32
#include <exception>
33
#include <ostream>
34
#include <string>
35
36
#include "common/config.h"
37
#include "common/status.h"
38
#include "exprs/vexpr.h"
39
#include "exprs/vexpr_context.h"
40
#include "format/arrow/arrow_row_batch.h"
41
#include "format/arrow/arrow_utils.h"
42
#include "format/parquet/parquet_arrow_block_convertor.h"
43
#include "io/fs/file_writer.h"
44
#include "runtime/exec_env.h"
45
#include "runtime/runtime_state.h"
46
#include "util/debug_util.h"
47
#include "util/timezone_utils.h"
48
49
namespace doris {
50
namespace {
51
constexpr int64_t HADOOP_LZ4_DEFAULT_BUFFER_SIZE = 256 * 1024;
52
} // namespace
53
54
ParquetOutputStream::ParquetOutputStream(doris::io::FileWriter* file_writer)
55
46
        : _file_writer(file_writer), _cur_pos(0), _written_len(0) {
56
46
    set_mode(arrow::io::FileMode::WRITE);
57
46
}
Unexecuted instantiation: _ZN5doris19ParquetOutputStreamC2EPNS_2io10FileWriterE
_ZN5doris19ParquetOutputStreamC1EPNS_2io10FileWriterE
Line
Count
Source
55
46
        : _file_writer(file_writer), _cur_pos(0), _written_len(0) {
56
46
    set_mode(arrow::io::FileMode::WRITE);
57
46
}
58
59
46
ParquetOutputStream::~ParquetOutputStream() {
60
46
    arrow::Status st = Close();
61
46
    if (!st.ok()) {
62
0
        LOG(WARNING) << "close parquet file error: " << st.ToString();
63
0
    }
64
46
}
65
66
1.44k
arrow::Status ParquetOutputStream::Write(const void* data, int64_t nbytes) {
67
1.44k
    if (_is_closed) {
68
0
        return arrow::Status::OK();
69
0
    }
70
1.44k
    size_t written_len = nbytes;
71
1.44k
    Status st = _file_writer->append({static_cast<const uint8_t*>(data), written_len});
72
1.44k
    if (!st.ok()) {
73
0
        return arrow::Status::IOError(st.to_string());
74
0
    }
75
1.44k
    _cur_pos += written_len;
76
1.44k
    _written_len += written_len;
77
1.44k
    return arrow::Status::OK();
78
1.44k
}
79
80
2.24k
arrow::Result<int64_t> ParquetOutputStream::Tell() const {
81
2.24k
    return _cur_pos;
82
2.24k
}
83
84
92
arrow::Status ParquetOutputStream::Close() {
85
92
    if (!_is_closed) {
86
46
        Defer defer {[this] { _is_closed = true; }};
87
46
        Status st = _file_writer->close();
88
46
        if (!st.ok()) {
89
0
            LOG(WARNING) << "close parquet output stream failed: " << st;
90
0
            return arrow::Status::IOError(st.to_string());
91
0
        }
92
46
    }
93
92
    return arrow::Status::OK();
94
92
}
95
96
12
int64_t ParquetOutputStream::get_written_len() const {
97
12
    return _written_len;
98
12
}
99
100
0
void ParquetOutputStream::set_written_len(int64_t written_len) {
101
0
    _written_len = written_len;
102
0
}
103
104
Result<TParquetCompressionType::type> ParquetBuildHelper::to_parquet_compression_type(
105
11
        TFileCompressType::type compression_type) {
106
11
    switch (compression_type) {
107
10
    case TFileCompressType::PLAIN:
108
10
        return TParquetCompressionType::UNCOMPRESSED;
109
1
    case TFileCompressType::SNAPPYBLOCK:
110
1
        return TParquetCompressionType::SNAPPY;
111
0
    case TFileCompressType::ZSTD:
112
0
        return TParquetCompressionType::ZSTD;
113
0
    case TFileCompressType::GZ:
114
0
        return TParquetCompressionType::GZIP;
115
0
    case TFileCompressType::LZ4BLOCK:
116
0
        return TParquetCompressionType::LZ4_HADOOP;
117
0
    default:
118
0
        return ResultError(Status::InternalError("Unsupported compress type {} with parquet",
119
0
                                                 to_string(compression_type)));
120
11
    }
121
11
}
122
123
void ParquetBuildHelper::build_compression_type(
124
        ::parquet::WriterProperties::Builder& builder,
125
46
        const TParquetCompressionType::type& compression_type) {
126
46
    switch (compression_type) {
127
16
    case TParquetCompressionType::SNAPPY: {
128
16
        builder.compression(arrow::Compression::SNAPPY);
129
16
        break;
130
0
    }
131
0
    case TParquetCompressionType::GZIP: {
132
0
        builder.compression(arrow::Compression::GZIP);
133
0
        break;
134
0
    }
135
0
    case TParquetCompressionType::BROTLI: {
136
0
        builder.compression(arrow::Compression::BROTLI);
137
0
        break;
138
0
    }
139
0
    case TParquetCompressionType::ZSTD: {
140
0
        builder.compression(arrow::Compression::ZSTD);
141
0
        break;
142
0
    }
143
2
    case TParquetCompressionType::LZ4: {
144
2
        builder.compression(arrow::Compression::LZ4);
145
2
        break;
146
0
    }
147
2
    case TParquetCompressionType::LZ4_HADOOP: {
148
        // Hadoop-framed LZ4 -> Parquet thrift codec "LZ4" (deprecated). This matches what
149
        // Spark/Iceberg writes for `write.parquet.compression-codec=lz4`. Arrow 17 emits one
150
        // Hadoop LZ4 block per Parquet page/dictionary page, while Hadoop JVM readers default to
151
        // a 256 KiB LZ4 codec buffer, so keep page targets below that buffer size.
152
2
        builder.compression(arrow::Compression::LZ4_HADOOP);
153
2
        builder.data_pagesize(HADOOP_LZ4_DEFAULT_BUFFER_SIZE / 2);
154
2
        builder.dictionary_pagesize_limit(HADOOP_LZ4_DEFAULT_BUFFER_SIZE / 2);
155
2
        break;
156
0
    }
157
    // arrow do not support lzo and bz2 compression type.
158
    // case TParquetCompressionType::LZO: {
159
    //     builder.compression(arrow::Compression::LZO);
160
    //     break;
161
    // }
162
    // case TParquetCompressionType::BZ2: {
163
    //     builder.compression(arrow::Compression::BZ2);
164
    //     break;
165
    // }
166
26
    case TParquetCompressionType::UNCOMPRESSED: {
167
26
        builder.compression(arrow::Compression::UNCOMPRESSED);
168
26
        break;
169
0
    }
170
0
    default:
171
0
        builder.compression(arrow::Compression::SNAPPY);
172
46
    }
173
46
}
174
175
void ParquetBuildHelper::build_version(::parquet::WriterProperties::Builder& builder,
176
46
                                       const TParquetVersion::type& parquet_version) {
177
46
    switch (parquet_version) {
178
38
    case TParquetVersion::PARQUET_1_0: {
179
38
        builder.version(::parquet::ParquetVersion::PARQUET_1_0);
180
38
        break;
181
0
    }
182
8
    case TParquetVersion::PARQUET_2_LATEST: {
183
8
        builder.version(::parquet::ParquetVersion::PARQUET_2_LATEST);
184
8
        break;
185
0
    }
186
0
    default:
187
0
        builder.version(::parquet::ParquetVersion::PARQUET_1_0);
188
46
    }
189
46
}
190
191
VParquetWriter::VParquetWriter(RuntimeState* state, doris::io::FileWriter* file_writer,
192
                               const VExprContextSPtrs& output_vexpr_ctxs,
193
                               std::vector<std::string> column_names, bool output_object_data,
194
                               const ParquetFileOptions& parquet_options)
195
46
        : VFileFormatTransformer(state, output_vexpr_ctxs, output_object_data),
196
46
          _column_names(std::move(column_names)),
197
46
          _parquet_options(parquet_options) {
198
46
    _outstream = std::shared_ptr<ParquetOutputStream>(new ParquetOutputStream(file_writer));
199
46
}
200
201
VParquetWriter::VParquetWriter(RuntimeState* state, doris::io::FileWriter* file_writer,
202
                               const VExprContextSPtrs& output_vexpr_ctxs,
203
                               std::vector<TParquetSchema> parquet_schemas, bool output_object_data,
204
                               const ParquetFileOptions& parquet_options)
205
0
        : VFileFormatTransformer(state, output_vexpr_ctxs, output_object_data),
206
0
          _parquet_schemas(std::move(parquet_schemas)),
207
0
          _parquet_options(parquet_options) {
208
0
    _outstream = std::shared_ptr<ParquetOutputStream>(new ParquetOutputStream(file_writer));
209
0
}
210
211
46
Status VParquetWriter::_parse_properties() {
212
46
    try {
213
46
        arrow::MemoryPool* pool = ExecEnv::GetInstance()->arrow_memory_pool();
214
215
        //build parquet writer properties
216
46
        ::parquet::WriterProperties::Builder builder;
217
46
        ParquetBuildHelper::build_compression_type(builder, _parquet_options.compression_type);
218
46
        ParquetBuildHelper::build_version(builder, _parquet_options.parquet_version);
219
46
        const auto& query_options = _state->query_options();
220
        // Preserve the current enabled default explicitly, including when older FEs omit the field.
221
46
        if (query_options.enable_parquet_write_page_index) {
222
40
            builder.enable_write_page_index();
223
40
        } else {
224
6
            builder.disable_write_page_index();
225
6
        }
226
46
        int64_t page_size = query_options.parquet_write_data_page_size;
227
46
        DORIS_CHECK_GT(page_size, 0);
228
        // A 1 MiB session target must still respect the 128 KiB Hadoop-framed LZ4 limit.
229
46
        if (_parquet_options.compression_type == TParquetCompressionType::LZ4_HADOOP) {
230
2
            page_size = std::min(page_size, HADOOP_LZ4_DEFAULT_BUFFER_SIZE / 2);
231
2
        }
232
46
        builder.data_pagesize(page_size);
233
46
        if (_parquet_options.parquet_disable_dictionary) {
234
5
            builder.disable_dictionary();
235
41
        } else {
236
41
            builder.enable_dictionary();
237
41
        }
238
46
        builder.created_by(
239
46
                fmt::format("{}({})", doris::get_short_version(), ::parquet::DEFAULT_CREATED_BY));
240
46
        builder.max_row_group_length(std::numeric_limits<int64_t>::max());
241
46
        builder.memory_pool(pool);
242
46
        _parquet_writer_properties = builder.build();
243
244
        //build arrow  writer properties
245
46
        ::parquet::ArrowWriterProperties::Builder arrow_builder;
246
46
        if (_parquet_options.enable_int96_timestamps) {
247
8
            arrow_builder.enable_force_write_int96_timestamps();
248
8
        }
249
46
        arrow_builder.store_schema();
250
46
        _arrow_properties = arrow_builder.build();
251
46
    } catch (const ::parquet::ParquetException& e) {
252
0
        return Status::InternalError("parquet writer parse properties error: {}", e.what());
253
0
    }
254
46
    return Status::OK();
255
46
}
256
257
std::unique_ptr<ArrowBlockConvertor> VParquetWriter::_create_arrow_block_convertor(
258
        DataTypes types, std::vector<std::string> names, const std::string& timezone_name,
259
16
        const cctz::time_zone& timezone, bool enable_int96_timestamps) const {
260
16
    return std::make_unique<ParquetArrowBlockConvertor>(
261
16
            std::move(types), std::move(names), timezone_name, timezone, enable_int96_timestamps);
262
16
}
263
264
43
Status VParquetWriter::write(const Block& block) {
265
43
    if (block.rows() == 0) {
266
0
        return Status::OK();
267
0
    }
268
269
    // serialize
270
43
    std::shared_ptr<arrow::RecordBatch> result;
271
43
    RETURN_IF_ERROR(_arrow_block_convertor->convert_to_arrow(
272
43
            block, ExecEnv::GetInstance()->arrow_memory_pool(), &result));
273
43
    if (_write_size == 0) {
274
43
        RETURN_DORIS_STATUS_IF_ERROR(_writer->NewBufferedRowGroup());
275
43
    }
276
43
    RETURN_DORIS_STATUS_IF_ERROR(_writer->WriteRecordBatch(*result));
277
43
    _write_size += block.bytes();
278
43
    if (_write_size >= doris::config::min_row_group_size) {
279
7
        _write_size = 0;
280
7
    }
281
43
    return Status::OK();
282
43
}
283
284
44
arrow::Status VParquetWriter::_open_file_writer() {
285
44
    ARROW_ASSIGN_OR_RAISE(_writer, ::parquet::arrow::FileWriter::Open(
286
44
                                           *_arrow_block_convertor->arrow_schema(),
287
44
                                           ExecEnv::GetInstance()->arrow_memory_pool(), _outstream,
288
44
                                           _parquet_writer_properties, _arrow_properties));
289
44
    return arrow::Status::OK();
290
44
}
291
292
44
Status VParquetWriter::open() {
293
44
    _timezone = _state->timezone();
294
44
    _timezone_obj = _state->timezone_obj();
295
44
    if (_parquet_options.enable_int96_timestamps && _parquet_options.int96_timezone.has_value()) {
296
4
        _timezone = *_parquet_options.int96_timezone;
297
        // Cache the override on this writer, never mutate the shared query RuntimeState.
298
4
        if (!TimezoneUtils::find_cctz_time_zone(_timezone, _timezone_obj)) {
299
0
            return Status::InvalidArgument("Invalid Parquet INT96 writer timezone: {}", _timezone);
300
0
        }
301
4
    }
302
44
    RETURN_IF_ERROR(_parse_properties());
303
44
    DataTypes types;
304
44
    types.reserve(_output_vexpr_ctxs.size());
305
436
    for (const auto& context : _output_vexpr_ctxs) {
306
436
        types.emplace_back(context->root()->data_type());
307
436
    }
308
44
    std::vector<std::string> names = _column_names;
309
44
    if (!_parquet_schemas.empty()) {
310
0
        names.clear();
311
0
        names.reserve(_parquet_schemas.size());
312
0
        for (const auto& schema : _parquet_schemas) {
313
0
            names.emplace_back(schema.schema_column_name);
314
0
        }
315
0
    }
316
44
    _arrow_block_convertor =
317
44
            _create_arrow_block_convertor(std::move(types), std::move(names), _timezone,
318
44
                                          _timezone_obj, _parquet_options.enable_int96_timestamps);
319
44
    RETURN_IF_ERROR(_arrow_block_convertor->init());
320
44
    try {
321
44
        RETURN_DORIS_STATUS_IF_ERROR(_open_file_writer());
322
44
    } catch (const ::parquet::ParquetStatusException& e) {
323
0
        LOG(WARNING) << "parquet file writer open error: " << e.what();
324
0
        return Status::InternalError("parquet file writer open error: {}", e.what());
325
0
    }
326
44
    if (_writer == nullptr) {
327
0
        return Status::InternalError("Failed to create file writer");
328
0
    }
329
44
    return Status::OK();
330
44
}
331
332
12
int64_t VParquetWriter::written_len() {
333
12
    return _outstream->get_written_len();
334
12
}
335
336
44
Status VParquetWriter::close() {
337
44
    try {
338
44
        if (_writer != nullptr) {
339
44
            RETURN_DORIS_STATUS_IF_ERROR(_writer->Close());
340
44
        }
341
44
        RETURN_DORIS_STATUS_IF_ERROR(_outstream->Close());
342
343
44
    } catch (const std::exception& e) {
344
0
        LOG(WARNING) << "Parquet writer close error: " << e.what();
345
0
        return Status::IOError(e.what());
346
0
    }
347
348
44
    return Status::OK();
349
44
}
350
351
} // namespace doris