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 +4 -4
- data/CHANGELOG.md +14 -0
- data/ext/duckdb/aggregate_function.c +291 -53
- data/ext/duckdb/appender.c +22 -6
- data/ext/duckdb/connection.c +35 -20
- data/ext/duckdb/connection.h +2 -1
- data/ext/duckdb/database.c +68 -3
- data/ext/duckdb/database.h +12 -0
- data/ext/duckdb/function_executor.c +166 -32
- data/ext/duckdb/function_executor.h +18 -6
- data/ext/duckdb/instance_cache.c +76 -2
- data/ext/duckdb/instance_cache.h +6 -0
- data/ext/duckdb/prepared_statement.c +8 -1
- data/ext/duckdb/scalar_function.c +13 -20
- data/ext/duckdb/table_function.c +9 -14
- data/ext/duckdb/table_function_bind_info.c +28 -0
- data/ext/duckdb/table_function_bind_info.h +1 -0
- data/ext/duckdb/table_function_function_info.c +22 -0
- data/ext/duckdb/table_function_init_info.c +19 -0
- data/lib/duckdb/appender.rb +1 -2
- data/lib/duckdb/converter/int_to_sym.rb +3 -0
- data/lib/duckdb/converter.rb +1 -2
- data/lib/duckdb/data_chunk.rb +1 -2
- data/lib/duckdb/prepared_statement.rb +1 -2
- data/lib/duckdb/table_function/function_info.rb +1 -2
- data/lib/duckdb/table_function/init_info.rb +1 -2
- data/lib/duckdb/table_function.rb +1 -2
- data/lib/duckdb/vector.rb +1 -2
- data/lib/duckdb/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: caab56325522ea94df9e9cdc12c54b20f2335c2a8d93df0d1fce965643e78cf0
|
|
4
|
+
data.tar.gz: a572bfa7d8f4ff0dfac85c34ace463b272446e1de7f3fad6bd790de055f25787
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
/*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
441
|
-
one.target_state = tgt[i]
|
|
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
|
|
453
|
-
*
|
|
454
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
data/ext/duckdb/appender.c
CHANGED
|
@@ -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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
356
|
+
return state_to_rbool(duckdb_append_varchar_length(ctx->appender, pval, (idx_t)len));
|
|
341
357
|
}
|
|
342
358
|
|
|
343
359
|
/* :nodoc: */
|