duckdb 1.4.2.0 → 1.4.4.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/.github/copilot-instructions.md +169 -0
  3. data/.github/workflows/linter.yml +30 -0
  4. data/.github/workflows/test_on_macos.yml +7 -2
  5. data/.github/workflows/test_on_ubuntu.yml +7 -2
  6. data/.github/workflows/test_on_windows.yml +8 -3
  7. data/.rubocop.yml +34 -0
  8. data/CHANGELOG.md +74 -0
  9. data/Dockerfile +2 -2
  10. data/Gemfile +12 -0
  11. data/Gemfile.lock +70 -7
  12. data/Rakefile +8 -6
  13. data/bin/console +4 -3
  14. data/duckdb.gemspec +2 -6
  15. data/ext/duckdb/bind_info.c +243 -0
  16. data/ext/duckdb/bind_info.h +13 -0
  17. data/ext/duckdb/connection.c +60 -2
  18. data/ext/duckdb/connection.h +1 -0
  19. data/ext/duckdb/data_chunk.c +137 -0
  20. data/ext/duckdb/data_chunk.h +13 -0
  21. data/ext/duckdb/duckdb.c +7 -0
  22. data/ext/duckdb/function_info.c +65 -0
  23. data/ext/duckdb/function_info.h +13 -0
  24. data/ext/duckdb/init_info.c +65 -0
  25. data/ext/duckdb/init_info.h +13 -0
  26. data/ext/duckdb/logical_type.c +42 -0
  27. data/ext/duckdb/memory_helper.c +339 -0
  28. data/ext/duckdb/memory_helper.h +8 -0
  29. data/ext/duckdb/result.c +8 -9
  30. data/ext/duckdb/result.h +1 -0
  31. data/ext/duckdb/ruby-duckdb.h +7 -0
  32. data/ext/duckdb/scalar_function.c +336 -1
  33. data/ext/duckdb/scalar_function.h +2 -0
  34. data/ext/duckdb/table_function.c +403 -0
  35. data/ext/duckdb/table_function.h +16 -0
  36. data/ext/duckdb/vector.c +201 -0
  37. data/ext/duckdb/vector.h +13 -0
  38. data/lib/duckdb/appender.rb +21 -11
  39. data/lib/duckdb/bind_info.rb +32 -0
  40. data/lib/duckdb/casting.rb +35 -0
  41. data/lib/duckdb/connection.rb +123 -2
  42. data/lib/duckdb/converter.rb +32 -47
  43. data/lib/duckdb/data_chunk.rb +114 -0
  44. data/lib/duckdb/extracted_statements.rb +1 -1
  45. data/lib/duckdb/function_info.rb +21 -0
  46. data/lib/duckdb/init_info.rb +22 -0
  47. data/lib/duckdb/instance_cache.rb +0 -4
  48. data/lib/duckdb/interval.rb +1 -1
  49. data/lib/duckdb/logical_type.rb +46 -5
  50. data/lib/duckdb/prepared_statement.rb +22 -8
  51. data/lib/duckdb/result.rb +1 -0
  52. data/lib/duckdb/scalar_function.rb +119 -0
  53. data/lib/duckdb/table_function.rb +191 -0
  54. data/lib/duckdb/vector.rb +20 -0
  55. data/lib/duckdb/version.rb +1 -1
  56. data/lib/duckdb.rb +8 -0
  57. data/sample/async_query.rb +2 -0
  58. data/sample/async_query_stream.rb +2 -0
  59. data/sample/issue922.rb +54 -0
  60. data/sample/issue922_benchmark.rb +169 -0
  61. data/sample/issue930.rb +49 -0
  62. data/sample/issue930_benchmark.rb +70 -0
  63. metadata +30 -57
@@ -2,24 +2,65 @@
2
2
 
3
3
  VALUE cDuckDBScalarFunction;
4
4
 
5
+ static void mark(void *);
5
6
  static void deallocate(void *);
6
7
  static VALUE allocate(VALUE klass);
