Coverage Report

Created: 2026-07-17 11:15

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/exprs/aggregate/aggregate_function_python_udaf.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 "exprs/aggregate/aggregate_function_python_udaf.h"
19
20
#include <arrow/array.h>
21
#include <arrow/memory_pool.h>
22
#include <arrow/record_batch.h>
23
#include <arrow/type.h>
24
#include <fmt/format.h>
25
26
#include "common/exception.h"
27
#include "common/logging.h"
28
#include "core/block/block.h"
29
#include "core/column/column_nullable.h"
30
#include "core/column/column_vector.h"
31
#include "core/data_type/data_type_factory.hpp"
32
#include "core/data_type/data_type_nullable.h"
33
#include "core/data_type/define_primitive_type.h"
34
#include "format/arrow/arrow_block_convertor.h"
35
#include "format/arrow/arrow_row_batch.h"
36
#include "runtime/exec_env.h"
37
#include "runtime/user_function_cache.h"
38
#include "udf/python/python_env.h"
39
#include "udf/python/python_server.h"
40
#include "util/timezone_utils.h"
41
42
namespace doris {
43
44
0
Status AggregatePythonUDAFData::create(int64_t place) {
45
0
    DCHECK(client) << "Client must be set before calling create";
46
0
    RETURN_IF_ERROR(client->create(place));
47
0
    return Status::OK();
48
0
}
49
50
Status AggregatePythonUDAFData::add(int64_t place_id, const IColumn** columns,
51
                                    int64_t row_num_start, int64_t row_num_end,
52
0
                                    const DataTypes& argument_types) {
53
0
    DCHECK(client) << "Client must be set before calling add";
54
55
    // Zero-copy: Use full columns with range specification
56
0
    Block input_block;
57
0
    for (size_t i = 0; i < argument_types.size(); ++i) {
58
0
        input_block.insert(
59
0
                ColumnWithTypeAndName(columns[i]->get_ptr(), argument_types[i], std::to_string(i)));
60
0
    }
61
62
0
    std::shared_ptr<arrow::Schema> schema;
63
0
    RETURN_IF_ERROR(
64
0
            get_arrow_schema_from_block(input_block, &schema, TimezoneUtils::default_time_zone));
65
0
    cctz::time_zone timezone_obj;
66
0
    TimezoneUtils::find_cctz_time_zone(TimezoneUtils::default_time_zone, timezone_obj);
67
68
0
    std::shared_ptr<arrow::RecordBatch> batch;
69
    // Zero-copy: convert only the specified range
70
0
    RETURN_IF_ERROR(convert_to_arrow_batch(input_block, schema,
71
0
                                           ExecEnv::GetInstance()->arrow_memory_pool(), &batch,
72
0
                                           timezone_obj, row_num_start, row_num_end));
73
    // Send the batch (already sliced in convert_to_arrow_batch)
74
    // Single place mode: no places column needed
75
0
    RETURN_IF_ERROR(client->accumulate(place_id, true, *batch, 0, batch->num_rows()));
76
0
    return Status::OK();
77
0
}
78
79
Status AggregatePythonUDAFData::add_batch(AggregateDataPtr* places, size_t place_offset,
80
                                          size_t num_rows, const IColumn** columns,
81
                                          const DataTypes& argument_types, size_t start,
82
0
                                          size_t end) {
83
0
    DCHECK(client) << "Client must be set before calling add_batch";
84
0
    DCHECK(end > start) << "end must be greater than start";
85
0
    DCHECK(end <= num_rows) << "end must not exceed num_rows";
86
87
0
    size_t slice_rows = end - start;
88
0
    Block input_block;
89
0
    for (size_t i = 0; i < argument_types.size(); ++i) {
90
0
        DCHECK(columns[i]->size() == num_rows) << "Column size must match num_rows";
91
0
        input_block.insert(
92
0
                ColumnWithTypeAndName(columns[i]->get_ptr(), argument_types[i], std::to_string(i)));
93
0
    }
94
95
0
    auto places_col = ColumnInt64::create(num_rows);
96
0
    auto& places_data = places_col->get_data();
97
98
    // Fill places column with place IDs for the slice [start, end)
99
0
    for (size_t i = start; i < end; ++i) {
100
0
        places_data[i] = reinterpret_cast<int64_t>(places[i] + place_offset);
101
0
    }
102
103
0
    static DataTypePtr places_type =
104
0
            DataTypeFactory::instance().create_data_type(PrimitiveType::TYPE_BIGINT, false);
105
0
    input_block.insert(ColumnWithTypeAndName(std::move(places_col), places_type, "places"));
106
107
0
    std::shared_ptr<arrow::Schema> schema;
108
0
    RETURN_IF_ERROR(
109
0
            get_arrow_schema_from_block(input_block, &schema, TimezoneUtils::default_time_zone));
110
0
    cctz::time_zone timezone_obj;
111
0
    TimezoneUtils::find_cctz_time_zone(TimezoneUtils::default_time_zone, timezone_obj);
112
113
0
    std::shared_ptr<arrow::RecordBatch> batch;
114
    // Zero-copy: convert only the [start, end) range
115
    // This slice includes the places column automatically
116
0
    RETURN_IF_ERROR(convert_to_arrow_batch(input_block, schema,
117
0
                                           ExecEnv::GetInstance()->arrow_memory_pool(), &batch,
118
0
                                           timezone_obj, start, end));
119
    // Send entire batch (already contains places column) to Python
120
    // place_id=0 is ignored when is_single_place=false
121
0
    RETURN_IF_ERROR(client->accumulate(0, false, *batch, 0, slice_rows));
122
0
    return Status::OK();
123
0
}
124
125
0
Status AggregatePythonUDAFData::merge(const AggregatePythonUDAFData& rhs, int64_t place) {
126
0
    DCHECK(client) << "Client must be set before calling merge";
127
128
    // Get serialized state from rhs (already stored in serialize_data by read())
129
0
    auto serialized_state = arrow::Buffer::Wrap(
130
0
            reinterpret_cast<const uint8_t*>(rhs.serialize_data.data()), rhs.serialize_data.size());
131
0
    RETURN_IF_ERROR(client->merge(place, serialized_state));
132
0
    return Status::OK();
133
0
}
134
135
0
Status AggregatePythonUDAFData::write(BufferWritable& buf, int64_t place) const {
136
0
    DCHECK(client) << "Client must be set before calling write";
137
138
    // Serialize state from Python server
139
0
    std::shared_ptr<arrow::Buffer> serialized_state;
140
0
    RETURN_IF_ERROR(client->serialize(place, &serialized_state));
141
0
    const char* data = reinterpret_cast<const char*>(serialized_state->data());
142
0
    size_t size = serialized_state->size();
143
0
    buf.write_binary(StringRef {data, size});
144
0
    return Status::OK();
145
0
}
146
147
0
void AggregatePythonUDAFData::read(BufferReadable& buf) {
148
    // Read serialized state from buffer into serialize_data
149
    // This will be used later by merge() in deserialize_and_merge()
150
0
    buf.read_binary(serialize_data);
151
0
}
152
153
0
Status AggregatePythonUDAFData::reset(int64_t place) {
154
0
    DCHECK(client) << "Client must be set before calling reset";
155
0
    RETURN_IF_ERROR(client->reset(place));
156
    // After reset, state still exists but is back to initial state
157
0
    return Status::OK();
158
0
}
159
160
0
Status AggregatePythonUDAFData::destroy(int64_t place) {
161
0
    DCHECK(client) << "Client must be set before calling destroy";
162
0
    RETURN_IF_ERROR(client->destroy(place));
163
0
    return Status::OK();
164
0
}
165
166
Status AggregatePythonUDAFData::get(IColumn& to, const DataTypePtr& result_type,
167
0
                                    int64_t place) const {
168
0
    DCHECK(client) << "Client must be set before calling get";
169
170
    // Get final result from Python server
171
0
    std::shared_ptr<arrow::RecordBatch> result;
172
0
    RETURN_IF_ERROR(client->finalize(place, &result));
173
174
    // Convert Arrow RecordBatch to Block
175
0
    Block result_block;
176
0
    DataTypes types = {result_type};
177
0
    cctz::time_zone timezone_obj;
178
0
    TimezoneUtils::find_cctz_time_zone(TimezoneUtils::default_time_zone, timezone_obj);
179
0
    RETURN_IF_ERROR(convert_from_arrow_batch(result, types, &result_block, timezone_obj));
180
181
    // Insert the result value into output column
182
0
    if (result_block.rows() != 1) {
183
0
        return Status::InternalError("Expected 1 row in result block, got {}", result_block.rows());
184
0
    }
185
186
0
    auto& result_column = result_block.get_by_position(0).column;
187
0
    to.insert_from(*result_column, 0);
188
0
    return Status::OK();
189
0
}
190
191
0
Status AggregatePythonUDAF::open() {
192
    // Build function metadata from TFunction
193
0
    _func_meta.id = _fn.id;
194
0
    _func_meta.name = _fn.name.function_name;
195
196
    // For UDAF, symbol is in aggregate_fn
197
0
    if (_fn.__isset.aggregate_fn && _fn.aggregate_fn.__isset.symbol) {
198
0
        _func_meta.symbol = _fn.aggregate_fn.symbol;
199
0
    } else {
200
0
        return Status::InvalidArgument("Python UDAF symbol is not set");
201
0
    }
202
203
    // Determine load type (inline code or module)
204
0
    if (!_fn.function_code.empty()) {
205
0
        _func_meta.type = PythonUDFLoadType::INLINE;
206
0
        _func_meta.location = "inline";
207
0
        _func_meta.inline_code = _fn.function_code;
208
0
    } else if (!_fn.hdfs_location.empty()) {
209
0
        _func_meta.type = PythonUDFLoadType::MODULE;
210
0
        _func_meta.location = _fn.hdfs_location;
211
0
        _func_meta.checksum = _fn.checksum;
212
0
    } else {
213
0
        _func_meta.type = PythonUDFLoadType::UNKNOWN;
214
0
        _func_meta.location = "unknown";
215
0
    }
216
217
0
    _func_meta.input_types = argument_types;
218
0
    _func_meta.return_type = _return_type;
219
0
    _func_meta.client_type = PythonClientType::UDAF;
220
221
    // Get Python version
222
0
    if (_fn.__isset.runtime_version && !_fn.runtime_version.empty()) {
223
0
        RETURN_IF_ERROR(PythonVersionManager::instance().get_version(_fn.runtime_version,
224
0
                                                                     &_python_version));
225
0
    } else {
226
0
        return Status::InvalidArgument("Python UDAF runtime version is not set");
227
0
    }
228
229
0
    _func_meta.runtime_version = _python_version.full_version;
230
0
    RETURN_IF_ERROR(_func_meta.check());
231
0
    _func_meta.always_nullable = _return_type->is_nullable();
232
233
0
    LOG(INFO) << fmt::format("Creating Python UDAF: {}, runtime_version: {}, func_meta: {}",
234
0
                             _fn.name.function_name, _python_version.to_string(),
235
0
                             _func_meta.to_string());
236
237
0
    if (_func_meta.type == PythonUDFLoadType::MODULE) {
238
0
        RETURN_IF_ERROR(UserFunctionCache::instance()->get_pypath(
239
0
                _func_meta.id, _func_meta.location, _func_meta.checksum, &_func_meta.location));
240
0
    }
241
242
0
    return Status::OK();
243
0
}
244
245
0
void AggregatePythonUDAF::create(AggregateDataPtr __restrict place) const {
246
0
    std::call_once(_schema_init_flag, [this]() {
247
0
        std::vector<std::shared_ptr<arrow::Field>> fields;
248
249
0
        std::string timezone = TimezoneUtils::default_time_zone;
250
0
        for (size_t i = 0; i < argument_types.size(); ++i) {
251
0
            std::shared_ptr<arrow::DataType> arrow_type;
252
0
            Status st = convert_to_arrow_type(argument_types[i], &arrow_type, timezone);
253
0
            if (!st.ok()) {
254
0
                throw doris::Exception(ErrorCode::INTERNAL_ERROR,
255
0
                                       "Failed to convert argument type {} to Arrow type: {}", i,
256
0
                                       st.to_string());
257
0
            }
258
0
            fields.push_back(create_arrow_field_with_metadata(
259
0
                    std::to_string(i), arrow_type, argument_types[i]->is_nullable(),
260
0
                    argument_types[i]->get_primitive_type()));
261
0
        }
262
263
        // Add places column for GROUP BY aggregation (always included, NULL in single-place mode)
264
0
        fields.push_back(arrow::field("places", arrow::int64()));
265
        // Add binary_data column for merge operations
266
0
        fields.push_back(arrow::field("binary_data", arrow::binary()));
267
0
        _schema = arrow::schema(fields);
268
0
    });
269
270
    // Initialize the data structure
271
0
    new (place) Data();
272
0
    DCHECK(reinterpret_cast<Data*>(place)) << "Place must not be null";
273
274
0
    if (Status st = PythonServerManager::instance().get_client(
275
0
                _func_meta, _python_version, &(this->data(place).client), _schema);
276
0
        UNLIKELY(!st.ok())) {
277
0
        this->data(place).~Data();
278
0
        throw doris::Exception(ErrorCode::INTERNAL_ERROR, "Failed to get Python UDAF client: {}",
279
0
                               st.to_string());
280
0
    }
281
282
    // Initialize UDAF state in Python server
283
0
    int64_t place_id = reinterpret_cast<int64_t>(place);
284
0
    if (Status st = this->data(place).create(place_id); UNLIKELY(!st.ok())) {
285
0
        this->data(place).~Data();
286
0
        throw doris::Exception(ErrorCode::INTERNAL_ERROR, st.to_string());
287
0
    }
288
0
}
289
290
0
void AggregatePythonUDAF::destroy(AggregateDataPtr __restrict place) const noexcept {
291
0
    try {
292
0
        int64_t place_id = reinterpret_cast<int64_t>(place);
293
294
        // Destroy state in Python server
295
0
        if (this->data(place).client) {
296
0
            Status st = this->data(place).destroy(place_id);
297
0
            if (UNLIKELY(!st.ok())) {
298
0
                LOG(WARNING) << "Failed to destroy Python UDAF state for place_id=" << place_id
299
0
                             << ", function=" << _func_meta.name << ": " << st.to_string();
300
0
            }
301
302
0
            this->data(place).client.reset();
303
0
        }
304
305
0
        this->data(place).~Data();
306
0
    } catch (const std::exception& e) {
307
0
        LOG(ERROR) << "Exception in AggregatePythonUDAF::destroy: " << e.what();
308
0
    } catch (...) {
309
0
        LOG(ERROR) << "Unknown exception in AggregatePythonUDAF::destroy";
310
0
    }
311
0
}
312
313
void AggregatePythonUDAF::add(AggregateDataPtr __restrict place, const IColumn** columns,
314
0
                              ssize_t row_num, Arena&) const {
315
0
    int64_t place_id = reinterpret_cast<int64_t>(place);
316
0
    Status st = this->data(place).add(place_id, columns, row_num, row_num + 1, argument_types);
317
0
    if (UNLIKELY(!st.ok())) {
318
0
        throw doris::Exception(ErrorCode::INTERNAL_ERROR, st.to_string());
319
0
    }
320
0
}
321
322
void AggregatePythonUDAF::add_batch(size_t batch_size, AggregateDataPtr* places,
323
                                    size_t place_offset, const IColumn** columns, Arena&,
324
0
                                    bool /*agg_many*/) const {
325
0
    if (batch_size == 0) return;
326
327
0
    size_t start = 0;
328
0
    while (start < batch_size) {
329
        // Get the starting place for this segment
330
0
        AggregateDataPtr start_place = places[start] + place_offset;
331
0
        auto& start_place_data = this->data(start_place);
332
        // Get the process for this segment
333
0
        const auto* current_process = start_place_data.client->get_process().get();
334
335
        // Scan forward to find the end of this consecutive segment (same process)
336
0
        size_t end = start + 1;
337
0
        while (end < batch_size) {
338
0
            AggregateDataPtr end_place = places[end] + place_offset;
339
0
            auto& end_place_data = this->data(end_place);
340
0
            const auto* next_process = end_place_data.client->get_process().get();
341
            // If different process, end the current segment
342
0
            if (*next_process != *current_process) break;
343
0
            ++end;
344
0
        }
345
346
        // Send this segment to Python with zero-copy
347
        // Pass places array and let add_batch construct place_ids on-demand
348
0
        Status st = start_place_data.add_batch(places, place_offset, batch_size, columns,
349
0
                                               argument_types, start, end);
350
351
0
        if (UNLIKELY(!st.ok())) {
352
0
            throw doris::Exception(ErrorCode::INTERNAL_ERROR,
353
0
                                   "Failed to send segment to Python: " + st.to_string());
354
0
        }
355
356
0
        start = end;
357
0
    }
358
0
}
359
360
void AggregatePythonUDAF::add_batch_single_place(size_t batch_size, AggregateDataPtr place,
361
0
                                                 const IColumn** columns, Arena&) const {
362
0
    int64_t place_id = reinterpret_cast<int64_t>(place);
363
0
    Status st = this->data(place).add(place_id, columns, 0, batch_size, argument_types);
364
0
    if (UNLIKELY(!st.ok())) {
365
0
        throw doris::Exception(ErrorCode::INTERNAL_ERROR, st.to_string());
366
0
    }
367
0
}
368
369
void AggregatePythonUDAF::add_range_single_place(int64_t partition_start, int64_t partition_end,
370
                                                 int64_t frame_start, int64_t frame_end,
371
                                                 AggregateDataPtr place, const IColumn** columns,
372
                                                 Arena& arena, UInt8* current_window_empty,
373
0
                                                 UInt8* current_window_has_inited) const {
374
    // Calculate actual frame range
375
0
    frame_start = std::max<int64_t>(frame_start, partition_start);
376
0
    frame_end = std::min<int64_t>(frame_end, partition_end);
377
378
0
    if (frame_start >= frame_end) {
379
0
        if (!*current_window_has_inited) {
380
0
            *current_window_empty = true;
381
0
        }
382
0
        return;
383
0
    }
384
385
0
    int64_t place_id = reinterpret_cast<int64_t>(place);
386
0
    Status st = this->data(place).add(place_id, columns, frame_start, frame_end, argument_types);
387
0
    if (UNLIKELY(!st.ok())) {
388
0
        throw doris::Exception(ErrorCode::INTERNAL_ERROR, st.to_string());
389
0
    }
390
391
0
    *current_window_empty = false;
392
0
    *current_window_has_inited = true;
393
0
}
394
395
0
void AggregatePythonUDAF::reset(AggregateDataPtr place) const {
396
0
    int64_t place_id = reinterpret_cast<int64_t>(place);
397
0
    Status st = this->data(place).reset(place_id);
398
0
    if (UNLIKELY(!st.ok())) {
399
0
        throw doris::Exception(ErrorCode::INTERNAL_ERROR, st.to_string());
400
0
    }
401
0
}
402
403
void AggregatePythonUDAF::merge(AggregateDataPtr __restrict place, ConstAggregateDataPtr rhs,
404
0
                                Arena&) const {
405
0
    int64_t place_id = reinterpret_cast<int64_t>(place);
406
0
    Status st = this->data(place).merge(this->data(rhs), place_id);
407
0
    if (UNLIKELY(!st.ok())) {
408
0
        throw doris::Exception(ErrorCode::INTERNAL_ERROR, st.to_string());
409
0
    }
410
0
}
411
412
void AggregatePythonUDAF::serialize(ConstAggregateDataPtr __restrict place,
413
0
                                    BufferWritable& buf) const {
414
0
    int64_t place_id = reinterpret_cast<int64_t>(place);
415
0
    Status st = this->data(place).write(buf, place_id);
416
0
    if (UNLIKELY(!st.ok())) {
417
0
        throw doris::Exception(ErrorCode::INTERNAL_ERROR, st.to_string());
418
0
    }
419
0
}
420
421
void AggregatePythonUDAF::deserialize(AggregateDataPtr __restrict place, BufferReadable& buf,
422
0
                                      Arena&) const {
423
0
    this->data(place).read(buf);
424
0
}
425
426
void AggregatePythonUDAF::insert_result_into(ConstAggregateDataPtr __restrict place,
427
0
                                             IColumn& to) const {
428
0
    int64_t place_id = reinterpret_cast<int64_t>(place);
429
0
    Status st = this->data(place).get(to, _return_type, place_id);
430
0
    if (UNLIKELY(!st.ok())) {
431
0
        throw doris::Exception(ErrorCode::INTERNAL_ERROR, st.to_string());
432
0
    }
433
0
}
434
435
} // namespace doris