Skip to content

Commit 3bd0964

Browse files
sdk: typed varargs (#563)
NB: this is analogous to #254 , but rebased and simplified sdk: typed varargs + required explicit arity Introduce typed C++ wrappers for varargs VDFs (vsql::VarArgs, vsql::AnyArg) and the matching .varargs() / zero-arity .param() builder methods, so an extension author can write a variadic SQL function without touching raw ABI types. Also tightens the v2 builder to require an explicit arity declaration before .build(). What changes for users Before, an extension author who wanted a varargs SQL function had to drop out of the typed SDK and write a raw-ABI body — the typed wrappers covered fixed-arity only: // Old: raw vef_vdf_func_t signature, manual invalue extraction void ba_concat_all(vef_context_t *ctx, vef_vdf_args_t *args, vef_vdf_result_t *result) { for (unsigned int i = 0; i < args->value_count; i++) { vef_invalue_t v = vsql::func_builder::get_invalue(ctx, args, i); if (v.is_null) { result->type = VEF_RESULT_NULL; return; } memcpy(result->str_buf + i * N, v.bin_value, N); } result->type = VEF_RESULT_VALUE; result->actual_len = args->value_count * N; } After: // New: typed VarArgs + AnyArg, no raw ABI in the body void ba_concat_all(vsql::VarArgs args, vsql::StringResult out) { auto dst = out.buffer(); size_t off = 0; for (auto a : args) { if (a.is_null()) { out.set_null(); return; } auto bytes = a.as_custom(); memcpy(dst.data() + off, bytes.data(), bytes.size()); off += bytes.size(); } out.set_length(off); } Registration picks up .varargs() (and zero-arity .param()): .func(make_func<&ba_len>("ba_len") .returns(INT) .param() // explicit zero-arity .build()) .func(make_func<&ba_concat_all>("ba_concat_all") .returns(STRING) .varargs() .prerun<&ba_concat_all_prerun>() .build()) The prerun hook is still raw ABI in this PR; the typed variant lands with #551 (typed vsql::PrerunArgs / PrerunResult), and ba_concat_all_prerun will convert to typed form when that merges. Required-arity change The v2 builder now rejects make_func<&fn>("name").returns(...).build() without an arity call. You must pick exactly one of: .param(TYPE) (one or more, for fixed-arity typed args) .param() (zero-arity) .varargs() Eight in-tree test-extensions had implicit zero-arity functions and pick up an explicit .param() in this PR. The v1 builder (villagesql::func_builder, raw-ABI) is unchanged — that cow has left the barn.
1 parent 0922485 commit 3bd0964

22 files changed

Lines changed: 502 additions & 53 deletions

File tree

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,46 @@ FROM test_bytearray t1, test_bytearray t2
6868
WHERE t1.id = 1 AND t2.id = 2;
6969
concatenated
7070
'hello world123'
71+
# Test ba_len function (zero-arity, returns a constant)
72+
SELECT vsql_simple.ba_len() AS len;
73+
len
74+
8
75+
# ba_concat (declared with two params) rejects a single-arg call
76+
SELECT vsql_simple.ba_concat(t.data) FROM test_bytearray t WHERE t.id = 1;
77+
ERROR HY000: Cannot initialize function 'ba_concat': wrong number of arguments (expected 2, got 1)
78+
# Test ba_concat_all function (varargs, returns STRING)
79+
SELECT vsql_simple.ba_concat_all(t.data) AS one
80+
FROM test_bytearray t WHERE t.id = 1;
81+
one
82+
hello
83+
SELECT vsql_simple.ba_concat_all(t1.data, t2.data, t3.data) AS three
84+
FROM test_bytearray t1, test_bytearray t2, test_bytearray t3
85+
WHERE t1.id = 1 AND t2.id = 2 AND t3.id = 3;
86+
three
87+
hello world123abcdefgh
88+
SELECT CONCAT("'", vsql_simple.ba_concat_all(t1.data, t2.data, t3.data), "'") AS three
89+
FROM test_bytearray t1, test_bytearray t2, test_bytearray t3
90+
WHERE t1.id = 1 AND t2.id = 2 AND t3.id = 3;
91+
three
92+
'hello world123abcdefgh'
93+
# ba_concat_all rejects zero arguments via prerun
94+
SELECT vsql_simple.ba_concat_all();
95+
ERROR HY000: Can't initialize function 'ba_concat_all'; ba_concat_all requires at least one argument
96+
# ba_concat_all rejects non-BYTEARRAY arguments via prerun
97+
SELECT vsql_simple.ba_concat_all(1);
98+
ERROR HY000: Can't initialize function 'ba_concat_all'; ba_concat_all: argument 0 must be BYTEARRAY
99+
# ba_concat_all propagates NULL
100+
SELECT vsql_simple.ba_concat_all(t1.data, NULL) AS with_null
101+
FROM test_bytearray t1 WHERE t1.id = 1;
102+
with_null
103+
NULL
104+
# ba_concat_all treats a non-NULL STRING literal as NULL: prerun lets
105+
# STRING through so NULL literals (typed STRING) work, but cannot
106+
# distinguish them from real strings, so the body checks is_custom().
107+
SELECT vsql_simple.ba_concat_all(t1.data, 'abc') AS with_str
108+
FROM test_bytearray t1 WHERE t1.id = 1;
109+
with_str
110+
NULL
71111
# Clean up
72112
DROP TABLE test_bytearray;
73113
# Uninstall the extension

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,41 @@ 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_len function (zero-arity, returns a constant)
48+
SELECT vsql_simple.ba_len() AS len;
49+
50+
--echo # ba_concat (declared with two params) rejects a single-arg call
51+
--error ER_VILLAGESQL_GENERIC_ERROR
52+
SELECT vsql_simple.ba_concat(t.data) FROM test_bytearray t WHERE t.id = 1;
53+
54+
--echo # Test ba_concat_all function (varargs, returns STRING)
55+
SELECT vsql_simple.ba_concat_all(t.data) AS one
56+
FROM test_bytearray t WHERE t.id = 1;
57+
SELECT vsql_simple.ba_concat_all(t1.data, t2.data, t3.data) AS three
58+
FROM test_bytearray t1, test_bytearray t2, test_bytearray t3
59+
WHERE t1.id = 1 AND t2.id = 2 AND t3.id = 3;
60+
SELECT CONCAT("'", vsql_simple.ba_concat_all(t1.data, t2.data, t3.data), "'") AS three
61+
FROM test_bytearray t1, test_bytearray t2, test_bytearray t3
62+
WHERE t1.id = 1 AND t2.id = 2 AND t3.id = 3;
63+
64+
--echo # ba_concat_all rejects zero arguments via prerun
65+
--error ER_CANT_INITIALIZE_UDF
66+
SELECT vsql_simple.ba_concat_all();
67+
68+
--echo # ba_concat_all rejects non-BYTEARRAY arguments via prerun
69+
--error ER_CANT_INITIALIZE_UDF
70+
SELECT vsql_simple.ba_concat_all(1);
71+
72+
--echo # ba_concat_all propagates NULL
73+
SELECT vsql_simple.ba_concat_all(t1.data, NULL) AS with_null
74+
FROM test_bytearray t1 WHERE t1.id = 1;
75+
76+
--echo # ba_concat_all treats a non-NULL STRING literal as NULL: prerun lets
77+
--echo # STRING through so NULL literals (typed STRING) work, but cannot
78+
--echo # distinguish them from real strings, so the body checks is_custom().
79+
SELECT vsql_simple.ba_concat_all(t1.data, 'abc') AS with_str
80+
FROM test_bytearray t1 WHERE t1.id = 1;
81+
4782
--echo # Clean up
4883
DROP TABLE test_bytearray;
4984

mysql-test/suite/villagesql/std_data/simple_vdf.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@ VEF_GENERATE_ENTRY_POINTS(
4444
make_extension()
4545
.func(make_func<&simple_int_func_impl>("simple_int_func")
4646
.returns(INT)
47+
.param()
4748
.build())
4849
.func(make_func<&simple_string_func_impl>("simple_string_func")
4950
.returns(STRING)
5051
.buffer_size(100)
52+
.param()
5153
.build())
5254
.func(make_func<&simple_test_impl>("simple_test")
5355
.returns(INT)

mysql-test/suite/villagesql/std_data/simple_vdf_alt.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ VEF_GENERATE_ENTRY_POINTS(
4040
make_extension()
4141
.func(make_func<&simple_int_func_impl>("simple_int_func")
4242
.returns(INT)
43+
.param()
4344
.build())
4445
.func(make_func<&alt_string_func_impl>("alt_string_func")
4546
.returns(STRING)
4647
.buffer_size(100)
48+
.param()
4749
.build()))

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

Lines changed: 76 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,56 @@ void ba_concat(CustomArg a, CustomArg b, StringResult out) {
125125
out.set_length(kBytearrayLen * 2);
126126
}
127127

128+
// BA_LEN: return the fixed length of a BYTEARRAY (zero-arity constant).
129+
// Demonstrates .param() (zero-arity) on the func builder.
130+
void ba_len(IntResult out) { out.set(static_cast<long long>(kBytearrayLen)); }
131+
132+
// BA_CONCAT_ALL: concatenate any number of BYTEARRAY values into a STRING.
133+
// Demonstrates .varargs() on the func builder paired with a prerun that
134+
// validates argument types and sizes the result buffer.
135+
//
136+
// Prerun: validate that all arguments are BYTEARRAY (or NULL literals,
137+
// which appear as VEF_TYPE_STRING in the prerun arg-type array) and ask
138+
// 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");
145+
return;
146+
}
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);
153+
return;
154+
}
155+
}
156+
result->result_buffer_size = args->arg_count * kBytearrayLen;
157+
}
158+
159+
void ba_concat_all(VarArgs args, StringResult out) {
160+
auto dst = out.buffer();
161+
size_t off = 0;
162+
for (auto a : args) {
163+
// Prerun accepts VEF_TYPE_STRING so NULL literals (which arrive typed as
164+
// STRING) pass type-check, but it cannot distinguish a NULL literal from
165+
// a non-NULL string literal like 'abc'. Treat any non-custom argument
166+
// here as NULL so we never call as_custom() on a STRING value.
167+
if (a.is_null() || !a.is_custom()) {
168+
out.set_null();
169+
return;
170+
}
171+
auto bytes = a.as_custom();
172+
memcpy(dst.data() + off, bytes.data(), bytes.size());
173+
off += bytes.size();
174+
}
175+
out.set_length(off);
176+
}
177+
128178
static constexpr const char kBytearrayTypeName[] = "bytearray";
129179

