Coverage Report

Created: 2026-08-13 15:45

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/exprs/aggregate/aggregate_function_foreach.h
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
// This file is copied from
18
// https://github.com/ClickHouse/ClickHouse/blob/master/src/AggregateFunctions/Combinators/AggregateFunctionForEach.h
19
// and modified by Doris
20
21
#pragma once
22
23
#include "common/status.h"
24
#include "core/assert_cast.h"
25
#include "core/column/column_nullable.h"
26
#include "core/data_type/data_type_array.h"
27
#include "core/data_type/data_type_nullable.h"
28
#include "exec/common/arithmetic_overflow.h"
29
#include "exprs/aggregate/aggregate_function.h"
30
#include "exprs/function/array/function_array_utils.h"
31
32
namespace doris {
33
34
struct AggregateFunctionForEachData {
35
    size_t dynamic_array_size = 0;
36
    char* array_of_aggregate_datas = nullptr;
37
};
38
39
/** Adaptor for aggregate functions.
40
  * Adding -ForEach suffix to aggregate function
41
  *  will convert that aggregate function to a function, accepting arrays,
42
  *  and applies aggregation for each corresponding elements of arrays independently,
43
  *  returning arrays of aggregated values on corresponding positions.
44
  *
45
  * Example: sumForEach of:
46
  *  [1, 2],
47
  *  [3, 4, 5],
48
  *  [6, 7]
49
  * will return:
50
  *  [10, 13, 5]
51
  *
52
  * TODO Allow variable number of arguments.
53
  */
54
class AggregateFunctionForEach : public AggregateFunctionNonFinalBase,
55
                                 public IAggregateFunctionDataHelper<AggregateFunctionForEachData,
56
                                                                     AggregateFunctionForEach>,
57
                                 VarargsExpression,
58
                                 NullableAggregateFunction {
59
protected:
60
    using Base =
61
            IAggregateFunctionDataHelper<AggregateFunctionForEachData, AggregateFunctionForEach>;
62
63
    AggregateFunctionPtr nested_function;
64
    const size_t nested_size_of_data;
65
    const size_t num_arguments;
66
67
    AggregateFunctionForEachData& ensure_aggregate_data(AggregateDataPtr __restrict place,
68
37
                                                        size_t new_size, Arena& arena) const {
69
37
        AggregateFunctionForEachData& state = data(place);
70
71
        /// Ensure we have aggregate states for new_size elements, allocate
72
        /// from arena if needed. When reallocating, we can't copy the
73
        /// states to new buffer with memcpy, because they may contain pointers
74
        /// to themselves. In particular, this happens when a state contains
75
        /// a PODArrayWithStackMemory, which stores small number of elements
76
        /// inline. This is why we create new empty states in the new buffer,
77
        /// and merge the old states to them.
78
37
        size_t old_size = state.dynamic_array_size;
79
37
        if (old_size < new_size) {
80
37
            static constexpr size_t MAX_ARRAY_SIZE = 100 * 1000000000ULL;
81
37
            if (new_size > MAX_ARRAY_SIZE) {
82
0
                throw Exception(ErrorCode::INTERNAL_ERROR,
83
0
                                "Suspiciously large array size ({}) in -ForEach aggregate function",
84
0
                                new_size);
85
0
            }
86
87
37
            size_t allocation_size = 0;
88
37
            if (common::mul_overflow(new_size, nested_size_of_data, allocation_size)) {
89
0
                throw Exception(ErrorCode::INTERNAL_ERROR,
90
0
                                "Allocation size ({} * {}) overflows in -ForEach aggregate "
91
0
                                "function, but it should've been prevented by previous checks",
92
0
                                new_size, nested_size_of_data);
93
0
            }
94
95
37
            char* old_state = state.array_of_aggregate_datas;
96
97
37
            char* new_state =
98
37
                    arena.aligned_alloc(allocation_size, nested_function->align_of_data());
99
100
37
            size_t num_created = 0;
101
37
            try {
102
173
                for (; num_created < new_size; ++num_created) {
103
136
                    nested_function->create(&new_state[num_created * nested_size_of_data]);
104
136
                }
105
106
40
                for (size_t i = 0; i < old_size; ++i) {
107
3
                    nested_function->merge(&new_state[i * nested_size_of_data],
108
3
                                           &old_state[i * nested_size_of_data], arena);
109
3
                }
110
37
            } catch (...) {
111
4
                for (size_t i = 0; i < num_created; ++i) {
112
3
                    nested_function->destroy(&new_state[i * nested_size_of_data]);
113
3
                }
114
115
1
                throw;
116
1
            }
117
118
37
            for (size_t i = 0; i < old_size; ++i) {
119
1
                nested_function->destroy(&old_state[i * nested_size_of_data]);
120
1
            }
121
122
36
            state.array_of_aggregate_datas = new_state;
123
36
            state.dynamic_array_size = new_size;
124
36
        }
125
126
36
        return state;
127
37
    }
128
129
public:
130
    constexpr static auto AGG_FOREACH_SUFFIX = "_foreach";
131
    AggregateFunctionForEach(AggregateFunctionPtr nested_function_, const DataTypes& arguments)
132
5
            : Base(arguments),
133
5
              nested_function {std::move(nested_function_)},
134
5
              nested_size_of_data(nested_function->size_of_data()),
135
5
              num_arguments(arguments.size()) {
136
5
        if (arguments.empty()) {
137
0
            throw Exception(ErrorCode::INTERNAL_ERROR,
138
0
                            "Aggregate function {} require at least one argument", get_name());
139
0
        }
140
5
    }
141
2
    void set_version(const int version_) override {
142
2
        Base::set_version(version_);
143
2
        nested_function->set_version(version_);
144
2
    }
145
146
2
    String get_name() const override { return nested_function->get_name() + AGG_FOREACH_SUFFIX; }
147
148
4
    DataTypePtr get_return_type() const override {
149
4
        return std::make_shared<DataTypeArray>(nested_function->get_return_type());
150
4
    }
151
152
35
    void destroy(AggregateDataPtr __restrict place) const noexcept override {
153
35
        AggregateFunctionForEachData& state = data(place);
154
155
35
        char* nested_state = state.array_of_aggregate_datas;
156
167
        for (size_t i = 0; i < state.dynamic_array_size; ++i) {
157
132
            nested_function->destroy(nested_state);
158
132
            nested_state += nested_size_of_data;
159
132
        }
160
35
    }
161
162
4
    bool is_trivial() const override {
163
4
        return std::is_trivial_v<Data> && nested_function->is_trivial();
164
4
    }
165
166
    void merge(AggregateDataPtr __restrict place, ConstAggregateDataPtr rhs,
167
9
               Arena& arena) const override {
168
9
        const AggregateFunctionForEachData& rhs_state = data(rhs);
169
9
        AggregateFunctionForEachData& state =
170
9
                ensure_aggregate_data(place, rhs_state.dynamic_array_size, arena);
171
172
9
        const char* rhs_nested_state = rhs_state.array_of_aggregate_datas;
173
9
        char* nested_state = state.array_of_aggregate_datas;
174
175
44
        for (size_t i = 0; i < state.dynamic_array_size && i < rhs_state.dynamic_array_size; ++i) {
176
35
            nested_function->merge(nested_state, rhs_nested_state, arena);
177
178
35
            rhs_nested_state += nested_size_of_data;
179
35
            nested_state += nested_size_of_data;
180
35
        }
181
9
    }
182
183
12
    void serialize(ConstAggregateDataPtr __restrict place, BufferWritable& buf) const override {
184
12
        const AggregateFunctionForEachData& state = data(place);
185
12
        buf.write_binary(state.dynamic_array_size);
186
12
        const char* nested_state = state.array_of_aggregate_datas;
187
56
        for (size_t i = 0; i < state.dynamic_array_size; ++i) {
188
44
            nested_function->serialize(nested_state, buf);
189
44
            nested_state += nested_size_of_data;
190
44
        }
191
12
    }
192
193
    void deserialize(AggregateDataPtr __restrict place, BufferReadable& buf,
194
9
                     Arena& arena) const override {
195
9
        AggregateFunctionForEachData& state = data(place);
196
197
9
        size_t new_size = 0;
198
9
        buf.read_binary(new_size);
199
200
9
        ensure_aggregate_data(place, new_size, arena);
201
202
9
        char* nested_state = state.array_of_aggregate_datas;
203
44
        for (size_t i = 0; i < new_size; ++i) {
204
35
            nested_function->deserialize(nested_state, buf, arena);
205
35
            nested_state += nested_size_of_data;
206
35
        }
207
9
    }
208
209
13
    void insert_result_into(ConstAggregateDataPtr __restrict place, IColumn& to) const override {
210
13
        const AggregateFunctionForEachData& state = data(place);
211
212
13
        auto& arr_to = assert_cast<ColumnArray&, TypeCheckOnRelease::DISABLE>(to);
213
13
        auto& offsets_to = arr_to.get_offsets();
214
13
        IColumn* elems_to = &arr_to.get_data();
215
13
        ColumnNullable* nullable_elems_to = nullptr;
216
13
        if (!nested_function->get_return_type()->is_nullable()) {
217
13
            nullable_elems_to = assert_cast<ColumnNullable*, TypeCheckOnRelease::DISABLE>(elems_to);
218
13
            elems_to = nullable_elems_to->get_nested_column_ptr().get();
219
13
        }
220
221
13
        char* nested_state = state.array_of_aggregate_datas;
222
64
        for (size_t i = 0; i < state.dynamic_array_size; ++i) {
223
51
            nested_function->insert_result_into(nested_state, *elems_to);
224
51
            if (nullable_elems_to != nullptr) {
225
51
                nullable_elems_to->get_null_map_data().push_back(0);
226
51
            }
227
51
            nested_state += nested_size_of_data;
228
51
        }
229
230
13
        offsets_to.push_back(offsets_to.back() + state.dynamic_array_size);
231
13
    }
232
233
12
    void check_result_column_type(const IColumn& to) const override {
234
12
        const auto* arr_to = check_and_get_column<ColumnArray>(to);
235
12
        if (UNLIKELY(arr_to == nullptr)) {
236
0
            throw doris::Exception(Status::InternalError(
237
0
                    "Aggregate function {} result type check failed: Column type {} is not "
238
0
                    "ColumnArray",
239
0
                    get_name(), to.get_name()));
240
0
        }
241
242
12
        const IColumn* elems_to = &arr_to->get_data();
243
12
        if (!nested_function->get_return_type()->is_nullable()) {
244
12
            const auto* nullable_elems_to = check_and_get_column<ColumnNullable>(*elems_to);
245
12
            if (UNLIKELY(nullable_elems_to == nullptr)) {
246
0
                throw doris::Exception(Status::InternalError(
247
0
                        "Aggregate function {} result type check failed: Array nested column "
248
0
                        "type {} is not ColumnNullable",
249
0
                        get_name(), elems_to->get_name()));
250
0
            }
251
12
            elems_to = &nullable_elems_to->get_nested_column();
252
12
        }
253
12
        nested_function->check_result_column_type(*elems_to);
254
12
    }
255
256
    void add(AggregateDataPtr __restrict place, const IColumn** columns, ssize_t row_num,
257
19
             Arena& arena) const override {
258
19
        std::vector<const IColumn*> nested(num_arguments);
259
260
38
        for (size_t i = 0; i < num_arguments; ++i) {
261
19
            nested[i] = &assert_cast<const ColumnArray&, TypeCheckOnRelease::DISABLE>(*columns[i])
262
19
                                 .get_data();
263
19
        }
264
265
19
        const auto& first_array_column =
266
19
                assert_cast<const ColumnArray&, TypeCheckOnRelease::DISABLE>(*columns[0]);
267
19
        const auto& offsets = first_array_column.get_offsets();
268
269
19
        size_t begin = offsets[row_num - 1];
270
19
        size_t end = offsets[row_num];
271
272
        /// Sanity check. NOTE We can implement specialization for a case with single argument, if the check will hurt performance.
273
19
        for (size_t i = 1; i < num_arguments; ++i) {
274
0
            const auto& ith_column =
275
0
                    assert_cast<const ColumnArray&, TypeCheckOnRelease::DISABLE>(*columns[i]);
276
0
            const auto& ith_offsets = ith_column.get_offsets();
277
278
0
            if (ith_offsets[row_num] != end ||
279
0
                (row_num != 0 && ith_offsets[row_num - 1] != begin)) {
280
0
                throw Exception(ErrorCode::INTERNAL_ERROR,
281
0
                                "Arrays passed to {} aggregate function have different sizes",
282
0
                                get_name());
283
0
            }
284
0
        }
285
286
19
        AggregateFunctionForEachData& state = ensure_aggregate_data(place, end - begin, arena);
287
288
19
        char* nested_state = state.array_of_aggregate_datas;
289
82
        for (size_t i = begin; i < end; ++i) {
290
63
            nested_function->add(nested_state, nested.data(), i, arena);
291
63
            nested_state += nested_size_of_data;
292
63
        }
293
19
    }
294
295
14
    void check_input_columns_type(const IColumn** columns) const override {
296
14
        std::vector<const IColumn*> nested(num_arguments);
297
28
        for (size_t i = 0; i < num_arguments; ++i) {
298
14
            const auto* array_column = check_and_get_column<ColumnArray>(*columns[i]);
299
14
            if (UNLIKELY(array_column == nullptr)) {
300
0
                throw doris::Exception(Status::InternalError(
301
0
                        "Aggregate function {} argument {} type check failed: Column type {} is "
302
0
                        "not ColumnArray",
303
0
                        get_name(), i, columns[i]->get_name()));
304
0
            }
305
14
            nested[i] = &array_column->get_data();
306
14
        }
307
14
        nested_function->check_input_columns_type(nested.data());
308
14
    }
309
};
310
} // namespace doris