Coverage Report

Created: 2026-07-09 05:00

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/udf/python/python_udaf_client.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 "udf/python/python_udaf_client.h"
19
20
#include <arrow/array/builder_base.h>
21
#include <arrow/array/builder_binary.h>
22
#include <arrow/array/builder_primitive.h>
23
#include <arrow/flight/client.h>
24
#include <arrow/flight/server.h>
25
#include <arrow/io/memory.h>
26
#include <arrow/ipc/writer.h>
27
#include <arrow/record_batch.h>
28
#include <arrow/type.h>
29
30
#include "common/compiler_util.h"
31
#include "common/status.h"
32
#include "format/arrow/arrow_utils.h"
33
#include "runtime/exec_env.h"
34
#include "udf/python/python_udf_meta.h"
35
#include "udf/python/python_udf_runtime.h"
36
#include "util/unaligned.h"
37
38
namespace doris {
39
40
// Unified response structure for UDAF operations
41
// Arrow Schema: [success: bool, rows_processed: int64, data: binary]
42
// Different operations use different fields:
43
// - CREATE/MERGE/RESET/DESTROY: use success only
44
// - ACCUMULATE: use success + rows_processed (number of rows processed)
45
// - SERIALIZE: use success + data (serialized_state)
46
// - FINALIZE: use success + data (serialized result, may be null)
47
// - Any failed operation: use success=false + data (UTF-8 error message)
48
//
49
// This unified schema allows all operations to return consistent format,
50
// solving Arrow Flight's limitation that all responses must have the same schema.
51
static const std::shared_ptr<arrow::Schema> kUnifiedUDAFResponseSchema = arrow::schema({
52
        arrow::field("success", arrow::boolean()),
53
        arrow::field("rows_processed", arrow::int64()),
54
        arrow::field("serialized_data", arrow::binary()),
55
});
56
57
Status PythonUDAFClient::make_udaf_failure_status(
58
        const std::shared_ptr<arrow::RecordBatch>& response, const char* operation,
59
11
        int64_t place_id) {
60
11
    if (response == nullptr || response->num_rows() != 1 ||
61
11
        response->num_columns() != kUnifiedUDAFResponseSchema->num_fields()) [[unlikely]] {
62
3
        return Status::InternalError("Invalid {} failure response for place_id={}", operation,
63
3
                                     place_id);
64
3
    }
65
66
8
    auto data_array = std::static_pointer_cast<arrow::BinaryArray>(response->column(2));
67
8
    if (data_array->IsNull(0)) {
68
1
        return Status::InternalError("{} operation failed for place_id={}", operation, place_id);
69
1
    }
70
71
7
    const auto* offsets = data_array->raw_value_offsets();
72
7
    if (offsets == nullptr) [[unlikely]] {
73
0
        return Status::InternalError("Invalid {} failure response for place_id={}: null offsets",
74
0
                                     operation, place_id);
75
0
    }
76
    // Arrow Flight buffers may be unaligned after IPC deserialization
77
7
    int32_t offset_start = unaligned_load<int32_t>(offsets);
78
7
    int32_t offset_end = unaligned_load<int32_t>(offsets + 1);
79
80
7
    int32_t length = offset_end - offset_start;
81
7
    if (length <= 0) {
82
1
        return Status::InternalError("{} operation failed for place_id={}", operation, place_id);
83
1
    }
84
6
    const uint8_t* data = data_array->value_data()->data() + offset_start;
85
6
    std::string error_message(reinterpret_cast<const char*>(data), length);
86
6
    return Status::InternalError("{} operation failed for place_id={}: {}", operation, place_id,
87
6
                                 error_message);
88
7
}
89
90
#ifdef BE_TEST
91
Status PythonUDAFClient::make_udaf_failure_status_for_test(
92
        const std::shared_ptr<arrow::RecordBatch>& response, const char* operation,
93
        int64_t place_id) {
94
    return make_udaf_failure_status(response, operation, place_id);
95
}
96
#endif
97
98
Status PythonUDAFClient::create(const PythonUDFMeta& func_meta, ProcessPtr process,
99
                                const std::shared_ptr<arrow::Schema>& data_schema,
100
2.68k
                                PythonUDAFClientPtr* client) {
101
2.68k
    PythonUDAFClientPtr python_udaf_client = std::make_shared<PythonUDAFClient>();
102
2.68k
    RETURN_IF_ERROR(python_udaf_client->init(func_meta, std::move(process), data_schema));
103
2.68k
    *client = std::move(python_udaf_client);
104
2.68k
    return Status::OK();
105
2.68k
}
106
107
Status PythonUDAFClient::init(const PythonUDFMeta& func_meta, ProcessPtr process,
108
2.68k
                              const std::shared_ptr<arrow::Schema>& data_schema) {
109
2.68k
    _schema = data_schema;
110
2.68k
    return PythonClient::init(func_meta, std::move(process));
111
2.68k
}
112
113
2.68k
Status PythonUDAFClient::create(int64_t place_id) {
114
2.68k
    std::shared_ptr<arrow::RecordBatch> request_batch;
115
2.68k
    RETURN_IF_ERROR(_get_empty_request_batch(&request_batch));
116
117
2.68k
    UDAFMetadata metadata {
118
2.68k
            .meta_version = UDAF_METADATA_VERSION,
119
2.68k
            .operation = static_cast<uint8_t>(UDAFOperation::CREATE),
120
2.68k
            .is_single_place = 0,
121
2.68k
            .place_id = place_id,
122
2.68k
            .row_start = 0,
123
2.68k
            .row_end = 0,
124
2.68k
    };
125
126
2.68k
    std::shared_ptr<arrow::RecordBatch> response_batch;
127
2.68k
    RETURN_IF_ERROR(_send_request(metadata, request_batch, &response_batch));
128
129
    // Parse unified response_batch: [success: bool, rows_processed: int64, serialized_data: binary]
130
2.65k
    if (response_batch->num_rows() != 1) {
131
0
        return Status::InternalError("Invalid CREATE response_batch: expected 1 row");
132
0
    }
133
134
2.65k
    auto success_array = std::static_pointer_cast<arrow::BooleanArray>(response_batch->column(0));
135
2.65k
    if (!success_array->Value(0)) {
136
0
        return make_udaf_failure_status(response_batch, "CREATE", place_id);
137
0
    }
138
139
2.65k
    _created_place_id = place_id;
140
2.65k
    return Status::OK();
141
2.65k
}
142
143
Status PythonUDAFClient::accumulate(int64_t place_id, bool is_single_place,
144
                                    const arrow::RecordBatch& input, int64_t row_start,
145
2.63k
                                    int64_t row_end) {
146
    // Validate input parameters
147
2.63k
    if (UNLIKELY(row_start < 0 || row_end < row_start || row_end > input.num_rows())) {
148
0
        return Status::InvalidArgument(
149
0
                "Invalid row range: row_start={}, row_end={}, input.num_rows={}", row_start,
150
0
                row_end, input.num_rows());
151
0
    }
152
153
    // In multi-place mode, input RecordBatch must contain "places" column as last column
154
2.63k
    if (UNLIKELY(!is_single_place &&
155
2.63k
                 (input.num_columns() == 0 ||
156
2.63k
                  input.schema()->field(input.num_columns() - 1)->name() != "places"))) {
157
0
        return Status::InternalError(
158
0
                "In multi-place mode, input RecordBatch must contain 'places' column as the "
159
0
                "last column");
160
0
    }
161
162
    // Create request batch: input data + NULL binary_data column
163
2.63k
    std::shared_ptr<arrow::RecordBatch> request_batch;
164
2.63k
    RETURN_IF_ERROR(_create_data_request_batch(input, &request_batch));
165
166
    // Create metadata structure
167
2.63k
    UDAFMetadata metadata {
168
2.63k
            .meta_version = UDAF_METADATA_VERSION,
169
2.63k
            .operation = static_cast<uint8_t>(UDAFOperation::ACCUMULATE),
170
2.63k
            .is_single_place = static_cast<uint8_t>(is_single_place ? 1 : 0),
171
2.63k
            .place_id = place_id,
172
2.63k
            .row_start = row_start,
173
2.63k
            .row_end = row_end,
174
2.63k
    };
175
176
    // Send to server with metadata in app_metadata
177
2.63k
    std::shared_ptr<arrow::RecordBatch> response;
178
2.63k
    RETURN_IF_ERROR(_send_request(metadata, request_batch, &response));
179
180
    // Parse unified response: [success: bool, rows_processed: int64, serialized_data: binary]
181
2.63k
    if (response->num_rows() != 1) {
182
0
        return Status::InternalError("Invalid ACCUMULATE response: expected 1 row");
183
0
    }
184
185
2.63k
    auto success_array = std::static_pointer_cast<arrow::BooleanArray>(response->column(0));
186
2.63k
    auto rows_processed_array = std::static_pointer_cast<arrow::Int64Array>(response->column(1));
187
188
2.63k
    if (!success_array->Value(0)) {
189
1
        return make_udaf_failure_status(response, "ACCUMULATE", place_id);
190
1
    }
191
192
    // Arrow Flight buffers may be unaligned after IPC deserialization.
193
2.63k
    const auto* raw_ptr = rows_processed_array->raw_values();
194
2.63k
    if (raw_ptr == nullptr) {
195
0
        return Status::InternalError("ACCUMULATE response has null rows_processed array");
196
0
    }
197
2.63k
    int64_t rows_processed = unaligned_load<int64_t>(raw_ptr);
198
199
2.63k
    int64_t expected_rows = row_end - row_start;
200
201
2.63k
    if (rows_processed < expected_rows) {
202
0
        return Status::InternalError(
203
0
                "ACCUMULATE operation only processed {} out of {} rows for place_id={}",
204
0
                rows_processed, expected_rows, place_id);
205
0
    }
206
2.63k
    return Status::OK();
207
2.63k
}
208
209
Status PythonUDAFClient::serialize(int64_t place_id,
210
579
                                   std::shared_ptr<arrow::Buffer>* serialized_state) {
211
579
    std::shared_ptr<arrow::RecordBatch> request_batch;
212
579
    RETURN_IF_ERROR(_get_empty_request_batch(&request_batch));
213
214
579
    UDAFMetadata metadata {
215
579
            .meta_version = UDAF_METADATA_VERSION,
216
579
            .operation = static_cast<uint8_t>(UDAFOperation::SERIALIZE),
217
579
            .is_single_place = 0,
218
579
            .place_id = place_id,
219
579
            .row_start = 0,
220
579
            .row_end = 0,
221
579
    };
222
223
579
    std::shared_ptr<arrow::RecordBatch> response;
224
579
    RETURN_IF_ERROR(_send_request(metadata, request_batch, &response));
225
226
    // Parse unified response: [success: bool, rows_processed: int64, serialized_data: binary]
227
579
    auto success_array = std::static_pointer_cast<arrow::BooleanArray>(response->column(0));
228
579
    auto data_array = std::static_pointer_cast<arrow::BinaryArray>(response->column(2));
229
230
579
    if (!success_array->Value(0)) {
231
0
        return make_udaf_failure_status(response, "SERIALIZE", place_id);
232
0
    }
233
234
    // Arrow Flight buffers may be unaligned after IPC deserialization.
235
579
    const auto* offsets = data_array->raw_value_offsets();
236
579
    if (offsets == nullptr) {
237
0
        return Status::InternalError("SERIALIZE response has null offsets");
238
0
    }
239
579
    int32_t offset_start = unaligned_load<int32_t>(offsets);
240
579
    int32_t offset_end = unaligned_load<int32_t>(offsets + 1);
241
242
579
    int32_t length = offset_end - offset_start;
243
244
579
    if (length == 0) {
245
0
        return Status::InternalError("SERIALIZE operation returned empty data for place_id={}",
246
0
                                     place_id);
247
0
    }
248
249
579
    const uint8_t* data = data_array->value_data()->data() + offset_start;
250
579
    *serialized_state = arrow::Buffer::Wrap(data, length);
251
579
    return Status::OK();
252
579
}
253
254
Status PythonUDAFClient::merge(int64_t place_id,
255
579
                               const std::shared_ptr<arrow::Buffer>& serialized_state) {
256
579
    std::shared_ptr<arrow::RecordBatch> request_batch;
257
579
    RETURN_IF_ERROR(_create_binary_request_batch(serialized_state, &request_batch));
258
259
579
    UDAFMetadata metadata {
260
579
            .meta_version = UDAF_METADATA_VERSION,
261
579
            .operation = static_cast<uint8_t>(UDAFOperation::MERGE),
262
579
            .is_single_place = 0,
263
579
            .place_id = place_id,
264
579
            .row_start = 0,
265
579
            .row_end = 0,
266
579
    };
267
268
579
    std::shared_ptr<arrow::RecordBatch> response;
269
579
    RETURN_IF_ERROR(_send_request(metadata, request_batch, &response));
270
271
    // Parse unified response: [success: bool, rows_processed: int64, serialized_data: binary]
272
579
    if (response->num_rows() != 1) {
273
0
        return Status::InternalError("Invalid MERGE response: expected 1 row");
274
0
    }
275
276
579
    auto success_array = std::static_pointer_cast<arrow::BooleanArray>(response->column(0));
277
579
    if (!success_array->Value(0)) {
278
1
        return make_udaf_failure_status(response, "MERGE", place_id);
279
1
    }
280
281
578
    return Status::OK();
282
579
}
283
284
2.28k
Status PythonUDAFClient::finalize(int64_t place_id, std::shared_ptr<arrow::RecordBatch>* output) {
285
2.28k
    std::shared_ptr<arrow::RecordBatch> request_batch;
286
2.28k
    RETURN_IF_ERROR(_get_empty_request_batch(&request_batch));
287
288
2.28k
    UDAFMetadata metadata {
289
2.28k
            .meta_version = UDAF_METADATA_VERSION,
290
2.28k
            .operation = static_cast<uint8_t>(UDAFOperation::FINALIZE),
291
2.28k
            .is_single_place = 0,
292
2.28k
            .place_id = place_id,
293
2.28k
            .row_start = 0,
294
2.28k
            .row_end = 0,
295
2.28k
    };
296
297
2.28k
    std::shared_ptr<arrow::RecordBatch> response_batch;
298
2.28k
    RETURN_IF_ERROR(_send_request(metadata, request_batch, &response_batch));
299
300
    // Parse unified response_batch: [success: bool, rows_processed: int64, serialized_data: binary]
301
2.28k
    auto success_array = std::static_pointer_cast<arrow::BooleanArray>(response_batch->column(0));
302
2.28k
    auto data_array = std::static_pointer_cast<arrow::BinaryArray>(response_batch->column(2));
303
304
2.28k
    if (!success_array->Value(0)) {
305
2
        return make_udaf_failure_status(response_batch, "FINALIZE", place_id);
306
2
    }
307
308
    // Arrow Flight buffers may be unaligned after IPC deserialization.
309
2.28k
    const auto* offsets = data_array->raw_value_offsets();
310
2.28k
    if (offsets == nullptr) {
311
0
        return Status::InternalError("FINALIZE response has null offsets");
312
0
    }
313
2.28k
    int32_t offset_start = unaligned_load<int32_t>(offsets);
314
2.28k
    int32_t offset_end = unaligned_load<int32_t>(offsets + 1);
315
316
2.28k
    int32_t length = offset_end - offset_start;
317
318
2.28k
    if (length == 0) {
319
0
        return Status::InternalError("FINALIZE operation returned empty data for place_id={}",
320
0
                                     place_id);
321
0
    }
322
323
2.28k
    const uint8_t* data = data_array->value_data()->data() + offset_start;
324
2.28k
    auto buffer = arrow::Buffer::Wrap(data, length);
325
2.28k
    auto input_stream = std::make_shared<arrow::io::BufferReader>(buffer);
326
327
2.28k
    auto reader_result = arrow::ipc::RecordBatchStreamReader::Open(input_stream);
328
2.28k
    if (UNLIKELY(!reader_result.ok())) {
329
0
        return Status::InternalError("Failed to deserialize FINALIZE result: {}",
330
0
                                     reader_result.status().message());
331
0
    }
332
2.28k
    auto reader = std::move(reader_result).ValueOrDie();
333
334
2.28k
    auto batch_result = reader->Next();
335
2.28k
    if (UNLIKELY(!batch_result.ok())) {
336
0
        return Status::InternalError("Failed to read FINALIZE result: {}",
337
0
                                     batch_result.status().message());
338
0
    }
339
340
2.28k
    *output = std::move(batch_result).ValueOrDie();
341
342
2.28k
    return Status::OK();
343
2.28k
}
344
345
670
Status PythonUDAFClient::reset(int64_t place_id) {
346
670
    std::shared_ptr<arrow::RecordBatch> request_batch;
347
670
    RETURN_IF_ERROR(_get_empty_request_batch(&request_batch));
348
349
670
    UDAFMetadata metadata {
350
670
            .meta_version = UDAF_METADATA_VERSION,
351
670
            .operation = static_cast<uint8_t>(UDAFOperation::RESET),
352
670
            .is_single_place = 0,
353
670
            .place_id = place_id,
354
670
            .row_start = 0,
355
670
            .row_end = 0,
356
670
    };
357
358
670
    std::shared_ptr<arrow::RecordBatch> response;
359
670
    RETURN_IF_ERROR(_send_request(metadata, request_batch, &response));
360
361
    // Parse unified response: [success: bool, rows_processed: int64, serialized_data: binary]
362
670
    if (response->num_rows() != 1) {
363
0
        return Status::InternalError("Invalid RESET response: expected 1 row");
364
0
    }
365
366
670
    auto success_array = std::static_pointer_cast<arrow::BooleanArray>(response->column(0));
367
670
    if (!success_array->Value(0)) {
368
0
        return make_udaf_failure_status(response, "RESET", place_id);
369
0
    }
370
371
670
    return Status::OK();
372
670
}
373
374
2.65k
Status PythonUDAFClient::destroy(int64_t place_id) {
375
2.65k
    std::shared_ptr<arrow::RecordBatch> request_batch;
376
2.65k
    RETURN_IF_ERROR(_get_empty_request_batch(&request_batch));
377
378
2.65k
    UDAFMetadata metadata {
379
2.65k
            .meta_version = UDAF_METADATA_VERSION,
380
2.65k
            .operation = static_cast<uint8_t>(UDAFOperation::DESTROY),
381
2.65k
            .is_single_place = 0,
382
2.65k
            .place_id = place_id,
383
2.65k
            .row_start = 0,
384
2.65k
            .row_end = 0,
385
2.65k
    };
386
387
2.65k
    std::shared_ptr<arrow::RecordBatch> response;
388
2.65k
    Status st = _send_request(metadata, request_batch, &response);
389
390
    // Always clear tracking, even if RPC failed
391
2.65k
    _created_place_id.reset();
392
393
2.65k
    if (!st.ok()) {
394
0
        LOG(WARNING) << "Failed to destroy place_id=" << place_id << ": " << st.to_string();
395
0
        return st;
396
0
    }
397
398
    // Parse unified response: [success: bool, rows_processed: int64, serialized_data: binary]
399
2.65k
    if (response->num_rows() != 1) {
400
0
        return Status::InternalError("Invalid DESTROY response: expected 1 row");
401
0
    }
402
403
2.65k
    auto success_array = std::static_pointer_cast<arrow::BooleanArray>(response->column(0));
404
405
2.65k
    if (!success_array->Value(0)) {
406
0
        LOG(WARNING) << "DESTROY operation failed for place_id=" << place_id;
407
0
        return make_udaf_failure_status(response, "DESTROY", place_id);
408
0
    }
409
410
2.65k
    return Status::OK();
411
2.65k
}
412
413
2.68k
Status PythonUDAFClient::close() {
414
2.68k
    if (!_inited || !_writer) return Status::OK();
415
416
    // Destroy the place if it exists (cleanup on client destruction)
417
2.65k
    if (_created_place_id.has_value()) {
418
0
        int64_t place_id = _created_place_id.value();
419
0
        Status st = destroy(place_id);
420
0
        if (!st.ok()) {
421
0
            LOG(WARNING) << "Failed to destroy place_id=" << place_id
422
0
                         << " during close: " << st.to_string();
423
            // Clear tracking even on failure to prevent issues in base class close
424
0
            _created_place_id.reset();
425
0
        }
426
0
    }
427
428
2.65k
    return PythonClient::close();
429
2.68k
}
430
431
Status PythonUDAFClient::_send_request(const UDAFMetadata& metadata,
432
                                       const std::shared_ptr<arrow::RecordBatch>& request_batch,
433
12.0k
                                       std::shared_ptr<arrow::RecordBatch>* response_batch) {
434
12.0k
    DCHECK(response_batch != nullptr);
435
436
    // Create app_metadata buffer from metadata struct
437
12.0k
    auto app_metadata =
438
12.0k
            arrow::Buffer::Wrap(reinterpret_cast<const uint8_t*>(&metadata), sizeof(metadata));
439
440
12.0k
    std::lock_guard<std::mutex> lock(_operation_mutex);
441
442
    // Check if writer/reader are still valid (they could be reset by handle_error)
443
12.0k
    if (UNLIKELY(!_writer || !_reader)) {
444
0
        return Status::InternalError("{} writer/reader have been closed due to previous error",
445
0
                                     _operation_name);
446
0
    }
447
448
    // Begin stream on first call (using data schema: argument_types + places + binary_data)
449
12.0k
    if (UNLIKELY(!_begin)) {
450
2.68k
        auto begin_res = _writer->Begin(_schema);
451
2.68k
        if (!begin_res.ok()) {
452
28
            return handle_error(begin_res);
453
28
        }
454
2.65k
        _begin = true;
455
2.65k
    }
456
457
    // Write batch with metadata in app_metadata
458
12.0k
    auto write_res = _writer->WriteWithMetadata(*request_batch, app_metadata);
459
12.0k
    if (!write_res.ok()) {
460
0
        return handle_error(write_res);
461
0
    }
462
463
    // Read unified response: [success: bool, rows_processed: int64, serialized_data: binary]
464
12.0k
    auto read_res = _reader->Next();
465
12.0k
    if (!read_res.ok()) {
466
0
        return handle_error(read_res.status());
467
0
    }
468
469
12.0k
    arrow::flight::FlightStreamChunk chunk = std::move(*read_res);
470
12.0k
    if (!chunk.data) {
471
0
        return Status::InternalError("Received empty RecordBatch from {} server", _operation_name);
472
0
    }
473
474
    // Validate unified response schema
475
12.0k
    if (!chunk.data->schema()->Equals(kUnifiedUDAFResponseSchema)) {
476
0
        return Status::InternalError(
477
0
                "Invalid response schema: expected [success: bool, rows_processed: int64, "
478
0
                "serialized_data: binary], got {}",
479
0
                chunk.data->schema()->ToString());
480
0
    }
481
482
12.0k
    *response_batch = std::move(chunk.data);
483
12.0k
    return Status::OK();
484
12.0k
}
485
486
Status PythonUDAFClient::_create_data_request_batch(const arrow::RecordBatch& input_data,
487
2.64k
                                                    std::shared_ptr<arrow::RecordBatch>* out) {
488
    // Determine if input has places column
489
2.64k
    int num_input_columns = input_data.num_columns();
490
2.64k
    bool has_places = false;
491
2.64k
    if (num_input_columns > 0 &&
492
2.64k
        input_data.schema()->field(num_input_columns - 1)->name() == "places") {
493
778
        has_places = true;
494
778
    }
495
496
    // Expected schema structure: [argument_types..., places, binary_data]
497
    // - Input in single-place mode: [argument_types...]
498
    // - Input in multi-place mode: [argument_types..., places]
499
2.64k
    std::vector<std::shared_ptr<arrow::Array>> columns;
500
    // Copy argument_types columns
501
2.64k
    int num_arg_columns = has_places ? (num_input_columns - 1) : num_input_columns;
502
503
5.89k
    for (int i = 0; i < num_arg_columns; ++i) {
504
3.25k
        columns.push_back(input_data.column(i));
505
3.25k
    }
506
507
    // Add places column
508
2.64k
    if (has_places) {
509
        // Use existing places column from input
510
778
        columns.push_back(input_data.column(num_input_columns - 1));
511
1.86k
    } else {
512
        // Create NULL places column for single-place mode
513
1.86k
        arrow::Int64Builder places_builder(ExecEnv::GetInstance()->arrow_memory_pool());
514
1.86k
        std::shared_ptr<arrow::Array> places_array;
515
1.86k
        RETURN_DORIS_STATUS_IF_ERROR(places_builder.AppendNulls(input_data.num_rows()));
516
1.86k
        RETURN_DORIS_STATUS_IF_ERROR(places_builder.Finish(&places_array));
517
1.86k
        columns.push_back(places_array);
518
1.86k
    }
519
520
    // Add NULL binary_data column
521
2.64k
    arrow::BinaryBuilder binary_builder(ExecEnv::GetInstance()->arrow_memory_pool());
522
2.64k
    std::shared_ptr<arrow::Array> binary_array;
523
2.64k
    RETURN_DORIS_STATUS_IF_ERROR(binary_builder.AppendNulls(input_data.num_rows()));
524
2.64k
    RETURN_DORIS_STATUS_IF_ERROR(binary_builder.Finish(&binary_array));
525
2.64k
    columns.push_back(binary_array);
526
527
2.64k
    *out = arrow::RecordBatch::Make(_schema, input_data.num_rows(), columns);
528
2.64k
    return Status::OK();
529
2.64k
}
530
531
Status PythonUDAFClient::_create_binary_request_batch(
532
        const std::shared_ptr<arrow::Buffer>& binary_data,
533
579
        std::shared_ptr<arrow::RecordBatch>* out) {
534
579
    std::vector<std::shared_ptr<arrow::Array>> columns;
535
536
    // Create NULL arrays for data columns (all columns except the last binary_data column)
537
    // Schema: [argument_types..., places, binary_data]
538
579
    int num_data_columns = _schema->num_fields() - 1;
539
1.91k
    for (int i = 0; i < num_data_columns; ++i) {
540
1.33k
        std::unique_ptr<arrow::ArrayBuilder> builder;
541
1.33k
        std::shared_ptr<arrow::Array> null_array;
542
1.33k
        RETURN_DORIS_STATUS_IF_ERROR(arrow::MakeBuilder(ExecEnv::GetInstance()->arrow_memory_pool(),
543
1.33k
                                                        _schema->field(i)->type(), &builder));
544
1.33k
        RETURN_DORIS_STATUS_IF_ERROR(builder->AppendNull());
545
1.33k
        RETURN_DORIS_STATUS_IF_ERROR(builder->Finish(&null_array));
546
1.33k
        columns.push_back(null_array);
547
1.33k
    }
548
549
    // Create binary_data column
550
579
    arrow::BinaryBuilder binary_builder(ExecEnv::GetInstance()->arrow_memory_pool());
551
579
    std::shared_ptr<arrow::Array> binary_array;
552
579
    RETURN_DORIS_STATUS_IF_ERROR(
553
579
            binary_builder.Append(binary_data->data(), static_cast<int32_t>(binary_data->size())));
554
579
    RETURN_DORIS_STATUS_IF_ERROR(binary_builder.Finish(&binary_array));
555
579
    columns.push_back(binary_array);
556
557
579
    *out = arrow::RecordBatch::Make(_schema, 1, columns);
558
579
    return Status::OK();
559
579
}
560
561
8.86k
Status PythonUDAFClient::_get_empty_request_batch(std::shared_ptr<arrow::RecordBatch>* out) {
562
    // Return cached batch if already created
563
8.86k
    if (_empty_request_batch) {
564
6.18k
        *out = _empty_request_batch;
565
6.18k
        return Status::OK();
566
6.18k
    }
567
568
    // Create empty batch on first use (all columns NULL, 1 row)
569
2.67k
    std::vector<std::shared_ptr<arrow::Array>> columns;
570
571
11.4k
    for (int i = 0; i < _schema->num_fields(); ++i) {
572
8.81k
        auto field = _schema->field(i);
573
8.81k
        std::unique_ptr<arrow::ArrayBuilder> builder;
574
8.81k
        std::shared_ptr<arrow::Array> null_array;
575
8.81k
        RETURN_DORIS_STATUS_IF_ERROR(arrow::MakeBuilder(ExecEnv::GetInstance()->arrow_memory_pool(),
576
8.81k
                                                        field->type(), &builder));
577
8.81k
        RETURN_DORIS_STATUS_IF_ERROR(builder->AppendNull());
578
8.81k
        RETURN_DORIS_STATUS_IF_ERROR(builder->Finish(&null_array));
579
8.81k
        columns.push_back(null_array);
580
8.81k
    }
581
582
2.67k
    _empty_request_batch = arrow::RecordBatch::Make(_schema, 1, columns);
583
2.67k
    *out = _empty_request_batch;
584
2.67k
    return Status::OK();
585
2.67k
}
586
587
} // namespace doris