130180
constexpr auto BYTEARRAY = vsql::make_type<kBytearrayTypeName>()
@@ -135,22 +185,29 @@ constexpr auto BYTEARRAY = vsql::make_type<kBytearrayTypeName>()
135185
.compare<&bytearray_compare>()
136186
.build();
137187

138-
VEF_GENERATE_ENTRY_POINTS(make_extension()
139-
.type(BYTEARRAY)
140-
.func(make_func<&rot13>("rot13")
141-
.returns(BYTEARRAY)
142-
.param(BYTEARRAY)
143-
.build())
144-
.func(make_func<&even_chars>("even_chars")
145-
.returns(BYTEARRAY)
146-
.param(BYTEARRAY)
147-
.build())
148-
.func(make_func<&odd_chars>("odd_chars")
149-
.returns(BYTEARRAY)
150-
.param(BYTEARRAY)
151-
.build())
152-
.func(make_func<&ba_concat>("ba_concat")
153-
.returns(STRING)
154-
.param(BYTEARRAY)
155-
.param(BYTEARRAY)
156-
.build()))
188+
VEF_GENERATE_ENTRY_POINTS(
189+
make_extension()
190+
.type(BYTEARRAY)
191+
.func(make_func<&rot13>("rot13")
192+
.returns(BYTEARRAY)
193+
.param(BYTEARRAY)
194+
.build())
195+
.func(make_func<&even_chars>("even_chars")
196+
.returns(BYTEARRAY)
197+
.param(BYTEARRAY)
198+
.build())
199+
.func(make_func<&odd_chars>("odd_chars")
200+
.returns(BYTEARRAY)
201+
.param(BYTEARRAY)
202+
.build())
203+
.func(make_func<&ba_concat>("ba_concat")
204+
.returns(STRING)
205+
.param(BYTEARRAY)
206+
.param(BYTEARRAY)
207+
.build())
208+
.func(make_func<&ba_len>("ba_len").returns(INT).param().build())
209+
.func(make_func<&ba_concat_all>("ba_concat_all")
210+
.returns(STRING)
211+
.varargs()
212+
.prerun<&ba_concat_all_prerun>()
213+
.build()))

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#ifndef VILLAGESQL_ABI_TYPES_H_
1818
#define VILLAGESQL_ABI_TYPES_H_
1919

