Skip to content

Commit 1363ce3

Browse files
committed
prerun improvements
1 parent e73a25f commit 1363ce3

9 files changed

Lines changed: 459 additions & 19 deletions

File tree

mysql-test/suite/villagesql/extension/r/extension_simple_type_usage.result

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ FROM test_bytearray t1, test_bytearray t2
6868
WHERE t1.id = 1 AND t2.id = 2;
6969
concatenated
7070
'hello world123'
71+
# Test ba_call_index (exercises typed prerun/postrun + state-passing VDF)
72+
SELECT vsql_simple.ba_call_index() as idx FROM test_bytearray ORDER BY id;
73+
idx
74+
1
75+
2
76+
3
7177
# Clean up
7278
DROP TABLE test_bytearray;
7379
# Uninstall the extension

mysql-test/suite/villagesql/extension/t/extension_simple_type_usage.test

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ SELECT CONCAT("'", vsql_simple.ba_concat(t1.data, t2.data), "'") as concatenated
4444
FROM test_bytearray t1, test_bytearray t2
4545
WHERE t1.id = 1 AND t2.id = 2;
4646

47+
--echo # Test ba_call_index (exercises typed prerun/postrun + state-passing VDF)
48+
SELECT vsql_simple.ba_call_index() as idx FROM test_bytearray ORDER BY id;
49+
4750
--echo # Clean up
4851
DROP TABLE test_bytearray;
4952

villagesql/examples/vsql-simple/src/extension.cc

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,25 @@ void odd_chars(CustomArg in, CustomResult out) {
112112
out.set_length(kBytearrayLen);
113113
}
114114

