duckdb 1.5.5.0 → 1.5.5.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: 534e22d8df0424b7551b9ca68678d4da9f2e7a8055272dabe7c31d9f63c7b5b4
4
- data.tar.gz: 5a1a9289dd5b20de755dd2c5c17242a353fe695ff27e47e5c862bc62004d1875
3
+ metadata.gz: caab56325522ea94df9e9cdc12c54b20f2335c2a8d93df0d1fce965643e78cf0
4
+ data.tar.gz: a572bfa7d8f4ff0dfac85c34ace463b272446e1de7f3fad6bd790de055f25787
5
5
  SHA512:
6
- metadata.gz: 5afc359f5b6f5c972a34f0662e0f94360d91a12fd6a3db3e116f54eea486436c2855e0ee8f1949d8aff42c7b171e2efbeded84989b5bf335b57c7b9e7d5ee497
7
- data.tar.gz: 7f5d628c60766814253cf9971b90f3fc1911d63d376cbce414dcff92f8e550dbccc676832435349b925c26449aa18d2dff394bd0e33d300ee1e57cc8ff8f5f5f
6
+ metadata.gz: c150cfd69f78f5106596211b5d09432ef60678942c9fdfca4a634eff25052ec3704381ab85bbf9ae9e82dc5b177f1e226c25b95afa33e57a225e9bf4c284efad
7
+ data.tar.gz: 163925f3b77e0bf445b23d37a0e3e45d0aab4d981c58b73d196af7157964c8e33f20147a8d062a13d68becd71f57a4e5906fc9d1fbbbd716c1b8319aedb7ce58
data/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  # Unreleased
6
6
 
