duckdb 1.5.5.0 → 1.5.6.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/ext/duckdb/aggregate_function.c +298 -60
  4. data/ext/duckdb/aggregate_function_set.c +3 -4
  5. data/ext/duckdb/appender.c +33 -7
  6. data/ext/duckdb/arrow_array_stream.c +6 -2
  7. data/ext/duckdb/arrow_import.c +1 -1
  8. data/ext/duckdb/client_context.c +1 -1
  9. data/ext/duckdb/column.c +1 -1
  10. data/ext/duckdb/column.h +0 -1
  11. data/ext/duckdb/config.c +1 -1
  12. data/ext/duckdb/connection.c +48 -22
  13. data/ext/duckdb/connection.h +2 -1
  14. data/ext/duckdb/data_chunk.c +1 -1
  15. data/ext/duckdb/database.c +80 -4
  16. data/ext/duckdb/database.h +12 -0
  17. data/ext/duckdb/expression.c +1 -1
  18. data/ext/duckdb/extracted_statements.c +1 -1
  19. data/ext/duckdb/function_executor.c +166 -32
  20. data/ext/duckdb/function_executor.h +18 -6
  21. data/ext/duckdb/instance_cache.c +85 -3
  22. data/ext/duckdb/instance_cache.h +6 -0
  23. data/ext/duckdb/logical_type.c +1 -1
  24. data/ext/duckdb/pending_result.c +6 -1
  25. data/ext/duckdb/prepared_statement.c +14 -2
  26. data/ext/duckdb/result.c +6 -1
  27. data/ext/duckdb/result.h +1 -1
  28. data/ext/duckdb/scalar_function.c +18 -25
  29. data/ext/duckdb/scalar_function_bind_info.c +1 -1
  30. data/ext/duckdb/scalar_function_set.c +3 -4
  31. data/ext/duckdb/table_description.c +1 -1
  32. data/ext/duckdb/table_function.c +16 -21
  33. data/ext/duckdb/table_function_bind_info.c +29 -1
  34. data/ext/duckdb/table_function_bind_info.h +1 -0
  35. data/ext/duckdb/table_function_function_info.c +23 -1
  36. data/ext/duckdb/table_function_init_info.c +20 -1
  37. data/ext/duckdb/value.c +2 -2
  38. data/ext/duckdb/vector.c +1 -1
  39. data/lib/duckdb/appender.rb +1 -2
  40. data/lib/duckdb/converter/int_to_sym.rb +3 -0
  41. data/lib/duckdb/converter.rb +1 -2
  42. data/lib/duckdb/data_chunk.rb +1 -2
  43. data/lib/duckdb/prepared_statement.rb +1 -2
  44. data/lib/duckdb/table_function/function_info.rb +1 -2
  45. data/lib/duckdb/table_function/init_info.rb +1 -2
  46. data/lib/duckdb/table_function.rb +1 -2
  47. data/lib/duckdb/vector.rb +1 -2
  48. data/lib/duckdb/version.rb +1 -1
  49. metadata +2 -2
@@ -76,6 +76,52 @@ static int g_executor_started = 0;
76
76
  */
77
77
  static VALUE g_proxy_threads = Qnil;
78
78
 
79
+ /*
80
+ * Replace embedded NUL bytes so the message can be handed to a C API.
81
+ * Returns str unchanged when there is nothing to replace.
82
+ */
83
+ static VALUE message_without_nul(VALUE str) {
84
+ long len = RSTRING_LEN(str);
85
+ long i;
86
+ VALUE copy;
87
+ char *p;
88
+
89
+ if (memchr(RSTRING_PTR(str), '\0', (size_t)len) == NULL) {
90
+ return str;
91
+ }
92
+
93
+ copy = rb_str_new(RSTRING_PTR(str), len);
94
+ p = RSTRING_PTR(copy);
95
+ for (i = 0; i < len; i++) {
96
+ if (p[i] == '\0') {
97
+ p[i] = ' ';
98
+ }
99
+ }
100
+ return copy;
101
+ }
102
+
103
+ static VALUE pending_error_message(VALUE errinfo) {
104
+ return message_without_nul(rb_obj_as_string(rb_funcall(errinfo, rb_intern("message"), 0)));
105
+ }
106
+
107
+ VALUE rbduckdb_pending_error_message(void) {
108
+ VALUE errinfo = rb_errinfo();
109
+ VALUE msg;
110
+ int state = 0;
111
+
112
+ rb_set_errinfo(Qnil);
113
+ if (errinfo == Qnil) {
114
+ return rb_str_new_cstr("unknown error");
115
+ }
116
+
117
+ msg = rb_protect(pending_error_message, errinfo, &state);
118
+ if (state) {
119
+ rb_set_errinfo(Qnil);
120
+ return rb_str_new_cstr("an exception was raised, but its message could not be read");
121
+ }
122
+ return msg;
123
+ }
124
+
79
125
  /* Data passed to the executor wait function */
