duckdb 1.5.0.0 → 1.5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/test_on_macos.yml +11 -0
- data/.github/workflows/test_on_ubuntu.yml +13 -0
- data/CHANGELOG.md +6 -1
- data/Gemfile.lock +1 -1
- data/ext/duckdb/connection.c +33 -1
- data/ext/duckdb/scalar_function.c +320 -28
- data/lib/duckdb/connection.rb +2 -7
- data/lib/duckdb/scalar_function.rb +0 -2
- data/lib/duckdb/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 249a190d19c8c4b8ff32f06621a8fd2cba6690008a593e33603761810e3f2263
|
|
4
|
+
data.tar.gz: f0dd19a5a1b197cf22731b40741771e36b04a59b28388fb68fde6af76233d68e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 381f2d2ac0b028e0a8fb2b8ab0f18f22f876f81f868d7921cb6952eaa5059a35f6d87e4bdf39b823de25fa5e0b59f349efdc77507e361aeaac032bf402211b3a
|
|
7
|
+
data.tar.gz: 8472e39a5f8e1136193f718ad436989fba3ddffe1c5072adc775a59490c08f5a141d2a2826a314413a5a5472aea2c92af467f479e7f2f013fb06bc9630bfd3d0
|
|
@@ -69,9 +69,20 @@ jobs:
|
|
|
69
69
|
run: |
|
|
70
70
|
bundle exec rake build -- --with-duckdb-include=${GITHUB_WORKSPACE}/duckdb-v${DUCKDB_VERSION}/src/include --with-duckdb-lib=${GITHUB_WORKSPACE}/duckdb-v${DUCKDB_VERSION}/build/release/src/
|
|
71
71
|
|
|
72
|
+
- name: Setup MySQL
|
|
73
|
+
run: |
|
|
74
|
+
brew install mysql
|
|
75
|
+
brew services start mysql
|
|
76
|
+
sleep 5
|
|
77
|
+
mysql -u root -e "CREATE DATABASE test_db;"
|
|
78
|
+
mysql -u root -e "CREATE USER 'test_user'@'127.0.0.1' IDENTIFIED BY 'test_password';"
|
|
79
|
+
mysql -u root -e "GRANT ALL PRIVILEGES ON test_db.* TO 'test_user'@'127.0.0.1';"
|
|
80
|
+
mysql -u root -e "FLUSH PRIVILEGES;"
|
|
81
|
+
|
|
72
82
|
- name: test with Ruby ${{ matrix.ruby }}
|
|
73
83
|
env:
|
|
74
84
|
DUCKDB_VERSION: ${{ matrix.duckdb }}
|
|
85
|
+
MYSQL_TEST: 1
|
|
75
86
|
run: |
|
|
76
87
|
export DYLD_LIBRARY_PATH=${GITHUB_WORKSPACE}/duckdb-v${DUCKDB_VERSION}/build/release/src:${DYLD_LIBRARY_PATH}
|
|
77
88
|
env RUBYOPT=-W:deprecated rake test
|
|
@@ -23,6 +23,18 @@ jobs:
|
|
|
23
23
|
ruby: ['3.2.9', '3.3.10', '3.4.9', '4.0.1', 'head']
|
|
24
24
|
duckdb: ['1.4.4', '1.5.0']
|
|
25
25
|
|
|
26
|
+
services:
|
|
27
|
+
mysql:
|
|
28
|
+
image: mysql:8.0
|
|
29
|
+
env:
|
|
30
|
+
MYSQL_ROOT_PASSWORD: root
|
|
31
|
+
MYSQL_DATABASE: test_db
|
|
32
|
+
MYSQL_USER: test_user
|
|
33
|
+
MYSQL_PASSWORD: test_password
|
|
34
|
+
ports:
|
|
35
|
+
- 3306:3306
|
|
36
|
+
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
|
37
|
+
|
|
26
38
|
steps:
|
|
27
39
|
- uses: actions/checkout@v4
|
|
28
40
|
|
|
@@ -72,6 +84,7 @@ jobs:
|
|
|
72
84
|
- name: test with Ruby ${{ matrix.ruby }}
|
|
73
85
|
env:
|
|
74
86
|
DUCKDB_VERSION: ${{ matrix.duckdb }}
|
|
87
|
+
MYSQL_TEST: 1
|
|
75
88
|
run: |
|
|
76
89
|
env RUBYOPT=-W:deprecated rake test
|
|
77
90
|
|
data/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,12 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
# Unreleased
|
|
6
6
|
|
|
7
|
-
# 1.5.
|
|
7
|
+
# 1.5.0.1 - 2026-03-17
|
|
8
|
+
|
|
9
|
+
- enable `DuckDB::ScalarFunction` work with duckdb multi threads.
|
|
10
|
+
- fix mysql_query fails
|
|
11
|
+
|
|
12
|
+
# 1.5.0.0 - 2026-03-15
|
|
8
13
|
- bump duckdb to 1.5.0 on CI.
|
|
9
14
|
|
|
10
15
|
## Breaking changes
|
data/Gemfile.lock
CHANGED
data/ext/duckdb/connection.c
CHANGED
|
@@ -147,6 +147,24 @@ static VALUE duckdb_connection_connect(VALUE self, VALUE oDuckDBDatabase) {
|
|
|
147
147
|
return self;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
struct query_nogvl_args {
|
|
151
|
+
duckdb_connection con;
|
|
152
|
+
const char *sql;
|
|
153
|
+
duckdb_result *out_result;
|
|
154
|
+
duckdb_state retval;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/*
|
|
158
|
+
* Execute duckdb_query without the GVL.
|
|
159
|
+
* This allows other Ruby threads (including the scalar function executor thread)
|
|
160
|
+
* to acquire the GVL while the query runs.
|
|
161
|
+
*/
|
|
162
|
+
static void *duckdb_query_nogvl(void *arg) {
|
|
163
|
+
struct query_nogvl_args *a = (struct query_nogvl_args *)arg;
|
|
164
|
+
a->retval = duckdb_query(a->con, a->sql, a->out_result);
|
|
165
|
+
return NULL;
|
|
166
|
+
}
|
|
167
|
+
|
|
150
168
|
/* :nodoc: */
|
|
151
169
|
static VALUE duckdb_connection_query_sql(VALUE self, VALUE str) {
|
|
152
170
|
rubyDuckDBConnection *ctx;
|
|
@@ -161,7 +179,21 @@ static VALUE duckdb_connection_query_sql(VALUE self, VALUE str) {
|
|
|
161
179
|
rb_raise(eDuckDBError, "Database connection closed");
|
|
162
180
|
}
|
|
163
181
|
|
|
164
|
-
|
|
182
|
+
/* Extract C string before releasing GVL (StringValueCStr is a Ruby operation) */
|
|
183
|
+
const char *sql = StringValueCStr(str);
|
|
184
|
+
|
|
185
|
+
struct query_nogvl_args args = {
|
|
186
|
+
.con = ctx->con,
|
|
187
|
+
.sql = sql,
|
|
188
|
+
.out_result = &(ctxr->result),
|
|
189
|
+
.retval = DuckDBError,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
rb_thread_call_without_gvl(duckdb_query_nogvl, &args, RUBY_UBF_IO, 0);
|
|
193
|
+
|
|
194
|
+
RB_GC_GUARD(str);
|
|
195
|
+
|
|
196
|
+
if (args.retval == DuckDBError) {
|
|
165
197
|
rb_raise(eDuckDBError, "%s", duckdb_result_error(&(ctxr->result)));
|
|
166
198
|
}
|
|
167
199
|
return result;
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
#include "ruby-duckdb.h"
|
|
2
2
|
|
|
3
|
+
/*
|
|
4
|
+
* Cross-platform threading primitives.
|
|
5
|
+
* MSVC (mswin) does not provide <pthread.h>.
|
|
6
|
+
* MinGW-w64 (mingw, ucrt) provides <pthread.h> via winpthreads.
|
|
7
|
+
*
|
|
8
|
+
* See also: FFI gem's approach in ext/ffi_c/Function.c
|
|
9
|
+
* https://github.com/ffi/ffi/blob/master/ext/ffi_c/Function.c
|
|
10
|
+
*/
|
|
11
|
+
#ifdef _MSC_VER
|
|
12
|
+
#include <windows.h>
|
|
13
|
+
#else
|
|
14
|
+
#include <pthread.h>
|
|
15
|
+
#endif
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
* Thread detection functions (available since Ruby 2.3).
|
|
19
|
+
* Used to determine the correct dispatch path for scalar function callbacks.
|
|
20
|
+
*/
|
|
21
|
+
extern int ruby_thread_has_gvl_p(void);
|
|
22
|
+
extern int ruby_native_thread_p(void);
|
|
23
|
+
|
|
3
24
|
VALUE cDuckDBScalarFunction;
|
|
4
25
|
|
|
5
26
|
static void mark(void *);
|
|
@@ -17,6 +38,7 @@ static void vector_set_value_at(duckdb_vector vector, duckdb_logical_type elemen
|
|
|
17
38
|
|
|
18
39
|
struct callback_arg {
|
|
19
40
|
rubyDuckDBScalarFunction *ctx;
|
|
41
|
+
duckdb_function_info info;
|
|
20
42
|
duckdb_data_chunk input;
|
|
21
43
|
duckdb_vector output;
|
|
22
44
|
duckdb_logical_type output_type;
|
|
@@ -31,6 +53,261 @@ static VALUE process_rows(VALUE arg);
|
|
|
31
53
|
static VALUE process_no_param_rows(VALUE arg);
|
|
32
54
|
static VALUE cleanup_callback(VALUE arg);
|
|
33
55
|
|
|
56
|
+
/*
|
|
57
|
+
* ============================================================================
|
|
58
|
+
* Global Executor Thread
|
|
59
|
+
* ============================================================================
|
|
60
|
+
*
|
|
61
|
+
* DuckDB calls scalar function callbacks from its own worker threads, which
|
|
62
|
+
* are NOT Ruby threads. Ruby's GVL (Global VM Lock) cannot be acquired from
|
|
63
|
+
* non-Ruby threads (rb_thread_call_with_gvl crashes with rb_bug).
|
|
64
|
+
*
|
|
65
|
+
* Solution (modeled after FFI gem's async callback dispatcher):
|
|
66
|
+
* - A global Ruby "executor" thread waits for callback requests.
|
|
67
|
+
* - DuckDB worker threads enqueue requests via pthread mutex/condvar and block.
|
|
68
|
+
* - The executor thread processes callbacks with the GVL, then signals completion.
|
|
69
|
+
*
|
|
70
|
+
* When the callback is invoked from a Ruby thread (e.g., threads=1 where DuckDB
|
|
71
|
+
* uses the calling thread), we use rb_thread_call_with_gvl directly, avoiding
|
|
72
|
+
* the executor overhead.
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
/* Per-callback request, stack-allocated on the DuckDB worker thread */
|
|
76
|
+
struct callback_request {
|
|
77
|
+
struct callback_arg *cb_arg;
|
|
78
|
+
int done;
|
|
79
|
+
#ifdef _MSC_VER
|
|
80
|
+
CRITICAL_SECTION done_lock;
|
|
81
|
+
CONDITION_VARIABLE done_cond;
|
|
82
|
+
#else
|
|
83
|
+
pthread_mutex_t done_mutex;
|
|
84
|
+
pthread_cond_t done_cond;
|
|
85
|
+
#endif
|
|
86
|
+
struct callback_request *next;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/* Global executor state */
|
|
90
|
+
#ifdef _MSC_VER
|
|
91
|
+
static CRITICAL_SECTION g_executor_lock;
|
|
92
|
+
static CONDITION_VARIABLE g_executor_cond;
|
|
93
|
+
static int g_sync_initialized = 0;
|
|
94
|
+
#else
|
|
95
|
+
static pthread_mutex_t g_executor_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
96
|
+
static pthread_cond_t g_executor_cond = PTHREAD_COND_INITIALIZER;
|
|
97
|
+
#endif
|
|
98
|
+
static struct callback_request *g_request_list = NULL;
|
|
99
|
+
static VALUE g_executor_thread = Qnil;
|
|
100
|
+
static int g_executor_started = 0;
|
|
101
|
+
|
|
102
|
+
/* Data passed to the executor wait function */
|
|
103
|
+
struct executor_wait_data {
|
|
104
|
+
struct callback_request *request;
|
|
105
|
+
int stop;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/* Runs without GVL: blocks on condvar waiting for a callback request */
|
|
109
|
+
static void *executor_wait_func(void *data) {
|
|
110
|
+
struct executor_wait_data *w = (struct executor_wait_data *)data;
|
|
111
|
+
|
|
112
|
+
w->request = NULL;
|
|
113
|
+
|
|
114
|
+
#ifdef _MSC_VER
|
|
115
|
+
EnterCriticalSection(&g_executor_lock);
|
|
116
|
+
while (!w->stop && g_request_list == NULL) {
|
|
117
|
+
SleepConditionVariableCS(&g_executor_cond, &g_executor_lock, INFINITE);
|
|
118
|
+
}
|
|
119
|
+
if (g_request_list != NULL) {
|
|
120
|
+
w->request = g_request_list;
|
|
121
|
+
g_request_list = g_request_list->next;
|
|
122
|
+
}
|
|
123
|
+
LeaveCriticalSection(&g_executor_lock);
|
|
124
|
+
#else
|
|
125
|
+
pthread_mutex_lock(&g_executor_mutex);
|
|
126
|
+
while (!w->stop && g_request_list == NULL) {
|
|
127
|
+
pthread_cond_wait(&g_executor_cond, &g_executor_mutex);
|
|
128
|
+
}
|
|
129
|
+
if (g_request_list != NULL) {
|
|
130
|
+
w->request = g_request_list;
|
|
131
|
+
g_request_list = g_request_list->next;
|
|
132
|
+
}
|
|
133
|
+
pthread_mutex_unlock(&g_executor_mutex);
|
|
134
|
+
#endif
|
|
135
|
+
|
|
136
|
+
return NULL;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* Unblock function: called by Ruby to interrupt the executor (e.g., VM shutdown) */
|
|
140
|
+
static void executor_stop_func(void *data) {
|
|
141
|
+
struct executor_wait_data *w = (struct executor_wait_data *)data;
|
|
142
|
+
|
|
143
|
+
#ifdef _MSC_VER
|
|
144
|
+
EnterCriticalSection(&g_executor_lock);
|
|
145
|
+
w->stop = 1;
|
|
146
|
+
WakeConditionVariable(&g_executor_cond);
|
|
147
|
+
LeaveCriticalSection(&g_executor_lock);
|
|
148
|
+
#else
|
|
149
|
+
pthread_mutex_lock(&g_executor_mutex);
|
|
150
|
+
w->stop = 1;
|
|
151
|
+
pthread_cond_signal(&g_executor_cond);
|
|
152
|
+
pthread_mutex_unlock(&g_executor_mutex);
|
|
153
|
+
#endif
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* Execute a callback (called with GVL held) */
|
|
157
|
+
static VALUE execute_callback(VALUE varg) {
|
|
158
|
+
struct callback_arg *arg = (struct callback_arg *)varg;
|
|
159
|
+
|
|
160
|
+
if (arg->col_count == 0) {
|
|
161
|
+
rb_ensure(process_no_param_rows, (VALUE)arg, cleanup_callback, (VALUE)arg);
|
|
162
|
+
} else {
|
|
163
|
+
rb_ensure(process_rows, (VALUE)arg, cleanup_callback, (VALUE)arg);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return Qnil;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/*
|
|
170
|
+
* Execute a callback with rb_protect and report any Ruby exception
|
|
171
|
+
* to DuckDB via duckdb_scalar_function_set_error.
|
|
172
|
+
*/
|
|
173
|
+
static void execute_callback_protected(struct callback_arg *arg) {
|
|
174
|
+
int exception_state;
|
|
175
|
+
|
|
176
|
+
rb_protect(execute_callback, (VALUE)arg, &exception_state);
|
|
177
|
+
if (exception_state) {
|
|
178
|
+
VALUE errinfo = rb_errinfo();
|
|
179
|
+
if (errinfo != Qnil) {
|
|
180
|
+
VALUE msg = rb_funcall(errinfo, rb_intern("message"), 0);
|
|
181
|
+
duckdb_scalar_function_set_error(arg->info, StringValueCStr(msg));
|
|
182
|
+
}
|
|
183
|
+
rb_set_errinfo(Qnil);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* The executor thread main loop (Ruby thread) */
|
|
188
|
+
static VALUE executor_thread_func(void *data) {
|
|
189
|
+
struct executor_wait_data w;
|
|
190
|
+
w.stop = 0;
|
|
191
|
+
|
|
192
|
+
while (!w.stop) {
|
|
193
|
+
/* Release GVL and wait for a callback request */
|
|
194
|
+
rb_thread_call_without_gvl(executor_wait_func, &w, executor_stop_func, &w);
|
|
195
|
+
|
|
196
|
+
if (w.request != NULL) {
|
|
197
|
+
struct callback_request *req = w.request;
|
|
198
|
+
|
|
199
|
+
/* Execute the Ruby callback with the GVL */
|
|
200
|
+
execute_callback_protected(req->cb_arg);
|
|
201
|
+
|
|
202
|
+
/* Signal the DuckDB worker thread that the callback is done */
|
|
203
|
+
#ifdef _MSC_VER
|
|
204
|
+
EnterCriticalSection(&req->done_lock);
|
|
205
|
+
req->done = 1;
|
|
206
|
+
WakeConditionVariable(&req->done_cond);
|
|
207
|
+
LeaveCriticalSection(&req->done_lock);
|
|
208
|
+
#else
|
|
209
|
+
pthread_mutex_lock(&req->done_mutex);
|
|
210
|
+
req->done = 1;
|
|
211
|
+
pthread_cond_signal(&req->done_cond);
|
|
212
|
+
pthread_mutex_unlock(&req->done_mutex);
|
|
213
|
+
#endif
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return Qnil;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/*
|
|
221
|
+
* Start the global executor thread (must be called from a Ruby thread).
|
|
222
|
+
*
|
|
223
|
+
* Thread safety: This function is only called from
|
|
224
|
+
* rbduckdb_scalar_function_set_function(), which is a Ruby method and
|
|
225
|
+
* always runs with the GVL held. The GVL serializes all calls, so the
|
|
226
|
+
* g_executor_started check-then-set is safe without an extra mutex.
|
|
227
|
+
*/
|
|
228
|
+
static void ensure_executor_started(void) {
|
|
229
|
+
if (g_executor_started) return;
|
|
230
|
+
|
|
231
|
+
#ifdef _MSC_VER
|
|
232
|
+
if (!g_sync_initialized) {
|
|
233
|
+
InitializeCriticalSection(&g_executor_lock);
|
|
234
|
+
InitializeConditionVariable(&g_executor_cond);
|
|
235
|
+
g_sync_initialized = 1;
|
|
236
|
+
}
|
|
237
|
+
#endif
|
|
238
|
+
|
|
239
|
+
g_executor_thread = rb_thread_create(executor_thread_func, NULL);
|
|
240
|
+
rb_global_variable(&g_executor_thread);
|
|
241
|
+
g_executor_started = 1;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/*
|
|
245
|
+
* Dispatch a callback to the global executor thread.
|
|
246
|
+
* Called from a DuckDB worker thread (non-Ruby thread).
|
|
247
|
+
* The caller blocks until the callback is processed.
|
|
248
|
+
*/
|
|
249
|
+
static void dispatch_callback_to_executor(struct callback_arg *arg) {
|
|
250
|
+
struct callback_request req;
|
|
251
|
+
|
|
252
|
+
req.cb_arg = arg;
|
|
253
|
+
req.done = 0;
|
|
254
|
+
req.next = NULL;
|
|
255
|
+
|
|
256
|
+
#ifdef _MSC_VER
|
|
257
|
+
InitializeCriticalSection(&req.done_lock);
|
|
258
|
+
InitializeConditionVariable(&req.done_cond);
|
|
259
|
+
|
|
260
|
+
/* Enqueue the request */
|
|
261
|
+
EnterCriticalSection(&g_executor_lock);
|
|
262
|
+
req.next = g_request_list;
|
|
263
|
+
g_request_list = &req;
|
|
264
|
+
WakeConditionVariable(&g_executor_cond);
|
|
265
|
+
LeaveCriticalSection(&g_executor_lock);
|
|
266
|
+
|
|
267
|
+
/* Wait for the executor to process our callback */
|
|
268
|
+
EnterCriticalSection(&req.done_lock);
|
|
269
|
+
while (!req.done) {
|
|
270
|
+
SleepConditionVariableCS(&req.done_cond, &req.done_lock, INFINITE);
|
|
271
|
+
}
|
|
272
|
+
LeaveCriticalSection(&req.done_lock);
|
|
273
|
+
|
|
274
|
+
DeleteCriticalSection(&req.done_lock);
|
|
275
|
+
#else
|
|
276
|
+
pthread_mutex_init(&req.done_mutex, NULL);
|
|
277
|
+
pthread_cond_init(&req.done_cond, NULL);
|
|
278
|
+
|
|
279
|
+
/* Enqueue the request */
|
|
280
|
+
pthread_mutex_lock(&g_executor_mutex);
|
|
281
|
+
req.next = g_request_list;
|
|
282
|
+
g_request_list = &req;
|
|
283
|
+
pthread_cond_signal(&g_executor_cond);
|
|
284
|
+
pthread_mutex_unlock(&g_executor_mutex);
|
|
285
|
+
|
|
286
|
+
/* Wait for the executor to process our callback */
|
|
287
|
+
pthread_mutex_lock(&req.done_mutex);
|
|
288
|
+
while (!req.done) {
|
|
289
|
+
pthread_cond_wait(&req.done_cond, &req.done_mutex);
|
|
290
|
+
}
|
|
291
|
+
pthread_mutex_unlock(&req.done_mutex);
|
|
292
|
+
|
|
293
|
+
pthread_cond_destroy(&req.done_cond);
|
|
294
|
+
pthread_mutex_destroy(&req.done_mutex);
|
|
295
|
+
#endif
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/*
|
|
299
|
+
* Wrapper for rb_thread_call_with_gvl: executes the callback after
|
|
300
|
+
* re-acquiring the GVL. Used when a Ruby thread (without GVL) is the caller.
|
|
301
|
+
*/
|
|
302
|
+
static void *callback_with_gvl(void *data) {
|
|
303
|
+
execute_callback_protected((struct callback_arg *)data);
|
|
304
|
+
return NULL;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/* ============================================================================
|
|
308
|
+
* End of Executor Thread
|
|
309
|
+
* ============================================================================ */
|
|
310
|
+
|
|
34
311
|
static const rb_data_type_t scalar_function_data_type = {
|
|
35
312
|
"DuckDB/ScalarFunction",
|
|
36
313
|
{mark, deallocate, memsize, compact},
|
|
@@ -112,6 +389,14 @@ static VALUE rbduckdb_scalar_function_add_parameter(VALUE self, VALUE logical_ty
|
|
|
112
389
|
return self;
|
|
113
390
|
}
|
|
114
391
|
|
|
392
|
+
/*
|
|
393
|
+
* The DuckDB callback entry point.
|
|
394
|
+
*
|
|
395
|
+
* Three dispatch paths (modeled after FFI gem):
|
|
396
|
+
* 1. Ruby thread WITH GVL -> call directly
|
|
397
|
+
* 2. Ruby thread WITHOUT GVL -> rb_thread_call_with_gvl
|
|
398
|
+
* 3. Non-Ruby thread -> dispatch to global executor thread
|
|
399
|
+
*/
|
|
115
400
|
static void scalar_function_callback(duckdb_function_info info, duckdb_data_chunk input, duckdb_vector output) {
|
|
116
401
|
rubyDuckDBScalarFunction *ctx;
|
|
117
402
|
idx_t i;
|
|
@@ -120,7 +405,7 @@ static void scalar_function_callback(duckdb_function_info info, duckdb_data_chun
|
|
|
120
405
|
ctx = (rubyDuckDBScalarFunction *)duckdb_scalar_function_get_extra_info(info);
|
|
121
406
|
|
|
122
407
|
if (ctx == NULL || ctx->function_proc == Qnil) {
|
|
123
|
-
|
|
408
|
+
/* Mark all rows as NULL to avoid returning uninitialized data */
|
|
124
409
|
idx_t row_count = duckdb_data_chunk_get_size(input);
|
|
125
410
|
uint64_t *validity;
|
|
126
411
|
duckdb_vector_ensure_validity_writable(output);
|
|
@@ -131,8 +416,9 @@ static void scalar_function_callback(duckdb_function_info info, duckdb_data_chun
|
|
|
131
416
|
return;
|
|
132
417
|
}
|
|
133
418
|
|
|
134
|
-
|
|
419
|
+
/* Initialize callback argument structure */
|
|
135
420
|
arg.ctx = ctx;
|
|
421
|
+
arg.info = info;
|
|
136
422
|
arg.input = input;
|
|
137
423
|
arg.output = output;
|
|
138
424
|
arg.output_type = duckdb_vector_get_column_type(output);
|
|
@@ -142,14 +428,18 @@ static void scalar_function_callback(duckdb_function_info info, duckdb_data_chun
|
|
|
142
428
|
arg.row_count = duckdb_data_chunk_get_size(input);
|
|
143
429
|
arg.col_count = duckdb_data_chunk_get_column_count(input);
|
|
144
430
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
431
|
+
if (ruby_native_thread_p()) {
|
|
432
|
+
if (ruby_thread_has_gvl_p()) {
|
|
433
|
+
/* Case 1: Ruby thread with GVL - call directly */
|
|
434
|
+
execute_callback_protected(&arg);
|
|
435
|
+
} else {
|
|
436
|
+
/* Case 2: Ruby thread without GVL - reacquire GVL */
|
|
437
|
+
rb_thread_call_with_gvl(callback_with_gvl, &arg);
|
|
438
|
+
}
|
|
439
|
+
} else {
|
|
440
|
+
/* Case 3: Non-Ruby thread - dispatch to executor */
|
|
441
|
+
dispatch_callback_to_executor(&arg);
|
|
149
442
|
}
|
|
150
|
-
|
|
151
|
-
// Process rows with proper cleanup on exception
|
|
152
|
-
rb_ensure(process_rows, (VALUE)&arg, cleanup_callback, (VALUE)&arg);
|
|
153
443
|
}
|
|
154
444
|
|
|
155
445
|
static VALUE process_no_param_rows(VALUE varg) {
|
|
@@ -171,28 +461,28 @@ static VALUE process_rows(VALUE varg) {
|
|
|
171
461
|
idx_t i, j;
|
|
172
462
|
VALUE result;
|
|
173
463
|
|
|
174
|
-
|
|
464
|
+
/* Allocate arrays to hold input vectors and their types */
|
|
175
465
|
arg->input_vectors = ALLOC_N(duckdb_vector, arg->col_count);
|
|
176
466
|
arg->input_types = ALLOC_N(duckdb_logical_type, arg->col_count);
|
|
177
467
|
arg->args = ALLOC_N(VALUE, arg->col_count);
|
|
178
468
|
|
|
179
|
-
|
|
469
|
+
/* Get all input vectors and their types */
|
|
180
470
|
for (j = 0; j < arg->col_count; j++) {
|
|
181
471
|
arg->input_vectors[j] = duckdb_data_chunk_get_vector(arg->input, j);
|
|
182
472
|
arg->input_types[j] = duckdb_vector_get_column_type(arg->input_vectors[j]);
|
|
183
473
|
}
|
|
184
474
|
|
|
185
|
-
|
|
475
|
+
/* Process each row */
|
|
186
476
|
for (i = 0; i < arg->row_count; i++) {
|
|
187
|
-
|
|
477
|
+
/* Build arguments array for this row using vector_value_at */
|
|
188
478
|
for (j = 0; j < arg->col_count; j++) {
|
|
189
479
|
arg->args[j] = rbduckdb_vector_value_at(arg->input_vectors[j], arg->input_types[j], i);
|
|
190
480
|
}
|
|
191
481
|
|
|
192
|
-
|
|
482
|
+
/* Call the Ruby block with the arguments */
|
|
193
483
|
result = rb_funcallv(arg->ctx->function_proc, rb_intern("call"), arg->col_count, arg->args);
|
|
194
484
|
|
|
195
|
-
|
|
485
|
+
/* Write result to output using helper function */
|
|
196
486
|
vector_set_value_at(arg->output, arg->output_type, i, result);
|
|
197
487
|
}
|
|
198
488
|
|
|
@@ -203,7 +493,7 @@ static VALUE cleanup_callback(VALUE varg) {
|
|
|
203
493
|
struct callback_arg *arg = (struct callback_arg *)varg;
|
|
204
494
|
idx_t j;
|
|
205
495
|
|
|
206
|
-
|
|
496
|
+
/* Destroy all logical types */
|
|
207
497
|
if (arg->input_types != NULL) {
|
|
208
498
|
for (j = 0; j < arg->col_count; j++) {
|
|
209
499
|
duckdb_destroy_logical_type(&arg->input_types[j]);
|
|
@@ -211,7 +501,7 @@ static VALUE cleanup_callback(VALUE varg) {
|
|
|
211
501
|
}
|
|
212
502
|
duckdb_destroy_logical_type(&arg->output_type);
|
|
213
503
|
|
|
214
|
-
|
|
504
|
+
/* Free allocated memory */
|
|
215
505
|
if (arg->args != NULL) {
|
|
216
506
|
xfree(arg->args);
|
|
217
507
|
}
|
|
@@ -230,7 +520,7 @@ static void vector_set_value_at(duckdb_vector vector, duckdb_logical_type elemen
|
|
|
230
520
|
void* vector_data;
|
|
231
521
|
uint64_t *validity;
|
|
232
522
|
|
|
233
|
-
|
|
523
|
+
/* Handle NULL values */
|
|
234
524
|
if (value == Qnil) {
|
|
235
525
|
duckdb_vector_ensure_validity_writable(vector);
|
|
236
526
|
validity = duckdb_vector_get_validity(vector);
|
|
@@ -276,7 +566,7 @@ static void vector_set_value_at(duckdb_vector vector, duckdb_logical_type elemen
|
|
|
276
566
|
((double *)vector_data)[index] = NUM2DBL(value);
|
|
277
567
|
break;
|
|
278
568
|
case DUCKDB_TYPE_VARCHAR: {
|
|
279
|
-
|
|
569
|
+
/* VARCHAR requires special API, not direct array assignment */
|
|
280
570
|
VALUE str = rb_obj_as_string(value);
|
|
281
571
|
const char *str_ptr = StringValuePtr(str);
|
|
282
572
|
idx_t str_len = RSTRING_LEN(str);
|
|
@@ -284,7 +574,7 @@ static void vector_set_value_at(duckdb_vector vector, duckdb_logical_type elemen
|
|
|
284
574
|
break;
|
|
285
575
|
}
|
|
286
576
|
case DUCKDB_TYPE_BLOB: {
|
|
287
|
-
|
|
577
|
+
/* BLOB uses same API as VARCHAR, but expects binary data */
|
|
288
578
|
VALUE str = rb_obj_as_string(value);
|
|
289
579
|
const char *str_ptr = StringValuePtr(str);
|
|
290
580
|
idx_t str_len = RSTRING_LEN(str);
|
|
@@ -292,7 +582,7 @@ static void vector_set_value_at(duckdb_vector vector, duckdb_logical_type elemen
|
|
|
292
582
|
break;
|
|
293
583
|
}
|
|
294
584
|
case DUCKDB_TYPE_TIMESTAMP: {
|
|
295
|
-
|
|
585
|
+
/* Convert Ruby Time to DuckDB timestamp (microseconds since epoch) */
|
|
296
586
|
if (!rb_obj_is_kind_of(value, rb_cTime)) {
|
|
297
587
|
rb_raise(rb_eTypeError, "Expected Time object for TIMESTAMP");
|
|
298
588
|
}
|
|
@@ -311,8 +601,7 @@ static void vector_set_value_at(duckdb_vector vector, duckdb_logical_type elemen
|
|
|
311
601
|
break;
|
|
312
602
|
}
|
|
313
603
|
case DUCKDB_TYPE_DATE: {
|
|
314
|
-
|
|
315
|
-
// Ruby Date is defined in date library, check with Class name
|
|
604
|
+
/* Convert Ruby Date to DuckDB date */
|
|
316
605
|
VALUE date_class = rb_const_get(rb_cObject, rb_intern("Date"));
|
|
317
606
|
if (!rb_obj_is_kind_of(value, date_class)) {
|
|
318
607
|
rb_raise(rb_eTypeError, "Expected Date object for DATE");
|
|
@@ -327,7 +616,7 @@ static void vector_set_value_at(duckdb_vector vector, duckdb_logical_type elemen
|
|
|
327
616
|
break;
|
|
328
617
|
}
|
|
329
618
|
case DUCKDB_TYPE_TIME: {
|
|
330
|
-
|
|
619
|
+
/* Convert Ruby Time to DuckDB time (time-of-day only) */
|
|
331
620
|
if (!rb_obj_is_kind_of(value, rb_cTime)) {
|
|
332
621
|
rb_raise(rb_eTypeError, "Expected Time object for TIME");
|
|
333
622
|
}
|
|
@@ -368,12 +657,15 @@ static VALUE rbduckdb_scalar_function_set_function(VALUE self) {
|
|
|
368
657
|
duckdb_scalar_function_set_extra_info(p->scalar_function, p, NULL);
|
|
369
658
|
duckdb_scalar_function_set_function(p->scalar_function, scalar_function_callback);
|
|
370
659
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
660
|
+
/*
|
|
661
|
+
* Mark as volatile to prevent constant folding during query optimization.
|
|
662
|
+
* This prevents DuckDB from evaluating the function at planning time.
|
|
663
|
+
*/
|
|
375
664
|
duckdb_scalar_function_set_volatile(p->scalar_function);
|
|
376
665
|
|
|
666
|
+
/* Ensure the global executor thread is running for multi-thread dispatch */
|
|
667
|
+
ensure_executor_started();
|
|
668
|
+
|
|
377
669
|
return self;
|
|
378
670
|
}
|
|
379
671
|
|
data/lib/duckdb/connection.rb
CHANGED
|
@@ -34,6 +34,8 @@ module DuckDB
|
|
|
34
34
|
|
|
35
35
|
def query_multi_sql(sql)
|
|
36
36
|
stmts = ExtractedStatements.new(self, sql)
|
|
37
|
+
return query_sql(sql) if stmts.size == 1
|
|
38
|
+
|
|
37
39
|
result = nil
|
|
38
40
|
stmts.each do |stmt|
|
|
39
41
|
result = stmt.execute
|
|
@@ -137,8 +139,6 @@ module DuckDB
|
|
|
137
139
|
|
|
138
140
|
# Registers a scalar function with the connection.
|
|
139
141
|
#
|
|
140
|
-
# Scalar functions with Ruby callbacks require single-threaded execution.
|
|
141
|
-
#
|
|
142
142
|
# @overload register_scalar_function(scalar_function)
|
|
143
143
|
# Register a pre-created ScalarFunction object.
|
|
144
144
|
# @param scalar_function [DuckDB::ScalarFunction] the scalar function to register
|
|
@@ -151,12 +151,10 @@ module DuckDB
|
|
|
151
151
|
# @param parameter_types [Array<DuckDB::LogicalType>, nil] multiple parameter types
|
|
152
152
|
# @yield [*args] the function implementation
|
|
153
153
|
#
|
|
154
|
-
# @raise [DuckDB::Error] if threads setting is not 1
|
|
155
154
|
# @raise [ArgumentError] if both object and keywords/block are provided
|
|
156
155
|
# @return [void]
|
|
157
156
|
#
|
|
158
157
|
# @example Register pre-created function
|
|
159
|
-
# con.execute('SET threads=1')
|
|
160
158
|
# sf = DuckDB::ScalarFunction.create(
|
|
161
159
|
# name: :triple,
|
|
162
160
|
# return_type: DuckDB::LogicalType::INTEGER,
|
|
@@ -165,7 +163,6 @@ module DuckDB
|
|
|
165
163
|
# con.register_scalar_function(sf)
|
|
166
164
|
#
|
|
167
165
|
# @example Register inline (single parameter)
|
|
168
|
-
# con.execute('SET threads=1')
|
|
169
166
|
# con.register_scalar_function(
|
|
170
167
|
# name: :triple,
|
|
171
168
|
# return_type: DuckDB::LogicalType::INTEGER,
|
|
@@ -173,7 +170,6 @@ module DuckDB
|
|
|
173
170
|
# ) { |v| v * 3 }
|
|
174
171
|
#
|
|
175
172
|
# @example Register inline (multiple parameters)
|
|
176
|
-
# con.execute('SET threads=1')
|
|
177
173
|
# con.register_scalar_function(
|
|
178
174
|
# name: :add,
|
|
179
175
|
# return_type: DuckDB::LogicalType::INTEGER,
|
|
@@ -187,7 +183,6 @@ module DuckDB
|
|
|
187
183
|
raise ArgumentError, 'Cannot pass both ScalarFunction object and block' if block_given?
|
|
188
184
|
end
|
|
189
185
|
|
|
190
|
-
check_threads
|
|
191
186
|
sf = scalar_function || ScalarFunction.create(**kwargs, &)
|
|
192
187
|
_register_scalar_function(sf)
|
|
193
188
|
end
|
|
@@ -4,8 +4,6 @@ module DuckDB
|
|
|
4
4
|
# DuckDB::ScalarFunction encapsulates DuckDB's scalar function
|
|
5
5
|
#
|
|
6
6
|
# @note DuckDB::ScalarFunction is experimental.
|
|
7
|
-
# @note DuckDB::ScalarFunction must be used with threads=1 in DuckDB.
|
|
8
|
-
# Set this with: connection.execute('SET threads=1')
|
|
9
7
|
class ScalarFunction
|
|
10
8
|
# Create and configure a scalar function in one call
|
|
11
9
|
#
|
data/lib/duckdb/version.rb
CHANGED