7
+ # 1.5.5.1 - 2026-08-29
8
+ - fix `DuckDB::Database#close` deadlocking when DuckDB tears down a pipeline that still owes a UDF callback, as a failed aggregate window query does on DuckDB 1.4.x. `duckdb_close` joins DuckDB's worker threads, and a worker running a callback waits for the executor thread, which waits for the GVL the closing thread was holding. The GVL is now released for the duration of `duckdb_close`.
9
+ - fix a segfault when an aggregate UDF was used as a window function over a constant frame, such as `OVER ()` or `OVER (PARTITION BY x)` (issue #1446). DuckDB's `CAPIAggregateUpdate` does not flatten the state vector the way its combine and finalize counterparts do, so it hands the update callback a single state while reporting the partition's full row count; reading one state per row then ran off the end of the allocation. Such a chunk is now recognised and every row aggregated into the one state DuckDB supplied.
10
+ - fix an aggregate UDF state that has lost its registry entry being passed to the user's `update`/`combine`/`finalize` proc as `nil`, indistinguishable from an `init` proc that legitimately returned `nil`. Such a state now reports an internal error instead of silently producing wrong results (issue #1445).
11
+ - fix `DuckDB::Appender.create_query` reading past the end of its column name array when the array is shorter than the types array, and freeing a column name before DuckDB copies it when the array holds `to_str` objects. A column name array whose size differs from the types array now raises `ArgumentError` (PR #1453).
12
+ - fix a bound or appended `VARCHAR` being silently truncated at an embedded NUL byte, so that a String validated on the Ruby side is now stored in full (PR #1451). Affects `Connection#query(sql, *args)`, `PreparedStatement#bind`/`#bind_varchar` and `Appender#append_varchar`.
13
+ - fix an exception raised while converting a row for an aggregate UDF's update callback — including the `Timeout::Error` from wrapping a query in `Timeout.timeout` — unwinding into DuckDB instead of aborting the query, which could wedge the process for good (PR #1450).
14
+ - fix a permanent hang when a UDF callback raised an exception whose message contained a NUL byte (or whose `#message` raised): the error reporter raised while reporting, killing the callback executor thread and stranding every later UDF callback in the process (PR #1448).
15
+ - add `DuckDB::TableFunction::BindInfo#bind_data=` to store an arbitrary Ruby object as a custom table function's bind data.
16
+ - add `DuckDB::TableFunction::FunctionInfo#bind_data` to retrieve, during execution, the object stored by `BindInfo#bind_data=`.
17
+ - add `DuckDB::TableFunction::InitInfo#bind_data` to retrieve, during the init phase, the object stored by `BindInfo#bind_data=`.
18
+ - support the statement types added in DuckDB 1.5.5: `#statement_type` now returns `:copy_database`, `:update_extensions` and `:merge_into` instead of raising `DuckDB::Error: Unknown statement type`.
19
+ - fix a use-after-free when a function was registered through `DuckDB::InstanceCache`: two `get_or_create` calls on the same path returned separate `DuckDB::Database` objects over one shared instance, so collecting either one freed functions the other's connections could still resolve. `get_or_create` now returns the same `DuckDB::Database` for the same path.
20
+
7
21
  # 1.5.5.0 - 2026-07-27
8
22
 
9
23
  - bump up DuckDB 1.5.5 on CI.
@@ -22,9 +22,33 @@ static VALUE g_aggregate_state_registry;
22
22
  */
23
23
  static unsigned long long g_next_state_id = 0;
24
24
 
25
+ /*
26
+ * Addresses of every state buffer DuckDB has initialised and not yet
27
+ * destroyed, used as a set: keys are the buffer addresses, the values are
28
+ * unused.
29
+ *
30
+ * This exists only to make a state pointer testable without dereferencing it.
31
+ * DuckDB's CAPIAggregateUpdate flattens the input vectors but not the state
32
+ * vector, although its combine and finalize counterparts both flatten theirs.
33
+ * A window frame that is constant over the whole partition -- `OVER ()` --
34
+ * makes DuckDB pass a constant vector holding one pointer while reporting the
35
+ * partition's full row count, so states[1] onwards read off the end of an
36
+ * eight-byte allocation and yield unrelated heap bytes. Dereferencing those to
37
+ * reach state_id is the crash; membership here is the only test that does not.
38
+ * See issue #1446.
39
+ *
40
+ * Protected from GC via rb_gc_register_mark_object on init.
41
+ */
42
+ static VALUE g_aggregate_state_addresses;
43
+
25
44
  typedef struct {
26
45
  unsigned long long state_id;
27
- VALUE ruby_state;
46
+ /* The address this state was initialised at. DuckDB rolls partial states
47
+ * up by memcpy'ing the whole buffer elsewhere and destroying the copy, so
48
+ * the copy is the only thing that can still name the original address --
49
+ * and that address has to leave g_aggregate_state_addresses when the state
50
+ * dies, or the set grows by one entry per query. */
51
+ void *origin;
28
52
  } ruby_aggregate_state;
29
53
 
30
54
  static void mark(void *);
@@ -155,6 +179,23 @@ static inline void state_registry_store(ruby_aggregate_state *state, VALUE value
155
179
  rb_hash_aset(g_aggregate_state_registry, state_registry_key(state), value);
156
180
  }
157
181
 
182
+ /*
183
+ * Read a state's Ruby VALUE back out of the registry.
184
+ *
185
+ * The registry is the only place the VALUE may be read from: a copy cached
186
+ * in the state buffer would be a raw VALUE in a C struct DuckDB allocates,
187
+ * which GC compaction does not update.
188
+ *
189
+ * Returns Qundef, not Qnil, when the state has no entry: nil is a legitimate
190
+ * state (a user's init proc may return it), so the two must not be conflated.
191
+ * Every live state has an entry, so Qundef means a bug in this file; callers
192
+ * report it through report_missing_state_to_duckdb rather than handing the
193
+ * user's proc a state it never returned.
194
+ */
195
+ static inline VALUE state_registry_load(ruby_aggregate_state *state) {
196
+ return rb_hash_lookup2(g_aggregate_state_registry, state_registry_key(state), Qundef);
197
+ }
198
+
158
199
  /*
159
200
  * Remove a state entry from the registry. Safe to call even if the
160
201
  * entry was already removed (rb_hash_delete is a no-op for missing keys).
@@ -163,18 +204,81 @@ static inline void state_registry_remove(ruby_aggregate_state *state) {
163
204
  rb_hash_delete(g_aggregate_state_registry, state_registry_key(state));
164
205
  }
165
206
 
207
+ /*
208
+ * Key into g_aggregate_state_addresses. Takes the bare address, so testing
209
+ * membership never dereferences the pointer; that is the whole point of the
210
+ * set.
211
+ */
212
+ static inline VALUE state_address_key(const void *addr) {
213
+ return ULL2NUM((unsigned long long)(uintptr_t)addr);
214
+ }
215
+
216
+ static inline int state_address_is_live(ruby_aggregate_state *state) {
217
+ if (state == NULL) {
218
+ return 0;
219
+ }
220
+ return RTEST(rb_hash_lookup2(g_aggregate_state_addresses, state_address_key(state), Qfalse));
221
+ }
222
+
223
+ /*
224
+ * Birth and death of a state, in both maps at once.
225
+ *
226
+ * The address set is only meaningful as a dereference-free stand-in for "this
227
+ * state has a registry entry", so the two must never disagree. Keeping every
228
+ * add and remove inside this pair is what stops a later call site from
229
+ * updating one and forgetting the other -- which is how the address set came
230
+ * to leak an entry per query the first time round.
231
+ *
232
+ * (state_registry_store on its own is not a birth: the update and combine
233
+ * callbacks use it to replace a registered state's value, and the address is
234
+ * already live by then.)
235
+ */
236
+ static inline void state_register(ruby_aggregate_state *state, VALUE value) {
237
+ state_registry_store(state, value);
238
+ rb_hash_aset(g_aggregate_state_addresses, state_address_key(state), Qtrue);
239
+ }
240
+
241
+ /*
242
+ * Idempotent, and harmless for a state that was never registered. Only safe on
243
+ * a pointer already known to be dereferenceable: a live state, or one from a
244
+ * callback whose state vector DuckDB flattened.
245
+ *
246
+ * Drops the address the state was initialised at as well as its own. DuckDB
247
+ * rolls partial states up by memcpy'ing the whole buffer elsewhere and
248
+ * destroying the copy, so the copy is the only thing left that can still name
249
+ * the original address.
250
+ */
251
+ static inline void state_release(ruby_aggregate_state *state) {
252
+ state_registry_remove(state);
253
+ rb_hash_delete(g_aggregate_state_addresses, state_address_key(state));
254
+ if (state->origin != (void *)state) {
255
+ rb_hash_delete(g_aggregate_state_addresses, state_address_key(state->origin));
256
+ }
257
+ }
258
+
166
259
  /*
167
260
  * Report a pending Ruby exception to DuckDB via
168
261
  * duckdb_aggregate_function_set_error and clear it from errinfo.
169
262
  * Caller must only invoke this when rb_protect reported exception_state != 0.
170
263
  */
171
264
  static void report_ruby_error_to_duckdb(duckdb_function_info info) {
172
- VALUE errinfo = rb_errinfo();
173
- if (errinfo != Qnil) {
174
- VALUE msg = rb_funcall(errinfo, rb_intern("message"), 0);
175
- duckdb_aggregate_function_set_error(info, StringValueCStr(msg));
176
- }
177
- rb_set_errinfo(Qnil);
265
+ VALUE msg = rbduckdb_pending_error_message();
266
+ duckdb_aggregate_function_set_error(info, StringValueCStr(msg));
267
+ RB_GC_GUARD(msg);
268
+ }
269
+
270
+ /*
271
+ * Report a state that has no registry entry. Not reachable from Ruby code:
272
+ * it means a state was released while DuckDB still held it, which would
273
+ * otherwise pass nil to the user's proc and silently corrupt the result.
274
+ */
275
+ static void report_missing_state_to_duckdb(duckdb_function_info info, ruby_aggregate_state *state) {
276
+ char msg[128];
277
+
278
+ snprintf(msg, sizeof(msg),
279
+ "aggregate state %llu has no registry entry (ruby-duckdb internal error)",
280
+ (unsigned long long)state->state_id);
281
+ duckdb_aggregate_function_set_error(info, msg);
178
282
  }
179
283
 
180
284
  /* state_size callback: constant buffer per state. */
@@ -201,9 +305,9 @@ static void execute_init_callback_protected(void *user_data) {
201
305
  int exception_state;
202
306
  VALUE result;
203
307
 
204
- /* Initialise buffer to a safe value before calling Ruby. */
205
- state->ruby_state = Qnil;
308
+ /* Assign the ID before calling Ruby: the registry entry is keyed on it. */
206
309
  state->state_id = ++g_next_state_id;
310
+ state->origin = state;
207
311
 
208
312
  result = rb_protect(call_init_proc, (VALUE)arg, &exception_state);
209
313
  if (exception_state) {
@@ -211,8 +315,7 @@ static void execute_init_callback_protected(void *user_data) {
211
315
  return;
212
316
  }
213
317
 
214
- state->ruby_state = result;
215
- state_registry_store(state, result);
318
+ state_register(state, result);
216
319
  }
217
320
 
218
321
  static void state_init_callback(duckdb_function_info info, duckdb_aggregate_state state_p) {
@@ -223,10 +326,13 @@ static void state_init_callback(duckdb_function_info info, duckdb_aggregate_stat
223
326
  if (ctx == NULL || ctx->init_proc == Qnil) {
224
327
  /* Defensive: maybe_set_functions only wires callbacks when init_proc
225
328
  * is set, so this branch should be unreachable in practice. Zero the
226
- * buffer anyway to keep the Ruby state slot well-defined. */
329
+ * ID anyway, and leave the state unregistered: the callbacks then
330
+ * report it rather than running the user's proc on a state that was
331
+ * never initialised. Set origin so state_release can read it if
332
+ * DuckDB destroys the buffer. */
227
333
  ruby_aggregate_state *state = (ruby_aggregate_state *)state_p;
228
- state->ruby_state = Qnil;
229
334
  state->state_id = 0;
335
+ state->origin = state;
230
336
  return;
231
337
  }
232
338
 
@@ -245,20 +351,118 @@ struct update_callback_arg {
245
351
  duckdb_aggregate_state *states;
246
352
  duckdb_vector *input_vectors;
247
353
  duckdb_logical_type *input_types;
248
- VALUE *args;
249
354
  idx_t row_count;
250
355
  idx_t col_count;
356
+ /* Non-zero when `states` holds a single pointer shared by every row
357
+ * instead of one per row; set by resolve_state_layout. */
358
+ int constant_states;
251
359
  };
252
360
 
253
361
  struct update_one_arg {
254
362
  VALUE update_proc;
255
- int argc;
256
- VALUE *argv;
363
+ VALUE args;
257
364
  };
258
365
 
259
366
  static VALUE call_update_proc(VALUE varg) {
260
367
  struct update_one_arg *arg = (struct update_one_arg *)varg;
261
- return rb_funcallv(arg->update_proc, rb_intern("call"), arg->argc, arg->argv);
368
+ return rb_apply(arg->update_proc, rb_intern("call"), arg->args);
369
+ }
370
+
371
+ /*
372
+ * DuckDB does not call the destroy callback once update has failed, so the
373
+ * chunk's registry entries have to be dropped here or the Ruby VALUEs leak.
374
+ * Rows in a chunk may share a state; state_registry_remove is idempotent.
375
+ *
376
+ * Only the states this chunk actually used are released. Under the constant
377
+ * layout that is states[0] alone: everything past it is adjacent heap, which
378
+ * was observed to hold live states belonging to *other* partitions, and
379
+ * releasing one of those would drop a registry entry this chunk never owned.
380
+ *
381
+ * Entries that are not live states are skipped rather than dropped, because
382
+ * reading state_id out of adjacent heap is the very crash this file avoids.
383
+ * Skipping them loses nothing: a pointer that is not a live state has no
384
+ * registry entry to remove.
385
+ */
386
+ static void release_chunk_states(struct update_callback_arg *arg) {
387
+ ruby_aggregate_state **states = (ruby_aggregate_state **)arg->states;
388
+ idx_t count = arg->constant_states ? (idx_t)1 : arg->row_count;
389
+ idx_t i;
390
+
391
+ for (i = 0; i < count; i++) {
392
+ if (state_address_is_live(states[i])) {
393
+ state_release(states[i]);
394
+ }
395
+ }
396
+ }
397
+
398
+ /*
399
+ * Work out how `states` is laid out before anything dereferences it.
400
+ *
401
+ * DuckDB passes one of two shapes. Normally it is row-indexed: every entry is
402
+ * a state DuckDB has initialised, and rows may share one. When the window
403
+ * frame is constant over the partition, DuckDB instead builds a constant
404
+ * vector holding that partition's single state and forgets to flatten it into
405
+ * row_count copies, so only a short prefix of the array really is that state
406
+ * and the rest is whatever bytes follow an eight-byte allocation.
407
+ *
408
+ * A row-indexed array cannot contain an entry that is not a live state, so one
409
+ * dead entry is proof of the constant layout -- and proof reached without ever
410
+ * dereferencing the pointer, which is what the crash in issue #1446 was.
411
+ *
412
+ * Be clear about what this is: a heuristic with a known ceiling, not a fix.
413
+ * The real fix is one line in DuckDB, `state.Flatten(count)` in
414
+ * CAPIAggregateUpdate, and this whole mechanism can be deleted once a release
415
+ * carrying it is the oldest DuckDB supported here.
416
+ *
417
+ * Two things it does not do:
418
+ *
419
+ * - It still reads states[i] past the end of that eight-byte allocation.
420
+ * Not dereferencing the result is what stops the SIGSEGV, but the read
421
+ * itself remains out of bounds and would trip a sanitiser.
422
+ * - Only one direction is proof. The bytes past the constant vector are
423
+ * ordinary heap and were observed to hold live state pointers belonging
424
+ * to other partitions, so "every entry is live" is evidence, not proof.
425
+ * That is the safe way round -- it falls back to the row-indexed reading
426
+ * DuckDB intends -- but it is probability, not a guarantee.
427
+ *
428
+ * It also rests on every state reaching update having been initialised at the
429
+ * address it is passed at. That does not hold for states in general: DuckDB
430
+ * rolls partial states up by memcpy, and the destroy callback is observed
431
+ * receiving copies at addresses it never initialised. Those copies are made to
432
+ * be combined and finalized, after the rows are updated, so update should only
433
+ * ever see states DuckDB is still accumulating into -- but if that assumption
434
+ * ever broke for an entry past index 0, the chunk would be read as constant
435
+ * and the rows aggregated into states[0], silently. Attempts to reproduce that
436
+ * (400k rows, 50k groups, 8 threads, forcing hash-table repartitioning) did
437
+ * not manage it.
438
+ */
439
+ static int resolve_state_layout(struct update_callback_arg *arg) {
440
+ ruby_aggregate_state **states = (ruby_aggregate_state **)arg->states;
441
+ idx_t i;
442
+
443
+ arg->constant_states = 0;
444
+
445
+ if (arg->row_count == 0) {
446
+ return 1;
447
+ }
448
+
449
+ if (!state_address_is_live(states[0])) {
450
+ /* Not even the one entry DuckDB always fills is a state of ours;
451
+ * there is nothing here that can be aggregated into. */
452
+ duckdb_aggregate_function_set_error(
453
+ arg->info, "aggregate update received a state array holding no live state "
454
+ "(ruby-duckdb internal error)");
455
+ return 0;
456
+ }
457
+
458
+ for (i = 1; i < arg->row_count; i++) {
459
+ if (!state_address_is_live(states[i])) {
460
+ arg->constant_states = 1;
461
+ return 1;
462
+ }
463
+ }
464
+
465
+ return 1;
262
466
  }
263
467
 
264
468
  /*
@@ -268,19 +472,23 @@ static VALUE call_update_proc(VALUE varg) {
268
472
  * or the Ruby proc call raises, allocated buffers and logical types are
269
473
  * released on the unwind path.
270
474
  *
271
- * Ruby exceptions raised by the user's proc are caught inline via
272
- * rb_protect and reported to DuckDB as scalar errors; other Ruby
273
- * exceptions (e.g. from vector_value_at) propagate and are cleaned up
274
- * by rb_ensure.
475
+ * Ruby exceptions raised by the user's proc are caught inline via rb_protect
476
+ * and reported to DuckDB; anything else (vector_value_at, or an async
477
+ * exception such as Timeout::Error) unwinds to execute_update_callback_protected.
478
+ *
479
+ * arg->constant_states must already have been set by resolve_state_layout.
275
480
  */
276
481
  static VALUE update_process_rows(VALUE varg) {
277
482
  struct update_callback_arg *arg = (struct update_callback_arg *)varg;
278
483
  ruby_aggregate_state **states = (ruby_aggregate_state **)arg->states;
279
484
  idx_t i, j;
280
485
 
486
+ /* A Ruby Array, not a plain buffer: converting a later column can trigger
487
+ * a GC, and the earlier columns' objects must stay reachable. */
488
+ VALUE args = rb_ary_new_capa((long)arg->col_count + 1);
489
+
281
490
  arg->input_vectors = ALLOC_N(duckdb_vector, arg->col_count);
282
491
  arg->input_types = ALLOC_N(duckdb_logical_type, arg->col_count);
283
- arg->args = ALLOC_N(VALUE, arg->col_count + 1);
284
492
 
285
493
  for (j = 0; j < arg->col_count; j++) {
286
494
  arg->input_vectors[j] = duckdb_data_chunk_get_vector(arg->input, j);
@@ -288,9 +496,10 @@ static VALUE update_process_rows(VALUE varg) {
288
496
  }
289
497
 
290
498
  for (i = 0; i < arg->row_count; i++) {
291
- ruby_aggregate_state *state = states[i];
499
+ ruby_aggregate_state *state = arg->constant_states ? states[0] : states[i];
292
500
  struct update_one_arg one;
293
501
  int exception_state;
502
+ VALUE ruby_state;
294
503
  VALUE ret;
295
504
 
296
505
  /*
@@ -314,35 +523,35 @@ static VALUE update_process_rows(VALUE varg) {
314
523
  }
315
524
  }
316
525
 
317
- arg->args[0] = state->ruby_state;
526
+ ruby_state = state_registry_load(state);
527
+ if (ruby_state == Qundef) {
528
+ report_missing_state_to_duckdb(arg->info, state);
529
+ release_chunk_states(arg);
530
+ RB_GC_GUARD(args);
531
+ return Qnil;
532
+ }
533
+
534
+ rb_ary_store(args, 0, ruby_state);
318
535
  for (j = 0; j < arg->col_count; j++) {
319
- arg->args[j + 1] = rbduckdb_vector_value_at(arg->input_vectors[j], arg->input_types[j], i);
536
+ rb_ary_store(args, (long)j + 1, rbduckdb_vector_value_at(arg->input_vectors[j], arg->input_types[j], i));
320
537
  }
321
538
 
322
539
  one.update_proc = arg->ctx->update_proc;
323
- one.argc = (int)(arg->col_count + 1);
324
- one.argv = arg->args;
540
+ one.args = args;
325
541
 
326
542
  ret = rb_protect(call_update_proc, (VALUE)&one, &exception_state);
327
543
  if (exception_state) {
328
544
  report_ruby_error_to_duckdb(arg->info);
329
- /*
330
- * DuckDB does not call the destroy callback on the update error
331
- * path, so we must remove reachable states from the registry
332
- * ourselves to avoid leaking Ruby VALUEs. Iterate all rows in
333
- * the chunk — multiple rows may share the same state (same
334
- * group), but state_registry_remove is idempotent.
335
- */
336
- for (j = 0; j < arg->row_count; j++) {
337
- state_registry_remove(states[j]);
338
- }
545
+ release_chunk_states(arg);
546
+ RB_GC_GUARD(args);
339
547
  return Qnil;
340
548
  }
341
549
 
342
- state->ruby_state = ret;
343
550
  state_registry_store(state, ret);
344
551
  }
345
552
 
553
+ RB_GC_GUARD(args);
554
+
346
555
  return Qnil;
347
556
  }
348
557
 
@@ -356,9 +565,6 @@ static VALUE update_cleanup_callback(VALUE varg) {
356
565
  }
357
566
  xfree(arg->input_types);
358
567
  }
359
- if (arg->args != NULL) {
360
- xfree(arg->args);
361
- }
362
568
  if (arg->input_vectors != NULL) {
363
569
  xfree(arg->input_vectors);
364
570
  }
@@ -366,9 +572,30 @@ static VALUE update_cleanup_callback(VALUE varg) {
366
572
  return Qnil;
367
573
  }
368
574
 
575
+ static VALUE update_process_rows_ensured(VALUE varg) {
576
+ return rb_ensure(update_process_rows, varg, update_cleanup_callback, varg);
577
+ }
578
+
579
+ /*
580
+ * The scalar path has always protected its callback body; this one did not, so
581
+ * an exception from anywhere but the user's proc unwound into DuckDB's C++
582
+ * frames. Reachable from ordinary Ruby: Timeout.timeout around a query lands
583
+ * its Timeout::Error in the row loop and wedges the VM.
584
+ */
369
585
  static void execute_update_callback_protected(void *user_data) {
370
586
  struct update_callback_arg *arg = (struct update_callback_arg *)user_data;
371
- rb_ensure(update_process_rows, (VALUE)arg, update_cleanup_callback, (VALUE)arg);
587
+ int exception_state;
588
+
589
+ if (!resolve_state_layout(arg)) {
590
+ release_chunk_states(arg);
591
+ return;
592
+ }
593
+
594
+ rb_protect(update_process_rows_ensured, (VALUE)arg, &exception_state);
595
+ if (exception_state) {
596
+ report_ruby_error_to_duckdb(arg->info);
597
+ release_chunk_states(arg);
598
+ }
372
599
  }
373
600
 
374
601
  static void update_callback(duckdb_function_info info,
@@ -395,9 +622,9 @@ static void update_callback(duckdb_function_info info,
395
622
  arg.states = states;
396
623
  arg.input_vectors = NULL;
397
624
  arg.input_types = NULL;
398
- arg.args = NULL;
399
625
  arg.row_count = duckdb_data_chunk_get_size(input);
400
626
  arg.col_count = duckdb_data_chunk_get_column_count(input);
627
+ arg.constant_states = 0;
401
628
 
402
629
  rbduckdb_function_executor_dispatch(execute_update_callback_protected, &arg);
403
630
  }
@@ -437,8 +664,12 @@ static void execute_combine_callback_protected(void *user_data) {
437
664
  VALUE ret;
438
665
 
439
666
  one.combine_proc = arg->ctx->combine_proc;
440
- one.source_state = src[i]->ruby_state;
441
- one.target_state = tgt[i]->ruby_state;
667
+ one.source_state = state_registry_load(src[i]);
668
+ one.target_state = state_registry_load(tgt[i]);
669
+ if (one.source_state == Qundef || one.target_state == Qundef) {
670
+ report_missing_state_to_duckdb(arg->info, one.source_state == Qundef ? src[i] : tgt[i]);
671
+ return;
672
+ }
442
673
 
443
674
  ret = rb_protect(call_combine_proc, (VALUE)&one, &exception_state);
444
675
  if (exception_state) {
@@ -446,12 +677,13 @@ static void execute_combine_callback_protected(void *user_data) {
446
677
  return;
447
678
  }
448
679
 
449
- tgt[i]->ruby_state = ret;
450
680
  state_registry_store(tgt[i], ret);
451
681
 
452
- /* source state is consumed by combine; release its registry entry
453
- * so the Ruby VALUE can be GC'd. */
454
- state_registry_remove(src[i]);
682
+ /* The source entry is left in place: DuckDB reuses one source state
683
+ * across many combine calls (WindowSegmentTree does this for every
684
+ * frame), and the registry is now the only copy of the VALUE, so
685
+ * releasing it here would hand nil to the next combine. The entry is
686
+ * reclaimed by destroy_callback when DuckDB frees the state. */
455
687
  }
456
688
  }
457
689
 
@@ -529,7 +761,11 @@ static void execute_finalize_callback_protected(void *user_data) {
529
761
  VALUE ret;
530
762
 
531
763
  one.finalize_proc = arg->ctx->finalize_proc;
532
- one.ruby_state = state->ruby_state;
764
+ one.ruby_state = state_registry_load(state);
765
+ if (one.ruby_state == Qundef) {
766
+ report_missing_state_to_duckdb(arg->info, state);
767
+ goto cleanup;
768
+ }
533
769
 
534
770
  ret = rb_protect(call_finalize_proc, (VALUE)&one, &exception_state);
535
771
  if (exception_state) {
@@ -549,14 +785,14 @@ static void execute_finalize_callback_protected(void *user_data) {
549
785
  }
550
786
 
551
787
  /* Release Ruby state from the GC registry. */
552
- state_registry_remove(state);
788
+ state_release(state);
553
789
  }
554
790
 
555
791
  cleanup:
556
792
  /* Clean up registry entries for the current (failed) state and any
557
793
  remaining unprocessed states so we don't leak GC-registered objects. */
558
794
  for (; i < arg->count; i++) {
559
- state_registry_remove(states[i]);
795
+ state_release(states[i]);
560
796
  }
561
797
  duckdb_destroy_logical_type(&result_type);
562
798
  }
@@ -601,7 +837,7 @@ static void execute_destroy_callback(void *data) {
601
837
  ruby_aggregate_state **s = (ruby_aggregate_state **)arg->states;
602
838
  idx_t i;
603
839
  for (i = 0; i < arg->count; i++) {
604
- state_registry_remove(s[i]);
840
+ state_release(s[i]);
605
841
  }
606
842
  }
607
843
 
@@ -749,4 +985,6 @@ void rbduckdb_init_aggregate_function(void) {
749
985
 
750
986
  g_aggregate_state_registry = rb_hash_new();
751
987
  rb_gc_register_mark_object(g_aggregate_state_registry);
988
+ g_aggregate_state_addresses = rb_hash_new();
989
+ rb_gc_register_mark_object(g_aggregate_state_addresses);
752
990
  }
@@ -94,6 +94,8 @@ static VALUE appender_s_create_query(VALUE klass, VALUE con, VALUE query, VALUE
94
94
  idx_t column_count = 0;
95
95
  duckdb_logical_type *type_array = NULL;
96
96
  VALUE appender = Qnil;
97
+ VALUE column_name_strings = Qnil;
98
+ duckdb_state state;
97
99
 
98
100
  if (!rb_obj_is_kind_of(con, cDuckDBConnection)) {
99
101
  rb_raise(rb_eTypeError, "1st argument should be instance of DackDB::Connection");
@@ -116,17 +118,29 @@ static VALUE appender_s_create_query(VALUE klass, VALUE con, VALUE query, VALUE
116
118
  if (rb_obj_is_kind_of(columns, rb_cArray) == Qfalse) {
117
119
  rb_raise(rb_eTypeError, "4th argument should be an Array or nil");
118
120
  }
119
- idx_t col_count = RARRAY_LEN(columns);
120
- column_names = ALLOCA_N(const char *, (size_t)col_count);
121
- for (idx_t i = 0; i < col_count; i++) {
121
+ /* DuckDB reads column_count entries from column_names, so a shorter list
122
+ * makes it read uninitialized stack slots as column names. */
123
+ if ((idx_t)RARRAY_LEN(columns) != column_count) {
124
+ rb_raise(rb_eArgError, "column names size (%ld) must be the same as types size (%ld)",
125
+ RARRAY_LEN(columns), RARRAY_LEN(types));
126
+ }
127
+ /* Keep the converted Strings alive: StringValue on a to_str object
128
+ * returns a String that nothing else references. */
129
+ column_name_strings = rb_ary_new_capa((long)column_count);
130
+ column_names = ALLOCA_N(const char *, (size_t)column_count);
131
+ for (idx_t i = 0; i < column_count; i++) {
122
132
  VALUE col_name_val = rb_ary_entry(columns, i);
123
- column_names[i] = StringValuePtr(col_name_val);
133
+ StringValue(col_name_val);
134
+ rb_ary_push(column_name_strings, col_name_val);
135
+ column_names[i] = RSTRING_PTR(col_name_val);
124
136
  }
125
137
  }
126
138
  ctxcon = rbduckdb_get_struct_connection(con);
127
139
  appender = allocate(klass);
128
140
  TypedData_Get_Struct(appender, rubyDuckDBAppender, &appender_data_type, ctx);
129
- if (duckdb_appender_create_query(ctxcon->con, query_str, column_count, type_array, table_name, column_names, &ctx->appender) == DuckDBError) {
141
+ state = duckdb_appender_create_query(ctxcon->con, query_str, column_count, type_array, table_name, column_names, &ctx->appender);
142
+ RB_GC_GUARD(column_name_strings);
143
+ if (state == DuckDBError) {
130
144
  rb_raise(eDuckDBError, "failed to create appender from query");
131
145
  }
132
146
 
@@ -333,11 +347,13 @@ static VALUE appender__append_double(VALUE self, VALUE val) {
333
347
  /* :nodoc: */
334
348
  static VALUE appender__append_varchar(VALUE self, VALUE val) {
335
349
  rubyDuckDBAppender *ctx;
350
+ /* Length-aware, as duckdb_append_varchar would stop at an embedded NUL. */
336
351
  char *pval = StringValuePtr(val);
352
+ long len = RSTRING_LEN(val);
337
353
 
338
354
  TypedData_Get_Struct(self, rubyDuckDBAppender, &appender_data_type, ctx);
339
355
 
340
- return state_to_rbool(duckdb_append_varchar(ctx->appender, pval));
356
+ return state_to_rbool(duckdb_append_varchar_length(ctx->appender, pval, (idx_t)len));
341
357
  }
342
358
 
343
359
  /* :nodoc: */