80
126
  struct executor_wait_data {
81
127
  struct callback_request *request;
@@ -130,6 +176,49 @@ static void executor_stop_func(void *data) {
130
176
  #endif
131
177
  }
132
178
 
179
+ /*
180
+ * Discard an exception a dispatched callback let escape, so that one bad
181
+ * callback cannot take a dispatcher thread down with it and strand every
182
+ * later callback. Interrupt, SystemExit and thread kill still get through.
183
+ */
184
+ static void discard_callback_exception(int state) {
185
+ VALUE err = rb_errinfo();
186
+
187
+ if (err != Qnil && !rb_obj_is_kind_of(err, rb_eStandardError)) {
188
+ rb_jump_tag(state);
189
+ }
190
+ rb_set_errinfo(Qnil);
191
+ }
192
+
193
+ static VALUE request_run_body(VALUE varg) {
194
+ struct callback_request *req = (struct callback_request *)varg;
195
+ req->cb(req->user_data);
196
+ return Qnil;
197
+ }
198
+
199
+ /* Runs from rb_ensure: the worker waits on done_cond even if the callback raised. */
200
+ static VALUE request_signal_done(VALUE varg) {
201
+ struct callback_request *req = (struct callback_request *)varg;
202
+
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
+ return Qnil;
216
+ }
217
+
218
+ static VALUE request_run(VALUE varg) {
219
+ return rb_ensure(request_run_body, varg, request_signal_done, varg);
220
+ }
221
+
133
222
  /* The executor thread main loop (Ruby thread) */
134
223
  static VALUE executor_thread_func(void *data) {
135
224
  struct executor_wait_data w;
@@ -140,23 +229,12 @@ static VALUE executor_thread_func(void *data) {
140
229
  rb_thread_call_without_gvl(executor_wait_func, &w, executor_stop_func, &w);
141
230
 
142
231
  if (w.request != NULL) {
143
- struct callback_request *req = w.request;
232
+ int state = 0;
144
233
 
145
- /* Execute the callback with the GVL held */
146
- req->cb(req->user_data);
147
-
148
- /* Signal the DuckDB worker thread that the callback is done */
149
- #ifdef _MSC_VER
150
- EnterCriticalSection(&req->done_lock);
151
- req->done = 1;
152
- WakeConditionVariable(&req->done_cond);
153
- LeaveCriticalSection(&req->done_lock);
154
- #else
155
- pthread_mutex_lock(&req->done_mutex);
156
- req->done = 1;
157
- pthread_cond_signal(&req->done_cond);
158
- pthread_mutex_unlock(&req->done_mutex);
159
- #endif
234
+ rb_protect(request_run, (VALUE)w.request, &state);
235
+ if (state) {
236
+ discard_callback_exception(state);
237
+ }
160
238
  }
161
239
  }
162
240
 
@@ -326,6 +404,37 @@ static void proxy_stop_func(void *data) {
326
404
  #endif
327
405
  }
328
406
 
407
+ static VALUE proxy_run_body(VALUE data) {
408
+ struct worker_proxy *proxy = (struct worker_proxy *)data;
409
+ proxy->cb(proxy->user_data);
410
+ return Qnil;
411
+ }
412
+
413
+ /* Runs from rb_ensure: see request_signal_done. */
414
+ static VALUE proxy_signal_done(VALUE data) {
415
+ struct worker_proxy *proxy = (struct worker_proxy *)data;
416
+
417
+ #ifdef _MSC_VER
418
+ EnterCriticalSection(&proxy->lock);
419
+ proxy->has_request = 0;
420
+ proxy->request_done = 1;
421
+ WakeConditionVariable(&proxy->request_done_cond);
422
+ LeaveCriticalSection(&proxy->lock);
423
+ #else
424
+ pthread_mutex_lock(&proxy->lock);
425
+ proxy->has_request = 0;
426
+ proxy->request_done = 1;
427
+ pthread_cond_signal(&proxy->request_done_cond);
428
+ pthread_mutex_unlock(&proxy->lock);
429
+ #endif
430
+
431
+ return Qnil;
432
+ }
433
+
434
+ static VALUE proxy_run(VALUE data) {
435
+ return rb_ensure(proxy_run_body, data, proxy_signal_done, data);
436
+ }
437
+
329
438
  /* The proxy thread main loop. Runs as the body of rb_ensure (see below). */
330
439
  static VALUE proxy_loop_body(VALUE data) {
331
440
  struct worker_proxy *proxy = (struct worker_proxy *)data;
@@ -337,23 +446,12 @@ static VALUE proxy_loop_body(VALUE data) {
337
446
  if (proxy->stop_requested) break;
338
447
 
339
448
  if (proxy->has_request) {
340
- /* Execute the callback with the GVL held */
341
- proxy->cb(proxy->user_data);
449
+ int state = 0;
342
450
 
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
451
+ rb_protect(proxy_run, data, &state);
452
+ if (state) {
453
+ discard_callback_exception(state);
454
+ }
357
455
  }
358
456
  }
359
457
 
@@ -565,3 +663,39 @@ void rbduckdb_function_executor_dispatch_via_proxy(rbduckdb_function_callback_t
565
663
  void rbduckdb_function_executor_dispatch(rbduckdb_function_callback_t cb, void *user_data) {
566
664
  rbduckdb_function_executor_dispatch_via_proxy(cb, user_data, NULL);
567
665
  }
666
+
667
+ static VALUE g_function_data_registry = Qnil;
668
+ static unsigned long long g_function_data_next_id = 0;
669
+
670
+ static void function_data_registry_ensure(void) {
671
+ if (g_function_data_registry == Qnil) {
672
+ g_function_data_registry = rb_hash_new();
673
+ rb_global_variable(&g_function_data_registry);
674
+ }
675
+ }
676
+
677
+ void *rbduckdb_function_data_register(VALUE value) {
678
+ unsigned long long id;
679
+
680
+ function_data_registry_ensure();
681
+ id = ++g_function_data_next_id;
682
+ rb_hash_aset(g_function_data_registry, ULL2NUM(id), value);
683
+ return (void *)(uintptr_t)id;
684
+ }
685
+
686
+ VALUE rbduckdb_function_data_lookup(void *data) {
687
+ if (data == NULL || g_function_data_registry == Qnil) {
688
+ return Qnil;
689
+ }
690
+ return rb_hash_aref(g_function_data_registry, ULL2NUM((unsigned long long)(uintptr_t)data));
691
+ }
692
+
693
+ void rbduckdb_function_data_release(void *data) {
694
+ if (g_function_data_registry == Qnil) return;
695
+ rb_hash_delete(g_function_data_registry, ULL2NUM((unsigned long long)(uintptr_t)data));
696
+ }
697
+
698
+ void rbduckdb_function_data_destroy(void *data) {
699
+ if (data == NULL) return;
700
+ rbduckdb_function_executor_dispatch(rbduckdb_function_data_release, data);
701
+ }
@@ -18,8 +18,9 @@
18
18
  * A generic callback to be executed with the GVL held.
19
19
  * The user_data pointer is passed through unchanged from the dispatch call.
20
20
  *
21
- * The callback is responsible for catching Ruby exceptions (e.g. via
22
- * rb_protect) if needed — this module does not perform exception handling.
21
+ * The callback should still catch its own Ruby exceptions (e.g. via rb_protect)
22
+ * to report them to DuckDB. The dispatcher only guarantees that an exception it
23
+ * lets escape wakes the waiting worker instead of stranding it.
23
24
  */
24
25
  typedef void (*rbduckdb_function_callback_t)(void *user_data);
25
26
 
@@ -66,10 +67,8 @@ struct worker_proxy;
66
67
  * (typically by dispatching this through the global executor from a per-worker
67
68
  * init callback, which itself runs on a non-Ruby thread).
68
69
  *
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.
70
+ * May raise (NoMemError, Thread.new failure), so a wrapper dispatched to the
71
+ * executor should rb_protect this call and fall back to the global executor.
73
72
  */
74
73
  struct worker_proxy *rbduckdb_worker_proxy_create(void);
75
74
 
@@ -87,4 +86,17 @@ void rbduckdb_worker_proxy_destroy(void *proxy);
87
86
  */
88
87
  void rbduckdb_function_executor_dispatch_via_proxy(rbduckdb_function_callback_t cb, void *user_data, struct worker_proxy *proxy);
89
88
 
89
+ /*
90
+ * Take the pending Ruby exception's message as a String safe to hand to a
91
+ * DuckDB set_error entry point, and clear the exception. Always returns a
92
+ * String with no embedded NUL, so StringValueCStr on it cannot raise; #message
93
+ * runs under rb_protect, so neither can a hostile exception object.
94
+ */
95
+ VALUE rbduckdb_pending_error_message(void);
96
+
97
+ void *rbduckdb_function_data_register(VALUE value);
98
+ VALUE rbduckdb_function_data_lookup(void *data);
99
+ void rbduckdb_function_data_release(void *data);
100
+ void rbduckdb_function_data_destroy(void *data);
101
+
90
102
  #endif
@@ -3,7 +3,11 @@
3
3
  VALUE cDuckDBInstanceCache;
4
4
 
5
5
  static void deallocate(void * ctx);
6
+ static void mark(void *ctx);
7
+ static void compact(void *ctx);
6
8
  static VALUE allocate(VALUE klass);
9
+ static VALUE memoizable_path(VALUE vpath);
10
+ static VALUE find_cached_wrapper(rubyDuckDBInstanceCache *ctx, VALUE vpath);
7
11
  static size_t memsize(const void *p);
8
12
  static VALUE instance_cache_initialize(VALUE self);
9
13
  static VALUE instance_cache_get_or_create(int argc, VALUE *argv, VALUE self);
@@ -11,8 +15,8 @@ static VALUE instance_cache_destroy(VALUE self);
11
15
 
12
16
  static const rb_data_type_t instance_cache_data_type = {
13
17
  "DuckDB/InstanceCache",
14
- {NULL, deallocate, memsize,},
15
- 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
18
+ {mark, deallocate, memsize, compact},
19
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
16
20
  };
17
21
 
18
22
  static void deallocate(void * ctx) {
@@ -24,15 +28,62 @@ static void deallocate(void * ctx) {
24
28
  xfree(p);
25
29
  }
26
30
 
31
+ static void mark(void *ctx) {
32
+ rubyDuckDBInstanceCache *p = (rubyDuckDBInstanceCache *)ctx;
33
+
34
+ rb_gc_mark_movable(p->wrappers);
35
+ }
36
+
37
+ static void compact(void *ctx) {
38
+ rubyDuckDBInstanceCache *p = (rubyDuckDBInstanceCache *)ctx;
39
+
40
+ p->wrappers = rb_gc_location(p->wrappers);
41
+ }
42
+
27
43
  static size_t memsize(const void *p) {
28
44
  return sizeof(rubyDuckDBInstanceCache);
29
45
  }
30
46
 
31
47
  static VALUE allocate(VALUE klass) {
32
48
  rubyDuckDBInstanceCache *ctx = xcalloc((size_t)1, sizeof(rubyDuckDBInstanceCache));
49
+ ctx->wrappers = Qnil;
33
50
  return TypedData_Wrap_Struct(klass, &instance_cache_data_type, ctx);
34
51
  }
35
52
 
53
+ /*
54
+ * A path is memoizable only if two get_or_create calls on it share one DuckDB
55
+ * instance. An absent, empty or ":memory:" path gets a fresh instance every
56
+ * time, so those wrappers must stay distinct. Returns the path or Qnil.
57
+ */
58
+ static VALUE memoizable_path(VALUE vpath) {
59
+ if (NIL_P(vpath) || RSTRING_LEN(vpath) == 0) {
60
+ return Qnil;
61
+ }
62
+ if (rb_str_cmp(vpath, rb_str_new_cstr(":memory:")) == 0) {
63
+ return Qnil;
64
+ }
65
+ return vpath;
66
+ }
67
+
68
+ /*
69
+ * The wrapper this cache already holds for vpath, or Qnil. Entries whose
70
+ * wrapper has been collected are gone from the weak set already.
71
+ */
72
+ static VALUE find_cached_wrapper(rubyDuckDBInstanceCache *ctx, VALUE vpath) {
73
+ VALUE wrappers = rb_funcall(ctx->wrappers, rb_intern("keys"), 0);
74
+ long i;
75
+
76
+ for (i = 0; i < RARRAY_LEN(wrappers); i++) {
77
+ VALUE wrapper = rb_ary_entry(wrappers, i);
78
+ rubyDuckDB *db_ctx = rbduckdb_get_struct_database(wrapper);
79
+
80
+ if (!NIL_P(db_ctx->cached_path) && rb_str_equal(db_ctx->cached_path, vpath) == Qtrue) {
81
+ return wrapper;
82
+ }
83
+ }
84
+ return Qnil;
85
+ }
86
+
36
87
  static VALUE instance_cache_initialize(VALUE self) {
37
88
  rubyDuckDBInstanceCache *ctx;
38
89
 
@@ -43,6 +94,11 @@ static VALUE instance_cache_initialize(VALUE self) {
43
94
  rb_raise(eDuckDBError, "Failed to create instance cache");
44
95
  }
45
96
 
97
+ RB_OBJ_WRITE(self, &ctx->wrappers,
98
+ rb_funcall(rb_const_get(rb_const_get(rb_cObject, rb_intern("ObjectSpace")),
99
+ rb_intern("WeakMap")),
100
+ rb_intern("new"), 0));
101
+
46
102
  return self;
47
103
  }
48
104
 
@@ -50,6 +106,8 @@ static VALUE instance_cache_initialize(VALUE self) {
50
106
  static VALUE instance_cache_get_or_create(int argc, VALUE *argv, VALUE self) {
51
107
  VALUE vpath = Qnil;
52
108
  VALUE vconfig = Qnil;
109
+ VALUE memo_path;
110
+ VALUE obj;
53
111
  const char *path = NULL;
54
112
  char *error = NULL;
55
113
  duckdb_config config = NULL;
@@ -79,7 +137,31 @@ static VALUE instance_cache_get_or_create(int argc, VALUE *argv, VALUE self) {
79
137
  rb_raise(eDuckDBError, "Failed to get or create database from instance cache");
80
138
  }
81
139
  }
82
- return rbduckdb_create_database_obj(db);
140
+ /*
141
+ * duckdb_get_or_create_from_cache hands back a fresh duckdb_database handle
142
+ * over an instance it may already have returned before. Registered functions
143
+ * live in that shared instance's catalog, but each wrapper retains them
144
+ * separately, so a second wrapper's collection frees functions the first
145
+ * one's connections can still resolve. One wrapper per instance is what the
146
+ * catalog's lifetime actually is, so reuse the wrapper we already have.
147
+ */
148
+ memo_path = memoizable_path(vpath);
149
+ if (!NIL_P(memo_path)) {
150
+ VALUE cached = find_cached_wrapper(ctx, memo_path);
151
+
152
+ if (!NIL_P(cached)) {
153
+ /* Our existing wrapper keeps the instance alive; this handle is spare. */
154
+ duckdb_close(&db);
155
+ return cached;
156
+ }
157
+ }
158
+
159
+ obj = rbduckdb_create_database_obj(db);
160
+ if (!NIL_P(memo_path)) {
161
+ rbduckdb_database_set_cache_entry(obj, memo_path, ctx->wrappers);
162
+ rb_funcall(ctx->wrappers, rb_intern("[]="), 2, obj, Qtrue);
163
+ }
164
+ return obj;
83
165
  }
84
166
 
85
167
  static VALUE instance_cache_destroy(VALUE self) {
@@ -3,6 +3,12 @@
3
3
 
4
4
  struct _rubyDuckDBInstanceCache {
5
5
  duckdb_instance_cache instance_cache;
6
+ /*
7
+ * ObjectSpace::WeakMap used as a weak set of the DuckDB::Database wrappers
8
+ * this cache has handed out for file paths. Weak so a wrapper nobody holds
9
+ * is still collected, and its DuckDB instance closed, as before.
10
+ */
11
+ VALUE wrappers;
6
12
  };
7
13
 
8
14
  typedef struct _rubyDuckDBInstanceCache rubyDuckDBInstanceCache;
@@ -35,7 +35,7 @@ static VALUE logical_type_initialize(VALUE self, VALUE type_id_arg);
35
35
  static const rb_data_type_t logical_type_data_type = {
36
36
  "DuckDB/LogicalType",
37
37
  {NULL, deallocate, memsize,},
38
- 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
38
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
39
39
  };
40
40
 
41
41
  static void deallocate(void *ctx) {
@@ -12,10 +12,15 @@ static VALUE pending_result_execution_finished_p(VALUE self);
12
12
  static VALUE pending_result__state(VALUE self);
13
13
  static VALUE pending_result__execute_check_state(VALUE self);
14
14
 
15
+ /*
16
+ * No RUBY_TYPED_FREE_IMMEDIATELY: deallocate can block, and must not run
17
+ * during GC sweep: the pending result holds a ClientContext reference; dropping the last
18
+ * one rolls back an open transaction and may shut the database down.
19
+ */
15
20
  static const rb_data_type_t pending_result_data_type = {
16
21
  "DuckDB/PendingResult",
17
22
  {NULL, deallocate, memsize,},
18
- 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
23
+ 0, 0, RUBY_TYPED_WB_PROTECTED
19
24
  };
20
25
 
21
26
  static void deallocate(void *ctx) {
@@ -47,10 +47,15 @@ static VALUE prepared_statement__bind_uuid(VALUE self, VALUE vidx, VALUE val);
47
47
  static VALUE prepared_statement__bind_decimal(VALUE self, VALUE vidx, VALUE lower, VALUE upper, VALUE width, VALUE scale);
48
48
  static VALUE prepared_statement__bind_value(VALUE self, VALUE vidx, VALUE val);
49
49
 
50
+ /*
51
+ * No RUBY_TYPED_FREE_IMMEDIATELY: deallocate can block, and must not run
52
+ * during GC sweep: the statement holds a ClientContext reference; dropping the last one
53
+ * rolls back an open transaction and may shut the database down.
54
+ */
50
55
  static const rb_data_type_t prepared_statement_data_type = {
51
56
  "DuckDB/PreparedStatement",
52
57
  {NULL, deallocate, memsize,},
53
- 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
58
+ 0, 0, RUBY_TYPED_WB_PROTECTED
54
59
  };
55
60
 
56
61
  static void destroy_prepared_statement(rubyDuckDBPreparedStatement *p) {
@@ -345,10 +350,17 @@ static VALUE prepared_statement_bind_double(VALUE self, VALUE vidx, VALUE val) {
345
350
  static VALUE prepared_statement_bind_varchar(VALUE self, VALUE vidx, VALUE str) {
346
351
  rubyDuckDBPreparedStatement *ctx;
347
352
  idx_t idx = check_index(vidx);
353
+ const char *pstr;
354
+ long len;
348
355
 
349
356
  TypedData_Get_Struct(self, rubyDuckDBPreparedStatement, &prepared_statement_data_type, ctx);
350
357
 
351
- if (duckdb_bind_varchar(ctx->prepared_statement, idx, StringValuePtr(str)) == DuckDBError) {
358
+ /* Pass the byte length: duckdb_bind_varchar stops at the first NUL, so an
359
+ * embedded one stored a prefix of the String the caller had validated. */
360
+ pstr = StringValuePtr(str);
361
+ len = RSTRING_LEN(str);
362
+
363
+ if (duckdb_bind_varchar_length(ctx->prepared_statement, idx, pstr, (idx_t)len) == DuckDBError) {
352
364
  rb_raise(eDuckDBError, "fail to bind %llu parameter", (unsigned long long)idx);
353
365
  }
354
366
  return self;
data/ext/duckdb/result.c CHANGED
@@ -48,10 +48,15 @@ static VALUE vector_timestamp_tz(void* vector_data, idx_t row_idx);
48
48
  static VALUE vector_uuid(void* vector_data, idx_t row_idx);
49
49
  static VALUE vector_value(duckdb_vector vector, idx_t row_idx);
50
50
 
51
+ /*
52
+ * No RUBY_TYPED_FREE_IMMEDIATELY: deallocate can block, and must not run
53
+ * during GC sweep: a streaming result holds a ClientContext reference; dropping the last
54
+ * one rolls back an open transaction and may shut the database down.
55
+ */
51
56
  static const rb_data_type_t result_data_type = {
52
57
  "DuckDB/Result",
53
58
  {NULL, deallocate, memsize,},
54
- 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
59
+ 0, 0, RUBY_TYPED_WB_PROTECTED
55
60
  };
56
61
 
57
62
  static void deallocate(void *ctx) {
data/ext/duckdb/result.h CHANGED
@@ -6,7 +6,7 @@
6
6
  * object holds one reference, and each exported Arrow stream holds another,
7
7
  * so the duckdb_result stays valid for consumers that outlive the Ruby
8
8
  * objects. rbduckdb_result_unref() must not call any Ruby API: it runs from
9
- * GC sweep (deallocate) and from Arrow stream release callbacks.
9
+ * GC finalization (deallocate) and from Arrow stream release callbacks.
10
10
  */
11
11
  struct _rubyDuckDBResult {
12
12
  duckdb_result result;
@@ -35,7 +35,6 @@ struct callback_arg {
35
35
  duckdb_logical_type output_type;
36
36
  duckdb_vector *input_vectors;
37
37
  duckdb_logical_type *input_types;
38
- VALUE *args;
39
38
  idx_t row_count;
40
39
  idx_t col_count;
41
40
  };
@@ -71,25 +70,22 @@ static void execute_callback_protected(void *user_data) {
71
70
 
72
71
  rb_protect(execute_callback, (VALUE)arg, &exception_state);
73
72
  if (exception_state) {
74
- VALUE errinfo = rb_errinfo();
75
- if (errinfo != Qnil) {
76
- VALUE msg = rb_funcall(errinfo, rb_intern("message"), 0);
77
- duckdb_scalar_function_set_error(arg->info, StringValueCStr(msg));
78
- }
79
- rb_set_errinfo(Qnil);
73
+ VALUE msg = rbduckdb_pending_error_message();
74
+ duckdb_scalar_function_set_error(arg->info, StringValueCStr(msg));
75
+ RB_GC_GUARD(msg);
80
76
  }
81
77
  }
82
78
 
83
79
  static const rb_data_type_t scalar_function_data_type = {
84
80
  "DuckDB/ScalarFunction",
85
81
  {mark, deallocate, memsize, compact},
86
- 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
82
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
87
83
  };
88
84
 
89
85
  static void mark(void *ctx) {
90
86
  rubyDuckDBScalarFunction *p = (rubyDuckDBScalarFunction *)ctx;
91
- rb_gc_mark(p->function_proc);
92
- rb_gc_mark(p->bind_proc);
87
+ rb_gc_mark_movable(p->function_proc);
88
+ rb_gc_mark_movable(p->bind_proc);
93
89
  }
94
90
 
95
91
  static void deallocate(void * ctx) {
@@ -223,7 +219,6 @@ static void scalar_function_callback(duckdb_function_info info, duckdb_data_chun
223
219
  arg.output_type = duckdb_vector_get_column_type(output);
224
220
  arg.input_vectors = NULL;
225
221
  arg.input_types = NULL;
226
- arg.args = NULL;
227
222
  arg.row_count = duckdb_data_chunk_get_size(input);
228
223
  arg.col_count = duckdb_data_chunk_get_column_count(input);
229
224
 
@@ -252,11 +247,13 @@ static VALUE process_rows(VALUE varg) {
252
247
  struct callback_arg *arg = (struct callback_arg *)varg;
253
248
  idx_t i, j;
254
249
  VALUE result;
250
+ /* A Ruby Array, not a plain buffer: converting a later column can trigger
251
+ * a GC, and the earlier columns' objects must stay reachable. */
252
+ VALUE args = rb_ary_new_capa((long)arg->col_count);
255
253
 
256
254
  /* Allocate arrays to hold input vectors and their types */
257
255
  arg->input_vectors = ALLOC_N(duckdb_vector, arg->col_count);
258
256
  arg->input_types = ALLOC_N(duckdb_logical_type, arg->col_count);
259
- arg->args = ALLOC_N(VALUE, arg->col_count);
260
257
 
261
258
  /* Get all input vectors and their types */
262
259
  for (j = 0; j < arg->col_count; j++) {
@@ -268,16 +265,18 @@ static VALUE process_rows(VALUE varg) {
268
265
  for (i = 0; i < arg->row_count; i++) {
269
266
  /* Build arguments array for this row using vector_value_at */
270
267
  for (j = 0; j < arg->col_count; j++) {
271
- arg->args[j] = rbduckdb_vector_value_at(arg->input_vectors[j], arg->input_types[j], i);
268
+ rb_ary_store(args, (long)j, rbduckdb_vector_value_at(arg->input_vectors[j], arg->input_types[j], i));
272
269
  }
273
270
 
274
271
  /* Call the Ruby block with the arguments */
275
- result = rb_funcallv(arg->ctx->function_proc, rb_intern("call"), (int)arg->col_count, arg->args);
272
+ result = rb_apply(arg->ctx->function_proc, rb_intern("call"), args);
276
273
 
277
274
  /* Write result to output using helper function */
278
275
  rbduckdb_vector_set_value_at(arg->output, arg->output_type, i, result);
279
276
  }
280
277
 
278
+ RB_GC_GUARD(args);
279
+
281
280
  return Qnil;
282
281
  }
283
282
 
@@ -294,9 +293,6 @@ static VALUE cleanup_callback(VALUE varg) {
294
293
  duckdb_destroy_logical_type(&arg->output_type);
295
294
 
296
295
  /* Free allocated memory */
297
- if (arg->args != NULL) {
298
- xfree(arg->args);
299
- }
300
296
  if (arg->input_types != NULL) {
301
297
  xfree(arg->input_types);
302
298
  }
@@ -375,7 +371,7 @@ static VALUE scalar_function_set_function(VALUE self) {
375
371
 
376
372
  TypedData_Get_Struct(self, rubyDuckDBScalarFunction, &scalar_function_data_type, p);
377
373
 
378
- p->function_proc = rb_block_proc();
374
+ RB_OBJ_WRITE(self, &p->function_proc, rb_block_proc());
379
375
 
380
376
  duckdb_scalar_function_set_extra_info(p->scalar_function, p, NULL);
381
377
  duckdb_scalar_function_set_function(p->scalar_function, scalar_function_callback);
@@ -402,7 +398,7 @@ static VALUE scalar_function__set_bind(VALUE self) {
402
398
 
403
399
  TypedData_Get_Struct(self, rubyDuckDBScalarFunction, &scalar_function_data_type, p);
404
400
 
405
- p->bind_proc = rb_block_proc();
401
+ RB_OBJ_WRITE(self, &p->bind_proc, rb_block_proc());
406
402
 
407
403
  duckdb_scalar_function_set_extra_info(p->scalar_function, p, NULL);
408
404
  duckdb_scalar_function_set_bind(p->scalar_function, scalar_function_bind_callback);
@@ -444,12 +440,9 @@ static void execute_bind_callback_protected(void *user_data) {
444
440
 
445
441
  rb_protect(call_bind_proc, (VALUE)&arg, &exception_state);
446
442
  if (exception_state) {
447
- VALUE errinfo = rb_errinfo();
448
- if (errinfo != Qnil) {
449
- VALUE msg = rb_funcall(errinfo, rb_intern("message"), 0);
450
- duckdb_scalar_function_bind_set_error(darg->info, StringValueCStr(msg));
451
- }
452
- rb_set_errinfo(Qnil);
443
+ VALUE msg = rbduckdb_pending_error_message();
444
+ duckdb_scalar_function_bind_set_error(darg->info, StringValueCStr(msg));
445
+ RB_GC_GUARD(msg);
453
446
  }
454
447
  }
455
448
 
@@ -13,7 +13,7 @@ static VALUE scalar_function_bind_info_client_context(VALUE self);
13
13
  static const rb_data_type_t scalar_function_bind_info_data_type = {
14
14
  "DuckDB/ScalarFunction/BindInfo",
15
15
  {NULL, deallocate, memsize,},
16
- 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
16
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
17
17
  };
18
18
 
19
19
  static void deallocate(void *ctx) {
@@ -13,12 +13,12 @@ static VALUE scalar_function_set__add(VALUE self, VALUE scalar_function);
13
13
  static const rb_data_type_t scalar_function_set_data_type = {
14
14
  "DuckDB/ScalarFunctionSet",
15
15
  {mark, deallocate, memsize, compact},
16
- 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
16
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
17
17
  };
18
18
 
19
19
  static void mark(void *ctx) {
20
20
  rubyDuckDBScalarFunctionSet *p = (rubyDuckDBScalarFunctionSet *)ctx;
21
- rb_gc_mark(p->functions);
21
+ rb_gc_mark_movable(p->functions);
22
22
  }
23
23
 
24
24
  static void deallocate(void *ctx) {
@@ -35,8 +35,7 @@ static void compact(void *ctx) {
35
35
  static VALUE allocate(VALUE klass) {
36
36
  rubyDuckDBScalarFunctionSet *ctx = xcalloc((size_t)1, sizeof(rubyDuckDBScalarFunctionSet));
37
37
  VALUE obj = TypedData_Wrap_Struct(klass, &scalar_function_set_data_type, ctx);
38
- ctx->functions = rb_ary_new();
39
- RB_GC_GUARD(ctx->functions);
38
+ RB_OBJ_WRITE(obj, &ctx->functions, rb_ary_new());
40
39
  return obj;
41
40
  }
42
41