20+
#include <limits.h>
2021
#include <stdbool.h>
2122
#include <stddef.h>
2223
#include <stdint.h>
@@ -460,6 +461,12 @@ typedef struct {
460461
const char *custom_type;
461462
} vef_type_t;
462463

464+
// Sentinel value for vef_signature_t::param_count indicating that the function
465+
// accepts a variable number of arguments. When set, the framework skips
466+
// argument count and type validation and delegates to the prerun function
467+
// instead. `params` is NULL for varargs signatures.
468+
#define VEF_PARAM_VARARGS UINT_MAX
469+
463470
typedef struct {
464471
unsigned int param_count;
465472
const vef_type_t *params;

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

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
#include <villagesql/abi/types.h>
3535
#include <villagesql/vsql/func_types.h>
3636
#include <villagesql/vsql/type_params.h>
37+
#include <villagesql/vsql/var_args.h>
3738

3839
namespace vsql {
3940
namespace func_builder {
@@ -288,6 +289,7 @@ struct FuncWithMetadata {
288289
num_params(0),
289290
buffer_size(0),
290291
deterministic(false),
292+
is_varargs(false),
291293
check_params_cache_bound(nullptr),
292294
check_signature(nullptr) {}
293295

@@ -301,6 +303,10 @@ struct FuncWithMetadata {
301303
size_t num_params;
302304
size_t buffer_size;
303305
bool deterministic;
306+
// When true, the function accepts a variable number of arguments. The
307+
// server skips argument validation and delegates to prerun. num_params
308+
// and param_types are unused (a varargs StaticFuncDesc has NumParams == 0).
309+
bool is_varargs;
304310
bool (*check_params_cache_bound)();
305311
const char *(*check_signature)(const vef_type_t *, size_t,
306312
const vef_type_t &);
@@ -409,6 +415,34 @@ struct Wrapper {
409415
}
410416
};
411417

418+
// Generates a vef_vdf_func_t for a varargs VDF whose signature is
419+
// void func(vsql::VarArgs, ResultWrapper)
420+
//
421+
// The wrapper constructs a VarArgs view over args and the declared typed
422+
// result wrapper, then calls Func. Argument-count and argument-type
423+
// validation are skipped at this layer (varargs functions delegate that
424+
// to their prerun).
425+
template <auto Func>
426+
struct VarArgsWrapper {
427+
using Params = typename FuncParamTypes<decltype(Func)>::type;
428+
static_assert(
429+
std::tuple_size_v<Params> == 2,
430+
"vsql .varargs(): function must take exactly (VarArgs, ResultWrapper)");
431+
using ArgsParam = std::tuple_element_t<0, Params>;
432+
using ResultParam = std::tuple_element_t<1, Params>;
433+
static_assert(std::is_same_v<ArgsParam, ::vsql::VarArgs>,
434+
"vsql .varargs(): first parameter must be vsql::VarArgs");
435+
static_assert(is_result_wrapper<ResultParam>::value,
436+
"vsql .varargs(): second parameter must be a result wrapper "
437+
"(IntResult, RealResult, StringResult, CustomResult, or "
438+
"CustomResultWith<P>)");
439+
440+
static void invoke(vef_context_t * /*ctx*/, vef_vdf_args_t *args,
441+
vef_vdf_result_t *result) {
442+
Func(::vsql::VarArgs(args), ResultParam(result));
443+
}
444+
};
445+
412446
// Pre-fills result->type / result->error_msg with a default
413447
// "failed to encode '<input>'" warning, truncating long inputs. Both encode
414448
// wrappers call this before invoking the extension's from_string so that an
@@ -842,12 +876,24 @@ struct StaticFuncDesc {
842876
vef_vdf_accumulate_func_t accumulate_;
843877
size_t buffer_size_;
844878
bool deterministic_;
879+
bool is_varargs_;
845880
bool (*check_params_cache_bound_)();
846881
const char *(*check_signature_)(const vef_type_t *, size_t,
847882
const vef_type_t &);
848883

849884
constexpr const char *name() const { return name_; }
850-
constexpr size_t num_params() const { return NumParams; }
885+
// For varargs: reports VEF_PARAM_VARARGS so materialize_func_desc writes
886+
// the sentinel into vef_signature_t::param_count and the server (and
887+
// tooling that reads it back, e.g. extension_registration) treat the
888+
// function as variadic.
889+
constexpr size_t num_params() const {
890+
return is_varargs_ ? VEF_PARAM_VARARGS : NumParams;
891+
}
892+
// Varargs reads args->values (protocol-2 pointer-array layout) without a
893+
// protocol-1 fallback, so a varargs VDF cannot run on protocol 1.
894+
constexpr vef_protocol_t required_protocol() const {
895+
return is_varargs_ ? VEF_PROTOCOL_2 : VEF_PROTOCOL_1;
896+
}
851897
constexpr size_t buffer_size() const { return buffer_size_; }
852898
constexpr bool deterministic() const { return deterministic_; }
853899
constexpr auto check_params_cache_bound() const -> bool (*)() {
@@ -877,6 +923,7 @@ struct StaticFuncDesc {
877923
accumulate_(meta.accumulate),
878924
buffer_size_(meta.buffer_size),
879925
deterministic_(meta.deterministic),
926+
is_varargs_(meta.is_varargs),
880927
check_params_cache_bound_(meta.check_params_cache_bound),
881928
check_signature_(meta.check_signature) {
882929
for (size_t i = 0; i < NumParams && i < meta.num_params; ++i) {

villagesql/sdk/include/villagesql/detail/vef_register.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,13 @@ __attribute__((visibility("hidden"))) vef_func_desc_t *materialize_func_desc(
4646
static vef_func_desc_t desc;
4747

4848
signature.param_count = static_cast<unsigned int>(func_data.num_params());
49-
signature.params = func_data.num_params() > 0 ? func_data.params() : nullptr;
49+
// For varargs functions num_params() returns the VEF_PARAM_VARARGS sentinel
50+
// and params() is meaningless; null it out so the server reads
51+
// param_count alone.
52+
signature.params = (func_data.num_params() > 0 &&
53+
func_data.num_params() != VEF_PARAM_VARARGS)
54+
? func_data.params()
55+
: nullptr;
5056
signature.return_type = func_data.return_type();
5157

5258
desc.protocol = VEF_PROTOCOL_2;

villagesql/sdk/include/villagesql/vsql.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@
6868
// Typed argument/result wrappers: IntArg, RealArg, StringArg, CustomArg, etc.
6969
#include <villagesql/vsql/func_types.h>
7070

71+
// Varargs typed views: vsql::VarArgs and vsql::AnyArg
72+
#include <villagesql/vsql/var_args.h>
73+
7174
// Object-based type builder: vsql::make_type<Name>()
7275
#include <villagesql/vsql/type_builder.h>
7376

villagesql/sdk/include/villagesql/vsql/extension_builder.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ struct ExtensionBuilder {
7474
auto new_funcs = std::tuple_cat(funcs_, std::make_tuple(f));
7575
return ExtensionBuilder<decltype(new_funcs), TypeTuple,
7676
RequiredCapabilityTuple>{
77-
new_funcs, types_, required_capabilities_, min_protocol_};
77+
new_funcs, types_, required_capabilities_,
78+
require_atleast_min(f.required_protocol())};
7879
}
7980

8081
// Accepts a TypeObject (from vsql::make_type().build()) that carries embedded

0 commit comments

Comments
 (0)