Skip to content

Commit 2de8b9b

Browse files
vef run query
1 parent 4288343 commit 2de8b9b

17 files changed

Lines changed: 938 additions & 3 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Install extension
2+
INSTALL EXTENSION vsql_run_query_test;
3+
# run_query_count: basic row count
4+
CREATE TABLE t1 (id INT, val VARCHAR(32));
5+
INSERT INTO t1 VALUES (1, 'a'), (2, 'b'), (3, 'c');
6+
SELECT vsql_run_query_test.run_query_count('SELECT * FROM t1');
7+
vsql_run_query_test.run_query_count('SELECT * FROM t1')
8+
3
9+
# run_query_count: empty result set returns 0
10+
SELECT vsql_run_query_test.run_query_count('SELECT * FROM t1 WHERE 1=0');
11+
vsql_run_query_test.run_query_count('SELECT * FROM t1 WHERE 1=0')
12+
0
13+
# run_query_count: NULL sql returns NULL
14+
SELECT vsql_run_query_test.run_query_count(NULL);
15+
vsql_run_query_test.run_query_count(NULL)
16+
NULL
17+
# run_query_count: statement that returns no rows (INSERT) returns 0
18+
SELECT vsql_run_query_test.run_query_count('INSERT INTO t1 VALUES (4, ''d'')');
19+
vsql_run_query_test.run_query_count('INSERT INTO t1 VALUES (4, ''d'')')
20+
0
21+
SELECT COUNT(*) FROM t1;
22+
COUNT(*)
23+
4
24+
# run_query_first_col: returns first column of first row
25+
SELECT vsql_run_query_test.run_query_first_col('SELECT val FROM t1 ORDER BY id LIMIT 1');
26+
vsql_run_query_test.run_query_first_col('SELECT val FROM t1 ORDER BY id LIMIT 1')
27+
a
28+
# run_query_first_col: empty result set returns NULL
29+
SELECT vsql_run_query_test.run_query_first_col('SELECT val FROM t1 WHERE 1=0');
30+
vsql_run_query_test.run_query_first_col('SELECT val FROM t1 WHERE 1=0')
31+
NULL
32+
# run_query_first_col: NULL sql returns NULL
33+
SELECT vsql_run_query_test.run_query_first_col(NULL);
34+
vsql_run_query_test.run_query_first_col(NULL)
35+
NULL
36+
# run_query_first_col: only first row returned when multiple rows exist
37+
SELECT vsql_run_query_test.run_query_first_col('SELECT val FROM t1 ORDER BY id');
38+
vsql_run_query_test.run_query_first_col('SELECT val FROM t1 ORDER BY id')
39+
a
40+
# run_query_count: SQL error returns NULL with a warning
41+
SELECT vsql_run_query_test.run_query_count('SELECT * FROM no_such_table_xyz');
42+
vsql_run_query_test.run_query_count('SELECT * FROM no_such_table_xyz')
43+
NULL
44+
Warnings:
45+
Warning 3200 VDF error in function 'run_query_count': MySQL error 1146: Table 'test.no_such_table_xyz' doesn't exist
46+
# run_query_first_col: SQL error returns NULL with a warning
47+
SELECT vsql_run_query_test.run_query_first_col('SELECT * FROM no_such_table_xyz');
48+
vsql_run_query_test.run_query_first_col('SELECT * FROM no_such_table_xyz')
49+
NULL
50+
Warnings:
51+
Warning 3200 VDF error in function 'run_query_first_col': MySQL error 1146: Table 'test.no_such_table_xyz' doesn't exist
52+
# run_query_count: aggregation query
53+
SELECT vsql_run_query_test.run_query_count('SELECT @@global.max_connections');
54+
vsql_run_query_test.run_query_count('SELECT @@global.max_connections')
55+
1
56+
# run_query_first_col: system variable value via SQL
57+
SELECT vsql_run_query_test.run_query_first_col('SELECT 42');
58+
vsql_run_query_test.run_query_first_col('SELECT 42')
59+
42
60+
DROP TABLE t1;
61+
# Uninstall extension
62+
UNINSTALL EXTENSION vsql_run_query_test;
63+
# VDFs no longer exist after uninstall
64+
SELECT vsql_run_query_test.run_query_count('SELECT 1');
65+
ERROR 42000: FUNCTION vsql_run_query_test.run_query_count does not exist
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
INSTALL EXTENSION vsql_run_query_test;
2+
CREATE TABLE t1 (
3+
id INT PRIMARY KEY,
4+
c vsql_complex.COMPLEX,
5+
v TVECTOR(3)
6+
);
7+
INSERT INTO t1 VALUES (1, '(1.0, 2.0)', '[1, 2, 3]');
8+
INSERT INTO t1 VALUES (2, '(3.0, 4.0)', '[4, 5, 6]');
9+
# COMPLEX values are returned as their text representation
10+
SELECT vsql_run_query_test.run_query_first_col('SELECT c FROM t1 ORDER BY id LIMIT 1');
11+
vsql_run_query_test.run_query_first_col('SELECT c FROM t1 ORDER BY id LIMIT 1')
12+
(1,2)
13+
# TVECTOR values are returned as their text representation
14+
SELECT vsql_run_query_test.run_query_first_col('SELECT v FROM t1 ORDER BY id LIMIT 1');
15+
vsql_run_query_test.run_query_first_col('SELECT v FROM t1 ORDER BY id LIMIT 1')
16+
[1,2,3]
17+
# row count works on tables with custom type columns
18+
SELECT vsql_run_query_test.run_query_count('SELECT * FROM t1');
19+
vsql_run_query_test.run_query_count('SELECT * FROM t1')
20+
2
21+
DROP TABLE t1;
22+
UNINSTALL EXTENSION vsql_run_query_test;
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Test the run_query service via the vsql_run_query_test extension.
2+
# Covers: row counting, first-column extraction, NULL inputs, empty result
3+
# sets, SQL errors, and uninstall cleanup.
4+
5+
--echo # Install extension
6+
INSTALL EXTENSION vsql_run_query_test;
7+
8+
--echo # run_query_count: basic row count
9+
CREATE TABLE t1 (id INT, val VARCHAR(32));
10+
INSERT INTO t1 VALUES (1, 'a'), (2, 'b'), (3, 'c');
11+
SELECT vsql_run_query_test.run_query_count('SELECT * FROM t1');
12+
13+
--echo # run_query_count: empty result set returns 0
14+
SELECT vsql_run_query_test.run_query_count('SELECT * FROM t1 WHERE 1=0');
15+
16+
--echo # run_query_count: NULL sql returns NULL
17+
SELECT vsql_run_query_test.run_query_count(NULL);
18+
19+
--echo # run_query_count: statement that returns no rows (INSERT) returns 0
20+
SELECT vsql_run_query_test.run_query_count('INSERT INTO t1 VALUES (4, ''d'')');
21+
SELECT COUNT(*) FROM t1;
22+
23+
--echo # run_query_first_col: returns first column of first row
24+
SELECT vsql_run_query_test.run_query_first_col('SELECT val FROM t1 ORDER BY id LIMIT 1');
25+
26+
--echo # run_query_first_col: empty result set returns NULL
27+
SELECT vsql_run_query_test.run_query_first_col('SELECT val FROM t1 WHERE 1=0');
28+
29+
--echo # run_query_first_col: NULL sql returns NULL
30+
SELECT vsql_run_query_test.run_query_first_col(NULL);
31+
32+
--echo # run_query_first_col: only first row returned when multiple rows exist
33+
SELECT vsql_run_query_test.run_query_first_col('SELECT val FROM t1 ORDER BY id');
34+
35+
--echo # run_query_count: SQL error returns NULL with a warning
36+
SELECT vsql_run_query_test.run_query_count('SELECT * FROM no_such_table_xyz');
37+
38+
--echo # run_query_first_col: SQL error returns NULL with a warning
39+
SELECT vsql_run_query_test.run_query_first_col('SELECT * FROM no_such_table_xyz');
40+
41+
--echo # run_query_count: aggregation query
42+
SELECT vsql_run_query_test.run_query_count('SELECT @@global.max_connections');
43+
44+
--echo # run_query_first_col: system variable value via SQL
45+
SELECT vsql_run_query_test.run_query_first_col('SELECT 42');
46+
47+
DROP TABLE t1;
48+
49+
--echo # Uninstall extension
50+
UNINSTALL EXTENSION vsql_run_query_test;
51+
52+
--echo # VDFs no longer exist after uninstall
53+
--error ER_SP_DOES_NOT_EXIST
54+
SELECT vsql_run_query_test.run_query_count('SELECT 1');
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Test that run_query returns custom type values (COMPLEX, TVECTOR) as strings.
2+
# Custom types have no typed cb_get_* callback in the Command Services API —
3+
# they come through as their serialized text representation.
4+
5+
--source include/villagesql/install_complex_extension.inc
6+
--source include/villagesql/install_tvector_extension.inc
7+
INSTALL EXTENSION vsql_run_query_test;
8+
9+
CREATE TABLE t1 (
10+
id INT PRIMARY KEY,
11+
c vsql_complex.COMPLEX,
12+
v TVECTOR(3)
13+
);
14+
INSERT INTO t1 VALUES (1, '(1.0, 2.0)', '[1, 2, 3]');
15+
INSERT INTO t1 VALUES (2, '(3.0, 4.0)', '[4, 5, 6]');
16+
17+
--echo # COMPLEX values are returned as their text representation
18+
SELECT vsql_run_query_test.run_query_first_col('SELECT c FROM t1 ORDER BY id LIMIT 1');
19+
20+
--echo # TVECTOR values are returned as their text representation
21+
SELECT vsql_run_query_test.run_query_first_col('SELECT v FROM t1 ORDER BY id LIMIT 1');
22+
23+
--echo # row count works on tables with custom type columns
24+
SELECT vsql_run_query_test.run_query_count('SELECT * FROM t1');
25+
26+
DROP TABLE t1;
27+
28+
UNINSTALL EXTENSION vsql_run_query_test;
29+
--source include/villagesql/uninstall_tvector_extension.inc
30+
--source include/villagesql/uninstall_complex_extension.inc

