duckdb 1.5.3.0 → 1.5.5.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -0
  3. data/CLAUDE.md +4 -0
  4. data/README.md +52 -0
  5. data/duckdb.gemspec +1 -1
  6. data/ext/duckdb/aggregate_function.c +0 -1
  7. data/ext/duckdb/appender.c +17 -0
  8. data/ext/duckdb/arrow_array_stream.c +226 -0
  9. data/ext/duckdb/arrow_array_stream.h +61 -0
  10. data/ext/duckdb/arrow_import.c +165 -0
  11. data/ext/duckdb/arrow_import.h +6 -0
  12. data/ext/duckdb/blob.c +1 -1
  13. data/ext/duckdb/blob.h +1 -2
  14. data/ext/duckdb/config.c +1 -1
  15. data/ext/duckdb/config.h +1 -1
  16. data/ext/duckdb/connection.c +26 -4
  17. data/ext/duckdb/converter.h +1 -0
  18. data/ext/duckdb/conveter.c +39 -9
  19. data/ext/duckdb/data_chunk.c +10 -0
  20. data/ext/duckdb/data_chunk.h +1 -0
  21. data/ext/duckdb/duckdb.c +13 -11
  22. data/ext/duckdb/error.c +13 -1
  23. data/ext/duckdb/error.h +2 -3
  24. data/ext/duckdb/function_executor.c +308 -2
  25. data/ext/duckdb/function_executor.h +44 -0
  26. data/ext/duckdb/function_vector.c +14 -0
  27. data/ext/duckdb/prepared_statement.c +141 -5
  28. data/ext/duckdb/result.c +49 -3
  29. data/ext/duckdb/result.h +11 -0
  30. data/ext/duckdb/ruby-duckdb.h +3 -0
  31. data/ext/duckdb/scalar_function.c +97 -29
  32. data/ext/duckdb/scalar_function.h +2 -4
  33. data/ext/duckdb/scalar_function_bind_info.c +13 -13
  34. data/ext/duckdb/scalar_function_bind_info.h +1 -1
  35. data/ext/duckdb/scalar_function_set.c +9 -9
  36. data/ext/duckdb/scalar_function_set.h +2 -2
  37. data/ext/duckdb/table_description.c +19 -19
  38. data/ext/duckdb/table_description.h +1 -1
  39. data/ext/duckdb/table_function.c +94 -28
  40. data/ext/duckdb/table_function.h +2 -2
  41. data/ext/duckdb/table_function_bind_info.c +20 -20
  42. data/ext/duckdb/table_function_bind_info.h +2 -2
  43. data/ext/duckdb/table_function_function_info.c +5 -5
  44. data/ext/duckdb/table_function_function_info.h +2 -2
  45. data/ext/duckdb/table_function_init_info.c +70 -5
  46. data/ext/duckdb/table_function_init_info.h +2 -2
  47. data/ext/duckdb/util.c +8 -0
  48. data/ext/duckdb/util.h +1 -0
  49. data/ext/duckdb/value.c +526 -1
  50. data/lib/duckdb/appender.rb +23 -0
  51. data/lib/duckdb/arrow_array_stream.rb +33 -0
  52. data/lib/duckdb/connection.rb +73 -0
  53. data/lib/duckdb/converter/int_to_sym.rb +54 -1
  54. data/lib/duckdb/converter.rb +7 -7
  55. data/lib/duckdb/error.rb +26 -0
  56. data/lib/duckdb/function_type_validation.rb +1 -0
  57. data/lib/duckdb/logical_type.rb +6 -3
  58. data/lib/duckdb/prepared_statement.rb +32 -0
  59. data/lib/duckdb/scalar_function.rb +6 -6
  60. data/lib/duckdb/value.rb +400 -0
  61. data/lib/duckdb/version.rb +1 -1
  62. data/lib/duckdb.rb +2 -0
  63. metadata +10 -3
data/ext/duckdb/config.c CHANGED
@@ -79,7 +79,7 @@ static VALUE config_set_config(VALUE self, VALUE key, VALUE value) {
79
79
  return self;
80
80
  }
81
81
 
