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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6967e763877e2be25116e63383c683925023ba9d51f606da6ebf1479f3725218
4
- data.tar.gz: 4b024d154a89394ae168b066aff4073c3a26c1166177bccafad9e9c534dba1ea
3
+ metadata.gz: 249a190d19c8c4b8ff32f06621a8fd2cba6690008a593e33603761810e3f2263
4
+ data.tar.gz: f0dd19a5a1b197cf22731b40741771e36b04a59b28388fb68fde6af76233d68e
5
5
  SHA512:
6
- metadata.gz: 4ac2ab683a46d76848b78cbcbe6505fb7adba13d5ea17eeb3565993b956a348180a2c1db870593acbc6ec5a2ee9313db6c7668652706a542e90a67ba4e59f415
7
- data.tar.gz: bcbb01e9178545de06088061aa5a24aae405db4f9c970ef5383a636ae777ce791858c634fdc7f0fbb1d8812831c78221349c4df689f929a39648022625094824
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.5.0 - 2026-03-15
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- duckdb (1.5.0.0)
4
+ duckdb (1.5.0.1)
5
5
  bigdecimal (>= 3.1.4)
6
6
 
7
7
  GEM
@@ -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
- if (duckdb_query(ctx->con, StringValueCStr(str), &(ctxr->result)) == DuckDBError) {
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
- // Mark all rows as NULL to avoid returning uninitialized data
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
- // Initialize callback argument structure
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
- // If no parameters, call block once and replicate result to all rows
146
- if (arg.col_count == 0) {
147
- rb_ensure(process_no_param_rows, (VALUE)&arg, cleanup_callback, (VALUE)&arg);
148
- return;
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
- // Allocate arrays to hold input vectors and their types
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
- // Get all input vectors and their types
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
- // Process each row
475
+ /* Process each row */
186
476
  for (i = 0; i < arg->row_count; i++) {
187
- // Build arguments array for this row using vector_value_at
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
- // Call the Ruby block with the arguments
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
- // Write result to output using helper function
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
- // Destroy all logical types
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
- // Free allocated memory
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
- // Handle NULL values
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
- // VARCHAR requires special API, not direct array assignment
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
- // BLOB uses same API as VARCHAR, but expects binary data
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
- // Convert Ruby Time to DuckDB timestamp (microseconds since epoch)
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
- // Convert Ruby Date to DuckDB date
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
- // Convert Ruby Time to DuckDB time (time-of-day only)
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
- // Mark as volatile to prevent constant folding during query optimization
372
- // This prevents DuckDB from evaluating the function at planning time.
373
- // NOTE: Ruby scalar functions require single-threaded execution (PRAGMA threads=1)
374
- // because Ruby proc callbacks cannot be safely invoked from DuckDB worker threads.
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
 
@@ -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
  #
@@ -3,5 +3,5 @@
3
3
  module DuckDB
4
4
  # The version string of ruby-duckdb.
5
5
  # Currently, ruby-duckdb is NOT semantic versioning.
6
- VERSION = '1.5.0.0'
6
+ VERSION = '1.5.0.1'
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: duckdb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0.0
4
+ version: 1.5.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Masaki Suketa