duckdb 1.5.3.0 → 1.5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -0
  3. data/CLAUDE.md +4 -0
  4. data/README.md +52 -0
  5. data/duckdb.gemspec +1 -1
  6. data/ext/duckdb/aggregate_function.c +0 -1
  7. data/ext/duckdb/appender.c +17 -0
  8. data/ext/duckdb/arrow_array_stream.c +226 -0
  9. data/ext/duckdb/arrow_array_stream.h +61 -0
  10. data/ext/duckdb/arrow_import.c +165 -0
  11. data/ext/duckdb/arrow_import.h +6 -0
  12. data/ext/duckdb/blob.c +1 -1
  13. data/ext/duckdb/blob.h +1 -2
  14. data/ext/duckdb/config.c +1 -1
  15. data/ext/duckdb/config.h +1 -1
  16. data/ext/duckdb/connection.c +26 -4
  17. data/ext/duckdb/converter.h +1 -0
  18. data/ext/duckdb/conveter.c +39 -9
  19. data/ext/duckdb/data_chunk.c +10 -0
  20. data/ext/duckdb/data_chunk.h +1 -0
  21. data/ext/duckdb/duckdb.c +13 -11
  22. data/ext/duckdb/error.c +13 -1
  23. data/ext/duckdb/error.h +2 -3
  24. data/ext/duckdb/function_executor.c +308 -2
  25. data/ext/duckdb/function_executor.h +44 -0
  26. data/ext/duckdb/function_vector.c +14 -0
  27. data/ext/duckdb/prepared_statement.c +141 -5
  28. data/ext/duckdb/result.c +49 -3
  29. data/ext/duckdb/result.h +11 -0
  30. data/ext/duckdb/ruby-duckdb.h +3 -0
  31. data/ext/duckdb/scalar_function.c +97 -29
  32. data/ext/duckdb/scalar_function.h +2 -4
  33. data/ext/duckdb/scalar_function_bind_info.c +13 -13
  34. data/ext/duckdb/scalar_function_bind_info.h +1 -1
  35. data/ext/duckdb/scalar_function_set.c +9 -9
  36. data/ext/duckdb/scalar_function_set.h +2 -2
  37. data/ext/duckdb/table_description.c +19 -19
  38. data/ext/duckdb/table_description.h +1 -1
  39. data/ext/duckdb/table_function.c +94 -28
  40. data/ext/duckdb/table_function.h +2 -2
  41. data/ext/duckdb/table_function_bind_info.c +20 -20
  42. data/ext/duckdb/table_function_bind_info.h +2 -2
  43. data/ext/duckdb/table_function_function_info.c +5 -5
  44. data/ext/duckdb/table_function_function_info.h +2 -2
  45. data/ext/duckdb/table_function_init_info.c +70 -5
  46. data/ext/duckdb/table_function_init_info.h +2 -2
  47. data/ext/duckdb/util.c +8 -0
  48. data/ext/duckdb/util.h +1 -0
  49. data/ext/duckdb/value.c +526 -1
  50. data/lib/duckdb/appender.rb +23 -0
  51. data/lib/duckdb/arrow_array_stream.rb +33 -0
  52. data/lib/duckdb/connection.rb +73 -0
  53. data/lib/duckdb/converter/int_to_sym.rb +54 -1
  54. data/lib/duckdb/converter.rb +7 -7
  55. data/lib/duckdb/error.rb +26 -0
  56. data/lib/duckdb/function_type_validation.rb +1 -0
  57. data/lib/duckdb/logical_type.rb +6 -3
  58. data/lib/duckdb/prepared_statement.rb +32 -0
  59. data/lib/duckdb/scalar_function.rb +6 -6
  60. data/lib/duckdb/value.rb +400 -0
  61. data/lib/duckdb/version.rb +1 -1
  62. data/lib/duckdb.rb +2 -0
  63. metadata +10 -3
@@ -137,6 +137,20 @@ void rbduckdb_vector_set_value_at(duckdb_vector vector, duckdb_logical_type elem
137
137
  ((duckdb_time *)vector_data)[index] = time;
138
138
  break;
139
139
  }
