Skip to content

Commit e8474ac

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 e8474ac

22 files changed

Lines changed: 939 additions & 217 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/type_context-t.cc

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,8 @@ class TypeContextTest : public ::testing::Test {
159159
TEST_F(TypeContextTest, FixedLengthTypeUsesDescriptorValues) {
160160
villagesql::TypeDescriptor desc(
161161
villagesql::TypeDescriptorKey("COMPLEX", "test_ext", "1.0.0"), 1, 16, 256,
162-
dummy_encode, dummy_decode, dummy_compare);
162+
villagesql::EncodeOp(dummy_encode), villagesql::DecodeOp(dummy_decode),
163+
villagesql::CompareOp(dummy_compare));
163164
villagesql::TypeContextKey key("COMPLEX", "test_ext", "1.0.0");
164165
villagesql::TypeContext ctx(key, &desc);
165166

@@ -170,7 +171,8 @@ TEST_F(TypeContextTest, FixedLengthTypeUsesDescriptorValues) {
170171
TEST_F(TypeContextTest, ParameterizedTypeUsesResolvedValues) {
171172
villagesql::TypeDescriptor desc(
172173
villagesql::TypeDescriptorKey("VVECTOR", "test_ext", "1.0.0"), 1, -1, 0,
173-
dummy_encode, dummy_decode, dummy_compare, nullptr, nullptr,
174+
villagesql::EncodeOp(dummy_encode), villagesql::DecodeOp(dummy_decode),
175+
villagesql::CompareOp(dummy_compare), std::nullopt, nullptr,
174176
resolve_params_ok);
175177
villagesql::TypeParameters params({{"dimension", "1536"}});
176178
villagesql::TypeContextKey key(
@@ -185,7 +187,8 @@ TEST_F(TypeContextTest, ParameterizedTypeUsesResolvedValues) {
185187
TEST_F(TypeContextTest, ResolveParamsFailureFallsBackToDescriptor) {
186188
villagesql::TypeDescriptor desc(
187189
villagesql::TypeDescriptorKey("VVECTOR", "test_ext", "1.0.0"), 1, -1, 0,
188-
dummy_encode, dummy_decode, dummy_compare, nullptr, nullptr,
190+
villagesql::EncodeOp(dummy_encode), villagesql::DecodeOp(dummy_decode),
191+
villagesql::CompareOp(dummy_compare), std::nullopt, nullptr,
189192
resolve_params_fail);
190193
villagesql::TypeParameters params({{"dimension", "1536"}});
191194
villagesql::TypeContextKey key(
@@ -200,7 +203,8 @@ TEST_F(TypeContextTest, ResolveParamsFailureFallsBackToDescriptor) {
200203
TEST_F(TypeContextTest, EmptyParamsSkipsResolveCallback) {
201204
villagesql::TypeDescriptor desc(
202205
villagesql::TypeDescriptorKey("VVECTOR", "test_ext", "1.0.0"), 1, -1, 0,
203-
dummy_encode, dummy_decode, dummy_compare, nullptr, nullptr,
206+
villagesql::EncodeOp(dummy_encode), villagesql::DecodeOp(dummy_decode),
207+
villagesql::CompareOp(dummy_compare), std::nullopt, nullptr,
204208
resolve_params_fail);
205209
// No parameters — should use descriptor values directly, not call
206210
// resolve_params (which would fail)

unittest/gunit/villagesql/type_descriptor-t.cc

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ TEST_F(TypeDescriptorTest, Construction) {
8383
1, // implementation_type
8484
16, // persisted_length
8585
256, // max_decode_buffer_length
86-
dummy_encode, dummy_decode, dummy_compare, dummy_hash);
86+
villagesql::EncodeOp(dummy_encode), villagesql::DecodeOp(dummy_decode),
87+
villagesql::CompareOp(dummy_compare), villagesql::HashOp(dummy_hash));
8788

8889
// Check identity fields
8990
EXPECT_EQ(desc.type_name(), "MYTYPE");
@@ -98,23 +99,20 @@ TEST_F(TypeDescriptorTest, Construction) {
9899
EXPECT_EQ(desc.persisted_length(), 16);
99100
EXPECT_EQ(desc.max_decode_buffer_length(), 256);
100101

101-
// Check function pointers
102-
EXPECT_EQ(desc.encode(), dummy_encode);
103-
EXPECT_EQ(desc.decode(), dummy_decode);
104-
EXPECT_EQ(desc.compare(), dummy_compare);
105-
EXPECT_EQ(desc.hash(), dummy_hash);
102+
// Verify ops are set and dispatch to wrapped functions
103+
EXPECT_EQ(desc.compare_op().invoke(nullptr, 0, nullptr, 0), 0);
104+
EXPECT_EQ(desc.hash_op()->invoke(nullptr, 0), 42u);
106105
}
107106

108107
// Test TypeDescriptor with nullptr hash (optional)
109108
TEST_F(TypeDescriptorTest, ConstructionWithNullHash) {
110109
villagesql::TypeDescriptor desc(
111110
villagesql::TypeDescriptorKey("NOHASH", "ext", "1.0"), 0, 8, 64,
112-
dummy_encode, dummy_decode, dummy_compare, nullptr);
111+
villagesql::EncodeOp(dummy_encode), villagesql::DecodeOp(dummy_decode),
112+
villagesql::CompareOp(dummy_compare));
113113

114-
EXPECT_EQ(desc.hash(), nullptr);
115-
EXPECT_NE(desc.encode(), nullptr);
116-
EXPECT_NE(desc.decode(), nullptr);
117-
EXPECT_NE(desc.compare(), nullptr);
114+
EXPECT_FALSE(desc.hash_op().has_value());
115+
EXPECT_EQ(desc.compare_op().invoke(nullptr, 0, nullptr, 0), 0);
118116
EXPECT_EQ(desc.int_to_params(), nullptr);
119117
EXPECT_EQ(desc.resolve_params(), nullptr);
120118
}
@@ -124,7 +122,8 @@ TEST_F(TypeDescriptorTest, ConstructionWithNullHash) {
124122
TEST_F(TypeDescriptorTest, KeyTypeCompatibility) {
125123
villagesql::TypeDescriptor desc(
126124
villagesql::TypeDescriptorKey("TEST", "ext", "1.0"), 0, 8, 64,
127-
dummy_encode, dummy_decode, dummy_compare);
125+
villagesql::EncodeOp(dummy_encode), villagesql::DecodeOp(dummy_decode),
126+
villagesql::CompareOp(dummy_compare));
128127

129128
// Verify key_type is TypeDescriptorKey
130129
static_assert(std::is_same_v<villagesql::TypeDescriptor::key_type,
@@ -171,7 +170,8 @@ TEST_F(TypeDescriptorTest, ConstructionWithParamFunctions) {
171170
1, // implementation_type
172171
-1, // persisted_length (variable-length)
173172
0, // max_decode_buffer_length (determined by params)
174-
dummy_encode, dummy_decode, dummy_compare, dummy_hash,
173+
villagesql::EncodeOp(dummy_encode), villagesql::DecodeOp(dummy_decode),
174+
villagesql::CompareOp(dummy_compare), villagesql::HashOp(dummy_hash),
175175
dummy_int_to_params, dummy_resolve_params);
176176

177177
EXPECT_EQ(desc.persisted_length(), -1);

unittest/gunit/villagesql/victionary_client-t.cc

Lines changed: 8 additions & 14 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();
@@ -1912,9 +1911,7 @@ TEST_F(VictionaryClientTest, AcquireOrCreateNew) {
19121911
THD *fake_thd = reinterpret_cast<THD *>(0xA0C1);
19131912

19141913
// Create a TypeDescriptor for the type
1915-
TypeDescriptor type_desc(
1916-
TypeDescriptorKey("AOTEST_TYPE", "test_ext", "1.0.0"), 0, 16, 0, nullptr,
1917-
nullptr, nullptr, nullptr);
1914+
TypeDescriptor type_desc(TypeDescriptorKey("AOTEST_TYPE", "test_ext", "1.0.0"));
19181915

19191916
{
19201917
auto guard = client_->get_write_lock();
@@ -1962,9 +1959,7 @@ TEST_F(VictionaryClientTest, AcquireOrCreateExisting) {
19621959
THD *fake_thd = reinterpret_cast<THD *>(0xA0C2);
19631960

19641961
// Create a TypeDescriptor for the type
1965-
TypeDescriptor type_desc(
1966-
TypeDescriptorKey("AOTEST_TYPE2", "test_ext", "1.0.0"), 0, 32, 0, nullptr,
1967-
nullptr, nullptr, nullptr);
1962+
TypeDescriptor type_desc(TypeDescriptorKey("AOTEST_TYPE2", "test_ext", "1.0.0"));
19681963

19691964
{
19701965
auto guard = client_->get_write_lock();
@@ -2053,8 +2048,7 @@ TEST_F(VictionaryClientTest, AcquireOrCreateWithParameters) {
20532048
THD *fake_thd = reinterpret_cast<THD *>(0xA0C3);
20542049

20552050
// Create a TypeDescriptor for the type
2056-
TypeDescriptor type_desc(TypeDescriptorKey("VECTOR", "vector_ext", "2.0.0"),
2057-
0, 64, 0, nullptr, nullptr, nullptr, nullptr);
2051+
TypeDescriptor type_desc(TypeDescriptorKey("VECTOR", "vector_ext", "2.0.0"));
20582052

20592053
{
20602054
auto guard = client_->get_write_lock();

0 commit comments

Comments
 (0)