duckdb 1.5.3.0 → 1.5.4.0

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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/README.md +52 -0
  4. data/ext/duckdb/aggregate_function.c +0 -1
  5. data/ext/duckdb/appender.c +17 -0
  6. data/ext/duckdb/arrow_array_stream.c +226 -0
  7. data/ext/duckdb/arrow_array_stream.h +61 -0
  8. data/ext/duckdb/arrow_import.c +165 -0
  9. data/ext/duckdb/arrow_import.h +6 -0
  10. data/ext/duckdb/blob.c +1 -1
  11. data/ext/duckdb/blob.h +1 -2
  12. data/ext/duckdb/config.c +1 -1
  13. data/ext/duckdb/config.h +1 -1
  14. data/ext/duckdb/connection.c +3 -3
  15. data/ext/duckdb/converter.h +1 -0
  16. data/ext/duckdb/conveter.c +39 -9
  17. data/ext/duckdb/data_chunk.c +10 -0
  18. data/ext/duckdb/data_chunk.h +1 -0
  19. data/ext/duckdb/duckdb.c +13 -11
  20. data/ext/duckdb/error.c +1 -1
  21. data/ext/duckdb/error.h +1 -3
  22. data/ext/duckdb/function_executor.c +308 -2
  23. data/ext/duckdb/function_executor.h +44 -0
  24. data/ext/duckdb/prepared_statement.c +21 -0
  25. data/ext/duckdb/result.c +49 -3
  26. data/ext/duckdb/result.h +11 -0
  27. data/ext/duckdb/ruby-duckdb.h +3 -0
  28. data/ext/duckdb/scalar_function.c +97 -29
  29. data/ext/duckdb/scalar_function.h +2 -4
  30. data/ext/duckdb/scalar_function_bind_info.c +13 -13
  31. data/ext/duckdb/scalar_function_bind_info.h +1 -1
  32. data/ext/duckdb/scalar_function_set.c +9 -9
  33. data/ext/duckdb/scalar_function_set.h +2 -2
  34. data/ext/duckdb/table_description.c +19 -19
  35. data/ext/duckdb/table_description.h +1 -1
  36. data/ext/duckdb/table_function.c +94 -28
  37. data/ext/duckdb/table_function.h +2 -2
  38. data/ext/duckdb/table_function_bind_info.c +20 -20
  39. data/ext/duckdb/table_function_bind_info.h +2 -2
  40. data/ext/duckdb/table_function_function_info.c +5 -5
  41. data/ext/duckdb/table_function_function_info.h +2 -2
  42. data/ext/duckdb/table_function_init_info.c +70 -5
  43. data/ext/duckdb/table_function_init_info.h +2 -2
  44. data/lib/duckdb/appender.rb +23 -0
  45. data/lib/duckdb/arrow_array_stream.rb +33 -0
  46. data/lib/duckdb/connection.rb +54 -0
  47. data/lib/duckdb/prepared_statement.rb +17 -0
  48. data/lib/duckdb/version.rb +1 -1
  49. data/lib/duckdb.rb +1 -0
  50. metadata +6 -1
@@ -153,22 +153,21 @@ static int hex_nibble(unsigned char c)
153
153
 
154
154
  /*
155
155
  * Parse a canonical UUID string ("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") into
156
- * a duckdb_hugeint. Returns true on success, false on invalid input.
156
+ * two uint64_t halves. Returns true on success, false on invalid input.
157
157
  *
158
158
  * Iterates the 36-character string, skipping the four dash positions. The 32
159
- * hex nibbles are accumulated directly into two uint64_t halves (hi, lo) with
160
- * no bignum arithmetic or intermediate allocation. The sign-bit flip is applied
161
- * to hi before storing in upper, matching DuckDB's internal UUID representation.
159
+ * hex nibbles are accumulated directly into hi (upper 64 bits) and lo (lower
160
+ * 64 bits) with no bignum arithmetic or intermediate allocation.
162
161
  */
