Skip to content

Commit 0cdb76c

Browse files
dbentley-vsqlclaude
andcommitted
sdk: typed prerun/postrun hooks
Introduce typed C++ wrappers for the per-statement lifecycle hooks (prerun and postrun). Extension authors no longer touch raw ABI structs to write these hooks; only the typed signatures are accepted. Why --- Extension authors writing prerun/postrun previously had to: void my_prerun(vef_context_t *, vef_prerun_args_t *args, vef_prerun_result_t *result) { if (args->arg_count == 0) { result->type = VEF_RESULT_ERROR; snprintf(result->error_msg, VEF_MAX_ERROR_LEN, "..."); return; } result->result_buffer_size = N; result->user_data = new MyState{}; } Two problems: raw ABI in the user-facing API (against the SDK's direction; see #548) and bespoke error-reporting plumbing. After this PR: void my_prerun(vsql::PrerunArgs args, vsql::PrerunResult out) { if (args.size() == 0) { out.error("at least one argument required"); return; } out.request_buffer_size(N); out.set_user_data(new MyState{}); } void my_postrun(vsql::PostrunArgs args) { args.delete_state<MyState>(); } void my_vdf(MyState &state, IntResult out) { ... } State lifetime stays explicit in this PR: prerun stashes the pointer, postrun frees it. A follow-up will add auto-cleanup via .state<T>() (implementable without ABI changes; see TODO in pre_post_run.h). Where to start reviewing ------------------------ Read the layers in this order: 1. villagesql/sdk/include/villagesql/vsql/pre_post_run.h New public types: PrerunArgs, PrerunArgType, PrerunResult, PostrunArgs. This is what extension authors see. 2. villagesql/examples/vsql-simple/src/extension.cc The ba_call_index demo: set_user_data in prerun, mutable State& in the VDF, delete_state in postrun. 3. mysql-test/suite/villagesql/extension/t/extension_simple_type_usage.test What it looks like at the SQL surface. 4. villagesql/sdk/include/villagesql/extension.h Refreshed user-facing docs for the typed hook shape. The remaining file is SDK plumbing: 5. villagesql/sdk/include/villagesql/vsql/func_builder.h - .prerun<>() / .postrun<>() are typed-only; static_assert rejects raw vef_prerun_func_t / vef_postrun_func_t. - FuncBuilder gained a HasPrerun template flag; .prerun<>() flips it true, and build() static_asserts that void(State&,...) and void(void*,...) signatures require it. Registering a state-style VDF without a prerun is a compile error, not a runtime null deref. - WrapperTypedState / WrapperVoidState dispatch on the first param of the VDF: State& / const State& / void* selects how user_data is forwarded. - typed_prerun_thunk / typed_postrun_thunk adapt the user's typed signature to the raw ABI shape the server invokes. Not in this PR -------------- - A `.state<T>()` builder that records State at the template level and routes typed signatures of the form `void(T&, ...)`. The SDK would install both prerun and postrun thunks to manage the typed state's lifetime via the existing user_data slot — no ABI side channel needed. Will land as a follow-up. - Typed VarArgs/AnyArg wrappers for varargs VDFs (the other raw-ABI place in extension code, only reached via PR #254). - vef_postrun_result_t is empty in the ABI today, so there is no PostrunResult wrapper. Will add when the struct grows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3bd0964 commit 0cdb76c

7 files changed

Lines changed: 437 additions & 54 deletions

File tree

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ ERROR HY000: Cannot initialize function 'ba_concat': wrong number of arguments (
7979
SELECT vsql_simple.ba_concat_all(t.data) AS one
8080
FROM test_bytearray t WHERE t.id = 1;
8181
one
82-
hello
82+
hello
8383
SELECT vsql_simple.ba_concat_all(t1.data, t2.data, t3.data) AS three
8484
FROM test_bytearray t1, test_bytearray t2, test_bytearray t3
8585
WHERE t1.id = 1 AND t2.id = 2 AND t3.id = 3;
@@ -108,6 +108,12 @@ SELECT vsql_simple.ba_concat_all(t1.data, 'abc') AS with_str
108108
FROM test_bytearray t1 WHERE t1.id = 1;
109109
with_str
110110
NULL
111+
# Test ba_call_index (exercises typed prerun/postrun + state-passing VDF)
112+
SELECT vsql_simple.ba_call_index() as idx FROM test_bytearray ORDER BY id;
113+
idx
114+
1
115+
2
116+
3
111117
# Clean up
112118
DROP TABLE test_bytearray;
113119
# 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
@@ -79,6 +79,9 @@ FROM test_bytearray t1 WHERE t1.id = 1;
7979
SELECT vsql_simple.ba_concat_all(t1.data, 'abc') AS with_str
8080
FROM test_bytearray t1 WHERE t1.id = 1;
8181

82+
--echo # Test ba_call_index (exercises typed prerun/postrun + state-passing VDF)
83+
SELECT vsql_simple.ba_call_index() as idx FROM test_bytearray ORDER BY id;
84+
8285
--echo # Clean up
8386
DROP TABLE test_bytearray;
8487

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,28 @@ 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/postrun + State& pattern: prerun
117+
// allocates a counter, each row reads-and-increments via the VDF's State&
118+
// first parameter, and postrun frees.
119+
120+
struct CallCounter {
121+
long long n = 0;
122+
};
123+
124+
void ba_call_index_prerun(PrerunArgs, PrerunResult out) {
125+
out.set_user_data(new CallCounter{});
126+
}
127+
128+
void ba_call_index(CallCounter &state, IntResult out) {
129+
state.n++;
130+
out.set(state.n);
131+
}
132+
133+
void ba_call_index_postrun(PostrunArgs args) {
134+
args.delete_state<CallCounter>();
135+
}
136+
115137
// BA_CONCAT: concatenate two bytearrays (returns STRING with 16 bytes)
116138
void ba_concat(CustomArg a, CustomArg b, StringResult out) {
117139
if (a.is_null() || b.is_null()) {
@@ -205,6 +227,12 @@ VEF_GENERATE_ENTRY_POINTS(
205227
.param(BYTEARRAY)
206228
.param(BYTEARRAY)
207229
.build())
230+
.func(make_func<&ba_call_index>("ba_call_index")
231+
.returns(INT)
232+
.param()
233+
.prerun<&ba_call_index_prerun>()
234+
.postrun<&ba_call_index_postrun>()
235+
.build())
208236
.func(make_func<&ba_len>("ba_len").returns(INT).param().build())
209237
.func(make_func<&ba_concat_all>("ba_concat_all")
210238
.returns(STRING)

villagesql/sdk/include/villagesql/detail/func_builder.h

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
#include <villagesql/abi/types.h>
3535
#include <villagesql/vsql/func_types.h>
36+
#include <villagesql/vsql/pre_post_run.h>
3637
#include <villagesql/vsql/type_params.h>
3738
#include <villagesql/vsql/var_args.h>
3839

@@ -378,6 +379,47 @@ struct AggResultWithOutputWrapper {
378379
}
379380
};
380381

382+
// Wrappers that convert a typed user prerun/postrun (void(PrerunArgs,
383+
// PrerunResult) / void(PostrunArgs)) into the raw vef_*_func_t ABI shape.
384+
// Used by FuncBuilder::prerun/postrun when the user's hook is typed.
385+
386+
template <auto Hook>
387+
void typed_prerun_wrapper(vef_context_t *, vef_prerun_args_t *args,
388+
vef_prerun_result_t *result) {
389+
Hook(PrerunArgs(args), PrerunResult(result));
390+
}
391+
392+
template <auto Hook>
393+
void typed_postrun_wrapper(vef_context_t *, vef_postrun_args_t *args,
394+
vef_postrun_result_t *) {
395+
Hook(PostrunArgs(args));
396+
}
397+
398+
// Predicates used by FuncBuilder::prerun/postrun to decide which wrapper
399+
// (if any) to install. Each is true exactly when the hook's signature
400+
// matches the typed shape for its slot.
401+
402+
template <auto Hook>
403+
constexpr bool is_typed_prerun() {
404+
using Params = typename FuncParamTypes<decltype(Hook)>::type;
405+
if constexpr (std::tuple_size_v<Params> != 2) {
406+
return false;
407+
} else {
408+
return std::is_same_v<std::tuple_element_t<0, Params>, PrerunArgs> &&
409+
std::is_same_v<std::tuple_element_t<1, Params>, PrerunResult>;
410+
}
411+
}
412+
413+
template <auto Hook>
414+
constexpr bool is_typed_postrun() {
415+
using Params = typename FuncParamTypes<decltype(Hook)>::type;
416+
if constexpr (std::tuple_size_v<Params> != 1) {
417+
return false;
418+
} else {
419+
return std::is_same_v<std::tuple_element_t<0, Params>, PostrunArgs>;
420+
}
421+
}
422+
381423
// Generates a vef_vdf_func_t that unpacks vef_vdf_args_t and adapts each
382424
// argument and result to the declared typed wrapper parameter of Func.
383425
//
@@ -443,6 +485,80 @@ struct VarArgsWrapper {
443485
}
444486
};
445487

488+
// Wrapper for VDFs whose first parameter is `State&` — typed per-statement
489+
// state allocated in prerun via PrerunResult::emplace_state<State>. The
490+
// wrapper dereferences args->user_data as State* and forwards a reference.
491+
//
492+
// Param tuple shape: <State&, TypedArg..., ResultWrapper>. NumParams is the
493+
// SQL argument count (not counting state or result), so typed args live at
494+
// indices [1, NumParams].
495+
template <auto Func, typename State, size_t NumParams>
496+
struct WrapperTypedState {
497+
static void invoke(vef_context_t *ctx, vef_vdf_args_t *args,
498+
vef_vdf_result_t *result) {
499+
invoke_impl(ctx, args, result, std::make_index_sequence<NumParams>{});
500+
}
501+
502+
private:
503+
template <size_t... Is>
504+
static void invoke_impl(vef_context_t *ctx, vef_vdf_args_t *args,
505+
vef_vdf_result_t *result,
506+
std::index_sequence<Is...>) {
507+
using Params = typename FuncParamTypes<decltype(Func)>::type;
508+
State &state = *static_cast<State *>(args->user_data);
509+
std::array<vef_invalue_t, NumParams> vals{
510+
get_invalue(ctx, args, static_cast<unsigned int>(Is))...};
511+
Func(state, make_arg<std::tuple_element_t<1 + Is, Params>>(&vals[Is])...,
512+
make_result<std::tuple_element_t<1 + NumParams, Params>>(result));
513+
}
514+
515+
template <typename T>
516+
static T make_arg(vef_invalue_t *v) {
517+
return T(v);
518+
}
519+
template <typename T>
520+
static T make_result(vef_vdf_result_t *r) {
521+
return T(r);
522+
}
523+
};
524+
525+
// Wrapper for VDFs whose first parameter is `void*` — raw escape hatch for
526+
// extensions that manage state with custom allocators, polymorphic state,
527+
// or anything that doesn't fit emplace_state<T>. The wrapper forwards
528+
// args->user_data straight through.
529+
//
530+
// Param tuple shape: <void*, TypedArg..., ResultWrapper>. Same indexing as
531+
// WrapperTypedState.
532+
template <auto Func, size_t NumParams>
533+
struct WrapperVoidState {
534+
static void invoke(vef_context_t *ctx, vef_vdf_args_t *args,
535+
vef_vdf_result_t *result) {
536+
invoke_impl(ctx, args, result, std::make_index_sequence<NumParams>{});
537+
}
538+
539+
private:
540+
template <size_t... Is>
541+
static void invoke_impl(vef_context_t *ctx, vef_vdf_args_t *args,
542+
vef_vdf_result_t *result,
543+
std::index_sequence<Is...>) {
544+
using Params = typename FuncParamTypes<decltype(Func)>::type;
545+
std::array<vef_invalue_t, NumParams> vals{
546+
get_invalue(ctx, args, static_cast<unsigned int>(Is))...};
547+
Func(args->user_data,
548+
make_arg<std::tuple_element_t<1 + Is, Params>>(&vals[Is])...,
549+
make_result<std::tuple_element_t<1 + NumParams, Params>>(result));
550+
}
551+
552+
template <typename T>
553+
static T make_arg(vef_invalue_t *v) {
554+
return T(v);
555+
}
556+
template <typename T>
557+
static T make_result(vef_vdf_result_t *r) {
558+
return T(r);
559+
}
560+
};
561+
446562
// Pre-fills result->type / result->error_msg with a default
447563
// "failed to encode '<input>'" warning, truncating long inputs. Both encode
448564
// wrappers call this before invoking the extension's from_string so that an

villagesql/sdk/include/villagesql/vsql.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@
6060
// .build()))
6161
//
6262
// For aggregate VDFs (SQL SUM, COUNT, etc.), see make_aggregate_func in
63-
// vsql/func_builder.h. For full documentation see the individual headers below.
63+
// vsql/func_builder.h. For per-statement lifecycle hooks (prerun/postrun),
64+
// see vsql/pre_post_run.h. For full documentation see the individual
65+
// headers below.
6466

6567
// Typed function and type-operation builders (rejects raw ABI signatures).
6668
#include <villagesql/vsql/func_builder.h>

0 commit comments

Comments
 (0)