7
8
  static size_t memsize(const void *p);
9
+ static void compact(void *);
8
10
  static VALUE duckdb_scalar_function_initialize(VALUE self);
9
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_add_parameter(VALUE self, VALUE logical_type);
14
+ static VALUE rbduckdb_scalar_function_set_function(VALUE self);
15
+ static void scalar_function_callback(duckdb_function_info info, duckdb_data_chunk input, duckdb_vector output);
16
+ static void vector_set_value_at(duckdb_vector vector, duckdb_logical_type element_type, idx_t index, VALUE value);
17
+
18
+ struct callback_arg {
19
+ rubyDuckDBScalarFunction *ctx;
20
+ duckdb_data_chunk input;
21
+ duckdb_vector output;
22
+ duckdb_logical_type output_type;
23
+ duckdb_vector *input_vectors;
24
+ duckdb_logical_type *input_types;
25
+ VALUE *args;
26
+ idx_t row_count;
27
+ idx_t col_count;
28
+ };
29
+
30
+ static VALUE process_rows(VALUE arg);
31
+ static VALUE process_no_param_rows(VALUE arg);
32
+ static VALUE cleanup_callback(VALUE arg);
10
33
 
11
34
  static const rb_data_type_t scalar_function_data_type = {
12
35
  "DuckDB/ScalarFunction",
13
- {NULL, deallocate, memsize,},
36
+ {mark, deallocate, memsize, compact},
14
37
  0, 0, RUBY_TYPED_FREE_IMMEDIATELY
15
38
  };
16
39
 
40
+ static void mark(void *ctx) {
41
+ rubyDuckDBScalarFunction *p = (rubyDuckDBScalarFunction *)ctx;
42
+ rb_gc_mark(p->function_proc);
43
+ }
44
+
17
45
  static void deallocate(void * ctx) {
18
46
  rubyDuckDBScalarFunction *p = (rubyDuckDBScalarFunction *)ctx;
19
47
  duckdb_destroy_scalar_function(&(p->scalar_function));
20
48
  xfree(p);
21
49
  }
22
50
 
