Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
INSTALL EXTENSION vsql_preview_ping_test;
# ping() returns a positive integer
SELECT vsql_preview_ping_test.ping() > 0 AS ping_positive;
ping_positive
1
# Consecutive calls return strictly increasing values
SELECT vsql_preview_ping_test.ping() < vsql_preview_ping_test.ping() AS ping_increases;
ping_increases
1
UNINSTALL EXTENSION vsql_preview_ping_test;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
call mtr.add_suppression("required capability not registered");
INSTALL EXTENSION vsql_preview_unknown_cap_test;
ERROR HY000: Failed to load VEF extension 'vsql_preview_unknown_cap_test': required capability not registered: vsql::nonexistent
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Test the preview ping capability for vsql_preview_ping_test extension.
# Verifies that the ping() VDF returns an incrementing counter, proving
# the server-side preview capability was populated correctly.

INSTALL EXTENSION vsql_preview_ping_test;

--echo # ping() returns a positive integer
SELECT vsql_preview_ping_test.ping() > 0 AS ping_positive;

--echo # Consecutive calls return strictly increasing values
SELECT vsql_preview_ping_test.ping() < vsql_preview_ping_test.ping() AS ping_increases;

UNINSTALL EXTENSION vsql_preview_ping_test;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Test that requesting an unknown preview capability fails at install time.
# The extension requests "vsql::nonexistent" which is never registered.
# INSTALL EXTENSION should fail with an error naming the missing capability.

call mtr.add_suppression("required capability not registered");

--error ER_VILLAGESQL_GENERIC_ERROR
INSTALL EXTENSION vsql_preview_unknown_cap_test;
2 changes: 2 additions & 0 deletions villagesql/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -382,4 +382,6 @@ IF(NOT WITHOUT_SERVER)

add_custom_target(storage_test_veb ALL)
add_dependencies(storage_test_veb copy_vsql_storage_test_veb)

include(test-extensions/CMakeLists.txt)
ENDIF()
49 changes: 49 additions & 0 deletions villagesql/sdk/include/villagesql/abi/preview/ping.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) 2026 VillageSQL Contributors
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License, version 2.0, for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, see <https://www.gnu.org/licenses/>.

#ifndef VILLAGESQL_ABI_PREVIEW_PING_H
#define VILLAGESQL_ABI_PREVIEW_PING_H

#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif

// Preview capability: "vsql::ping"
//
// A trivial capability used to exercise and test the preview capability
// registration system. The server provides a single ping() function that
// returns a monotonically incrementing counter.
//
// Capability name: VEF_PREVIEW_PING_NAME
// Capability version: VEF_PREVIEW_PING_VERSION

#define VEF_PREVIEW_PING_NAME "vsql::ping"
#define VEF_PREVIEW_PING_VERSION 1

// Returns a monotonically incrementing counter. Used to verify that the
// capability system is wired up correctly end-to-end.
typedef uint64_t (*vef_ping_fn)(void);

typedef struct {
vef_ping_fn ping;
} vef_preview_ping_t;

#ifdef __cplusplus
}
#endif

#endif // VILLAGESQL_ABI_PREVIEW_PING_H
34 changes: 34 additions & 0 deletions villagesql/sdk/include/villagesql/abi/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,12 @@ typedef enum : unsigned int {
// + get_variable/set_variable/read_keyring/write_keyring
// function pointers in vef_register_arg_t: access to
// MySQL system variables and keyring component.
// + Preview capability system: extensions declare named
// capabilities they require in vef_registration_t;
// the server populates their function pointers before
// vef_register() returns.
// (vef_required_capability_t, required_capabilities,
// required_capability_count in vef_registration_t)
} vef_protocol_t;

// Max length of error messages in caller-provided buffers.
Expand Down Expand Up @@ -872,6 +878,25 @@ typedef struct {
};
} vef_status_var_desc_t;

