Skip to content

Commit f64fdab

Browse files
sdk: typed prerun/postrun (#551)
* 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> * demo: state_writeback test showing void* user_data writeback gap * fix previous VDF * vef: convert ba_concat_all_prerun to typed PrerunArgs/PrerunResult ---------
1 parent 3bd0964 commit f64fdab

10 files changed

Lines changed: 637 additions & 66 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
@@ -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
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# restart: --veb-dir=MYSQLTEST_VARDIR/veb
2+
Creating extension test_state_writeback using SDK...
3+
Created test_state_writeback.veb
4+
INSTALL EXTENSION test_state_writeback;
5+
CREATE TABLE seq (id INT PRIMARY KEY, s VARCHAR(16));
6+
INSERT INTO seq VALUES
7+
(1, 'a'),
8+
(2, 'bb'),
9+
(3, 'ccc'),
10+
(4, 'dd'),
11+
(5, 'ee'),
12+
(6, 'ffffffff');
13+
# First row prev is NULL; later rows reflect prior s.
14+
# Rows 4-5 reuse the same buffer (shorter than capacity);
15+
# row 6 reallocs because 'ffffffff' doesn't fit.
16+
SELECT id, s, test_state_writeback.previous(s) AS prev FROM seq ORDER BY id;
17+
id s prev
18+
1 a NULL
19+
2 bb a
20+
3 ccc bb
21+
4 dd ccc
22+
5 ee dd
23+
6 ffffffff ee
24+
DROP TABLE seq;
25+
UNINSTALL EXTENSION test_state_writeback;

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# previous(s) returns the s passed to the previous row (or NULL on the
2+
# first row). Implemented via the typed prerun/postrun + `void*&` user_data
3+
# slot (WrapperVoidStarRefState) introduced in #551.
4+
#
5+
# The VDF mallocs storage on first need, reuses it when subsequent strings
6+
# fit, and reallocates when a longer string arrives. Postrun frees the
7+
# final buffer.
8+
9+
# Setup VEB directory for testing
10+
--let $veb_dir = $MYSQLTEST_VARDIR/veb
11+
--exec mkdir -p $veb_dir
12+
--replace_result $MYSQLTEST_VARDIR MYSQLTEST_VARDIR
13+
--let $restart_parameters = restart: --veb-dir=$veb_dir
14+
--source include/restart_mysqld.inc
15+
16+
--let $extension_name = test_state_writeback
17+
--let $extension_version = 1.0.0
18+
--let $extension_source = $MYSQL_TEST_DIR/suite/villagesql/std_data/state_writeback_vdf.cc
19+
--source include/villagesql/create_extension_sdk.inc
20+
21+
INSTALL EXTENSION test_state_writeback;
22+
23+
CREATE TABLE seq (id INT PRIMARY KEY, s VARCHAR(16));
24+
INSERT INTO seq VALUES
25+
(1, 'a'),
26+
(2, 'bb'),
27+
(3, 'ccc'),
28+
(4, 'dd'),
29+
(5, 'ee'),
30+
(6, 'ffffffff');
31+
32+
--echo # First row prev is NULL; later rows reflect prior s.
33+
--echo # Rows 4-5 reuse the same buffer (shorter than capacity);
34+
--echo # row 6 reallocs because 'ffffffff' doesn't fit.
35+
SELECT id, s, test_state_writeback.previous(s) AS prev FROM seq ORDER BY id;
36+
37+
DROP TABLE seq;
38+
UNINSTALL EXTENSION test_state_writeback;
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/* Copyright (c) 2026 VillageSQL Contributors
2+
*
3+
* This program is free software; you can redistribute it and/or
4+
* modify it under the terms of the GNU General Public License
5+
* as published by the Free Software Foundation; either version 2
6+
* of the License, or (at your option) any later version.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License
14+
* along with this program; if not, see <https://www.gnu.org/licenses/>.
15+
*/
16+
17+
// Test-only extension demonstrating the typed prerun/postrun + `void*&`
18+
// user_data slot (WrapperVoidStarRefState) from PR #551.
19+
//
20+
// previous(s) returns the s passed to the previous row, or NULL on the
21+
// first row. Storage is a malloc'd buffer laid out as:
22+
// [size_t capacity][size_t length][capacity bytes of payload]
23+
// capacity is the payload bytes currently available; length is how many
24+
// of those are in use. When a longer string arrives we free the buffer
25+
// and malloc a new one; shorter strings reuse the existing allocation.
26+
27+
#include <villagesql/vsql.h>
28+
29+
#include <cstdlib>
30+
#include <cstring>
31+
32+
using namespace vsql;
33+
34+
struct Hdr {
35+
size_t capacity;
36+
size_t length;
37+
};
38+
39+
static void previous_prerun(PrerunArgs, PrerunResult out) {
40+
out.set_user_data(nullptr);
41+
}
42+
43+
void previous_vdf(void *&user_data, StringArg s, StringResult out) {
44+
// Emit the previous value, if any.
45+
if (user_data != nullptr) {
46+
auto *hdr = static_cast<Hdr *>(user_data);
47+
const char *bytes = reinterpret_cast<const char *>(hdr) + sizeof(Hdr);
48+
out.set(std::string_view(bytes, hdr->length));
49+
} else {
50+
out.set_null();
51+
}
52+
53+
// Stash the current row's value for the next call. A NULL input clears
54+
// the slot (and frees any buffer we were carrying).
55+
if (s.is_null()) {
56+
std::free(user_data);
57+
user_data = nullptr;
58+
return;
59+
}
60+
61+
auto sv = s.value();
62+
size_t needed = sv.size();
63+
auto *hdr = static_cast<Hdr *>(user_data);
64+
if (hdr == nullptr || hdr->capacity < needed) {
65+
std::free(user_data);
66+
hdr = static_cast<Hdr *>(std::malloc(sizeof(Hdr) + needed));
67+
hdr->capacity = needed;
68+
}
69+
hdr->length = needed;
70+
std::memcpy(reinterpret_cast<char *>(hdr) + sizeof(Hdr), sv.data(), needed);
71+
user_data = hdr;
72+
}
73+
74+
static void previous_postrun(PostrunArgs args) { std::free(args.user_data()); }
75+
76+
VEF_GENERATE_ENTRY_POINTS(
77+
make_extension().func(make_func<&previous_vdf>("previous")
78+
.returns(STRING)
79+
.param(STRING)
80+
.buffer_size(1024)
81+
.prerun<&previous_prerun>()
82+
.postrun<&previous_postrun>()
83+
.build()))

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

Lines changed: 37 additions & 13 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()) {
@@ -136,24 +158,20 @@ void ba_len(IntResult out) { out.set(static_cast<long long>(kBytearrayLen)); }
136158
// Prerun: validate that all arguments are BYTEARRAY (or NULL literals,
137159
// which appear as VEF_TYPE_STRING in the prerun arg-type array) and ask
138160
// the server to allocate arg_count * kBytearrayLen bytes of result buffer.
139-
void ba_concat_all_prerun(vef_context_t *, vef_prerun_args_t *args,
140-
vef_prerun_result_t *result) {
141-
if (args->arg_count == 0) {
142-
result->type = VEF_RESULT_ERROR;
143-
snprintf(result->error_msg, VEF_MAX_ERROR_LEN,
144-
"ba_concat_all requires at least one argument");
161+
void ba_concat_all_prerun(vsql::PrerunArgs args, vsql::PrerunResult out) {
162+
if (args.size() == 0) {
163+
out.error("ba_concat_all requires at least one argument");
145164
return;
146165
}
147-
for (unsigned int i = 0; i < args->arg_count; i++) {
148-
vef_type_id id = args->arg_types[i].id;
149-
if (id != VEF_TYPE_CUSTOM && id != VEF_TYPE_STRING) {
150-
result->type = VEF_RESULT_ERROR;
151-
snprintf(result->error_msg, VEF_MAX_ERROR_LEN,
152-
"ba_concat_all: argument %u must be BYTEARRAY", i);
166+
for (size_t i = 0; i < args.size(); i++) {
167+
auto t = args.type_at(i);
168+
if (!t.is_custom() && !t.is_str()) {
169+
out.error("ba_concat_all: argument " + std::to_string(i) +
170+
" must be BYTEARRAY");
153171
return;
154172
}
155173
}
156-
result->result_buffer_size = args->arg_count * kBytearrayLen;
174+
out.request_buffer_size(args.size() * kBytearrayLen);
157175
}
158176

159177
void ba_concat_all(VarArgs args, StringResult out) {
@@ -205,6 +223,12 @@ VEF_GENERATE_ENTRY_POINTS(
205223
.param(BYTEARRAY)
206224
.param(BYTEARRAY)
207225
.build())
226+
.func(make_func<&ba_call_index>("ba_call_index")
227+
.returns(INT)
228+
.param()
229+
.prerun<&ba_call_index_prerun>()
230+
.postrun<&ba_call_index_postrun>()
231+
.build())
208232
.func(make_func<&ba_len>("ba_len").returns(INT).param().build())
209233
.func(make_func<&ba_concat_all>("ba_concat_all")
210234
.returns(STRING)

0 commit comments

Comments
 (0)