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