140
+ case DUCKDB_TYPE_TIME_NS: {
141
+ if (!rb_obj_is_kind_of(value, rb_cTime)) {
142
+ rb_raise(rb_eTypeError, "Expected Time object for TIME_NS");
143
+ }
144
+
145
+ VALUE hour = rb_funcall(value, rb_intern("hour"), 0);
146
+ VALUE min = rb_funcall(value, rb_intern("min"), 0);
147
+ VALUE sec = rb_funcall(value, rb_intern("sec"), 0);
148
+ VALUE nsec = rb_funcall(value, rb_intern("nsec"), 0);
149
+
150
+ duckdb_time_ns time_ns = rbduckdb_to_duckdb_time_ns_from_value(hour, min, sec, nsec);
151
+ ((duckdb_time_ns *)vector_data)[index] = time_ns;
152
+ break;
153
+ }
140
154
  case DUCKDB_TYPE_TIME_TZ: {
141
155
  if (!rb_obj_is_kind_of(value, rb_cTime)) {
142
156
  rb_raise(rb_eTypeError, "Expected Time object for TIME_TZ");
@@ -14,6 +14,7 @@ static idx_t check_index(VALUE vidx);
14
14
 
15
15
  static VALUE prepared_statement_bind_parameter_index(VALUE self, VALUE name);
16
16
  static VALUE prepared_statement_parameter_name(VALUE self, VALUE vidx);
17
+ static VALUE prepared_statement_param_logical_type(VALUE self, VALUE vidx);
17
18
  static VALUE prepared_statement_clear_bindings(VALUE self);
18
19
  static VALUE prepared_statement_bind_bool(VALUE self, VALUE vidx, VALUE val);
19
20
  static VALUE prepared_statement_bind_int8(VALUE self, VALUE vidx, VALUE val);
@@ -27,6 +28,10 @@ static VALUE prepared_statement_bind_blob(VALUE self, VALUE vidx, VALUE blob);
27
28
  static VALUE prepared_statement_bind_null(VALUE self, VALUE vidx);
28
29
  static VALUE prepared_statement__statement_type(VALUE self);
29
30
  static VALUE prepared_statement__param_type(VALUE self, VALUE vidx);
31
+ static VALUE prepared_statement_column_count(VALUE self);
32
+ static VALUE prepared_statement_column_name(VALUE self, VALUE vidx);
33
+ static VALUE prepared_statement__column_type(VALUE self, VALUE vidx);
34
+ static VALUE prepared_statement_column_logical_type(VALUE self, VALUE vidx);
30
35
  static VALUE prepared_statement__bind_uint8(VALUE self, VALUE vidx, VALUE val);
31
36
  static VALUE prepared_statement__bind_uint16(VALUE self, VALUE vidx, VALUE val);
32
37
  static VALUE prepared_statement__bind_uint32(VALUE self, VALUE vidx, VALUE val);
@@ -38,6 +43,7 @@ static VALUE prepared_statement__bind_timestamp_tz(VALUE self, VALUE vidx, VALUE
38
43
  static VALUE prepared_statement__bind_interval(VALUE self, VALUE vidx, VALUE months, VALUE days, VALUE micros);
39
44
  static VALUE prepared_statement__bind_hugeint(VALUE self, VALUE vidx, VALUE lower, VALUE upper);
40
45
  static VALUE prepared_statement__bind_uhugeint(VALUE self, VALUE vidx, VALUE lower, VALUE upper);
46
+ static VALUE prepared_statement__bind_uuid(VALUE self, VALUE vidx, VALUE val);
41
47
  static VALUE prepared_statement__bind_decimal(VALUE self, VALUE vidx, VALUE lower, VALUE upper, VALUE width, VALUE scale);
42
48
  static VALUE prepared_statement__bind_value(VALUE self, VALUE vidx, VALUE val);
43
49
 
@@ -128,7 +134,6 @@ static VALUE prepared_statement_execute(VALUE self) {
128
134
  rubyDuckDBResult *ctxr;
129
135
  const char *error;
130
136
  VALUE result = rbduckdb_create_result();
131
- VALUE msg;
132
137
 
133
138
  TypedData_Get_Struct(self, rubyDuckDBPreparedStatement, &prepared_statement_data_type, ctx);
134
139
  ctxr = rbduckdb_get_struct_result(result);
@@ -145,11 +150,10 @@ static VALUE prepared_statement_execute(VALUE self) {
145
150
  if (state == DuckDBError) {
146
151
  error = duckdb_prepare_error(args.prepared_statement);
147
152
  if (error == NULL) {
148
- error = duckdb_result_error(args.out_result);
153
+ /* error originates from the result, which carries an error type */
154
+ rbduckdb_raise_result_error(args.out_result);
149
155
  }
150
- msg = rb_str_new2(error ? error : "Failed to execute prepared statement.");
151
-
152
- rb_raise(eDuckDBError, "%s", StringValuePtr(msg));
156
+ rb_raise(eDuckDBError, "%s", error);
153
157
  }
154
158
 
155
159
  return result;
@@ -205,6 +209,28 @@ static VALUE prepared_statement_parameter_name(VALUE self, VALUE vidx) {
205
209
  return vname;
206
210
  }
207
211
 
212
+ /*
213
+ * call-seq:
214
+ * prepared_statement.param_logical_type(param_index) -> DuckDB::LogicalType
215
+ *
216
+ * Returns the logical type of the parameter at the given (1-based) index.
217
+ * This provides richer type information than #param_type, e.g. decimal
218
+ * width/scale, nested types.
219
+ */
220
+ static VALUE prepared_statement_param_logical_type(VALUE self, VALUE vidx) {
221
+ rubyDuckDBPreparedStatement *ctx;
222
+ duckdb_logical_type logical_type;
223
+ idx_t idx = check_index(vidx);
224
+
225
+ TypedData_Get_Struct(self, rubyDuckDBPreparedStatement, &prepared_statement_data_type, ctx);
226
+
227
+ logical_type = duckdb_param_logical_type(ctx->prepared_statement, idx);
228
+ if (logical_type == NULL) {
229
+ rb_raise(eDuckDBError, "fail to get logical type of the parameter at %llu. parameter index is out of range.", (unsigned long long)idx);
230
+ }
231
+ return rbduckdb_create_logical_type(logical_type);
232
+ }
233
+
208
234
  /*
209
235
  * call-seq:
210
236
  * prepared_statement.clear_bindings -> DuckDB::PreparedStatement
@@ -359,6 +385,91 @@ static VALUE prepared_statement__statement_type(VALUE self) {
359
385
  return INT2FIX(duckdb_prepared_statement_type(ctx->prepared_statement));
360
386
  }
361
387
 
388
+ /*
389
+ * call-seq:
390
+ * prepared_statement.column_count -> Integer
391
+ *
392
+ * Returns the number of columns in the result set of the prepared statement
393
+ * without executing it.
394
+ *
395
+ * require 'duckdb'
396
+ * db = DuckDB::Database.open
397
+ * con = db.connect
398
+ * stmt = con.prepared_statement('SELECT 1 AS a, 2 AS b')
399
+ * stmt.column_count # => 2
400
+ */
401
+ static VALUE prepared_statement_column_count(VALUE self) {
402
+ rubyDuckDBPreparedStatement *ctx;
403
+ TypedData_Get_Struct(self, rubyDuckDBPreparedStatement, &prepared_statement_data_type, ctx);
404
+ return ULL2NUM(duckdb_prepared_statement_column_count(ctx->prepared_statement));
405
+ }
406
+
407
+ /*
408
+ * call-seq:
409
+ * prepared_statement.column_name(col_index) -> String
410
+ *
411
+ * Returns the name of the column at the specified index (0-based) of the
412
+ * result set of the prepared statement without executing it.
413
+ * Raises DuckDB::Error if the column index is out of range.
414
+ *
415
+ * require 'duckdb'
416
+ * db = DuckDB::Database.open
417
+ * con = db.connect
418
+ * stmt = con.prepared_statement('SELECT 1 AS a, 2 AS b')
419
+ * stmt.column_name(1) # => 'b'
420
+ */
421
+ static VALUE prepared_statement_column_name(VALUE self, VALUE vidx) {
422
+ rubyDuckDBPreparedStatement *ctx;
423
+ VALUE vname;
424
+ const char *name;
425
+ idx_t idx = NUM2ULL(vidx);
426
+
427
+ TypedData_Get_Struct(self, rubyDuckDBPreparedStatement, &prepared_statement_data_type, ctx);
428
+
429
+ name = duckdb_prepared_statement_column_name(ctx->prepared_statement, idx);
430
+ if (name == NULL) {
431
+ rb_raise(eDuckDBError, "fail to get column name at %llu. column index is out of range.", (unsigned long long)idx);
432
+ }
433
+ vname = rb_str_new2(name);
434
+ duckdb_free((void *)name);
435
+ return vname;
436
+ }
437
+
438
+ /* :nodoc: */
439
+ static VALUE prepared_statement__column_type(VALUE self, VALUE vidx) {
440
+ rubyDuckDBPreparedStatement *ctx;
441
+ TypedData_Get_Struct(self, rubyDuckDBPreparedStatement, &prepared_statement_data_type, ctx);
442
+ return INT2FIX(duckdb_prepared_statement_column_type(ctx->prepared_statement, NUM2ULL(vidx)));
443
+ }
444
+
445
+ /*
446
+ * call-seq:
447
+ * prepared_statement.column_logical_type(col_index) -> DuckDB::LogicalType
448
+ *
449
+ * Returns the logical type of the column at the specified index (0-based) of
450
+ * the result set of the prepared statement without executing it.
451
+ * Raises DuckDB::Error if the column index is out of range.
452
+ *
453
+ * require 'duckdb'
454
+ * db = DuckDB::Database.open
455
+ * con = db.connect
456
+ * stmt = con.prepared_statement('SELECT 1.5::DECIMAL(9, 4) AS a')
457
+ * stmt.column_logical_type(0).type # => :decimal
458
+ */
459
+ static VALUE prepared_statement_column_logical_type(VALUE self, VALUE vidx) {
460
+ rubyDuckDBPreparedStatement *ctx;
461
+ duckdb_logical_type logical_type;
462
+ idx_t idx = NUM2ULL(vidx);
463
+
464
+ TypedData_Get_Struct(self, rubyDuckDBPreparedStatement, &prepared_statement_data_type, ctx);
465
+
466
+ logical_type = duckdb_prepared_statement_column_logical_type(ctx->prepared_statement, idx);
467
+ if (logical_type == NULL) {
468
+ rb_raise(eDuckDBError, "fail to get column logical type at %llu. column index is out of range.", (unsigned long long)idx);
469
+ }
470
+ return rbduckdb_create_logical_type(logical_type);
471
+ }
472
+
362
473
  /* :nodoc: */
363
474
  static VALUE prepared_statement__param_type(VALUE self, VALUE vidx) {
364
475
  rubyDuckDBPreparedStatement *ctx;
@@ -558,6 +669,25 @@ static VALUE prepared_statement__bind_decimal(VALUE self, VALUE vidx, VALUE lowe
558
669
  return self;
559
670
  }
560
671
 
672
+ /* :nodoc: */
673
+ static VALUE prepared_statement__bind_uuid(VALUE self, VALUE vidx, VALUE val) {
674
+ rubyDuckDBPreparedStatement *ctx;
675
+ duckdb_uhugeint uhugeint;
676
+ duckdb_value uuid_val;
677
+ duckdb_state state;
678
+ idx_t idx = check_index(vidx);
679
+
680
+ TypedData_Get_Struct(self, rubyDuckDBPreparedStatement, &prepared_statement_data_type, ctx);
681
+ rbduckdb_uuid_str_to_uhugeint(val, &uhugeint);
682
+ uuid_val = duckdb_create_uuid(uhugeint);
683
+ state = duckdb_bind_value(ctx->prepared_statement, idx, uuid_val);
684
+ duckdb_destroy_value(&uuid_val);
685
+ if (state == DuckDBError) {
686
+ rb_raise(eDuckDBError, "fail to bind %llu parameter", (unsigned long long)idx);
687
+ }
688
+ return self;
689
+ }
690
+
561
691
  /* :nodoc: */
562
692
  static VALUE prepared_statement__bind_value(VALUE self, VALUE vidx, VALUE val) {
563
693
  rubyDuckDBPreparedStatement *ctx;
@@ -593,7 +723,11 @@ void rbduckdb_init_prepared_statement(void) {
593
723
  rb_define_method(cDuckDBPreparedStatement, "nparams", prepared_statement_nparams, 0);
594
724
  rb_define_method(cDuckDBPreparedStatement, "bind_parameter_index", prepared_statement_bind_parameter_index, 1);
595
725
  rb_define_method(cDuckDBPreparedStatement, "parameter_name", prepared_statement_parameter_name, 1);
726
+ rb_define_method(cDuckDBPreparedStatement, "param_logical_type", prepared_statement_param_logical_type, 1);
596
727
  rb_define_method(cDuckDBPreparedStatement, "clear_bindings", prepared_statement_clear_bindings, 0);
728
+ rb_define_method(cDuckDBPreparedStatement, "column_count", prepared_statement_column_count, 0);
729
+ rb_define_method(cDuckDBPreparedStatement, "column_name", prepared_statement_column_name, 1);
730
+ rb_define_method(cDuckDBPreparedStatement, "column_logical_type", prepared_statement_column_logical_type, 1);
597
731
  rb_define_method(cDuckDBPreparedStatement, "bind_bool", prepared_statement_bind_bool, 2);
598
732
  rb_define_method(cDuckDBPreparedStatement, "bind_int8", prepared_statement_bind_int8, 2);
599
733
  rb_define_method(cDuckDBPreparedStatement, "bind_int16", prepared_statement_bind_int16, 2);
@@ -610,6 +744,7 @@ void rbduckdb_init_prepared_statement(void) {
610
744
  rb_define_private_method(cDuckDBPreparedStatement, "_bind_uint64", prepared_statement__bind_uint64, 2);
611
745
  rb_define_private_method(cDuckDBPreparedStatement, "_statement_type", prepared_statement__statement_type, 0);
612
746
  rb_define_private_method(cDuckDBPreparedStatement, "_param_type", prepared_statement__param_type, 1);
747
+ rb_define_private_method(cDuckDBPreparedStatement, "_column_type", prepared_statement__column_type, 1);
613
748
  rb_define_private_method(cDuckDBPreparedStatement, "_bind_date", prepared_statement__bind_date, 4);
614
749
  rb_define_private_method(cDuckDBPreparedStatement, "_bind_time", prepared_statement__bind_time, 5);
615
750
  rb_define_private_method(cDuckDBPreparedStatement, "_bind_timestamp", prepared_statement__bind_timestamp, 8);
@@ -617,6 +752,7 @@ void rbduckdb_init_prepared_statement(void) {
617
752
  rb_define_private_method(cDuckDBPreparedStatement, "_bind_interval", prepared_statement__bind_interval, 4);
618
753
  rb_define_private_method(cDuckDBPreparedStatement, "_bind_hugeint", prepared_statement__bind_hugeint, 3);
619
754
  rb_define_private_method(cDuckDBPreparedStatement, "_bind_uhugeint", prepared_statement__bind_uhugeint, 3);
755
+ rb_define_private_method(cDuckDBPreparedStatement, "_bind_uuid", prepared_statement__bind_uuid, 2);
620
756
  rb_define_private_method(cDuckDBPreparedStatement, "_bind_decimal", prepared_statement__bind_decimal, 5);
621
757
  rb_define_private_method(cDuckDBPreparedStatement, "_bind_value", prepared_statement__bind_value, 2);
622
758
  }
data/ext/duckdb/result.c CHANGED
@@ -16,6 +16,7 @@ static VALUE result_columns(VALUE oDuckDBResult);
16
16
  static VALUE destroy_data_chunk(VALUE arg);
17
17
 
18
18
  static VALUE result__chunk_stream(VALUE oDuckDBResult);
19
+ static VALUE result_arrow_c_stream(VALUE oDuckDBResult);
19
20
  static VALUE yield_rows(VALUE arg);
20
21
  static VALUE result__return_type(VALUE oDuckDBResult);
21
22
  static VALUE result__statement_type(VALUE oDuckDBResult);
@@ -56,15 +57,30 @@ static const rb_data_type_t result_data_type = {
56
57
  static void deallocate(void *ctx) {
57
58
  rubyDuckDBResult *p = (rubyDuckDBResult *)ctx;
58
59
 
59
- duckdb_destroy_result(&(p->result));
60
- xfree(p);
60
+ rbduckdb_result_unref(p);
61
61
  }
62
62
 
63
63
  static VALUE allocate(VALUE klass) {
64
- rubyDuckDBResult *ctx = xcalloc((size_t)1, sizeof(rubyDuckDBResult));
64
+ rubyDuckDBResult *ctx = calloc((size_t)1, sizeof(rubyDuckDBResult));
65
+
66
+ if (ctx == NULL) {
67
+ rb_raise(rb_eNoMemError, "failed to allocate DuckDB::Result");
68
+ }
69
+ ctx->refcount = 1;
65
70
  return TypedData_Wrap_Struct(klass, &result_data_type, ctx);
66
71
  }
67
72
 
73
+ void rbduckdb_result_ref(rubyDuckDBResult *ctx) {
74
+ RUBY_ATOMIC_FETCH_ADD(ctx->refcount, 1);
75
+ }
76
+
77
+ void rbduckdb_result_unref(rubyDuckDBResult *ctx) {
78
+ if (RUBY_ATOMIC_FETCH_SUB(ctx->refcount, 1) == 1) {
79
+ duckdb_destroy_result(&(ctx->result));
80
+ free(ctx);
81
+ }
82
+ }
83
+
68
84
  static size_t memsize(const void *p) {
69
85
  return sizeof(rubyDuckDBResult);
70
86
  }
@@ -647,6 +663,35 @@ static VALUE vector_uuid(void* vector_data, idx_t row_idx) {
647
663
  return rbduckdb_uuid_to_ruby(((duckdb_hugeint *)vector_data)[row_idx]);
648
664
  }
649
665
 
666
+ /*
667
+ * call-seq:
668
+ * result.arrow_c_stream -> DuckDB::ArrowArrayStream
669
+ *
670
+ * [EXPERIMENTAL] Exports the result as an Arrow C stream
671
+ * (Arrow C Data Interface). The returned stream object satisfies the
672
+ * Ruby Arrow C stream protocol, so it can be consumed directly by
673
+ * ruby-polars, red-arrow and other Arrow consumers:
674
+ *
675
+ * result = con.query('SELECT * FROM users')
676
+ * df = Polars::DataFrame.new(result)
677
+ *
678
+ * The stream consumes the result's chunks; a result can be exported
679
+ * only once. This API is built on DuckDB's unstable Arrow C API and
680
+ * may change in any minor release.
681
+ */
682
+ static VALUE result_arrow_c_stream(VALUE oDuckDBResult) {
683
+ rubyDuckDBResult *ctx;
684
+ VALUE stream;
685
+
686
+ TypedData_Get_Struct(oDuckDBResult, rubyDuckDBResult, &result_data_type, ctx);
687
+ if (ctx->arrow_exported) {
688
+ rb_raise(eDuckDBError, "result is already exported as an Arrow stream");
689
+ }
690
+ stream = rbduckdb_create_arrow_array_stream(oDuckDBResult);
691
+ ctx->arrow_exported = true;
692
+ return stream;
693
+ }
694
+
650
695
  static VALUE vector_value(duckdb_vector vector, idx_t row_idx) {
651
696
  duckdb_logical_type ty;
652
697
  VALUE obj = Qnil;
@@ -671,6 +716,7 @@ void rbduckdb_init_result(void) {
671
716
  rb_define_method(cDuckDBResult, "rows_changed", result_rows_changed, 0);
672
717
  rb_define_method(cDuckDBResult, "columns", result_columns, 0);
673
718
  rb_define_private_method(cDuckDBResult, "_chunk_stream", result__chunk_stream, 0);
719
+ rb_define_method(cDuckDBResult, "arrow_c_stream", result_arrow_c_stream, 0);
674
720
  rb_define_private_method(cDuckDBResult, "_return_type", result__return_type, 0);
675
721
  rb_define_private_method(cDuckDBResult, "_statement_type", result__statement_type, 0);
676
722
 
data/ext/duckdb/result.h CHANGED
@@ -1,13 +1,24 @@
1
1
  #ifndef RUBY_DUCKDB_RESULT_H
2
2
  #define RUBY_DUCKDB_RESULT_H
3
3
 
4
+ /*
5
+ * Allocated with plain calloc/free and reference-counted: the Ruby Result
6
+ * object holds one reference, and each exported Arrow stream holds another,
7
+ * so the duckdb_result stays valid for consumers that outlive the Ruby
8
+ * objects. rbduckdb_result_unref() must not call any Ruby API: it runs from
9
+ * GC sweep (deallocate) and from Arrow stream release callbacks.
10
+ */
4
11
  struct _rubyDuckDBResult {
5
12
  duckdb_result result;
13
+ bool arrow_exported;
14
+ rb_atomic_t refcount;
6
15
  };
7
16
 
8
17
  typedef struct _rubyDuckDBResult rubyDuckDBResult;
9
18
 
10
19
  rubyDuckDBResult *rbduckdb_get_struct_result(VALUE obj);
20
+ void rbduckdb_result_ref(rubyDuckDBResult *ctx);
21
+ void rbduckdb_result_unref(rubyDuckDBResult *ctx);
11
22
  void rbduckdb_init_result(void);
12
23
  VALUE rbduckdb_create_result(void);
13
24
  VALUE rbduckdb_vector_value_at(duckdb_vector vector, duckdb_logical_type element_type, idx_t index);
@@ -6,6 +6,7 @@
6
6
 
7
7
  #include "ruby.h"
8
8
  #include "ruby/thread.h"
9
+ #include "ruby/atomic.h"
9
10
  #include <duckdb.h>
10
11
 
11
12
  #ifdef HAVE_DUCKDB_UNSAFE_VECTOR_ASSIGN_STRING_ELEMENT_LEN
@@ -50,6 +51,8 @@
50
51
  #include "./memory_helper.h"
51
52
  #include "./table_function.h"
52
53
  #include "./table_description.h"
54
+ #include "./arrow_array_stream.h"
55
+ #include "./arrow_import.h"
53
56
 
54
57
  extern VALUE mDuckDB;
55
58
  extern VALUE cDuckDBDatabase;
@@ -7,16 +7,24 @@ static void deallocate(void *);
7
7
  static VALUE allocate(VALUE klass);
8
8
  static size_t memsize(const void *p);
9
9
  static void compact(void *);
10
- static VALUE duckdb_scalar_function_initialize(VALUE self);
11
- static VALUE rbduckdb_scalar_function_set_name(VALUE self, VALUE name);
12
- static VALUE rbduckdb_scalar_function__set_return_type(VALUE self, VALUE logical_type);
13
- static VALUE rbduckdb_scalar_function__set_varargs(VALUE self, VALUE logical_type);
14
- static VALUE rbduckdb_scalar_function_add_parameter(VALUE self, VALUE logical_type);
15
- static VALUE rbduckdb_scalar_function__set_special_handling(VALUE self);
16
- static VALUE rbduckdb_scalar_function_set_function(VALUE self);
17
- static VALUE rbduckdb_scalar_function__set_bind(VALUE self);
10
+ static VALUE scalar_function_initialize(VALUE self);
11
+ static VALUE scalar_function_set_name(VALUE self, VALUE name);
12
+ static VALUE scalar_function__set_return_type(VALUE self, VALUE logical_type);
13
+ static VALUE scalar_function__set_varargs(VALUE self, VALUE logical_type);
14
+ static VALUE scalar_function__add_parameter(VALUE self, VALUE logical_type);
15
+ static VALUE scalar_function__set_special_handling(VALUE self);
16
+ static VALUE scalar_function_set_function(VALUE self);
17
+ static VALUE scalar_function__set_bind(VALUE self);
18
18
  static void scalar_function_callback(duckdb_function_info info, duckdb_data_chunk input, duckdb_vector output);
19
19
  static void scalar_function_bind_callback(duckdb_bind_info info);
20
+ #ifdef HAVE_DUCKDB_H_GE_V1_5_0
21
+ /*
22
+ * Thread detection functions (available since Ruby 2.3).
23
+ * Used to skip the proxy on Ruby threads.
24
+ */
25
+ extern int ruby_native_thread_p(void);
26
+ static void scalar_function_init_callback(duckdb_init_info info);
27
+ #endif
20
28
 
21
29
 
22
30
  struct callback_arg {
@@ -115,7 +123,7 @@ static size_t memsize(const void *p) {
115
123
  return sizeof(rubyDuckDBScalarFunction);
116
124
  }
117
125
 
118
- static VALUE duckdb_scalar_function_initialize(VALUE self) {
126
+ static VALUE scalar_function_initialize(VALUE self) {
119
127
  rubyDuckDBScalarFunction *p;
120
128
  TypedData_Get_Struct(self, rubyDuckDBScalarFunction, &scalar_function_data_type, p);
121
129
  p->scalar_function = duckdb_create_scalar_function();
@@ -124,7 +132,7 @@ static VALUE duckdb_scalar_function_initialize(VALUE self) {
124
132
  return self;
125
133
  }
126
134
 
127
- static VALUE rbduckdb_scalar_function_set_name(VALUE self, VALUE name) {
135
+ static VALUE scalar_function_set_name(VALUE self, VALUE name) {
128
136
  rubyDuckDBScalarFunction *p;
129
137
  TypedData_Get_Struct(self, rubyDuckDBScalarFunction, &scalar_function_data_type, p);
130
138
 
@@ -134,7 +142,7 @@ static VALUE rbduckdb_scalar_function_set_name(VALUE self, VALUE name) {
134
142
  return self;
135
143
  }
136
144
 
137
- static VALUE rbduckdb_scalar_function__set_return_type(VALUE self, VALUE logical_type) {
145
+ static VALUE scalar_function__set_return_type(VALUE self, VALUE logical_type) {
138
146
  rubyDuckDBScalarFunction *p;
139
147
  rubyDuckDBLogicalType *lt;
140
148
 
@@ -146,7 +154,7 @@ static VALUE rbduckdb_scalar_function__set_return_type(VALUE self, VALUE logical
146
154
  return self;
147
155
  }
148
156
 
149
- static VALUE rbduckdb_scalar_function__set_varargs(VALUE self, VALUE logical_type) {
157
+ static VALUE scalar_function__set_varargs(VALUE self, VALUE logical_type) {
150
158
  rubyDuckDBScalarFunction *p;
151
159
  rubyDuckDBLogicalType *lt;
152
160
 
@@ -158,7 +166,7 @@ static VALUE rbduckdb_scalar_function__set_varargs(VALUE self, VALUE logical_typ
158
166
  return self;
159
167
  }
160
168
 
161
- static VALUE rbduckdb_scalar_function__set_special_handling(VALUE self) {
169
+ static VALUE scalar_function__set_special_handling(VALUE self) {
162
170
  rubyDuckDBScalarFunction *p;
163
171
 
164
172
  TypedData_Get_Struct(self, rubyDuckDBScalarFunction, &scalar_function_data_type, p);
@@ -167,7 +175,7 @@ static VALUE rbduckdb_scalar_function__set_special_handling(VALUE self) {
167
175
  return self;
168
176
  }
169
177
 
170
- static VALUE rbduckdb_scalar_function_add_parameter(VALUE self, VALUE logical_type) {
178
+ static VALUE scalar_function__add_parameter(VALUE self, VALUE logical_type) {
171
179
  rubyDuckDBScalarFunction *p;
172
180
  rubyDuckDBLogicalType *lt;
173
181
 
@@ -191,6 +199,7 @@ static void scalar_function_callback(duckdb_function_info info, duckdb_data_chun
191
199
  rubyDuckDBScalarFunction *ctx;
192
200
  idx_t i;
193
201
  struct callback_arg arg;
202
+ struct worker_proxy *proxy = NULL;
194
203
 
195
204
  ctx = (rubyDuckDBScalarFunction *)duckdb_scalar_function_get_extra_info(info);
196
205
 
@@ -218,7 +227,11 @@ static void scalar_function_callback(duckdb_function_info info, duckdb_data_chun
218
227
  arg.row_count = duckdb_data_chunk_get_size(input);
219
228
  arg.col_count = duckdb_data_chunk_get_column_count(input);
220
229
 
221
- rbduckdb_function_executor_dispatch(execute_callback_protected, &arg);
230
+ #ifdef HAVE_DUCKDB_H_GE_V1_5_0
231
+ /* On DuckDB >= 1.5.0 each worker thread carries its own proxy (see init). */
232
+ proxy = (struct worker_proxy *)duckdb_scalar_function_get_state(info);
233
+ #endif
234
+ rbduckdb_function_executor_dispatch_via_proxy(execute_callback_protected, &arg, proxy);
222
235
  }
223
236
 
224
237
  static VALUE process_no_param_rows(VALUE varg) {
@@ -259,7 +272,7 @@ static VALUE process_rows(VALUE varg) {
259
272
  }
260
273
 
261
274
  /* Call the Ruby block with the arguments */
262
- result = rb_funcallv(arg->ctx->function_proc, rb_intern("call"), arg->col_count, arg->args);
275
+ result = rb_funcallv(arg->ctx->function_proc, rb_intern("call"), (int)arg->col_count, arg->args);
263
276
 
264
277
  /* Write result to output using helper function */
265
278
  rbduckdb_vector_set_value_at(arg->output, arg->output_type, i, result);
@@ -294,14 +307,66 @@ static VALUE cleanup_callback(VALUE varg) {
294
307
  return Qnil;
295
308
  }
296
309
 
297
- rubyDuckDBScalarFunction *get_struct_scalar_function(VALUE obj) {
310
+ #ifdef HAVE_DUCKDB_H_GE_V1_5_0
311
+ /*
312
+ * Per-worker init for the execute path (DuckDB >= 1.5.0).
313
+ *
314
+ * DuckDB calls this once on each worker thread that will run the execute
315
+ * callback. We create a per-worker proxy (allocating its Ruby thread under the
316
+ * GVL via the global executor, since this runs on a non-Ruby thread) and store
317
+ * it as per-worker state. The execute callback then dispatches through it
318
+ * instead of the shared global executor, so workers run callbacks concurrently.
319
+ * DuckDB invokes rbduckdb_worker_proxy_destroy when the state is freed.
320
+ */
321
+ struct create_proxy_callback_arg {
322
+ struct worker_proxy *proxy;
323
+ };
324
+
325
+ static VALUE create_proxy_callback(VALUE varg) {
326
+ struct create_proxy_callback_arg *arg = (struct create_proxy_callback_arg *)varg;
327
+ arg->proxy = rbduckdb_worker_proxy_create();
328
+ return Qnil;
329
+ }
330
+
331
+ /*
332
+ * rbduckdb_worker_proxy_create may raise (NoMemError, Thread.new failure),
333
+ * and the executor runs callbacks unprotected — a raise would longjmp past
334
+ * its done-signaling and block the waiting DuckDB worker forever. Swallow
335
+ * the exception instead: the proxy stays NULL, init sets no state, and the
336
+ * execute callback falls back to the global executor.
337
+ */
338
+ static void create_proxy_callback_protected(void *user_data) {
339
+ int exception_state;
340
+
341
+ rb_protect(create_proxy_callback, (VALUE)user_data, &exception_state);
342
+ if (exception_state) {
343
+ rb_set_errinfo(Qnil);
344
+ }
345
+ }
346
+
347
+ static void scalar_function_init_callback(duckdb_init_info info) {
348
+ struct create_proxy_callback_arg arg;
349
+
350
+ /* A Ruby calling thread runs the callback inline (Case 1/2); no proxy needed. */
351
+ if (ruby_native_thread_p()) return;
352
+
353
+ arg.proxy = NULL;
354
+ rbduckdb_function_executor_dispatch(create_proxy_callback_protected, &arg);
355
+
356
+ if (arg.proxy != NULL) {
357
+ duckdb_scalar_function_init_set_state(info, arg.proxy, rbduckdb_worker_proxy_destroy);
358
+ }
359
+ }
360
+ #endif
361
+
362
+ rubyDuckDBScalarFunction *rbduckdb_get_struct_scalar_function(VALUE obj) {
298
363
  rubyDuckDBScalarFunction *ctx;
299
364
  TypedData_Get_Struct(obj, rubyDuckDBScalarFunction, &scalar_function_data_type, ctx);
300
365
  return ctx;
301
366
  }
302
367
 
303
368
  /* :nodoc: */
304
- static VALUE rbduckdb_scalar_function_set_function(VALUE self) {
369
+ static VALUE scalar_function_set_function(VALUE self) {
305
370
  rubyDuckDBScalarFunction *p;
306
371
 
307
372
  if (!rb_block_given_p()) {
@@ -314,6 +379,10 @@ static VALUE rbduckdb_scalar_function_set_function(VALUE self) {
314
379
 
315
380
  duckdb_scalar_function_set_extra_info(p->scalar_function, p, NULL);
316
381
  duckdb_scalar_function_set_function(p->scalar_function, scalar_function_callback);
382
+ #ifdef HAVE_DUCKDB_H_GE_V1_5_0
383
+ /* Per-worker proxy threads for the execute path (DuckDB >= 1.5.0). */
384
+ duckdb_scalar_function_set_init(p->scalar_function, scalar_function_init_callback);
385
+ #endif
317
386
 
318
387
  /*
319
388
  * Mark as volatile to prevent constant folding during query optimization.
@@ -328,7 +397,7 @@ static VALUE rbduckdb_scalar_function_set_function(VALUE self) {
328
397
  }
329
398
 
330
399
  /* :nodoc: */
331
- static VALUE rbduckdb_scalar_function__set_bind(VALUE self) {
400
+ static VALUE scalar_function__set_bind(VALUE self) {
332
401
  rubyDuckDBScalarFunction *p;
333
402
 
334
403
  TypedData_Get_Struct(self, rubyDuckDBScalarFunction, &scalar_function_data_type, p);
@@ -406,19 +475,18 @@ static void scalar_function_bind_callback(duckdb_bind_info info) {
406
475
  }
407
476
 
408
477
 
409
- void rbduckdb_init_duckdb_scalar_function(void) {
478
+ void rbduckdb_init_scalar_function(void) {
410
479
  #if 0
411
480
  VALUE mDuckDB = rb_define_module("DuckDB");
412
481
  #endif
413
482
  cDuckDBScalarFunction = rb_define_class_under(mDuckDB, "ScalarFunction", rb_cObject);
414
483
  rb_define_alloc_func(cDuckDBScalarFunction, allocate);
415
- rb_define_method(cDuckDBScalarFunction, "initialize", duckdb_scalar_function_initialize, 0);
416
- rb_define_method(cDuckDBScalarFunction, "set_name", rbduckdb_scalar_function_set_name, 1);
417
- rb_define_method(cDuckDBScalarFunction, "name=", rbduckdb_scalar_function_set_name, 1);
418
- rb_define_private_method(cDuckDBScalarFunction, "_set_return_type", rbduckdb_scalar_function__set_return_type, 1);
419
- rb_define_private_method(cDuckDBScalarFunction, "_set_varargs", rbduckdb_scalar_function__set_varargs, 1);
420
- rb_define_private_method(cDuckDBScalarFunction, "_set_special_handling", rbduckdb_scalar_function__set_special_handling, 0);
421
- rb_define_private_method(cDuckDBScalarFunction, "_add_parameter", rbduckdb_scalar_function_add_parameter, 1);
422
- rb_define_method(cDuckDBScalarFunction, "set_function", rbduckdb_scalar_function_set_function, 0);
423
- rb_define_private_method(cDuckDBScalarFunction, "_set_bind", rbduckdb_scalar_function__set_bind, 0);
484
+ rb_define_method(cDuckDBScalarFunction, "initialize", scalar_function_initialize, 0);
485
+ rb_define_method(cDuckDBScalarFunction, "set_name", scalar_function_set_name, 1);
486
+ rb_define_private_method(cDuckDBScalarFunction, "_set_return_type", scalar_function__set_return_type, 1);
487
+ rb_define_private_method(cDuckDBScalarFunction, "_set_varargs", scalar_function__set_varargs, 1);
488
+ rb_define_private_method(cDuckDBScalarFunction, "_set_special_handling", scalar_function__set_special_handling, 0);
489
+ rb_define_private_method(cDuckDBScalarFunction, "_add_parameter", scalar_function__add_parameter, 1);
490
+ rb_define_method(cDuckDBScalarFunction, "set_function", scalar_function_set_function, 0);
491
+ rb_define_private_method(cDuckDBScalarFunction, "_set_bind", scalar_function__set_bind, 0);
424
492
  }
@@ -9,9 +9,7 @@ struct _rubyDuckDBScalarFunction {
9
9
 
10
10
  typedef struct _rubyDuckDBScalarFunction rubyDuckDBScalarFunction;
11
11
 
12
- void rbduckdb_init_duckdb_scalar_function(void);
13
- rubyDuckDBScalarFunction *get_struct_scalar_function(VALUE obj);
12
+ void rbduckdb_init_scalar_function(void);
13
+ rubyDuckDBScalarFunction *rbduckdb_get_struct_scalar_function(VALUE obj);
14
14
 
15
15
  #endif
16
-
17
-