// A single capability request in vef_registration_t.required_capabilities.
// The extension sets name, version, and a receive callback. The server calls
// receive() with the vtable pointer if the capability is registered; the
// callback assigns it into the extension's struct in a type-safe way.
typedef struct {
// Capability name, e.g. "vsql::ping". Must remain valid for the lifetime
// of the extension (use a string literal).
const char *name;
// Version of the capability struct the extension was compiled against.
uint32_t version;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit uneasy about adding yet another version field before we need it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this the point of the preview feature? It is versioned independently of the rest of the ABI; I asked about this in your doc.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed version

// Called by the server with the capability vtable pointer if registered.
// The extension assigns the vtable to its own capability struct.
void (*receive)(void *vtable);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need receive AND onload? Can't we just use 1?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

receive is internal plumbing, on_load is a user facing hook. Not used currently, but the thread PR will be using on_unload to clean up

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

// Compile-time hash of the ABI struct type, computed via
// villagesql::detail::abi_type_hash<AbiType>(). The server compares this
// against its own hash for the same (name, version) to detect mismatches.
size_t abi_type_hash;
} vef_required_capability_t;

typedef struct {
// protocol >= VEF_PROTOCOL_1
vef_protocol_t protocol;
Expand Down Expand Up @@ -899,6 +924,15 @@ typedef struct {
// protocol >= VEF_PROTOCOL_2
unsigned int status_var_count;
vef_status_var_desc_t **status_vars;

// protocol >= VEF_PROTOCOL_2
// Preview capabilities required by this extension. Each entry names a
// capability the extension needs (e.g. "vsql::ping"). The server populates
// the capability struct pointed to by each entry before vef_register()
// returns. If a capability is unavailable or the version is unsupported,
// its function pointers are left as nullptr.
unsigned int required_capability_count;
const vef_required_capability_t *required_capabilities;
} vef_registration_t;

// The returned objects can be freed when the registration is passed to the
Expand Down
44 changes: 44 additions & 0 deletions villagesql/sdk/include/villagesql/detail/capability_hash.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) 2026 VillageSQL Contributors
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, see <https://www.gnu.org/licenses/>.

#ifndef VILLAGESQL_DETAIL_CAPABILITY_HASH_H
#define VILLAGESQL_DETAIL_CAPABILITY_HASH_H

#include <cstddef>

namespace villagesql::detail {

// constexpr FNV-1a hash over a string.
constexpr size_t fnv1a_hash(const char *s, size_t len) {
size_t h = 14695981039346656037ULL;
for (size_t i = 0; i < len; ++i)
h = (h ^ static_cast<size_t>(s[i])) * 1099511628211ULL;
return h;
}

// Returns a compile-time hash of T's fully qualified type name via
// __PRETTY_FUNCTION__. Used to verify that the capability ABI struct type
// an extension was compiled against matches the type the server registered.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT - This doesn't include anything about the structure of the type, just the name; i.e. you could change the functions in the interface, and this would not change:

> cat t.cc
#include <iostream>
#include <string_view>

template <typename T>
constexpr std::string_view abi_hash_this() {
  return __PRETTY_FUNCTION__;
}

struct Foo {
  int foo_A();
  int foo_B(int);
};

int main() {
  std::cout << abi_hash_this<Foo>() << std::endl;
  return 0;
}
> clang++ t.cc -o t
> ./t
std::string_view abi_hash_this() [T = Foo]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked Claude if there are C++ features that could help with this problem, C++26 seems nice, but I don't think we can use it yet:

C++26 static reflection (P2996) is exactly what's needed here.

  C++26: std::meta (P2996)

  Accepted into C++26, this lets you enumerate a struct's non-static data members at compile time:

  #include <meta>

