Skip to content

Commit f14a0a6

Browse files
sdk: C++-ify aggregate funcs (#435)
1 parent 805bcbe commit f14a0a6

3 files changed

Lines changed: 256 additions & 63 deletions

File tree

villagesql/examples/vsql-complex/src/complex.cc

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -361,25 +361,15 @@ void complex_sum_accumulate(ComplexSumState &state, CustomArg val) {
361361
state = total;
362362
}
363363

364-
// TODO(villagesql-beta): convert to typed style: void(const ComplexSumState&,
365-
// CustomResult)
366-
void complex_sum_result(vef_context_t *ctx, vef_vdf_args_t *args,
367-
vef_vdf_result_t *out) {
368-
auto *state = static_cast<ComplexSumState *>(args->user_data);
369-
if (!state->has_value()) {
370-
out->type = VEF_RESULT_NULL;
371-
return;
372-
}
373-
if (out->max_bin_len < kComplexSize) {
374-
out->type = VEF_RESULT_ERROR;
375-
snprintf(out->error_msg, VEF_MAX_ERROR_LEN, "response buffer too small");
364+
void complex_sum_result(const ComplexSumState &state, CustomResult out) {
365+
if (!state.has_value()) {
366+
out.set_null();
376367
return;
377368
}
378-
Complex total = state->value();
369+
Complex total = state.value();
379370
total.canonicalize();
380-
store_complex(out->bin_buf, total);
381-
out->actual_len = kComplexSize;
382-
out->type = VEF_RESULT_VALUE;
371+
store_complex(out.buffer().data(), total);
372+
out.set_length(kComplexSize);
383373
}
384374

385375
// Type name NTTPs — required for auto-generating VDF names like
@@ -470,10 +460,10 @@ VEF_GENERATE_ENTRY_POINTS(
470460
.deterministic()
471461
.build())
472462
// Aggregate functions
473-
.func(make_func<&complex_sum_result>("complex_sum")
463+
.func(make_aggregate_func<ComplexSumState, &complex_sum_result>(
464+
"complex_sum")
474465
.returns(COMPLEX)
475466
.param(COMPLEX)
476-
.state<ComplexSumState>()
477467
.clear<&complex_sum_clear>()
478468
.accumulate<&complex_sum_accumulate>()
479469
.build()))

villagesql/sdk/include/villagesql/extension.h

Lines changed: 50 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -135,54 +135,77 @@
135135
// Aggregate VDFs (like SQL SUM, COUNT, etc.) accumulate state across rows
136136
// within each GROUP BY group, then return a final result per group.
137137
//
138-
// The recommended approach uses .state<T>() with typed callbacks. Define a
139-
// state type, then write clear, accumulate, and result functions using C++
140-
// types:
138+
// Use make_aggregate_func<State, &result_fn>() as the entry point. The State
139+
// type is explicit, and all three callback signatures are validated against it
140+
// at compile time.
141+
//
142+
// The result function always uses an output parameter, consistent with normal
143+
// VDFs. All three callbacks follow the same pattern:
144+
//
145+
// void my_clear(State &s) // reset state
146+
// void my_acc(State &s, TypedArg v, ...) // accumulate one row
147+
// void my_result(const State &s, ResultWrapper out) // produce final value
148+
//
149+
// Example — nullable integer sum:
141150
//
142151
// using SumState = std::optional<long long>;
143152
//
144-
// void my_clear(SumState &s) { s = std::nullopt; }
153+
// void my_clear(SumState &s) { s = std::nullopt; }
145154
// void my_acc(SumState &s, IntArg v) {
146155
// if (!v.is_null()) s = s.value_or(0) + v.value();
147156
// }
148-
// std::optional<long long> my_result(const SumState &s) { return s; }
157+
// void my_result(const SumState &s, IntResult out) {
158+
// if (!s.has_value()) { out.set_null(); return; }
159+
// out.set(s.value());
160+
// }
149161
//
150-
// make_func<&my_result>("my_sum")
162+
// make_aggregate_func<SumState, &my_result>("my_sum")
151163
// .returns(INT)
152164
// .param(INT)
153-
// .state<SumState>()
154165
// .clear<&my_clear>()
155166
// .accumulate<&my_acc>()
156167
// .build()
157168
//
158169
// How it works:
159-
// - .state<T>() generates prerun (allocates T via value-initialization) and
160-
// postrun (deletes T) automatically. T is stored in user_data.
161-
// - .clear<&fn>() wraps void(State&) -> vef_vdf_clear_func_t
162-
// - .accumulate<&fn>() wraps void(State&, TypedArgs...) ->
163-
// vef_vdf_accumulate_func_t. TypedArgs are deduced from the function
164-
// signature (IntArg, StringArg, etc.).
165-
// - The result function (make_func template parameter) can return T directly
166-
// (never NULL) or std::optional<T> (nullopt -> SQL NULL).
167-
//
168-
// For results that are never NULL (e.g., COUNT), use a plain state type:
170+
// - prerun/postrun are auto-generated: prerun allocates State via
171+
// value-initialization, postrun deletes it.
172+
// - .clear<&fn>() void(State&)
173+
// - .accumulate<&fn>() void(State&, TypedArgs...) — TypedArgs deduced from
174+
// the function signature (IntArg, StringArg, CustomArg, etc.).
175+
// Call .accumulate() after all .param() calls.
176+
// - Result function uses void(const State&, ResultWrapper) where
177+
// ResultWrapper is IntResult, RealResult, StringResult, CustomResult, or
178+
// CustomResultWith<P>. Use out.set_null() to return SQL NULL.
179+
//
180+
// Example — non-nullable count:
169181
//
170182
// using CountState = long long;
171183
// void count_clear(CountState &s) { s = 0; }
172-
// void count_acc(CountState &s, IntArg v) { if (!v.is_null()) s++; }
173-
// long long count_result(const CountState &s) { return s; }
174-
//
175-
// You can also use the raw ABI directly for full control:
184+
// void count_acc(CountState &s, IntArg v) { if (!v.is_null()) ++s; }
185+
// void count_result(const CountState &s, IntResult out) { out.set(s); }
176186
//
177-
// make_func<&raw_result>("my_agg")
187+
// make_aggregate_func<CountState, &count_result>("my_count")
178188
// .returns(INT).param(INT)
179-
// .prerun<&my_prerun>() // void(ctx, prerun_args, prerun_result)
180-
// .postrun<&my_postrun>() // void(ctx, postrun_args, postrun_result)
181-
// .clear<&my_clear>() // void(ctx, vdf_args)
182-
// .accumulate<&my_acc>() // void(ctx, vdf_args, vdf_result)
189+
// .clear<&count_clear>().accumulate<&count_acc>()
190+
// .build()
191+
//
192+
// Example — aggregate returning a custom (binary) type:
193+
//
194+
// using SumState = std::optional<MyType>;
195+
// void my_clear(SumState &s) { s = std::nullopt; }
196+
// void my_acc(SumState &s, CustomArg v) { /* update s */ }
197+
// void my_result(const SumState &s, CustomResult out) {
198+
// if (!s.has_value()) { out.set_null(); return; }
199+
// store_mytype(out.buffer().data(), s.value());
200+
// out.set_length(kMyTypeSize);
201+
// }
202+
//
203+
// make_aggregate_func<SumState, &my_result>("my_agg")
204+
// .returns(MYTYPE).param(MYTYPE)
205+
// .clear<&my_clear>().accumulate<&my_acc>()
183206
// .build()
184207
//
185-
// See aggregate_vdf.cc in the test suite for complete examples of both styles.
208+
// See aggregate_vdf.cc in the test suite for complete examples.
186209
//
187210
//
188211
// DEFINING TYPES

0 commit comments

Comments
 (0)