Skip to content

Commit b4b7e22

Browse files
sql query preview
1 parent d34aba6 commit b4b7e22

9 files changed

Lines changed: 746 additions & 4 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
SET PERSIST vsql_allow_preview_extensions = ON;
2+
INSTALL EXTENSION vsql_thread_worker_test;
3+
SET GLOBAL vsql_thread_worker_test.listener_enabled = ON;
4+
# Send QUERY SELECT 42
5+
SELECT vsql_thread_worker_test.active_port() INTO OUTFILE 'MYSQLTEST_VARDIR/tmp/tw_sq_port.txt';
6+
# last_monitor_result should be 42
7+
SELECT vsql_thread_worker_test.last_monitor_result();
8+
vsql_thread_worker_test.last_monitor_result()
9+
42
10+
SET GLOBAL vsql_thread_worker_test.listener_enabled = OFF;
11+
UNINSTALL EXTENSION vsql_thread_worker_test;
12+
SET PERSIST vsql_allow_preview_extensions = OFF;
13+
RESET PERSIST vsql_allow_preview_extensions;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Test thread_worker sql_query capability.
2+
#
3+
# Verifies:
4+
# - Worker accepts "QUERY <sql>" commands over TCP
5+
# - Worker executes the SQL via the sql_query capability
6+
# - last_monitor_result() VDF returns the first column of the first row
7+
8+
SET PERSIST vsql_allow_preview_extensions = ON;
9+
10+
INSTALL EXTENSION vsql_thread_worker_test;
11+
12+
SET GLOBAL vsql_thread_worker_test.listener_enabled = ON;
13+
14+
--let $port_file = $MYSQLTEST_VARDIR/tmp/tw_sq_port.txt
15+
16+
--echo # Send QUERY SELECT 42
17+
--replace_result $MYSQLTEST_VARDIR MYSQLTEST_VARDIR
18+
eval SELECT vsql_thread_worker_test.active_port() INTO OUTFILE '$port_file';
19+
--perl
20+
use IO::Socket::INET;
21+
open(my $fh, '<', $ENV{MYSQLTEST_VARDIR} . '/tmp/tw_sq_port.txt') or die $!;
22+
my $port = <$fh>; chomp $port;
23+
close($fh);
24+
my $sock = IO::Socket::INET->new(
25+
PeerAddr => "127.0.0.1",
26+
PeerPort => $port,
27+
Proto => "tcp",
28+
Timeout => 5,
29+
) or die "connect failed: $!";
30+
$sock->autoflush(1);
31+
print $sock "QUERY SELECT 42\n";
32+
shutdown($sock, 1);
33+
close($sock);
34+
EOF
35+
--remove_file $port_file
36+
37+
--let $wait_condition = SELECT vsql_thread_worker_test.last_monitor_result() = '42'
38+
--source include/wait_condition.inc
39+
40+
--echo # last_monitor_result should be 42
41+
SELECT vsql_thread_worker_test.last_monitor_result();
42+
43+
SET GLOBAL vsql_thread_worker_test.listener_enabled = OFF;
44+
45+
UNINSTALL EXTENSION vsql_thread_worker_test;
46+
47+
SET PERSIST vsql_allow_preview_extensions = OFF;
48+
RESET PERSIST vsql_allow_preview_extensions;
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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+
#ifndef VILLAGESQL_ABI_PREVIEW_SQL_QUERY_H
17+
#define VILLAGESQL_ABI_PREVIEW_SQL_QUERY_H
18+
19+
#include <stdbool.h>
20+
#include <stddef.h>
21+
22+
#ifdef __cplusplus
23+
extern "C" {
24+
#endif
25+
26+
// Preview capability: "vsql::sql_query"
27+
//
28+
// Provides SQL execution for background threads. The server wraps
29+
// MySQL's command service interface behind this vtable so that extensions
30+
// require no MySQL plugin service headers.
31+
//
32+
// Capability name: VEF_PREVIEW_SQL_QUERY_NAME
33+
34+
#define VEF_PREVIEW_SQL_QUERY_NAME "vsql::sql_query"
35+
36+
// Forward declaration — defined in abi/preview/thread_worker.h.
37+
struct vef_thread_handle_t;
38+
39+
// Opaque handle for an open SQL session.
40+
typedef struct vef_sql_session_t vef_sql_session_t;
41+
42+
// Opaque handle for a buffered query result.
43+
typedef struct vef_sql_result_t vef_sql_result_t;
44+
45+
// Open a session bound to the background thread's security context.
46+
// Returns NULL on failure. Must be closed with close_session.
47+
typedef vef_sql_session_t *(*vef_sql_open_session_fn)(
48+
struct vef_thread_handle_t *handle);
49+
50+
// Close a session opened with open_session.
51+
typedef void (*vef_sql_close_session_fn)(vef_sql_session_t *session);
52+
53+
// Execute a SQL statement. sql is UTF-8, sql_len bytes.
54+
// Returns a result handle on success, NULL on error.
55+
typedef vef_sql_result_t *(*vef_sql_execute_fn)(vef_sql_session_t *session,
56+
const char *sql,
57+
size_t sql_len);
58+
59+
// Fetch the next row. Returns true when a row was fetched; row_out and
60+
// lengths_out are then valid until the next fetch_row or close_result call.
61+
// NULL entries in row_out indicate SQL NULL column values.
62+
typedef bool (*vef_sql_fetch_row_fn)(vef_sql_result_t *result,
63+
const char ***row_out,
64+
const unsigned long **lengths_out);
65+
66+
// Number of columns in the result set. Valid after a successful execute().
67+
typedef unsigned int (*vef_sql_num_columns_fn)(vef_sql_result_t *result);
68+
69+
// Close the result handle. Must be called for every handle returned by execute.
70+
typedef void (*vef_sql_close_result_fn)(vef_sql_result_t *result);
71+
72+
// Server-provided vtable for SQL execution.
73+
typedef struct {
74+
vef_sql_open_session_fn open_session;
75+
vef_sql_close_session_fn close_session;
76+
vef_sql_execute_fn execute;
77+
vef_sql_fetch_row_fn fetch_row;
78+
vef_sql_num_columns_fn num_columns;
79+
vef_sql_close_result_fn close_result;
80+
} vef_preview_sql_query_t;
81+
82+
#ifdef __cplusplus
83+
}
84+
#endif
85+
86+
#endif // VILLAGESQL_ABI_PREVIEW_SQL_QUERY_H
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
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+
#ifndef VILLAGESQL_PREVIEW_SQL_QUERY_H
17+
#define VILLAGESQL_PREVIEW_SQL_QUERY_H
18+
19+
#include <cstdlib>
20+
#include <string_view>
21+
22+
#include <villagesql/abi/preview/sql_query.h>
23+
#include <villagesql/abi/preview/thread_worker.h>
24+
#include <villagesql/detail/capability_hash.h>
25+
#include <villagesql/vsql/extension_builder.h>
26+
27+
// Extension-local storage for the server-provided SQL execution function
28+
// pointers. Populated by sql_query::Capability::apply() during vef_register().
29+
namespace villagesql::sql {
30+
inline vef_sql_open_session_fn g_open_session = nullptr;
31+
inline vef_sql_close_session_fn g_close_session = nullptr;
32+
inline vef_sql_execute_fn g_execute = nullptr;
33+
inline vef_sql_fetch_row_fn g_fetch_row = nullptr;
34+
inline vef_sql_num_columns_fn g_num_columns = nullptr;
35+
inline vef_sql_close_result_fn g_close_result = nullptr;
36+
} // namespace villagesql::sql
37+
38+
namespace vsql::preview {
39+
40+
class SqlQuery;
41+
42+
// RAII wrapper around vef_sql_session_t. Owns the session lifetime.
43+
// Obtain via Session::open(handle); check with operator bool before use.
44+
class Session {
45+
public:
46+
static Session open(vef_thread_handle_t *handle) {
47+
vef_sql_session_t *s = nullptr;
48+
if (::villagesql::sql::g_open_session != nullptr)
49+
s = ::villagesql::sql::g_open_session(handle);
50+
return Session{s};
51+
}
52+
53+
~Session() {
54+
if (handle_ != nullptr && ::villagesql::sql::g_close_session != nullptr)
55+
::villagesql::sql::g_close_session(handle_);
56+
}
57+
58+
Session(const Session &) = delete;
59+
Session &operator=(const Session &) = delete;
60+
61+
Session(Session &&other) noexcept : handle_(other.handle_) {
62+
other.handle_ = nullptr;
63+
}
64+
65+
explicit operator bool() const { return handle_ != nullptr; }
66+
67+
// Entry point for SQL execution.
68+
SqlQuery sql(std::string_view query) const;
69+
70+
vef_sql_session_t *handle() const { return handle_; }
71+
72+
private:
73+
explicit Session(vef_sql_session_t *h) : handle_(h) {}
74+
vef_sql_session_t *handle_{nullptr};
75+
};
76+
77+
// Owns a buffered query result. Rows are fetched one at a time via next().
78+
// Column values from column_str() are valid only until the next next() call or
79+
// until Result goes out of scope — copy them if a longer lifetime is needed.
80+
class Result {
81+
public:
82+
explicit Result(vef_sql_result_t *handle) : handle_(handle) {}
83+
84+
~Result() {
85+
if (handle_ != nullptr && ::villagesql::sql::g_close_result != nullptr)
86+
::villagesql::sql::g_close_result(handle_);
87+
}
88+
89+
Result(const Result &) = delete;
90+
Result &operator=(const Result &) = delete;
91+
92+
Result(Result &&other) noexcept
93+
: handle_(other.handle_), row_(other.row_), lengths_(other.lengths_) {
94+
other.handle_ = nullptr;
95+
}
96+
97+
Result &operator=(Result &&other) noexcept {
98+
if (this != &other) {
99+
if (handle_ != nullptr && ::villagesql::sql::g_close_result != nullptr)
100+
::villagesql::sql::g_close_result(handle_);
101+
handle_ = other.handle_;
102+
row_ = other.row_;
103+
lengths_ = other.lengths_;
104+
other.handle_ = nullptr;
105+
}
106+
return *this;
107+
}
108+
109+
// Fetch the next row. Returns true if a row was fetched.
110+
bool next() {
111+
if (handle_ == nullptr || ::villagesql::sql::g_fetch_row == nullptr)
112+
return false;
113+
return ::villagesql::sql::g_fetch_row(handle_, &row_, &lengths_);
114+
}
115+
116+
// Returns true if the result handle is valid (execute succeeded).
117+
explicit operator bool() const { return handle_ != nullptr; }
118+
119+
// Number of columns in the result set.
120+
unsigned int num_columns() const {
121+
if (handle_ == nullptr || ::villagesql::sql::g_num_columns == nullptr)
122+
return 0;
123+
return ::villagesql::sql::g_num_columns(handle_);
124+
}
125+
126+
// Column value as a string_view. Valid until the next next() call.
127+
// Returns a null string_view (data() == nullptr) for SQL NULL.
128+
std::string_view column_str(unsigned int i) const {
129+
if (row_ == nullptr || row_[i] == nullptr) return {};
130+
return {row_[i], lengths_[i]};
131+
}
132+
133+
// Column value as a long long. Returns 0 for NULL.
134+
long long column_int(unsigned int i) const {
135+
if (row_ == nullptr || row_[i] == nullptr) return 0;
136+
return std::strtoll(row_[i], nullptr, 10);
137+
}
138+
139+
// Column value as a double. Returns 0.0 for NULL.
140+
double column_real(unsigned int i) const {
141+
if (row_ == nullptr || row_[i] == nullptr) return 0.0;
142+
return std::strtod(row_[i], nullptr);
143+
}
144+
145+
private:
146+
vef_sql_result_t *handle_{nullptr};
147+
const char **row_{nullptr};
148+
const unsigned long *lengths_{nullptr};
149+
};
150+
151+
// A SQL query bound to a session. Obtained via session.sql("...").
152+
// Not intended to be stored — construct and use inline.
153+
class SqlQuery {
154+
public:
155+
SqlQuery(const Session &session, std::string_view sql)
156+
: session_(session), sql_(sql) {}
157+
158+
// Execute the query. Returns an invalid Result (operator bool == false)
159+
// on error.
160+
Result execute() const {
161+
if (::villagesql::sql::g_execute == nullptr) return Result{nullptr};
162+
vef_sql_result_t *h = ::villagesql::sql::g_execute(
163+
session_.handle(), sql_.data(), sql_.size());
164+
return Result{h};
165+
}
166+
167+
// Execute the query and invoke fn once per row.
168+
// fn receives a const Result& valid only for the duration of the call.
169+
template <typename F>
170+
void for_each(F &&fn) const {
171+
Result result = execute();
172+
while (result.next()) fn(result);
173+
}
174+
175+
private:
176+
const Session &session_;
177+
std::string_view sql_;
178+
};
179+
180+
inline SqlQuery Session::sql(std::string_view query) const {
181+
return SqlQuery{*this, query};
182+
}
183+
184+
} // namespace vsql::preview
185+
186+
namespace vsql::preview::sql_query {
187+
188+
// C++ wrapper around vef_preview_sql_query_t.
189+
//
190+
// Usage:
191+
// static auto g_sql_query = vsql::preview::sql_query::make_capability();
192+
//
193+
// static void on_timer(vef_thread_handle_t *handle) {
194+
// auto session = vsql::preview::Session::open(handle);
195+
// if (!session) return;
196+
// session.sql("SELECT count(*) FROM t")
197+
// .for_each([](const vsql::preview::Result &r) {
198+
// g_count = r.column_int(0);
199+
// });
200+
// }
201+
//
202+
// Register with:
203+
// make_extension().with<preview::sql_query<g_sql_query>>()
204+
class Capability {
205+
public:
206+
static constexpr const char *kName = VEF_PREVIEW_SQL_QUERY_NAME;
207+
208+
bool available() const { return abi_.open_session != nullptr; }
209+
210+
// Populates extension-local globals from abi_. Called by cap_receive().
211+
void init_vtable() const {
212+
::villagesql::sql::g_open_session = abi_.open_session;
213+
::villagesql::sql::g_close_session = abi_.close_session;
214+
::villagesql::sql::g_execute = abi_.execute;
215+
::villagesql::sql::g_fetch_row = abi_.fetch_row;
216+
::villagesql::sql::g_num_columns = abi_.num_columns;
217+
::villagesql::sql::g_close_result = abi_.close_result;
218+
}
219+
220+
// Public so that cap_receive() can access abi_ via a pointer.
221+
// Do not access directly.
222+
vef_preview_sql_query_t abi_;
223+
};
224+
225+
inline Capability make_capability() { return Capability{}; }
226+
227+
} // namespace vsql::preview::sql_query
228+
229+
namespace vsql::preview {
230+
231+
// Traits type for registering the sql_query capability via
232+
// .with<preview_sql_query<cap>>(). Only available when this header is included.
233+
template <auto &cap>
234+
struct preview_sql_query {
235+
template <typename Inner>
236+
static constexpr auto bind(Inner builder) {
237+
using Cap = sql_query::Capability;
238+
return builder.required_capability(
239+
{Cap::kName, &::vsql::cap_receive<Cap, &cap>,
240+
::villagesql::detail::abi_type_hash<decltype(cap.abi_)>()});
241+
}
242+
};
243+
244+
} // namespace vsql::preview
245+
246+
#endif // VILLAGESQL_PREVIEW_SQL_QUERY_H

0 commit comments

Comments
 (0)