163
- static bool uuid_str_to_hugeint(const char *str, long len, duckdb_hugeint *out)
162
+ static bool parse_uuid_string(const char *str, long len, uint64_t *hi, uint64_t *lo)
164
163
  {
165
- // Expected format: 8-4-4-4-12 = 36 characters with dashes at fixed positions
166
164
  if (len != 36 ||
167
165
  str[8] != '-' || str[13] != '-' || str[18] != '-' || str[23] != '-') {
168
166
  return false;
169
167
  }
170
168
 
171
- uint64_t hi = 0, lo = 0;
169
+ *hi = 0;
170
+ *lo = 0;
172
171
  int nibble_idx = 0;
173
172
 
174
173
  for (int string_idx = 0; string_idx < 36; string_idx++) {
@@ -176,13 +175,27 @@ static bool uuid_str_to_hugeint(const char *str, long len, duckdb_hugeint *out)
176
175
  int nib = hex_nibble((unsigned char)str[string_idx]);
177
176
  if (nib < 0) return false;
178
177
  if (nibble_idx < 16) {
179
- hi = (hi << 4) | (uint64_t)nib;
178
+ *hi = (*hi << 4) | (uint64_t)nib;
180
179
  } else {
181
- lo = (lo << 4) | (uint64_t)nib;
180
+ *lo = (*lo << 4) | (uint64_t)nib;
182
181
  }
183
182
  nibble_idx++;
184
183
  }
185
184
 
185
+ return true;
186
+ }
187
+
188
+ /*
189
+ * Parse a canonical UUID string ("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") into
190
+ * a duckdb_hugeint. Returns true on success, false on invalid input.
191
+ *
192
+ * The sign-bit flip is applied to hi before storing in upper, matching
193
+ * DuckDB's internal UUID representation.
194
+ */
195
+ static bool uuid_str_to_hugeint(const char *str, long len, duckdb_hugeint *out)
196
+ {
197
+ uint64_t hi, lo;
198
+ if (!parse_uuid_string(str, len, &hi, &lo)) return false;
186
199
  // Apply the sign-bit flip to match DuckDB's internal UUID representation
187
200
  out->upper = (int64_t)(hi ^ DUCKDB_UUID_SIGN_BIT);
188
201
  out->lower = lo;
@@ -203,6 +216,23 @@ void rbduckdb_uuid_str_to_hugeint(VALUE uuid_str, duckdb_hugeint *out)
203
216
  }
204
217
  }
205
218
 