  template <typename T>
  constexpr size_t layout_hash() {
    size_t h = sizeof(T);
    // Iterate every non-static data member automatically
    for... (auto member : std::meta::nonstatic_data_members_of(^T)) {
      h = (h ^ std::meta::offset_of(member)) * 1099511628211ULL;
      h = (h ^ std::meta::size_of(member)) * 1099511628211ULL;
    }
    return h;
  }

  This would catch adding/removing fields, reordering them, and changing their types — all automatically, with no
  per-struct boilerplate. Clang has experimental support behind -freflection today, but it's not production-ready and
  the syntax is still slightly in flux.

  Practical today: Boost.PFR (C++17/20)

  For aggregate structs (plain C structs like vef_preview_ping_t qualify), Boost.PFR can enumerate fields without macros
   or annotations:

  #include <boost/pfr.hpp>

  template <typename T>
  constexpr size_t layout_hash() {
    size_t h = sizeof(T);
    // Hashes the size of each field in order — catches count and type changes
    boost::pfr::for_each_field(T{}, [&h](auto& field) {
      h = (h ^ sizeof(field)) * 1099511628211ULL;
    });
    return h;
  }

  Limitation: PFR gives you field values/types but not offsetof, so it catches field count and size changes but misses
  reordering of fields with the same type (e.g. swapping two uint32_ts). It also can't be constexpr in all cases.

  Summary

  ┌───────────────────────────────┬─────────────────────┬─────────────────┬───────────────┐
  │           Approach            │ Catches size change │ Catches reorder │ Available now │
  ├───────────────────────────────┼─────────────────────┼─────────────────┼───────────────┤
  │ Current (__PRETTY_FUNCTION__) │ No                  │ No              │ Yes           │
  ├───────────────────────────────┼─────────────────────┼─────────────────┼───────────────┤
  │ + sizeof(T)                   │ Yes                 │ No              │ Yes           │
  ├───────────────────────────────┼─────────────────────┼─────────────────┼───────────────┤
  │ Boost.PFR                     │ Mostly              │ No              │ Yes           │
  ├───────────────────────────────┼─────────────────────┼─────────────────┼───────────────┤
  │ C++26 std::meta               │ Yes                 │ Yes             │ Experimental  │
  └───────────────────────────────┴─────────────────────┴─────────────────┴───────────────┘

  For VillageSQL, the sizeof(T) improvement is the pragmatic near-term fix. C++26 reflection is the right long-term
  answer once it stabilizes — and since the ABI struct headers are simple C aggregates, they'd be ideal candidates for
  it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a new proposal via Claude (though maybe this macros should have PREVIEW in the name?):

❯ What about a C++ macro or something that hashes the text of the struct?

⏺ Yes — the preprocessor # (stringify) operator can do this. Since fnv1a_hash is already constexpr, you can hash the
  literal text of the struct body at compile time:

  #define VEF_DEFINE_ABI(TypeName, ...)                                \
    typedef struct { __VA_ARGS__ } TypeName;                           \
    namespace villagesql::detail {                                     \
    template <>                                                        \
    constexpr size_t abi_schema_hash<TypeName>() {                     \
      constexpr const char s[] = #__VA_ARGS__;                         \
      return fnv1a_hash(s, sizeof(s) - 1);                             \
    }                                                                  \
    }

  // Usage:
  VEF_DEFINE_ABI(vef_preview_ping_t,
    vef_ping_fn ping;
  )

  #__VA_ARGS__ captures "vef_ping_fn ping;" as a string literal at compile time. Add a field, reorder, change a type —
  the string changes, the hash changes.

  The real risk: clang-format. Macro arguments are generally left alone by clang-format, but if it ever reformats the
  whitespace inside (e.g. normalizes spacing around ;), the hash silently changes and extensions built before the
  reformat break at load time, even though the ABI is identical. You'd want a // clang-format off guard around uses of
  the macro.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

going with the sizeof(T) improvement — it catches the practically important cases (field additions/removals, type size changes) without external dependencies. Let me know if you think it is worth going further

template <typename T>
constexpr size_t abi_type_hash() {
const char *s = __PRETTY_FUNCTION__;
size_t len = 0;
while (s[len]) ++len;
return fnv1a_hash(s, len);
}

} // namespace villagesql::detail