82
- void rbduckdb_init_duckdb_config(void) {
82
+ void rbduckdb_init_config(void) {
83
83
  #if 0
84
84
  VALUE mDuckDB = rb_define_module("DuckDB");
85
85
  #endif
data/ext/duckdb/config.h CHANGED
@@ -9,6 +9,6 @@ typedef struct _rubyDuckDBConfig rubyDuckDBConfig;
9
9
 
10
10
  rubyDuckDBConfig *get_struct_config(VALUE obj);
11
11
 
12
- void rbduckdb_init_duckdb_config(void);
12
+ void rbduckdb_init_config(void);
13
13
 
14
14
  #endif
@@ -17,6 +17,7 @@ static VALUE connection__register_scalar_function_set(VALUE self, VALUE scalar_f
17
17
  static VALUE connection__register_aggregate_function(VALUE self, VALUE aggregate_function);
18
18
  static VALUE connection__register_aggregate_function_set(VALUE self, VALUE aggregate_function_set);
19
19
  static VALUE connection__register_table_function(VALUE self, VALUE table_function);
20
+ static VALUE connection__get_table_names(VALUE self, VALUE query, VALUE qualified);
20
21
 
21
22
  static const rb_data_type_t connection_data_type = {
22
23
  "DuckDB/Connection",
@@ -198,11 +199,31 @@ static VALUE connection__query_sql(VALUE self, VALUE str) {
198
199
  RB_GC_GUARD(str);
199
200
 
200
201
  if (args.retval == DuckDBError) {
201
- rb_raise(eDuckDBError, "%s", duckdb_result_error(&(ctxr->result)));
202
+ rbduckdb_raise_result_error(&(ctxr->result));
202
203
  }
203
204
  return result;
204
205
  }
205
206
 
207
+ /* :nodoc: */
208
+ static VALUE connection__get_table_names(VALUE self, VALUE query, VALUE qualified) {
209
+ rubyDuckDBConnection *ctx;
210
+ duckdb_value table_names;
211
+
212
+ ctx = rbduckdb_get_struct_connection(self);
213
+
214
+ if (!(ctx->con)) {
215
+ rb_raise(eDuckDBError, "Database connection closed");
216
+ }
217
+
218
+ table_names = duckdb_get_table_names(ctx->con, StringValueCStr(query), RTEST(qualified));
219
+
220
+ if (!table_names) {
221
+ rb_raise(eDuckDBError, "Failed to get table names from query");
222
+ }
223
+
224
+ return rbduckdb_value_new(table_names);
225
+ }
226
+
206
227
  /* :nodoc: */
207
228
  static VALUE connection__register_logical_type(VALUE self, VALUE logical_type) {
208
229
  rubyDuckDBConnection *ctxcon;
@@ -231,7 +252,7 @@ static VALUE connection__register_scalar_function(VALUE self, VALUE scalar_funct
231
252
  duckdb_state state;
232
253
 
233
254
  ctxcon = rbduckdb_get_struct_connection(self);
234
- ctxsf = get_struct_scalar_function(scalar_function);
255
+ ctxsf = rbduckdb_get_struct_scalar_function(scalar_function);
235
256
 
236
257
  state = duckdb_register_scalar_function(ctxcon->con, ctxsf->scalar_function);
237
258
 
@@ -252,7 +273,7 @@ static VALUE connection__register_scalar_function_set(VALUE self, VALUE scalar_f
252
273
  duckdb_state state;
253
274
 
254
275
  ctxcon = rbduckdb_get_struct_connection(self);
255
- ctxsfs = get_struct_scalar_function_set(scalar_function_set);
276
+ ctxsfs = rbduckdb_get_struct_scalar_function_set(scalar_function_set);
256
277
 
257
278
  state = duckdb_register_scalar_function_set(ctxcon->con, ctxsfs->scalar_function_set);
258
279
 
@@ -314,7 +335,7 @@ static VALUE connection__register_table_function(VALUE self, VALUE table_functio
314
335
  duckdb_state state;
315
336
 
316
337
  ctxcon = rbduckdb_get_struct_connection(self);
317
- ctxtf = get_struct_table_function(table_function);
338
+ ctxtf = rbduckdb_get_struct_table_function(table_function);
318
339
 
319
340
  state = duckdb_register_table_function(ctxcon->con, ctxtf->table_function);
320
341
 
@@ -346,4 +367,5 @@ void rbduckdb_init_connection(void) {
346
367
  rb_define_private_method(cDuckDBConnection, "_register_table_function", connection__register_table_function, 1);
347
368
  rb_define_private_method(cDuckDBConnection, "_connect", connection__connect, 1);
348
369
  rb_define_private_method(cDuckDBConnection, "_query_sql", connection__query_sql, 1);
370
+ rb_define_private_method(cDuckDBConnection, "_get_table_names", connection__get_table_names, 2);
349
371
  }
@@ -19,6 +19,7 @@ extern ID id__decimal_to_unscaled;
19
19
  VALUE rbduckdb_uuid_to_ruby(duckdb_hugeint h);
20
20
  VALUE rbduckdb_uuid_uhugeint_to_ruby(duckdb_uhugeint h);
21
21
  void rbduckdb_uuid_str_to_hugeint(VALUE uuid_str, duckdb_hugeint *out);
22
+ void rbduckdb_uuid_str_to_uhugeint(VALUE uuid_str, duckdb_uhugeint *out);
22
23
  VALUE rbduckdb_interval_to_ruby(duckdb_interval i);
23
24
  VALUE rbduckdb_hugeint_to_ruby(duckdb_hugeint h);
24
25
  VALUE rbduckdb_uhugeint_to_ruby(duckdb_uhugeint h);
@@ -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,19 @@
2
2
 
3
3
  VALUE eDuckDBError;
4
4
 
5
- void rbduckdb_init_duckdb_error(void) {
5
+ /* Raises DuckDB::Error from a failed result. C only reads the message and error
6
+ * type id off the result (the result is destroyed before Ruby sees it) and passes
7
+ * them to DuckDB::Error.new(message, error_type_id). */
8
+ void rbduckdb_raise_result_error(duckdb_result *result) {
9
+ const char *msg = duckdb_result_error(result);
10
+ if (!msg) {
11
+ msg = "DuckDB error";
12
+ }
13
+ VALUE exc = rb_funcall(eDuckDBError, rb_intern("new"), 2, rb_str_new_cstr(msg), INT2FIX(duckdb_result_error_type(result)));
14
+ rb_exc_raise(exc);
15
+ }
16
+
17
+ void rbduckdb_init_error(void) {
6
18
  #if 0
7
19
  VALUE mDuckDB = rb_define_module("DuckDB");
8
20
  #endif
data/ext/duckdb/error.h CHANGED
@@ -1,8 +1,7 @@
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
+ NORETURN(void rbduckdb_raise_result_error(duckdb_result *result));
5
6
 
6
7
  #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