115+
// BA_CALL_INDEX: return the 1-based index of this call within the current
116+
// statement. Demonstrates the typed prerun + State& pattern: prerun
117+
// allocates a counter via emplace_state<T> and each row reads-and-
118+
// increments. No postrun needed — the SDK's installed deleter frees the
119+
// CallCounter after the last row.
120+
121+
struct CallCounter {
122+
long long n = 0;
123+
};
124+
125+
void ba_call_index_prerun(PrerunArgs, PrerunResult out) {
126+
out.emplace_state<CallCounter>();
127+
}
128+
129+
void ba_call_index(CallCounter &state, IntResult out) {
130+
state.n++;
131+
out.set(state.n);
132+
}
133+
115134
// BA_CONCAT: concatenate two bytearrays (returns STRING with 16 bytes)
116135
void ba_concat(CustomArg a, CustomArg b, StringResult out) {
117136
if (a.is_null() || b.is_null()) {
@@ -153,4 +172,8 @@ VEF_GENERATE_ENTRY_POINTS(make_extension()
153172
.returns(STRING)
154173
.param(BYTEARRAY)
155174
.param(BYTEARRAY)
175+
.build())
176+
.func(make_func<&ba_call_index>("ba_call_index")
177+
.returns(INT)
178+
.prerun<&ba_call_index_prerun>()
156179
.build()))

villagesql/sdk/include/villagesql/abi/types.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,16 @@ typedef struct {
519519
// Extension-allocated state. Set this to pass data to vdf and postrun.
520520
// Caller initializes to NULL.
521521
void *user_data;
522+
523+
// Optional SDK-managed deleter for user_data. If non-NULL, the server
524+
// calls user_data_deleter(user_data) after the postrun returns (or after
525+
// the last vdf call, if no postrun was registered). Used by the C++ SDK's
526+
// PrerunResult::emplace_state<T> to install typed_delete<T> so extensions
527+
// do not have to write a postrun just to free state.
528+
//
529+
// Raw C ABI users typically leave this NULL and free user_data themselves
530+
// in their postrun.
531+
void (*user_data_deleter)(void *user_data);
522532
} vef_prerun_result_t;
523533

524534
typedef void (*vef_prerun_func_t)(vef_context_t *ctx, vef_prerun_args_t *args,

villagesql/sdk/include/villagesql/extension.h

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -114,19 +114,32 @@
114114
// PRERUN/POSTRUN FUNCTIONS
115115
// ------------------------
116116
//
117-
// For prerun/postrun functions (per-statement setup/teardown):
117+
// Prerun (per-statement setup) and postrun (per-statement teardown) hooks
118+
// use typed wrappers — see <villagesql/vsql/pre_post_run.h>. Raw ABI
119+
// signatures are not accepted.
120+
//
121+
// void my_prerun(vsql::PrerunArgs args, vsql::PrerunResult out) {
122+
// // validate args.type_at(i), call out.error(...) on failure,
123+
// // out.emplace_state<MyState>(...) for per-statement state, etc.
124+
// }
125+
// void my_postrun(vsql::PostrunArgs args) {
126+
// // optional; only needed if you used set_user_data with a non-C++
127+
// // allocator. emplace_state<T> is auto-freed by the SDK.
128+
// }
118129
//
119130
// make_func<&my_impl>("my_func")
120131
// .returns(STRING)
121-
// .prerun<&my_prerun>() // Called before first row
122-
// .postrun<&my_postrun>() // Called after last row
132+
// .prerun<&my_prerun>()
133+
// .postrun<&my_postrun>()
123134
// .build()
124135
//
125-
// Note: Prerun and postrun functions can be a cumbersome API. The func builder
126-
// already handles simple cases (e.g., type checking for functions with fixed
127-
// args and allocating fixed buffer sizes). We want to cover more cases. If
128-
// you find that you need to use prerun or postrun functions, please come talk
129-
// to us so we can understand your use case.
136+
// If your VDF wants the per-statement state, declare it as the first
137+
// parameter: void my_impl(MyState& s, IntArg a, IntResult out).
138+
//
139+
// The builder also handles common cases without you writing a hook (e.g.
140+
// arg-count/type validation for fixed-arity functions, fixed-size result
141+
// buffers). Reach for prerun only when you need custom validation, a
142+
// dynamically-sized result buffer, or per-statement state.
130143
//
131144
//
132145
// AGGREGATE FUNCTIONS

villagesql/sdk/include/villagesql/vsql/func_builder.h

Lines changed: 152 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
#include <villagesql/abi/types.h>
4848
#include <villagesql/vsql/func_types.h>
4949
#include <villagesql/vsql/maybe_params.h>
50+
#include <villagesql/vsql/pre_post_run.h>
5051
#include <villagesql/vsql/type_params_cache.h>
5152

5253
namespace vsql {
@@ -341,6 +342,51 @@ struct AggResultWithOutputWrapper {
341342
}
342343
};
343344

345+
// =============================================================================
346+
// Prerun / Postrun Adapters
347+
// =============================================================================
348+
349+
// Thunks that convert a typed user prerun/postrun (void(PrerunArgs,
350+
// PrerunResult) / void(PostrunArgs)) into the raw vef_*_func_t ABI shape.
351+
// Used by FuncBuilder::prerun/postrun when the user's hook is typed.
352+
353+
template <auto Hook>
354+
void typed_prerun_thunk(vef_context_t *, vef_prerun_args_t *args,
355+
vef_prerun_result_t *result) {
356+
Hook(PrerunArgs(args), PrerunResult(result));
357+
}
358+
359+
template <auto Hook>
360+
void typed_postrun_thunk(vef_context_t *, vef_postrun_args_t *args,
361+
vef_postrun_result_t *) {
362+
Hook(PostrunArgs(args));
363+
}
364+
365+
// Predicates used by FuncBuilder::prerun/postrun to decide which thunk
366+
// (if any) to install. Each is true exactly when the hook's signature
367+
// matches the typed shape for its slot.
368+
369+
template <auto Hook>
370+
constexpr bool is_typed_prerun() {
371+
using Params = typename FuncParamTypes<decltype(Hook)>::type;
372+
if constexpr (std::tuple_size_v<Params> != 2) {
373+
return false;
374+
} else {
375+
return std::is_same_v<std::tuple_element_t<0, Params>, PrerunArgs> &&
376+
std::is_same_v<std::tuple_element_t<1, Params>, PrerunResult>;
377+
}
378+
}
379+
380+
template <auto Hook>
381+
constexpr bool is_typed_postrun() {
382+
using Params = typename FuncParamTypes<decltype(Hook)>::type;
383+
if constexpr (std::tuple_size_v<Params> != 1) {
384+
return false;
385+
} else {
386+
return std::is_same_v<std::tuple_element_t<0, Params>, PostrunArgs>;
387+
}
388+
}
389+
344390
// =============================================================================
345391
// Wrapper Template
346392
// =============================================================================
@@ -383,6 +429,80 @@ struct Wrapper {
383429
}
384430
};
385431

432+
// Wrapper for VDFs whose first parameter is `State&` — typed per-statement
433+
// state allocated in prerun via PrerunResult::emplace_state<State>. The
434+
// wrapper dereferences args->user_data as State* and forwards a reference.
435+
//
436+
// Param tuple shape: <State&, TypedArg..., ResultWrapper>. NumParams is the
437+
// SQL argument count (not counting state or result), so typed args live at
438+
// indices [1, NumParams].
439+
template <auto Func, typename State, size_t NumParams>
440+
struct WrapperTypedState {
441+
static void invoke(vef_context_t *ctx, vef_vdf_args_t *args,
442+
vef_vdf_result_t *result) {
443+
invoke_impl(ctx, args, result, std::make_index_sequence<NumParams>{});
444+
}
445+
446+
private:
447+
template <size_t... Is>
448+
static void invoke_impl(vef_context_t *ctx, vef_vdf_args_t *args,
449+
vef_vdf_result_t *result,
450+
std::index_sequence<Is...>) {
451+
using Params = typename FuncParamTypes<decltype(Func)>::type;
452+
State &state = *static_cast<State *>(args->user_data);
453+
std::array<vef_invalue_t, NumParams> vals{
454+
get_invalue(ctx, args, static_cast<unsigned int>(Is))...};
455+
Func(state, make_arg<std::tuple_element_t<1 + Is, Params>>(&vals[Is])...,
456+
make_result<std::tuple_element_t<1 + NumParams, Params>>(result));
457+
}
458+
459+
template <typename T>
460+
static T make_arg(vef_invalue_t *v) {
461+
return T(v);
462+
}
463+
template <typename T>
464+
static T make_result(vef_vdf_result_t *r) {
465+
return T(r);
466+
}
467+
};
468+
469+
// Wrapper for VDFs whose first parameter is `void*` — raw escape hatch for
470+
// extensions that manage state with custom allocators, polymorphic state,
471+
// or anything that doesn't fit emplace_state<T>. The wrapper forwards
472+
// args->user_data straight through.
473+
//
474+
// Param tuple shape: <void*, TypedArg..., ResultWrapper>. Same indexing as
475+
// WrapperTypedState.
476+
template <auto Func, size_t NumParams>
477+
struct WrapperVoidState {
478+
static void invoke(vef_context_t *ctx, vef_vdf_args_t *args,
479+
vef_vdf_result_t *result) {
480+
invoke_impl(ctx, args, result, std::make_index_sequence<NumParams>{});
481+
}
482+
483+
private:
484+
template <size_t... Is>
485+
static void invoke_impl(vef_context_t *ctx, vef_vdf_args_t *args,
486+
vef_vdf_result_t *result,
487+
std::index_sequence<Is...>) {
488+
using Params = typename FuncParamTypes<decltype(Func)>::type;
489+
std::array<vef_invalue_t, NumParams> vals{
490+
get_invalue(ctx, args, static_cast<unsigned int>(Is))...};
491+
Func(args->user_data,
492+
make_arg<std::tuple_element_t<1 + Is, Params>>(&vals[Is])...,
493+
make_result<std::tuple_element_t<1 + NumParams, Params>>(result));
494+
}
495+
496+
template <typename T>
497+
static T make_arg(vef_invalue_t *v) {
498+
return T(v);
499+
}
500+
template <typename T>
501+
static T make_result(vef_vdf_result_t *r) {
502+
return T(r);
503+
}
504+
};
505+
386506
// =============================================================================
387507
// Type Operation VDF Wrappers
388508
// =============================================================================
@@ -962,15 +1082,21 @@ class FuncBuilder {
9621082
return *this;
9631083
}
9641084

965-
template <vef_prerun_func_t Hook>
1085+
template <auto Hook>
9661086
constexpr FuncBuilder<Func, NumParams> &prerun() {
967-
prerun_ = Hook;
1087+
static_assert(detail::is_typed_prerun<Hook>(),
1088+
"prerun<Hook>(): Hook must be void(PrerunArgs, "
1089+
"PrerunResult). Raw ABI signatures are not accepted.");
1090+
prerun_ = &detail::typed_prerun_thunk<Hook>;
9681091
return *this;
9691092
}
9701093

971-
template <vef_postrun_func_t Hook>
1094+
template <auto Hook>
9721095
constexpr FuncBuilder<Func, NumParams> &postrun() {
973-
postrun_ = Hook;
1096+
static_assert(detail::is_typed_postrun<Hook>(),
1097+
"postrun<Hook>(): Hook must be void(PostrunArgs). Raw ABI "
1098+
"signatures are not accepted.");
1099+
postrun_ = &detail::typed_postrun_thunk<Hook>;
9741100
return *this;
9751101
}
9761102

@@ -982,7 +1108,28 @@ class FuncBuilder {
9821108
using UniquePTuple = typename detail::unique_params_types<AllParams>::type;
9831109

9841110
detail::FuncWithMetadata meta{};
985-
meta.f = &detail::Wrapper<Func, NumParams>::invoke;
1111+
// Dispatch on the first parameter type to decide whether the function
1112+
// wants state passed in. Three shapes:
1113+
// void(TypedArgs..., ResultWrapper) — stateless
1114+
// void(State&, TypedArgs..., ResultWrapper) — typed state, mutable
1115+
// void(const State&, TypedArgs..., ResultWrapper) — typed state, const
1116+
// void(void*, TypedArgs..., ResultWrapper) — raw state
1117+
if constexpr (std::tuple_size_v<AllParams> >= 2) {
1118+
using First = std::tuple_element_t<0, AllParams>;
1119+
if constexpr (std::is_same_v<First, void *>) {
1120+
meta.f = &detail::WrapperVoidState<Func, NumParams>::invoke;
1121+
} else if constexpr (std::is_lvalue_reference_v<First>) {
1122+
// Both `State&` and `const State&` route to WrapperTypedState; the
1123+
// implicit conversion from `State&` to `const State&` happens at the
1124+
// call site if the user opted for read-only.
1125+
using State = std::remove_const_t<std::remove_reference_t<First>>;
1126+
meta.f = &detail::WrapperTypedState<Func, State, NumParams>::invoke;
1127+
} else {
1128+
meta.f = &detail::Wrapper<Func, NumParams>::invoke;
1129+
}
1130+
} else {
1131+
meta.f = &detail::Wrapper<Func, NumParams>::invoke;
1132+
}
9861133
meta.prerun = prerun_;
9871134
meta.postrun = postrun_;
9881135
meta.return_type = detail::to_vef_type(return_type_);

0 commit comments

Comments
 (0)