#endif // VILLAGESQL_DETAIL_CAPABILITY_HASH_H
25 changes: 21 additions & 4 deletions villagesql/sdk/include/villagesql/detail/vef_register.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ void vef_fill_status_var_ptrs(vef_status_var_desc_t **arr, const Ext &e,
...);
}

// Fills arr[I] with a copy of each vef_required_capability_t.
template <typename Ext, size_t... Is>
void vef_fill_required_capability_reqs(vef_required_capability_t *arr,
const Ext &e,
std::index_sequence<Is...>) {
((arr[Is] = e.template required_capability_at<Is>()), ...);
}

// Calls params_init_fn() for each type that has one.
template <typename Ext, size_t... Is>
void vef_init_type_params(const Ext &e, std::index_sequence<Is...>) {
Expand Down Expand Up @@ -133,11 +141,10 @@ const char *vef_check_params_cache(const Ext &e, std::index_sequence<Is...>) {
}

// Core registration logic called by VEF_GENERATE_ENTRY_POINTS.
// FuncCount, TypeCount, SysVarCount, and StatusVarCount are explicit template
// parameters so that array sizes are compile-time constants without relying on
// VLAs.
// The counts are explicit template parameters so that array sizes are
// compile-time constants without relying on VLAs.
template <typename Ext, size_t FuncCount, size_t TypeCount, size_t SysVarCount,
size_t StatusVarCount>
size_t StatusVarCount, size_t RequiredCapabilityCount>
vef_registration_t *vef_register_impl(vef_registration_t &reg,
bool &initialized,
vef_register_arg_t *arg, const Ext &ext) {
Expand Down Expand Up @@ -173,6 +180,8 @@ vef_registration_t *vef_register_impl(vef_registration_t &reg,
static vef_sys_var_desc_t *sys_var_ptrs[SysVarCount > 0 ? SysVarCount : 1];
static vef_status_var_desc_t
*status_var_ptrs[StatusVarCount > 0 ? StatusVarCount : 1];
static vef_required_capability_t required_capability_reqs
[RequiredCapabilityCount > 0 ? RequiredCapabilityCount : 1];

if constexpr (FuncCount > 0) {
vef_init_auto_names(ext, std::make_index_sequence<FuncCount>{});
Expand All @@ -190,6 +199,11 @@ vef_registration_t *vef_register_impl(vef_registration_t &reg,
vef_fill_status_var_ptrs(status_var_ptrs, ext,
std::make_index_sequence<StatusVarCount>{});
}
if constexpr (RequiredCapabilityCount > 0) {
vef_fill_required_capability_reqs(
required_capability_reqs, ext,
std::make_index_sequence<RequiredCapabilityCount>{});
}

if constexpr (FuncCount > 0) {
const char *unbound_vdf =
Expand Down Expand Up @@ -220,6 +234,9 @@ vef_registration_t *vef_register_impl(vef_registration_t &reg,
reg.sys_vars = SysVarCount > 0 ? sys_var_ptrs : nullptr;
reg.status_var_count = StatusVarCount;
reg.status_vars = StatusVarCount > 0 ? status_var_ptrs : nullptr;
reg.required_capability_count = RequiredCapabilityCount;
reg.required_capabilities =
RequiredCapabilityCount > 0 ? required_capability_reqs : nullptr;

initialized = true;
return &reg;
Expand Down
71 changes: 37 additions & 34 deletions villagesql/sdk/include/villagesql/extension_builder.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ struct ExtensionBuilder {
static constexpr size_t kTypeCount = std::tuple_size_v<TypeTuple>;
static constexpr size_t kSysVarCount = 0;
static constexpr size_t kStatusVarCount = 0;
static constexpr size_t kRequiredCapabilityCount = 0;
static constexpr bool kHasVsqlGlobals = false;

constexpr vef_protocol_t min_protocol() const { return min_protocol_; }
Expand Down Expand Up @@ -121,47 +122,49 @@ constexpr auto make_extension(std::string_view /*name*/,
// descriptors after registration for testing). Otherwise use
// VEF_GENERATE_ENTRY_POINTS which generates the full extern "C" entry points.

#define VEF_GENERATE_REGISTRATION(ext) \
namespace { \
vef_registration_t _vef_reg; \
bool _vef_reg_initialized = false; \
} \
\
static vef_registration_t *_vef_do_register(vef_register_arg_t *arg) { \
using namespace villagesql::extension_builder; \
static constexpr auto kExt = (ext); \
using ExtType = decltype(kExt); \
return villagesql::detail::vef_register_impl< \
decltype(kExt), ExtType::kFuncCount, ExtType::kTypeCount, \
ExtType::kSysVarCount, ExtType::kStatusVarCount>( \
_vef_reg, _vef_reg_initialized, arg, kExt); \
#define VEF_GENERATE_REGISTRATION(ext) \
namespace { \
vef_registration_t _vef_reg; \
bool _vef_reg_initialized = false; \
} \
\
static vef_registration_t *_vef_do_register(vef_register_arg_t *arg) { \
using namespace villagesql::extension_builder; \
static constexpr auto kExt = (ext); \
using ExtType = decltype(kExt); \
return villagesql::detail::vef_register_impl< \
decltype(kExt), ExtType::kFuncCount, ExtType::kTypeCount, \
ExtType::kSysVarCount, ExtType::kStatusVarCount, \
ExtType::kRequiredCapabilityCount>(_vef_reg, _vef_reg_initialized, \
arg, kExt); \
}

// VEF_GENERATE_ENTRY_POINTS
//
// Generates the extern "C" vef_register and vef_unregister functions.
// Must be called in a .cc file, not a header (defines functions/variables).

#define VEF_GENERATE_ENTRY_POINTS(ext) \
namespace { \
vef_registration_t vef_reg_; \
bool vef_reg_initialized_ = false; \
} \
\
extern "C" vef_registration_t *vef_register(vef_register_arg_t *arg) { \
using namespace villagesql::extension_builder; \
static constexpr auto kExt = (ext); \
using ExtType = decltype(kExt); \
return villagesql::detail::vef_register_impl< \
decltype(kExt), ExtType::kFuncCount, ExtType::kTypeCount, \
ExtType::kSysVarCount, ExtType::kStatusVarCount>( \
vef_reg_, vef_reg_initialized_, arg, kExt); \
} \
\
extern "C" void vef_unregister(vef_unregister_arg_t *arg, \
vef_registration_t *reg) { \
(void)arg; \
(void)reg; \
#define VEF_GENERATE_ENTRY_POINTS(ext) \
namespace { \
vef_registration_t vef_reg_; \
bool vef_reg_initialized_ = false; \
} \
\
extern "C" vef_registration_t *vef_register(vef_register_arg_t *arg) { \
using namespace villagesql::extension_builder; \
static constexpr auto kExt = (ext); \
using ExtType = decltype(kExt); \
return villagesql::detail::vef_register_impl< \
decltype(kExt), ExtType::kFuncCount, ExtType::kTypeCount, \
ExtType::kSysVarCount, ExtType::kStatusVarCount, \
ExtType::kRequiredCapabilityCount>(vef_reg_, vef_reg_initialized_, \
arg, kExt); \
} \
\
extern "C" void vef_unregister(vef_unregister_arg_t *arg, \
vef_registration_t *reg) { \
(void)arg; \
(void)reg; \
}

#endif // VILLAGESQL_SDK_EXTENSION_BUILDER_H
Loading
Loading