51
+ /*
52
+ * GC compaction callback - updates VALUE references that may have moved during compaction.
53
+ * This is critical for Ruby 2.7+ where GC can move objects in memory.
54
+ * Without this, the function_proc VALUE could become a stale pointer after compaction,
55
+ * leading to crashes when DuckDB invokes the callback.
56
+ */
57
+ static void compact(void *ctx) {
58
+ rubyDuckDBScalarFunction *p = (rubyDuckDBScalarFunction *)ctx;
59
+ if (p->function_proc != Qnil) {
60
+ p->function_proc = rb_gc_location(p->function_proc);
61
+ }
62
+ }
63
+
23
64
  static VALUE allocate(VALUE klass) {
24
65
  rubyDuckDBScalarFunction *ctx = xcalloc((size_t)1, sizeof(rubyDuckDBScalarFunction));
25
66
  return TypedData_Wrap_Struct(klass, &scalar_function_data_type, ctx);
@@ -33,6 +74,7 @@ static VALUE duckdb_scalar_function_initialize(VALUE self) {
33
74
  rubyDuckDBScalarFunction *p;
34
75
  TypedData_Get_Struct(self, rubyDuckDBScalarFunction, &scalar_function_data_type, p);
35
76
  p->scalar_function = duckdb_create_scalar_function();
77
+ p->function_proc = Qnil;
36
78
  return self;
37
79
  }
38
80
 
@@ -46,6 +88,295 @@ static VALUE rbduckdb_scalar_function_set_name(VALUE self, VALUE name) {
46
88
  return self;
47
89
  }
48
90
 
91
+ static VALUE rbduckdb_scalar_function__set_return_type(VALUE self, VALUE logical_type) {
92
+ rubyDuckDBScalarFunction *p;
93
+ rubyDuckDBLogicalType *lt;
94
+
95
+ TypedData_Get_Struct(self, rubyDuckDBScalarFunction, &scalar_function_data_type, p);
96
+ lt = get_struct_logical_type(logical_type);
97
+
98
+ duckdb_scalar_function_set_return_type(p->scalar_function, lt->logical_type);
99
+
100
+ return self;
101
+ }
102
+
103
+ static VALUE rbduckdb_scalar_function_add_parameter(VALUE self, VALUE logical_type) {
104
+ rubyDuckDBScalarFunction *p;
105
+ rubyDuckDBLogicalType *lt;
106
+
107
+ TypedData_Get_Struct(self, rubyDuckDBScalarFunction, &scalar_function_data_type, p);
108
+ lt = get_struct_logical_type(logical_type);
109
+
110
+ duckdb_scalar_function_add_parameter(p->scalar_function, lt->logical_type);
111
+
112
+ return self;
113
+ }
114
+
115
+ static void scalar_function_callback(duckdb_function_info info, duckdb_data_chunk input, duckdb_vector output) {
116
+ rubyDuckDBScalarFunction *ctx;
117
+ idx_t i;
118
+ struct callback_arg arg;
119
+
120
+ ctx = (rubyDuckDBScalarFunction *)duckdb_scalar_function_get_extra_info(info);
121
+
122
+ if (ctx == NULL || ctx->function_proc == Qnil) {
123
+ // Mark all rows as NULL to avoid returning uninitialized data
124
+ idx_t row_count = duckdb_data_chunk_get_size(input);
125
+ uint64_t *validity;
126
+ duckdb_vector_ensure_validity_writable(output);
127
+ validity = duckdb_vector_get_validity(output);
128
+ for (i = 0; i < row_count; i++) {
129
+ duckdb_validity_set_row_invalid(validity, i);
130
+ }
131
+ return;
132
+ }
133
+
134
+ // Initialize callback argument structure
135
+ arg.ctx = ctx;
136
+ arg.input = input;
137
+ arg.output = output;
138
+ arg.output_type = duckdb_vector_get_column_type(output);
139
+ arg.input_vectors = NULL;
140
+ arg.input_types = NULL;
141
+ arg.args = NULL;
142
+ arg.row_count = duckdb_data_chunk_get_size(input);
143
+ arg.col_count = duckdb_data_chunk_get_column_count(input);
144
+
145
+ // If no parameters, call block once and replicate result to all rows
146
+ if (arg.col_count == 0) {
147
+ rb_ensure(process_no_param_rows, (VALUE)&arg, cleanup_callback, (VALUE)&arg);
148
+ return;
149
+ }
150
+
151
+ // Process rows with proper cleanup on exception
152
+ rb_ensure(process_rows, (VALUE)&arg, cleanup_callback, (VALUE)&arg);
153
+ }
154
+
155
+ static VALUE process_no_param_rows(VALUE varg) {
156
+ struct callback_arg *arg = (struct callback_arg *)varg;
157
+ idx_t i;
158
+ VALUE result;
159
+
160
+ result = rb_funcall(arg->ctx->function_proc, rb_intern("call"), 0);
161
+
162
+ for (i = 0; i < arg->row_count; i++) {
163
+ vector_set_value_at(arg->output, arg->output_type, i, result);
164
+ }
165
+
166
+ return Qnil;
167
+ }
168
+
169
+ static VALUE process_rows(VALUE varg) {
170
+ struct callback_arg *arg = (struct callback_arg *)varg;
171
+ idx_t i, j;
172
+ VALUE result;
173
+
174
+ // Allocate arrays to hold input vectors and their types
175
+ arg->input_vectors = ALLOC_N(duckdb_vector, arg->col_count);
176
+ arg->input_types = ALLOC_N(duckdb_logical_type, arg->col_count);
177
+ arg->args = ALLOC_N(VALUE, arg->col_count);
178
+
179
+ // Get all input vectors and their types
180
+ for (j = 0; j < arg->col_count; j++) {
181
+ arg->input_vectors[j] = duckdb_data_chunk_get_vector(arg->input, j);
182
+ arg->input_types[j] = duckdb_vector_get_column_type(arg->input_vectors[j]);
183
+ }
184
+
185
+ // Process each row
186
+ for (i = 0; i < arg->row_count; i++) {
187
+ // Build arguments array for this row using vector_value_at
188
+ for (j = 0; j < arg->col_count; j++) {
189
+ arg->args[j] = rbduckdb_vector_value_at(arg->input_vectors[j], arg->input_types[j], i);
190
+ }
191
+
192
+ // Call the Ruby block with the arguments
193
+ result = rb_funcallv(arg->ctx->function_proc, rb_intern("call"), arg->col_count, arg->args);
194
+
195
+ // Write result to output using helper function
196
+ vector_set_value_at(arg->output, arg->output_type, i, result);
197
+ }
198
+
199
+ return Qnil;
200
+ }
201
+
202
+ static VALUE cleanup_callback(VALUE varg) {
203
+ struct callback_arg *arg = (struct callback_arg *)varg;
204
+ idx_t j;
205
+
206
+ // Destroy all logical types
207
+ if (arg->input_types != NULL) {
208
+ for (j = 0; j < arg->col_count; j++) {
209
+ duckdb_destroy_logical_type(&arg->input_types[j]);
210
+ }
211
+ }
212
+ duckdb_destroy_logical_type(&arg->output_type);
213
+
214
+ // Free allocated memory
215
+ if (arg->args != NULL) {
216
+ xfree(arg->args);
217
+ }
218
+ if (arg->input_types != NULL) {
219
+ xfree(arg->input_types);
220
+ }
221
+ if (arg->input_vectors != NULL) {
222
+ xfree(arg->input_vectors);
223
+ }
224
+
225
+ return Qnil;
226
+ }
227
+
228
+ static void vector_set_value_at(duckdb_vector vector, duckdb_logical_type element_type, idx_t index, VALUE value) {
229
+ duckdb_type type_id;
230
+ void* vector_data;
231
+ uint64_t *validity;
232
+
233
+ // Handle NULL values
234
+ if (value == Qnil) {
235
+ duckdb_vector_ensure_validity_writable(vector);
236
+ validity = duckdb_vector_get_validity(vector);
237
+ duckdb_validity_set_row_invalid(validity, index);
238
+ return;
239
+ }
240
+
241
+ type_id = duckdb_get_type_id(element_type);
242
+ vector_data = duckdb_vector_get_data(vector);
243
+
244
+ switch(type_id) {
245
+ case DUCKDB_TYPE_BOOLEAN:
246
+ ((bool *)vector_data)[index] = RTEST(value);
247
+ break;
248
+ case DUCKDB_TYPE_TINYINT:
249
+ ((int8_t *)vector_data)[index] = (int8_t)NUM2INT(value);
250
+ break;
251
+ case DUCKDB_TYPE_UTINYINT:
252
+ ((uint8_t *)vector_data)[index] = (uint8_t)NUM2UINT(value);
253
+ break;
254
+ case DUCKDB_TYPE_SMALLINT:
255
+ ((int16_t *)vector_data)[index] = (int16_t)NUM2INT(value);
256
+ break;
257
+ case DUCKDB_TYPE_USMALLINT:
258
+ ((uint16_t *)vector_data)[index] = (uint16_t)NUM2UINT(value);
259
+ break;
260
+ case DUCKDB_TYPE_INTEGER:
261
+ ((int32_t *)vector_data)[index] = NUM2INT(value);
262
+ break;
263
+ case DUCKDB_TYPE_UINTEGER:
264
+ ((uint32_t *)vector_data)[index] = (uint32_t)NUM2ULL(value);
265
+ break;
266
+ case DUCKDB_TYPE_BIGINT:
267
+ ((int64_t *)vector_data)[index] = NUM2LL(value);
268
+ break;
269
+ case DUCKDB_TYPE_UBIGINT:
270
+ ((uint64_t *)vector_data)[index] = NUM2ULL(value);
271
+ break;
272
+ case DUCKDB_TYPE_FLOAT:
273
+ ((float *)vector_data)[index] = (float)NUM2DBL(value);
274
+ break;
275
+ case DUCKDB_TYPE_DOUBLE:
276
+ ((double *)vector_data)[index] = NUM2DBL(value);
277
+ break;
278
+ case DUCKDB_TYPE_VARCHAR: {
279
+ // VARCHAR requires special API, not direct array assignment
280
+ VALUE str = rb_obj_as_string(value);
281
+ const char *str_ptr = StringValuePtr(str);
282
+ idx_t str_len = RSTRING_LEN(str);
283
+ duckdb_vector_assign_string_element_len(vector, index, str_ptr, str_len);
284
+ break;
285
+ }
286
+ case DUCKDB_TYPE_BLOB: {
287
+ // BLOB uses same API as VARCHAR, but expects binary data
288
+ VALUE str = rb_obj_as_string(value);
289
+ const char *str_ptr = StringValuePtr(str);
290
+ idx_t str_len = RSTRING_LEN(str);
291
+ duckdb_vector_assign_string_element_len(vector, index, str_ptr, str_len);
292
+ break;
293
+ }
294
+ case DUCKDB_TYPE_TIMESTAMP: {
295
+ // Convert Ruby Time to DuckDB timestamp (microseconds since epoch)
296
+ if (!rb_obj_is_kind_of(value, rb_cTime)) {
297
+ rb_raise(rb_eTypeError, "Expected Time object for TIMESTAMP");
298
+ }
299
+
300
+ duckdb_timestamp_struct ts_struct;
301
+ ts_struct.date.year = NUM2INT(rb_funcall(value, rb_intern("year"), 0));
302
+ ts_struct.date.month = NUM2INT(rb_funcall(value, rb_intern("month"), 0));
303
+ ts_struct.date.day = NUM2INT(rb_funcall(value, rb_intern("day"), 0));
304
+ ts_struct.time.hour = NUM2INT(rb_funcall(value, rb_intern("hour"), 0));
305
+ ts_struct.time.min = NUM2INT(rb_funcall(value, rb_intern("min"), 0));
306
+ ts_struct.time.sec = NUM2INT(rb_funcall(value, rb_intern("sec"), 0));
307
+ ts_struct.time.micros = NUM2INT(rb_funcall(value, rb_intern("usec"), 0));
308
+
309
+ duckdb_timestamp ts = duckdb_to_timestamp(ts_struct);
310
+ ((duckdb_timestamp *)vector_data)[index] = ts;
311
+ break;
312
+ }
313
+ case DUCKDB_TYPE_DATE: {
314
+ // Convert Ruby Date to DuckDB date
315
+ // Ruby Date is defined in date library, check with Class name
316
+ VALUE date_class = rb_const_get(rb_cObject, rb_intern("Date"));
317
+ if (!rb_obj_is_kind_of(value, date_class)) {
318
+ rb_raise(rb_eTypeError, "Expected Date object for DATE");
319
+ }
320
+
321
+ VALUE year = rb_funcall(value, rb_intern("year"), 0);
322
+ VALUE month = rb_funcall(value, rb_intern("month"), 0);
323
+ VALUE day = rb_funcall(value, rb_intern("day"), 0);
324
+
325
+ duckdb_date date = rbduckdb_to_duckdb_date_from_value(year, month, day);
326
+ ((duckdb_date *)vector_data)[index] = date;
327
+ break;
328
+ }
329
+ case DUCKDB_TYPE_TIME: {
330
+ // Convert Ruby Time to DuckDB time (time-of-day only)
331
+ if (!rb_obj_is_kind_of(value, rb_cTime)) {
332
+ rb_raise(rb_eTypeError, "Expected Time object for TIME");
333
+ }
334
+
335
+ VALUE hour = rb_funcall(value, rb_intern("hour"), 0);
336
+ VALUE min = rb_funcall(value, rb_intern("min"), 0);
337
+ VALUE sec = rb_funcall(value, rb_intern("sec"), 0);
338
+ VALUE usec = rb_funcall(value, rb_intern("usec"), 0);
339
+
340
+ duckdb_time time = rbduckdb_to_duckdb_time_from_value(hour, min, sec, usec);
341
+ ((duckdb_time *)vector_data)[index] = time;
342
+ break;
343
+ }
344
+ default:
345
+ rb_raise(rb_eArgError, "Unsupported return type for scalar function");
346
+ break;
347
+ }
348
+ }
349
+
350
+ rubyDuckDBScalarFunction *get_struct_scalar_function(VALUE obj) {
351
+ rubyDuckDBScalarFunction *ctx;
352
+ TypedData_Get_Struct(obj, rubyDuckDBScalarFunction, &scalar_function_data_type, ctx);
353
+ return ctx;
354
+ }
355
+
356
+ /* :nodoc: */
357
+ static VALUE rbduckdb_scalar_function_set_function(VALUE self) {
358
+ rubyDuckDBScalarFunction *p;
359
+
360
+ if (!rb_block_given_p()) {
361
+ rb_raise(rb_eArgError, "block is required");
362
+ }
363
+
364
+ TypedData_Get_Struct(self, rubyDuckDBScalarFunction, &scalar_function_data_type, p);
365
+
366
+ p->function_proc = rb_block_proc();
367
+
368
+ duckdb_scalar_function_set_extra_info(p->scalar_function, p, NULL);
369
+ duckdb_scalar_function_set_function(p->scalar_function, scalar_function_callback);
370
+
371
+ // Mark as volatile to prevent constant folding during query optimization
372
+ // This prevents DuckDB from evaluating the function at planning time.
373
+ // NOTE: Ruby scalar functions require single-threaded execution (PRAGMA threads=1)
374
+ // because Ruby proc callbacks cannot be safely invoked from DuckDB worker threads.
375
+ duckdb_scalar_function_set_volatile(p->scalar_function);
376
+
377
+ return self;
378
+ }
379
+
49
380
  void rbduckdb_init_duckdb_scalar_function(void) {
50
381
  #if 0
51
382
  VALUE mDuckDB = rb_define_module("DuckDB");
@@ -54,4 +385,8 @@ void rbduckdb_init_duckdb_scalar_function(void) {
54
385
  rb_define_alloc_func(cDuckDBScalarFunction, allocate);
55
386
  rb_define_method(cDuckDBScalarFunction, "initialize", duckdb_scalar_function_initialize, 0);
56
387
  rb_define_method(cDuckDBScalarFunction, "set_name", rbduckdb_scalar_function_set_name, 1);
388
+ rb_define_method(cDuckDBScalarFunction, "name=", rbduckdb_scalar_function_set_name, 1);
389
+ rb_define_private_method(cDuckDBScalarFunction, "_set_return_type", rbduckdb_scalar_function__set_return_type, 1);
390
+ rb_define_private_method(cDuckDBScalarFunction, "_add_parameter", rbduckdb_scalar_function_add_parameter, 1);
391
+ rb_define_method(cDuckDBScalarFunction, "set_function", rbduckdb_scalar_function_set_function, 0);
57
392
  }
@@ -3,11 +3,13 @@
3
3
 
4
4
  struct _rubyDuckDBScalarFunction {
5
5
  duckdb_scalar_function scalar_function;
6
+ VALUE function_proc;
6
7
  };
7
8
 
8
9
  typedef struct _rubyDuckDBScalarFunction rubyDuckDBScalarFunction;
9
10
 
10
11
  void rbduckdb_init_duckdb_scalar_function(void);
12
+ rubyDuckDBScalarFunction *get_struct_scalar_function(VALUE obj);
11
13
 
12
14
  #endif
13
15