sql/protocol_callback.cc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/* Copyright (c) 2000, 2025, Oracle and/or its affiliates.
2+
Copyright (c) 2026 VillageSQL Contributors
23
34
This program is free software; you can redistribute it and/or modify
45
it under the terms of the GNU General Public License, version 2.0,
@@ -168,7 +169,7 @@ bool Protocol_callback::store_field(const Field *field) {
168169
case CS_TEXT_REPRESENTATION: {
169170
if (field->is_null()) return store_null();
170171
StringBuffer<MAX_FIELD_WIDTH> buffer;
171-
return store(field->val_str(&buffer));
172+
return store(field->val_external_str(&buffer));
172173
}
173174
case CS_BINARY_REPRESENTATION:
174175
return field->send_to_protocol(this);

villagesql/CMakeLists.txt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,4 +330,30 @@ IF(NOT WITHOUT_SERVER)
330330

331331
add_custom_target(storage_test_veb ALL)
332332
add_dependencies(storage_test_veb copy_vsql_storage_test_veb)
333+
334+
ExternalProject_Add(vsql_run_query_test_extension
335+
SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/test-extensions/vsql-run-query-test
336+
BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/test-extensions/vsql-run-query-test-build
337+
CMAKE_GENERATOR ${CMAKE_GENERATOR}
338+
CMAKE_ARGS
339+
"-DCMAKE_PREFIX_PATH=${CMAKE_SOURCE_DIR}"
340+
"-DVillageSQLExtensionFramework_INCLUDE_DIR=${SDK_STAGING_DIR}/include-dev"
341+
"-DVillageSQL_VEB_INSTALL_DIR=${CMAKE_BINARY_DIR}/lib/veb"
342+
"-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}"
343+
DEPENDS sdk
344+
BUILD_ALWAYS ON
345+
INSTALL_COMMAND ""
346+
)
347+
348+
add_custom_target(copy_vsql_run_query_test_veb
349+
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/veb_output_directory
350+
COMMAND ${CMAKE_COMMAND} -E copy
351+
${CMAKE_CURRENT_BINARY_DIR}/test-extensions/vsql-run-query-test-build/vsql_run_query_test.veb
352+
${CMAKE_BINARY_DIR}/veb_output_directory/
353+
DEPENDS vsql_run_query_test_extension
354+
COMMENT "Copying vsql_run_query_test.veb to veb_output_directory"
355+
)
356+
357+
add_custom_target(run_query_test_veb ALL)
358+
add_dependencies(run_query_test_veb copy_vsql_run_query_test_veb)
333359
ENDIF()

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

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,9 @@ typedef enum : unsigned int {
173173
// + get_variable/set_variable/read_keyring/write_keyring
174174
// function pointers in vef_register_arg_t: access to
175175
// MySQL system variables and keyring component.
176+
// + run_query function pointer in vef_register_arg_t:
177+
// execute a SQL query from a background thread and
178+
// receive results row by row via callbacks.
176179
} vef_protocol_t;
177180

178181
// Max length of error messages in caller-provided buffers.
@@ -253,6 +256,75 @@ typedef vef_keyring_result_t (*vef_write_keyring_fn)(const char *data_id,
253256
const unsigned char *data,
254257
size_t data_len);
255258

259+
// =============================================================================
260+
// Query execution service (protocol >= VEF_PROTOCOL_2)
261+
// =============================================================================
262+
//
263+
// run_query lets extensions execute SQL from a background thread and receive
264+
// results row by row. It is intended for use from threads registered via
265+
// on_install / register_background_thread (Protocol 4), but can be called from
266+
// any thread that has a MySQL THD attached.
267+
//
268+
// Column metadata is delivered via vef_column_meta_fn before the first row.
269+
// Each row is delivered as an array of vef_col_value_t values via
270+
// vef_row_fn. All string values (including numeric types requested as text)
271+
// point into server-managed memory valid only for the duration of that
272+
// callback; copy any values you need to retain.
273+
//
274+
// Return values for callbacks:
275+
// 0 continue processing
276+
// 1 abort — the query is cancelled and run_query returns an error
277+
278+
// A single column value delivered to vef_row_fn.
279+
// The server always uses text representation (CS_TEXT_REPRESENTATION), so
280+
// every value arrives as a null-terminated string in `str` / `str_len`, or
281+
// is_null == true for SQL NULL.
282+
typedef struct {
283+
bool is_null;
284+
const char *str; // null-terminated; valid only during the row callback
285+
size_t str_len;
286+
} vef_col_value_t;
287+
288+
// Called once before the first row with column names and count.
289+
// col_names: array of col_count null-terminated column name strings.
290+
// col_count: number of columns in the result set.
291+
// ctx: the user context pointer passed to run_query.
292+
// Return 0 to continue, 1 to abort.
293+
typedef int (*vef_column_meta_fn)(const char *const *col_names,
294+
unsigned int col_count, void *ctx);
295+
296+
// Called once per result row.
297+
// values: array of col_count vef_col_value_t, one per column.
298+
// col_count: number of columns (matches the value from vef_column_meta_fn).
299+
// ctx: the user context pointer passed to run_query.
300+
// Return 0 to continue, 1 to abort.
301+
typedef int (*vef_row_fn)(const vef_col_value_t *values, unsigned int col_count,
302+
void *ctx);
303+
304+
typedef enum {
305+
VEF_QUERY_OK = 0, // query executed successfully
306+
VEF_QUERY_ERROR = 1, // server returned an error (SQL error)
307+
VEF_QUERY_ABORTED = 2, // a callback returned 1
308+
} vef_run_query_result_t;
309+
310+
// run_query: execute a SQL statement and stream rows to the caller.
311+
// sql: null-terminated SQL string.
312+
// sql_len: byte length of sql (not including the null terminator).
313+
// meta_cb: called once with column metadata before the first row;
314+
// may be NULL if metadata is not needed.
315+
// row_cb: called once per result row; may be NULL for statements
316+
// that return no rows (INSERT, SET, etc.).
317+
// ctx: opaque pointer forwarded to meta_cb and row_cb.
318+
// error_msg: on VEF_QUERY_ERROR, a null-terminated description is written
319+
// here; must point to a buffer of at least VEF_MAX_ERROR_LEN
320+
// bytes; may be NULL if the caller does not need the message.
321+
// Returns VEF_QUERY_OK, VEF_QUERY_ERROR, or VEF_QUERY_ABORTED.
322+
typedef vef_run_query_result_t (*vef_run_query_fn)(const char *sql,
323+
size_t sql_len,
324+
vef_column_meta_fn meta_cb,
325+
vef_row_fn row_cb, void *ctx,
326+
char *error_msg);
327+
256328
typedef struct {
257329
// protocol >= VEF_PROTOCOL_1
258330
vef_protocol_t protocol;
@@ -270,6 +342,7 @@ typedef struct {
270342
vef_set_variable_fn set_variable;
271343
vef_read_keyring_fn read_keyring;
272344
vef_write_keyring_fn write_keyring;
345+
vef_run_query_fn run_query;
273346
} vef_register_arg_t;
274347

275348
typedef struct {

villagesql/sdk/include/villagesql/extension_builder.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
#include <villagesql/sdk_version.h>
3535
#include <villagesql/type_builder.h>
3636
#include <villagesql/vsql/keyring.h>
37+
#include <villagesql/vsql/run_query.h>
3738
#include <villagesql/vsql/sys_var_builder.h>
3839

3940
namespace villagesql {
@@ -261,6 +262,7 @@ vef_registration_t *vef_register_impl(vef_registration_t &reg,
261262
villagesql::sys_var::g_set_variable = arg->set_variable;
262263
villagesql::keyring::g_read_keyring = arg->read_keyring;
263264
villagesql::keyring::g_write_keyring = arg->write_keyring;
265+
villagesql::g_run_query = arg->run_query;
264266
}
265267

266268
if (arg->protocol < ext.min_protocol()) {

villagesql/sdk/include/villagesql/vsql.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@
8282
// Keyring access: vsql::keyring::read(), vsql::keyring::write()
8383
#include <villagesql/vsql/keyring.h>
8484

85+
// SQL query execution from background threads: villagesql::run_query()
86+
#include <villagesql/vsql/run_query.h>
87+
8588
// Extension builder and VEF_GENERATE_ENTRY_POINTS macro
8689
#include <villagesql/extension_builder.h>
8790

@@ -100,9 +103,10 @@ using villagesql::func_builder::make_type_decode;
100103
using villagesql::func_builder::make_type_encode;
101104
using villagesql::func_builder::make_type_hash;
102105

103-
// Re-export sys_var and keyring namespaces
106+
// Re-export sys_var, keyring, and run_query
104107
namespace sys_var = villagesql::sys_var;
105108
namespace keyring = villagesql::keyring;
109+
using villagesql::run_query;
106110
using villagesql::sys_var_builder::make_sys_var_bool;
107111
using villagesql::sys_var_builder::make_sys_var_double;
108112
using villagesql::sys_var_builder::make_sys_var_int;

0 commit comments

Comments
 (0)