Skip to content

Commit afbf145

Browse files
vef: allow VDFs as type functions
Extend vef_type_desc_t (protocol 2) with encode/decode/compare/hash_vdf_name fields so extensions can name a registered VDF as the implementation of each type operation instead of providing a raw function pointer. Server-side: resolve VDF names at extension registration, validate signatures (including rejecting prerun/postrun hooks), and encapsulate fn-ptr vs VDF dispatch in new EncodeOp/DecodeOp/CompareOp/HashOp wrapper classes. These wrappers are registered in the TypeDescriptor; all call sites use invoke() which hide the protocol differences SDK: add overloaded .encode()/.decode()/.compare()/.hash() builder methods, that take the VDF. Convert vsql-complex and vsql-tvector examples to the new style.
1 parent 8fecb96 commit afbf145

20 files changed

Lines changed: 915 additions & 192 deletions

File tree

sql/sql_executor.cc

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3981,9 +3981,12 @@ static bool cmp_field_value(Field *field, ptrdiff_t diff) {
39813981
}
39823982

39833983
// Use custom comparison function for custom types
3984-
if (auto cmp_func = villagesql::GetCompareFunc(*field)) {
3985-
return cmp_func(field->data_ptr(), value1_length, field->data_ptr() + diff,
3986-
value2_length) != 0;
3984+
if (field->has_type_context()) {
3985+
if (auto result = villagesql::TryCompareCustomType(
3986+
*field->get_type_context(), field->data_ptr(), value1_length,
3987+
field->data_ptr() + diff, value2_length)) {
3988+
return *result != 0;
3989+
}
39873990
}
39883991

39893992
// Trailing space can't be skipped and length is different
@@ -4060,15 +4063,16 @@ ulonglong calc_field_hash(const Field *field, ulonglong *hash_val) {
40604063
const Field_json *json_field = down_cast<const Field_json *>(field);
40614064

40624065
crc = json_field->make_hash_key(*hash_val);
4063-
} else if (auto hash_fn = villagesql::GetHashFunc(*field)) {
4066+
} else if (auto hash = villagesql::TryComputeHash(*field, field->data_ptr(),
4067+
field->data_length())) {
40644068
// Custom type with custom hash function - use it.
40654069
// The hash function may return a constant (for comparison-based dedup)
40664070
// or canonicalize on the fly before hashing.
40674071
// If we didn't use this here, the key_type being VARBINARY* would
40684072
// cause us to fall to the else case and use the raw binary, which
40694073
// would not work for some custom types that have multiple binary
40704074
// representations for equivalent values (such as -0 and 0).
4071-
my_hash_combine(crc, hash_fn(field->data_ptr(), field->data_length()));
4075+
my_hash_combine(crc, *hash);
40724076
} else if (field->key_type() == HA_KEYTYPE_TEXT ||
40734077
field->key_type() == HA_KEYTYPE_VARTEXT1 ||
40744078
field->key_type() == HA_KEYTYPE_VARTEXT2) {

storage/innobase/rem/rem0cmp.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ inline int cmp_data(ulint mtype, ulint prtype, bool is_asc, const byte *data1,
416416
}
417417

418418
if (custom_column) {
419-
int ret = custom_column->compare()(data1, len1, data2, len2);
419+
int ret = custom_column->compare(data1, len1, data2, len2);
420420
return (is_asc ? ret : -ret);
421421
}
422422

storage/innobase/villagesql/custom_column.cc

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include "storage/innobase/include/dict0mem.h"
2626
#include "storage/innobase/include/ha_prototypes.h"
2727
#include "villagesql/include/error.h"
28+
#include "villagesql/schema/descriptor/type_descriptor.h"
2829
#include "villagesql/schema/victionary_client.h"
2930
#include "villagesql/types/util.h"
3031

@@ -48,20 +49,25 @@ Custom_column::Info Custom_column::get_from_position(const dict_index_t *index,
4849
return get_from_field(index->get_field(position));
4950
}
5051

52+
int Custom_column::compare(const unsigned char *data1, size_t len1,
53+
const unsigned char *data2, size_t len2) const {
54+
return compare_op_.invoke(data1, len1, data2, len2);
55+
}
56+
5157
void Custom_column::load(dict_table_t *table, dict_col_t *col,
5258
const Field *sql_field, const dd::Column *) {
5359
ut_ad(!col->custom_column);
54-
auto *custom_compare = villagesql::GetCompareFunc(*sql_field);
5560

56-
if (!custom_compare) return;
61+
if (!sql_field->has_type_context()) return;
5762

5863
void *mem = static_cast<Custom_column *>(
5964
mem_heap_zalloc(table->heap, sizeof(Custom_column)));
6065

6166
static_assert(std::is_trivially_destructible<Custom_column>::value,
6267
"Custom_column must be trivially destructible");
6368

64-
col->custom_column = new (mem) Custom_column(custom_compare);
69+
col->custom_column = new (mem)
70+
Custom_column(sql_field->get_type_context()->descriptor()->compare_op());
6571
}
6672

6773
void Custom_column::load_all(dict_table_t *table) {

storage/innobase/villagesql/custom_column.h

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
#define STORAGE_INNOBASE_VILLAGESQL_CUSTOM_COLUMN_H_
1919

2020
#include <utility>
21-
#include "villagesql/sdk/include/villagesql/abi/types.h"
21+
#include "villagesql/types/type_op.h"
2222

2323
// Forward declarations
2424
struct dict_table_t;
@@ -38,11 +38,14 @@ namespace innodb {
3838

3939
class Custom_column {
4040
public:
41-
using Compare = vef_compare_func_t;
4241
using Info = std::pair<Custom_column *, bool>;
4342

44-
Custom_column(Compare compare_fn) : compare_fn_(compare_fn) {}
45-
inline Compare compare() const { return compare_fn_; }
43+
explicit Custom_column(CompareOp compare_op)
44+
: compare_op_(std::move(compare_op)) {}
45+
46+
// Compare two values using the registered compare implementation.
47+
int compare(const unsigned char *data1, size_t len1,
48+
const unsigned char *data2, size_t len2) const;
4649

4750
// Get custom column descriptor and ascending flag from index position.
4851
static Info get_from_position(const dict_index_t *index, size_t position);
@@ -71,8 +74,7 @@ class Custom_column {
7174
static void load_all(dict_table_t *table);
7275

7376
private:
74-
// Custom column type comparison function defined by the extension.
75-
Compare compare_fn_{nullptr};
77+
CompareOp compare_op_;
7678
};
7779
} // namespace innodb
7880
} // namespace villagesql

storage/temptable/include/temptable/cell_calculator.h

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,10 +197,11 @@ inline size_t Cell_calculator::hash(const Cell &cell) const {
197197
}
198198

199199
// Custom types: check for custom hash function.
200-
// If hash_func is provided, use it. If nullptr, binary hash is safe
200+
// If provided, use it. If absent, binary hash is safe
201201
// (encode canonicalizes equivalent values like -0.0 → +0.0).
202-
if (auto hash_fn = villagesql::GetHashFunc(*m_mysql_field)) {
203-
return hash_fn(data, data_length);
202+
if (auto hash =
203+
villagesql::TryComputeHash(*m_mysql_field, data, data_length)) {
204+
return *hash;
204205
}
205206

206207
/*

unittest/gunit/villagesql/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
# List of all VillageSQL unit tests
1919
SET(VILLAGESQL_UNIT_TESTS
2020
abi_v1_check-t
21+
abi_v2_check-t
2122
semver-t
2223
type_context-t
2324
type_descriptor-t
@@ -58,6 +59,7 @@ ENDMACRO()
5859

5960
# Add all VillageSQL unit tests
6061
ADD_VILLAGESQL_TEST(abi_v1_check-t)
62+
ADD_VILLAGESQL_TEST(abi_v2_check-t)
6163
ADD_VILLAGESQL_TEST(semver-t)
6264
ADD_VILLAGESQL_TEST(type_context-t)
6365
ADD_VILLAGESQL_TEST(type_descriptor-t)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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+
// ABI v2 Compile-time Layout Checks
18+
//
19+
// Verifies the binary layout of protocol-2 fields in the main SDK headers.
20+
// A failure here means the server has broken ABI compatibility with extensions
21+
// compiled against VEF_PROTOCOL_2 headers.
22+
//
23+
// Sizes and offsets were derived from the 64-bit LP64 layout (Linux/macOS
24+
// x86-64 and ARM64) where:
25+
// pointer/size_t = 8 bytes (align 8), int/unsigned int = 4 bytes (align 4),
26+
// bool = 1 byte, double = 8 bytes, long long = 8 bytes,
27+
// enum : int / enum : unsigned int = 4 bytes (align 4).
28+
29+
#include <gtest/gtest.h>
30+
31+
#include <cstddef>
32+
33+
#include "villagesql/sdk/include/villagesql/abi/types.h"
34+
35+
// ---------------------------------------------------------------------------
36+
// vef_type_desc_t (protocol >= VEF_PROTOCOL_2 fields)
37+
//
38+
// Full layout including v1 and v2 fields:
39+
// vef_protocol_t protocol; // +0
40+
// [4 bytes padding]
41+
// const char *name; // +8
42+
// int64_t persisted_length; // +16
43+
// int64_t max_decode_buffer_length; // +24
44+
// vef_encode_func_t encode_func; // +32 (protocol >= 1)
45+
// vef_decode_func_t decode_func; // +40 (protocol >= 1)
46+
// vef_compare_func_t compare_func; // +48 (protocol >= 1)
47+
// vef_hash_func_t hash_func; // +56 (protocol >= 1)
48+
// vef_type_int_to_params_func_t int_to_params; // +64 (protocol >= 2)
49+
// vef_type_resolve_params_func_t resolve_params; // +72 (protocol >= 2)
50+
// const char *encode_vdf_name; // +80 (protocol >= 2)
51+
// const char *decode_vdf_name; // +88 (protocol >= 2)
52+
// const char *compare_vdf_name; // +96 (protocol >= 2)
53+
// const char *hash_vdf_name; // +104 (protocol >= 2)
54+
// ---------------------------------------------------------------------------
55+
static_assert(sizeof(vef_type_desc_t) == 112,
56+
"ABI v2 break: vef_type_desc_t size changed");
57+
static_assert(offsetof(vef_type_desc_t, int_to_params) == 64,
58+
"ABI v2 break: vef_type_desc_t::int_to_params offset changed");
59+
static_assert(offsetof(vef_type_desc_t, resolve_params) == 72,
60+
"ABI v2 break: vef_type_desc_t::resolve_params offset changed");
61+
static_assert(offsetof(vef_type_desc_t, encode_vdf_name) == 80,
62+
"ABI v2 break: vef_type_desc_t::encode_vdf_name offset changed");
63+
static_assert(offsetof(vef_type_desc_t, decode_vdf_name) == 88,
64+
"ABI v2 break: vef_type_desc_t::decode_vdf_name offset changed");
65+
static_assert(offsetof(vef_type_desc_t, compare_vdf_name) == 96,
66+
"ABI v2 break: vef_type_desc_t::compare_vdf_name offset changed");
67+
static_assert(offsetof(vef_type_desc_t, hash_vdf_name) == 104,
68+
"ABI v2 break: vef_type_desc_t::hash_vdf_name offset changed");
69+
70+
// Placeholder test so the binary links and runs.
71+
TEST(AbiV2Check, StaticAssertsPass) {}

unittest/gunit/villagesql/victionary_client-t.cc

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1657,7 +1657,8 @@ TEST_F(VictionaryClientTest, TypeDescriptorOperations) {
16571657
1, // implementation_type
16581658
16, // persisted_length
16591659
256, // max_decode_buffer_length
1660-
test_encode, test_decode, test_compare);
1660+
villagesql::EncodeOp(test_encode), villagesql::DecodeOp(test_decode),
1661+
villagesql::CompareOp(test_compare));
16611662

16621663
{
16631664
auto guard = client_->get_write_lock();
@@ -1697,9 +1698,6 @@ TEST_F(VictionaryClientTest, TypeDescriptorOperations) {
16971698
EXPECT_EQ(committed->implementation_type(), 1);
16981699
EXPECT_EQ(committed->persisted_length(), 16);
16991700
EXPECT_EQ(committed->max_decode_buffer_length(), 256);
1700-
EXPECT_EQ(committed->encode(), test_encode);
1701-
EXPECT_EQ(committed->decode(), test_decode);
1702-
EXPECT_EQ(committed->compare(), test_compare);
17031701
}
17041702
}
17051703

@@ -1709,7 +1707,8 @@ TEST_F(VictionaryClientTest, TypeDescriptorRollback) {
17091707

17101708
villagesql::TypeDescriptor desc(
17111709
villagesql::TypeDescriptorKey("ROLLBACK_TYPE", "ext", "1.0"), 0, 8, 64,
1712-
test_encode, test_decode, test_compare);
1710+
villagesql::EncodeOp(test_encode), villagesql::DecodeOp(test_decode),
1711+
villagesql::CompareOp(test_compare));
17131712

17141713
{
17151714
auto guard = client_->get_write_lock();
@@ -1788,7 +1787,7 @@ TEST_F(VictionaryClientTest, AcquireKeepsEntryAlive) {
17881787

17891788
// Create and commit a TypeDescriptor entry
17901789
TypeDescriptorKey key("REFCOUNT_TYPE", "ext", "1.0");
1791-
TypeDescriptor entry(key, 0, 42, 0, nullptr, nullptr, nullptr, nullptr);
1790+
TypeDescriptor entry(key);
17921791

17931792
{
17941793
auto guard = client_->get_write_lock();

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

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,22 @@ size_t hash_complex2(const unsigned char *data, size_t len) {
214214
return fnv1a_hash(canonical, kComplexSize);
215215
}
216216

217+
// Compare VDF: complex_compare(a, b) -> INT
218+
void complex_compare_impl(vef_context_t *ctx, vef_invalue_t *in_l,
219+
vef_invalue_t *in_r, vef_vdf_result_t *out) {
220+
out->int_value = cmp_complex(in_l->bin_value, in_l->bin_len, in_r->bin_value,
221+
in_r->bin_len);
222+
out->type = VEF_RESULT_VALUE;
223+
}
224+
225+
// Hash VDF: complex2_hash(c) -> INT
226+
void complex2_hash_impl(vef_context_t *ctx, vef_invalue_t *in,
227+
vef_vdf_result_t *out) {
228+
out->int_value =
229+
static_cast<long long>(hash_complex2(in->bin_value, in->bin_len));
230+
out->type = VEF_RESULT_VALUE;
231+
}
232+
217233
std::optional<Complex> TryLoadFromInValue(const vef_invalue_t *v) {
218234
if (v->bin_len != kComplexSize) {
219235
return std::nullopt;
@@ -446,21 +462,21 @@ VEF_GENERATE_ENTRY_POINTS(
446462
.type(make_type(COMPLEX)
447463
.persisted_length(kComplexSize)
448464
.max_decode_buffer_length(64)
449-
.encode(&encode_complex)
450-
.decode(&decode_complex)
451-
.compare(&cmp_complex)
465+
.encode("complex_from_string")
466+
.decode("complex_to_string")
467+
.compare("complex_compare")
452468
.build())
453469
// COMPLEX2 type without canonicalization (preserves -0.0)
454470
// Requires custom hash that canonicalizes -0 to +0 before hashing
455471
.type(make_type(COMPLEX2)
456472
.persisted_length(kComplexSize)
457473
.max_decode_buffer_length(64)
458-
.encode(&encode_complex2)
459-
.decode(&decode_complex)
460-
.compare(&cmp_complex)
461-
.hash(&hash_complex2)
474+
.encode("complex2_from_string")
475+
.decode("complex2_to_string")
476+
.compare("complex2_compare")
477+
.hash("complex2_hash")
462478
.build())
463-
// Type conversion functions
479+
// Type conversion functions (also serve as encode/decode VDFs)
464480
.func(make_func("complex_from_string")
465481
.from_string<&encode_complex>(COMPLEX))
466482
.func(
@@ -469,6 +485,24 @@ VEF_GENERATE_ENTRY_POINTS(
469485
.from_string<&encode_complex2>(COMPLEX2))
470486
.func(make_func("complex2_to_string")
471487
.to_string<&decode_complex>(COMPLEX2))
488+
// Compare and hash VDFs
489+
.func(make_func<&complex_compare_impl>("complex_compare")
490+
.returns(INT)
491+
.param(COMPLEX)
492+
.param(COMPLEX)
493+
.deterministic()
494+
.build())
495+
.func(make_func<&complex_compare_impl>("complex2_compare")
496+
.returns(INT)
497+
.param(COMPLEX2)
498+
.param(COMPLEX2)
499+
.deterministic()
500+
.build())
501+
.func(make_func<&complex2_hash_impl>("complex2_hash")
502+
.returns(INT)
503+
.param(COMPLEX2)
504+
.deterministic()
505+
.build())
472506
// Arithmetic functions
473507
.func(make_func<&complex_add_impl>("complex_add")
474508
.returns(COMPLEX)

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

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -224,13 +224,34 @@ int tvector_compare(const unsigned char *data1, size_t len1,
224224
return 0;
225225
}
226226

227-
VEF_GENERATE_ENTRY_POINTS(make_extension("vsql_tvector", "0.0.1")
228-
.type(make_type("TVECTOR")
229-
.persisted_length(-1)
230-
.max_decode_buffer_length(16)
231-
.encode(&tvector_encode)
232-
.decode(&tvector_decode)
233-
.compare(&tvector_compare)
234-
.int_to_params(&tvector_int_to_params)
235-
.resolve_params(&tvector_resolve_params)
236-
.build()))
227+
// Compare VDF: tvector_compare(a, b) -> INT
228+
void tvector_compare_impl(vef_context_t *ctx, vef_invalue_t *in_l,
229+
vef_invalue_t *in_r, vef_vdf_result_t *out) {
230+
out->int_value = tvector_compare(in_l->bin_value, in_l->bin_len,
231+
in_r->bin_value, in_r->bin_len);
232+
out->type = VEF_RESULT_VALUE;
233+
}
234+
235+
constexpr const char *TVECTOR = "TVECTOR";
236+
237+
VEF_GENERATE_ENTRY_POINTS(
238+
make_extension("vsql_tvector", "0.0.1")
239+
.type(make_type(TVECTOR)
240+
.persisted_length(-1)
241+
.max_decode_buffer_length(16)
242+
.encode("tvector_from_string")
243+
.decode("tvector_to_string")
244+
.compare("tvector_compare")
245+
.int_to_params(&tvector_int_to_params)
246+
.resolve_params(&tvector_resolve_params)
247+
.build())
248+
.func(make_func("tvector_from_string")
249+
.from_string<&tvector_encode>(TVECTOR))
250+
.func(
251+
make_func("tvector_to_string").to_string<&tvector_decode>(TVECTOR))
252+
.func(make_func<&tvector_compare_impl>("tvector_compare")
253+
.returns(INT)
254+
.param(TVECTOR)
255+
.param(TVECTOR)
256+
.deterministic()
257+
.build()))

0 commit comments

Comments
 (0)