219
+ /*
220
+ * Ruby-callable wrapper: parse a UUID string VALUE into a duckdb_uhugeint
221
+ * (no sign-bit flip), raising ArgumentError on invalid input.
222
+ */
223
+ void rbduckdb_uuid_str_to_uhugeint(VALUE uuid_str, duckdb_uhugeint *out)
224
+ {
225
+ StringValue(uuid_str);
226
+ const char *str = RSTRING_PTR(uuid_str);
227
+ long len = RSTRING_LEN(uuid_str);
228
+ uint64_t hi, lo;
229
+ if (!parse_uuid_string(str, len, &hi, &lo)) {
230
+ rb_raise(rb_eArgError, "Invalid UUID format: %"PRIsVALUE, uuid_str);
231
+ }
232
+ out->upper = hi;
233
+ out->lower = lo;
234
+ }
235
+
206
236
  VALUE rbduckdb_interval_to_ruby(duckdb_interval i) {
207
237
  return rb_funcall(mDuckDBConverter, id__to_interval_from_vector, 3,
208
238
  INT2NUM(i.months),
@@ -44,6 +44,16 @@ rubyDuckDBDataChunk *rbduckdb_get_struct_data_chunk(VALUE obj) {
44
44
  return ctx;
45
45
  }
46
46
 
47
+ VALUE rbduckdb_create_data_chunk(duckdb_data_chunk chunk, bool owned) {
48
+ VALUE obj = allocate(cDuckDBDataChunk);
49
+ rubyDuckDBDataChunk *ctx;
50
+
51
+ TypedData_Get_Struct(obj, rubyDuckDBDataChunk, &data_chunk_data_type, ctx);
52
+ ctx->data_chunk = chunk;
53
+ ctx->owned = owned;
54
+ return obj;
55
+ }
56
+
47
57
  static VALUE data_chunk_initialize(int argc, VALUE *argv, VALUE self) {
48
58
  rubyDuckDBDataChunk *ctx;
49
59
  VALUE logical_types;
@@ -9,6 +9,7 @@ struct _rubyDuckDBDataChunk {
9
9
  typedef struct _rubyDuckDBDataChunk rubyDuckDBDataChunk;
10
10
 
11
11
  rubyDuckDBDataChunk *rbduckdb_get_struct_data_chunk(VALUE obj);
12
+ VALUE rbduckdb_create_data_chunk(duckdb_data_chunk chunk, bool owned);
12
13
  void rbduckdb_init_data_chunk(void);
13
14
 
14
15
  #endif
data/ext/duckdb/duckdb.c CHANGED
@@ -41,7 +41,7 @@ Init_duckdb_native(void) {
41
41
  rb_define_singleton_method(mDuckDB, "library_version", duckdb_s_library_version, 0);
42
42
  rb_define_singleton_method(mDuckDB, "vector_size", duckdb_s_vector_size, 0);
43
43
 
44
- rbduckdb_init_duckdb_error();
44
+ rbduckdb_init_error();
45
45
  rbduckdb_init_database();
46
46
  rbduckdb_init_connection();
47
47
  rbduckdb_init_result();
@@ -49,28 +49,30 @@ Init_duckdb_native(void) {
49
49
  rbduckdb_init_logical_type();
50
50
  rbduckdb_init_prepared_statement();
51
51
  rbduckdb_init_pending_result();
52
- rbduckdb_init_duckdb_blob();
52
+ rbduckdb_init_blob();
53
53
  rbduckdb_init_appender();
54
- rbduckdb_init_duckdb_config();
54
+ rbduckdb_init_config();
55
55
  rbduckdb_init_converter();
56
56
  rbduckdb_init_extracted_statements();
57
57
  rbduckdb_init_instance_cache();
58
58
  rbduckdb_init_value();
59
- rbduckdb_init_duckdb_scalar_function();
60
- rbduckdb_init_duckdb_scalar_function_set();
59
+ rbduckdb_init_scalar_function();
60
+ rbduckdb_init_scalar_function_set();
61
61
  rbduckdb_init_aggregate_function();
62
62
  rbduckdb_init_aggregate_function_set();
63
63
  rbduckdb_init_expression();
64
64
  rbduckdb_init_client_context();
65
- rbduckdb_init_duckdb_scalar_function_bind_info();
65
+ rbduckdb_init_scalar_function_bind_info();
66
66
  rbduckdb_init_vector();
67
67
  rbduckdb_init_data_chunk();
68
68
  rbduckdb_init_memory_helper();
69
- rbduckdb_init_duckdb_table_function();
70
- rbduckdb_init_duckdb_table_function_bind_info();
71
- rbduckdb_init_duckdb_table_function_init_info();
72
- rbduckdb_init_duckdb_table_function_function_info();
69
+ rbduckdb_init_table_function();
70
+ rbduckdb_init_table_function_bind_info();
71
+ rbduckdb_init_table_function_init_info();
72
+ rbduckdb_init_table_function_function_info();
73
73
  #ifdef HAVE_DUCKDB_H_GE_V1_5_0
74
- rbduckdb_init_duckdb_table_description();
74
+ rbduckdb_init_table_description();
75
75
  #endif
76
+ rbduckdb_init_arrow_array_stream();
77
+ rbduckdb_init_arrow_import();
76
78
  }
data/ext/duckdb/error.c CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  VALUE eDuckDBError;
4
4
 
5
- void rbduckdb_init_duckdb_error(void) {
5
+ void rbduckdb_init_error(void) {
6
6
  #if 0
7
7
  VALUE mDuckDB = rb_define_module("DuckDB");
8
8
  #endif
data/ext/duckdb/error.h CHANGED
@@ -1,8 +1,6 @@
1
1
  #ifndef RUBY_DUCKDB_ERROR_H
2
2
  #define RUBY_DUCKDB_ERROR_H
3
3
 
4
- void rbduckdb_init_duckdb_error(void);
4
+ void rbduckdb_init_error(void);
5
5
 
6
6
  #endif
7
-
8
-
@@ -68,6 +68,14 @@ static struct callback_request *g_request_list = NULL;
68
68
  static VALUE g_executor_thread = Qnil;
69
69
  static int g_executor_started = 0;
70
70
 
71
+ /*
72
+ * GC-protection array holding every live per-worker proxy Ruby thread.
73
+ * Proxies are created from non-Ruby init hooks (via the global executor) and
74
+ * are not reachable from any marked object, so without this array the GC could
75
+ * collect a proxy thread while DuckDB still dispatches callbacks to it.
76
+ */
77
+ static VALUE g_proxy_threads = Qnil;
78
+
71
79
  /* Data passed to the executor wait function */
72
80
  struct executor_wait_data {
73
81
  struct callback_request *request;
@@ -166,6 +174,11 @@ void rbduckdb_function_executor_ensure_started(void) {
166
174
  }
167
175
  #endif
168
176
 
177
+ if (g_proxy_threads == Qnil) {
178
+ g_proxy_threads = rb_ary_new();
179
+ rb_global_variable(&g_proxy_threads);
180
+ }
181
+
169
182
  g_executor_thread = rb_thread_create(executor_thread_func, NULL);
170
183
  rb_global_variable(&g_executor_thread);
171
184
  g_executor_started = 1;
@@ -242,7 +255,293 @@ static void *callback_with_gvl(void *data) {
242
255
  return NULL;
243
256
  }
244
257
 
245
- void rbduckdb_function_executor_dispatch(rbduckdb_function_callback_t cb, void *user_data) {
258
+ /*
259
+ * ============================================================================
260
+ * Per-worker proxy thread
261
+ * ============================================================================
262
+ *
263
+ * One dedicated Ruby thread per DuckDB worker thread. Same hand-off protocol as
264
+ * the global executor (mutex + condvars), but private to a single worker so
265
+ * that callbacks from different workers no longer serialize through one queue.
266
+ *
267
+ * Pattern follows the FFI gem's async callback dispatcher:
268
+ * https://github.com/ffi/ffi/blob/master/ext/ffi_c/Function.c
269
+ */
270
+ struct worker_proxy {
271
+ VALUE ruby_thread;
272
+ volatile int stop_requested;
273
+ rbduckdb_function_callback_t cb;
274
+ void *user_data;
275
+ volatile int has_request;
276
+ volatile int request_done;
277
+ volatile int thread_exited;
278
+ #ifdef _MSC_VER
279
+ CRITICAL_SECTION lock;
280
+ CONDITION_VARIABLE request_cond;
281
+ CONDITION_VARIABLE request_done_cond;
282
+ CONDITION_VARIABLE thread_exit_cond;
283
+ #else
284
+ pthread_mutex_t lock;
285
+ pthread_cond_t request_cond;
286
+ pthread_cond_t request_done_cond;
287
+ pthread_cond_t thread_exit_cond;
288
+ #endif
289
+ };
290
+
291
+ /* Runs without GVL: the proxy waits for a callback request */
292
+ static void *proxy_wait_func(void *data) {
293
+ struct worker_proxy *proxy = (struct worker_proxy *)data;
294
+
295
+ #ifdef _MSC_VER
296
+ EnterCriticalSection(&proxy->lock);
297
+ while (!proxy->stop_requested && !proxy->has_request) {
298
+ SleepConditionVariableCS(&proxy->request_cond, &proxy->lock, INFINITE);
299
+ }
300
+ LeaveCriticalSection(&proxy->lock);
301
+ #else
302
+ pthread_mutex_lock(&proxy->lock);
303
+ while (!proxy->stop_requested && !proxy->has_request) {
304
+ pthread_cond_wait(&proxy->request_cond, &proxy->lock);
305
+ }
306
+ pthread_mutex_unlock(&proxy->lock);
307
+ #endif
308
+
309
+ return NULL;
310
+ }
311
+
312
+ /* Unblock function for the proxy thread (VM shutdown or Thread#kill) */
313
+ static void proxy_stop_func(void *data) {
314
+ struct worker_proxy *proxy = (struct worker_proxy *)data;
315
+
316
+ #ifdef _MSC_VER
317
+ EnterCriticalSection(&proxy->lock);
318
+ proxy->stop_requested = 1;
319
+ WakeConditionVariable(&proxy->request_cond);
320
+ LeaveCriticalSection(&proxy->lock);
321
+ #else
322
+ pthread_mutex_lock(&proxy->lock);
323
+ proxy->stop_requested = 1;
324
+ pthread_cond_signal(&proxy->request_cond);
325
+ pthread_mutex_unlock(&proxy->lock);
326
+ #endif
327
+ }
328
+
329
+ /* The proxy thread main loop. Runs as the body of rb_ensure (see below). */
330
+ static VALUE proxy_loop_body(VALUE data) {
331
+ struct worker_proxy *proxy = (struct worker_proxy *)data;
332
+
333
+ while (!proxy->stop_requested) {
334
+ /* Release the GVL and wait for a request */
335
+ rb_thread_call_without_gvl(proxy_wait_func, proxy, proxy_stop_func, proxy);
336
+
337
+ if (proxy->stop_requested) break;
338
+
339
+ if (proxy->has_request) {
340
+ /* Execute the callback with the GVL held */
341
+ proxy->cb(proxy->user_data);
342
+
343
+ /* Signal completion to the DuckDB worker thread */
344
+ #ifdef _MSC_VER
345
+ EnterCriticalSection(&proxy->lock);
346
+ proxy->has_request = 0;
347
+ proxy->request_done = 1;
348
+ WakeConditionVariable(&proxy->request_done_cond);
349
+ LeaveCriticalSection(&proxy->lock);
350
+ #else
351
+ pthread_mutex_lock(&proxy->lock);
352
+ proxy->has_request = 0;
353
+ proxy->request_done = 1;
354
+ pthread_cond_signal(&proxy->request_done_cond);
355
+ pthread_mutex_unlock(&proxy->lock);
356
+ #endif
357
+ }
358
+ }
359
+
360
+ return Qnil;
361
+ }
362
+
363
+ /*
364
+ * Teardown for the proxy thread. Run via rb_ensure so it executes even if an
365
+ * async exception (Thread#kill, VM shutdown via rb_thread_terminate_all)
366
+ * unwinds proxy_loop_body. If it were skipped, thread_exited would stay 0
367
+ * forever and rbduckdb_worker_proxy_destroy's join would deadlock.
368
+ */
369
+ static VALUE proxy_cleanup(VALUE data) {
370
+ struct worker_proxy *proxy = (struct worker_proxy *)data;
371
+
372
+ /* Stop being GC-protected now that we are about to exit */
373
+ if (g_proxy_threads != Qnil) {
374
+ rb_ary_delete(g_proxy_threads, proxy->ruby_thread);
375
+ }
376
+
377
+ /*
378
+ * Signal that this thread has finished and no longer touches the proxy
379
+ * struct. Only after this may rbduckdb_worker_proxy_destroy free it.
380
+ */
381
+ #ifdef _MSC_VER
382
+ EnterCriticalSection(&proxy->lock);
383
+ proxy->thread_exited = 1;
384
+ WakeConditionVariable(&proxy->thread_exit_cond);
385
+ LeaveCriticalSection(&proxy->lock);
386
+ #else
387
+ pthread_mutex_lock(&proxy->lock);
388
+ proxy->thread_exited = 1;
389
+ pthread_cond_signal(&proxy->thread_exit_cond);
390
+ pthread_mutex_unlock(&proxy->lock);
391
+ #endif
392
+
393
+ return Qnil;
394
+ }
395
+
396
+ /* The proxy thread entry point (Ruby thread). */
397
+ static VALUE proxy_thread_func(void *data) {
398
+ return rb_ensure(proxy_loop_body, (VALUE)data, proxy_cleanup, (VALUE)data);
399
+ }
400
+
401
+ struct worker_proxy *rbduckdb_worker_proxy_create(void) {
402
+ /*
403
+ * Use calloc (not xcalloc): rbduckdb_worker_proxy_destroy frees the struct
404
+ * from a non-Ruby thread where xfree is unsafe.
405
+ */
406
+ struct worker_proxy *proxy = calloc(1, sizeof(struct worker_proxy));
407
+ if (proxy == NULL) {
408
+ rb_raise(rb_eNoMemError, "failed to allocate worker_proxy");
409
+ }
410
+
411
+ proxy->stop_requested = 0;
412
+ proxy->has_request = 0;
413
+ proxy->request_done = 0;
414
+ proxy->thread_exited = 0;
415
+
416
+ #ifdef _MSC_VER
417
+ InitializeCriticalSection(&proxy->lock);
418
+ InitializeConditionVariable(&proxy->request_cond);
419
+ InitializeConditionVariable(&proxy->request_done_cond);
420
+ InitializeConditionVariable(&proxy->thread_exit_cond);
421
+ #else
422
+ pthread_mutex_init(&proxy->lock, NULL);
423
+ pthread_cond_init(&proxy->request_cond, NULL);
424
+ pthread_cond_init(&proxy->request_done_cond, NULL);
425
+ pthread_cond_init(&proxy->thread_exit_cond, NULL);
426
+ #endif
427
+
428
+ /*
429
+ * Lazy-init the GC-protection array so create never silently skips it (see
430
+ * the g_proxy_threads comment above); create runs with the GVL, so safe.
431
+ */
432
+ if (g_proxy_threads == Qnil) {
433
+ g_proxy_threads = rb_ary_new();
434
+ rb_global_variable(&g_proxy_threads);
435
+ }
436
+
437
+ proxy->ruby_thread = rb_thread_create(proxy_thread_func, proxy);
438
+ rb_ary_push(g_proxy_threads, proxy->ruby_thread);
439
+
440
+ return proxy;
441
+ }
442
+
443
+ /*
444
+ * Hand a callback to a proxy and block until it completes.
445
+ * Called from the DuckDB worker thread (non-Ruby thread) that owns this proxy.
446
+ */
447
+ static void dispatch_callback_to_proxy(struct worker_proxy *proxy, rbduckdb_function_callback_t cb, void *user_data) {
448
+ #ifdef _MSC_VER
449
+ EnterCriticalSection(&proxy->lock);
450
+ proxy->cb = cb;
451
+ proxy->user_data = user_data;
452
+ proxy->request_done = 0;
453
+ proxy->has_request = 1;
454
+ WakeConditionVariable(&proxy->request_cond);
455
+ LeaveCriticalSection(&proxy->lock);
456
+
457
+ EnterCriticalSection(&proxy->lock);
458
+ while (!proxy->request_done) {
459
+ SleepConditionVariableCS(&proxy->request_done_cond, &proxy->lock, INFINITE);
460
+ }
461
+ LeaveCriticalSection(&proxy->lock);
462
+ #else
463
+ pthread_mutex_lock(&proxy->lock);
464
+ proxy->cb = cb;
465
+ proxy->user_data = user_data;
466
+ proxy->request_done = 0;
467
+ proxy->has_request = 1;
468
+ pthread_cond_signal(&proxy->request_cond);
469
+ pthread_mutex_unlock(&proxy->lock);
470
+
471
+ pthread_mutex_lock(&proxy->lock);
472
+ while (!proxy->request_done) {
473
+ pthread_cond_wait(&proxy->request_done_cond, &proxy->lock);
474
+ }
475
+ pthread_mutex_unlock(&proxy->lock);
476
+ #endif
477
+ }
478
+
479
+ /* Blocks until the proxy thread has fully exited. Runs without the GVL. */
480
+ static void *proxy_join_func(void *data) {
481
+ struct worker_proxy *proxy = (struct worker_proxy *)data;
482
+
483
+ #ifdef _MSC_VER
484
+ EnterCriticalSection(&proxy->lock);
485
+ while (!proxy->thread_exited) {
486
+ SleepConditionVariableCS(&proxy->thread_exit_cond, &proxy->lock, INFINITE);
487
+ }
488
+ LeaveCriticalSection(&proxy->lock);
489
+ #else
490
+ pthread_mutex_lock(&proxy->lock);
491
+ while (!proxy->thread_exited) {
492
+ pthread_cond_wait(&proxy->thread_exit_cond, &proxy->lock);
493
+ }
494
+ pthread_mutex_unlock(&proxy->lock);
495
+ #endif
496
+
497
+ return NULL;
498
+ }
499
+
500
+ void rbduckdb_worker_proxy_destroy(void *data) {
501
+ struct worker_proxy *proxy = (struct worker_proxy *)data;
502
+ if (proxy == NULL) return;
503
+
504
+ /* Ask the proxy thread to stop. */
505
+ #ifdef _MSC_VER
506
+ EnterCriticalSection(&proxy->lock);
507
+ proxy->stop_requested = 1;
508
+ WakeConditionVariable(&proxy->request_cond);
509
+ LeaveCriticalSection(&proxy->lock);
510
+ #else
511
+ pthread_mutex_lock(&proxy->lock);
512
+ proxy->stop_requested = 1;
513
+ pthread_cond_signal(&proxy->request_cond);
514
+ pthread_mutex_unlock(&proxy->lock);
515
+ #endif
516
+
517
+ /*
518
+ * Wait until the proxy thread has fully exited. Before exiting it runs Ruby
519
+ * code (removing itself from the GC-protection array), which needs the GVL.
520
+ * DuckDB may invoke this destructor either from a worker thread (no GVL) or
521
+ * — depending on when it tears down the local state — from a Ruby thread
522
+ * that holds the GVL. In the latter case we must release the GVL while
523
+ * waiting, or the proxy thread could never acquire it and we would deadlock.
524
+ */
525
+ if (ruby_native_thread_p() && ruby_thread_has_gvl_p()) {
526
+ rb_thread_call_without_gvl(proxy_join_func, proxy, NULL, NULL);
527
+ } else {
528
+ proxy_join_func(proxy);
529
+ }
530
+
531
+ /* The proxy thread is gone; tear down OS primitives and free the struct. */
532
+ #ifdef _MSC_VER
533
+ DeleteCriticalSection(&proxy->lock);
534
+ #else
535
+ pthread_cond_destroy(&proxy->thread_exit_cond);
536
+ pthread_cond_destroy(&proxy->request_done_cond);
537
+ pthread_cond_destroy(&proxy->request_cond);
538
+ pthread_mutex_destroy(&proxy->lock);
539
+ #endif
540
+
541
+ free(proxy);
542
+ }
543
+
544
+ void rbduckdb_function_executor_dispatch_via_proxy(rbduckdb_function_callback_t cb, void *user_data, struct worker_proxy *proxy) {
246
545
  if (ruby_native_thread_p()) {
247
546
  if (ruby_thread_has_gvl_p()) {
248
547
  /* Case 1: Ruby thread with GVL - call directly */
@@ -254,8 +553,15 @@ void rbduckdb_function_executor_dispatch(rbduckdb_function_callback_t cb, void *
254
553
  arg.user_data = user_data;
255
554
  rb_thread_call_with_gvl(callback_with_gvl, &arg);
256
555
  }
556
+ } else if (proxy != NULL) {
557
+ /* Case 3a: Non-Ruby thread with a per-worker proxy */
558
+ dispatch_callback_to_proxy(proxy, cb, user_data);
257
559
  } else {
258
- /* Case 3: Non-Ruby thread - dispatch to executor */
560
+ /* Case 3b: Non-Ruby thread - dispatch to the global executor */
259
561
  dispatch_callback_to_executor(cb, user_data);
260
562
  }
261
563
  }
564
+
565
+ void rbduckdb_function_executor_dispatch(rbduckdb_function_callback_t cb, void *user_data) {
566
+ rbduckdb_function_executor_dispatch_via_proxy(cb, user_data, NULL);
567
+ }
@@ -43,4 +43,48 @@ void rbduckdb_function_executor_ensure_started(void);
43
43
  */
44
44
  void rbduckdb_function_executor_dispatch(rbduckdb_function_callback_t cb, void *user_data);
45
45
 
46
+ /*
47
+ * ============================================================================
48
+ * Per-worker proxy threads (DuckDB >= 1.5.0)
49
+ * ============================================================================
50
+ *
51
+ * The global executor above serializes every non-Ruby-thread callback through
52
+ * a single Ruby thread. A per-worker proxy instead gives each DuckDB worker
53
+ * thread its own dedicated Ruby thread, so callbacks from different workers can
54
+ * run concurrently — they compete for the GVL in round-robin fashion, which
55
+ * helps when callbacks release the GVL (e.g. on I/O).
56
+ *
57
+ * Proxies are created lazily from DuckDB's per-worker init hook and stored in
58
+ * DuckDB's thread-local state; the global executor remains the fallback.
59
+ */
60
+
61
+ /* Opaque per-worker proxy handle. */
62
+ struct worker_proxy;
63
+
64
+ /*
65
+ * Create a per-worker proxy thread. Must be called with the GVL held
66
+ * (typically by dispatching this through the global executor from a per-worker
67
+ * init callback, which itself runs on a non-Ruby thread).
68
+ *
69
+ * May raise (NoMemError, Thread.new failure). The executor runs callbacks
70
+ * unprotected, so a wrapper dispatched to it must rb_protect this call —
71
+ * otherwise a raise longjmps past the executor's done-signaling and the
72
+ * waiting DuckDB worker blocks forever.
73
+ */
74
+ struct worker_proxy *rbduckdb_worker_proxy_create(void);
75
+
76
+ /*
77
+ * Destroy a per-worker proxy. The signature matches duckdb_delete_callback_t so
78
+ * it can be handed directly to DuckDB. Safe to call from a non-Ruby thread: it
79
+ * touches only OS primitives and frees memory allocated with calloc.
80
+ */
81
+ void rbduckdb_worker_proxy_destroy(void *proxy);
82
+
83
+ /*
84
+ * Like rbduckdb_function_executor_dispatch, but on the non-Ruby-thread path
85
+ * (Case 3) it routes through the given per-worker proxy when non-NULL, falling
86
+ * back to the global executor when NULL. Cases 1 and 2 are unchanged.
87
+ */
88
+ void rbduckdb_function_executor_dispatch_via_proxy(rbduckdb_function_callback_t cb, void *user_data, struct worker_proxy *proxy);
89
+
46
90
  #endif
@@ -38,6 +38,7 @@ static VALUE prepared_statement__bind_timestamp_tz(VALUE self, VALUE vidx, VALUE
38
38
  static VALUE prepared_statement__bind_interval(VALUE self, VALUE vidx, VALUE months, VALUE days, VALUE micros);
39
39
  static VALUE prepared_statement__bind_hugeint(VALUE self, VALUE vidx, VALUE lower, VALUE upper);
40
40
  static VALUE prepared_statement__bind_uhugeint(VALUE self, VALUE vidx, VALUE lower, VALUE upper);
41
+ static VALUE prepared_statement__bind_uuid(VALUE self, VALUE vidx, VALUE val);
41
42
  static VALUE prepared_statement__bind_decimal(VALUE self, VALUE vidx, VALUE lower, VALUE upper, VALUE width, VALUE scale);
42
43
  static VALUE prepared_statement__bind_value(VALUE self, VALUE vidx, VALUE val);
43
44
 
@@ -558,6 +559,25 @@ static VALUE prepared_statement__bind_decimal(VALUE self, VALUE vidx, VALUE lowe
558
559
  return self;
559
560
  }
560
561
 
562
+ /* :nodoc: */
563
+ static VALUE prepared_statement__bind_uuid(VALUE self, VALUE vidx, VALUE val) {
564
+ rubyDuckDBPreparedStatement *ctx;
565
+ duckdb_uhugeint uhugeint;
566
+ duckdb_value uuid_val;
567
+ duckdb_state state;
568
+ idx_t idx = check_index(vidx);
569
+
570
+ TypedData_Get_Struct(self, rubyDuckDBPreparedStatement, &prepared_statement_data_type, ctx);
571
+ rbduckdb_uuid_str_to_uhugeint(val, &uhugeint);
572
+ uuid_val = duckdb_create_uuid(uhugeint);
573
+ state = duckdb_bind_value(ctx->prepared_statement, idx, uuid_val);
574
+ duckdb_destroy_value(&uuid_val);
575
+ if (state == DuckDBError) {
576
+ rb_raise(eDuckDBError, "fail to bind %llu parameter", (unsigned long long)idx);
577
+ }
578
+ return self;
579
+ }
580
+
561
581
  /* :nodoc: */
562
582
  static VALUE prepared_statement__bind_value(VALUE self, VALUE vidx, VALUE val) {
563
583
  rubyDuckDBPreparedStatement *ctx;
@@ -617,6 +637,7 @@ void rbduckdb_init_prepared_statement(void) {
617
637
  rb_define_private_method(cDuckDBPreparedStatement, "_bind_interval", prepared_statement__bind_interval, 4);
618
638
  rb_define_private_method(cDuckDBPreparedStatement, "_bind_hugeint", prepared_statement__bind_hugeint, 3);
619
639
  rb_define_private_method(cDuckDBPreparedStatement, "_bind_uhugeint", prepared_statement__bind_uhugeint, 3);
640
+ rb_define_private_method(cDuckDBPreparedStatement, "_bind_uuid", prepared_statement__bind_uuid, 2);
620
641
  rb_define_private_method(cDuckDBPreparedStatement, "_bind_decimal", prepared_statement__bind_decimal, 5);
621
642
  rb_define_private_method(cDuckDBPreparedStatement, "_bind_value", prepared_statement__bind_value, 2);
622
643
  }