duckdb 1.5.5.0 → 1.5.6.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/ext/duckdb/aggregate_function.c +298 -60
  4. data/ext/duckdb/aggregate_function_set.c +3 -4
  5. data/ext/duckdb/appender.c +33 -7
  6. data/ext/duckdb/arrow_array_stream.c +6 -2
  7. data/ext/duckdb/arrow_import.c +1 -1
  8. data/ext/duckdb/client_context.c +1 -1
  9. data/ext/duckdb/column.c +1 -1
  10. data/ext/duckdb/column.h +0 -1
  11. data/ext/duckdb/config.c +1 -1
  12. data/ext/duckdb/connection.c +48 -22
  13. data/ext/duckdb/connection.h +2 -1
  14. data/ext/duckdb/data_chunk.c +1 -1
  15. data/ext/duckdb/database.c +80 -4
  16. data/ext/duckdb/database.h +12 -0
  17. data/ext/duckdb/expression.c +1 -1
  18. data/ext/duckdb/extracted_statements.c +1 -1
  19. data/ext/duckdb/function_executor.c +166 -32
  20. data/ext/duckdb/function_executor.h +18 -6
  21. data/ext/duckdb/instance_cache.c +85 -3
  22. data/ext/duckdb/instance_cache.h +6 -0
  23. data/ext/duckdb/logical_type.c +1 -1
  24. data/ext/duckdb/pending_result.c +6 -1
  25. data/ext/duckdb/prepared_statement.c +14 -2
  26. data/ext/duckdb/result.c +6 -1
  27. data/ext/duckdb/result.h +1 -1
  28. data/ext/duckdb/scalar_function.c +18 -25
  29. data/ext/duckdb/scalar_function_bind_info.c +1 -1
  30. data/ext/duckdb/scalar_function_set.c +3 -4
  31. data/ext/duckdb/table_description.c +1 -1
  32. data/ext/duckdb/table_function.c +16 -21
  33. data/ext/duckdb/table_function_bind_info.c +29 -1
  34. data/ext/duckdb/table_function_bind_info.h +1 -0
  35. data/ext/duckdb/table_function_function_info.c +23 -1
  36. data/ext/duckdb/table_function_init_info.c +20 -1
  37. data/ext/duckdb/value.c +2 -2
  38. data/ext/duckdb/vector.c +1 -1
  39. data/lib/duckdb/appender.rb +1 -2
  40. data/lib/duckdb/converter/int_to_sym.rb +3 -0
  41. data/lib/duckdb/converter.rb +1 -2
  42. data/lib/duckdb/data_chunk.rb +1 -2
  43. data/lib/duckdb/prepared_statement.rb +1 -2
  44. data/lib/duckdb/table_function/function_info.rb +1 -2
  45. data/lib/duckdb/table_function/init_info.rb +1 -2
  46. data/lib/duckdb/table_function.rb +1 -2
  47. data/lib/duckdb/vector.rb +1 -2
  48. data/lib/duckdb/version.rb +1 -1
  49. metadata +2 -2
@@ -50,10 +50,20 @@ static VALUE appender__clear_columns(VALUE self);
50
50
  static VALUE appender__close(VALUE self);
51
51
  static VALUE state_to_rbool(duckdb_state state);
52
52
 
53
+ /*
54
+ * Keeps RUBY_TYPED_FREE_IMMEDIATELY although duckdb_appender_destroy flushes
55
+ * buffered rows (blocking I/O): when an unclosed appender is collected in the
56
+ * same GC as its connection, freeing it during sweep runs before the deferred
57
+ * Connection/Database free, so its rows still reach the table. Deferring it too
58
+ * would let the connection close first and lose rows. (The appender holds no
59
+ * reference to its connection, so if the connection is collected in an earlier
60
+ * GC the flush is lost anyway.) Freeing in sweep cannot shut the database down:
61
+ * DuckDB's BaseAppender holds only a weak_ptr<ClientContext>.
62
+ */
53
63
  static const rb_data_type_t appender_data_type = {
54
64
  "DuckDB/Appender",
55
65
  {NULL, deallocate, memsize,},
56
- 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
66
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
57
67
  };
58
68
 
59
69
  static void deallocate(void * ctx) {
@@ -94,6 +104,8 @@ static VALUE appender_s_create_query(VALUE klass, VALUE con, VALUE query, VALUE
94
104
  idx_t column_count = 0;
95
105
  duckdb_logical_type *type_array = NULL;
96
106
  VALUE appender = Qnil;
107
+ VALUE column_name_strings = Qnil;
108
+ duckdb_state state;
97
109
 
98
110
  if (!rb_obj_is_kind_of(con, cDuckDBConnection)) {
99
111
  rb_raise(rb_eTypeError, "1st argument should be instance of DackDB::Connection");
@@ -116,17 +128,29 @@ static VALUE appender_s_create_query(VALUE klass, VALUE con, VALUE query, VALUE
116
128
  if (rb_obj_is_kind_of(columns, rb_cArray) == Qfalse) {
117
129
  rb_raise(rb_eTypeError, "4th argument should be an Array or nil");
118
130
  }
119
- idx_t col_count = RARRAY_LEN(columns);
120
- column_names = ALLOCA_N(const char *, (size_t)col_count);
121
- for (idx_t i = 0; i < col_count; i++) {
131
+ /* DuckDB reads column_count entries from column_names, so a shorter list
132
+ * makes it read uninitialized stack slots as column names. */
133
+ if ((idx_t)RARRAY_LEN(columns) != column_count) {
134
+ rb_raise(rb_eArgError, "column names size (%ld) must be the same as types size (%ld)",
135
+ RARRAY_LEN(columns), RARRAY_LEN(types));
136
+ }
137
+ /* Keep the converted Strings alive: StringValue on a to_str object
138
+ * returns a String that nothing else references. */
139
+ column_name_strings = rb_ary_new_capa((long)column_count);
140
+ column_names = ALLOCA_N(const char *, (size_t)column_count);
141
+ for (idx_t i = 0; i < column_count; i++) {
122
142
  VALUE col_name_val = rb_ary_entry(columns, i);
123
- column_names[i] = StringValuePtr(col_name_val);
143
+ StringValue(col_name_val);
144
+ rb_ary_push(column_name_strings, col_name_val);
145
+ column_names[i] = RSTRING_PTR(col_name_val);
124
146
  }
125
147
  }
126
148
  ctxcon = rbduckdb_get_struct_connection(con);
127
149
  appender = allocate(klass);
128
150
  TypedData_Get_Struct(appender, rubyDuckDBAppender, &appender_data_type, ctx);
129
- if (duckdb_appender_create_query(ctxcon->con, query_str, column_count, type_array, table_name, column_names, &ctx->appender) == DuckDBError) {
151
+ state = duckdb_appender_create_query(ctxcon->con, query_str, column_count, type_array, table_name, column_names, &ctx->appender);
152
+ RB_GC_GUARD(column_name_strings);
153
+ if (state == DuckDBError) {
130
154
  rb_raise(eDuckDBError, "failed to create appender from query");
131
155
  }
132
156
 
@@ -333,11 +357,13 @@ static VALUE appender__append_double(VALUE self, VALUE val) {
333
357
  /* :nodoc: */
334
358
  static VALUE appender__append_varchar(VALUE self, VALUE val) {
335
359
  rubyDuckDBAppender *ctx;
360
+ /* Length-aware, as duckdb_append_varchar would stop at an embedded NUL. */
336
361
  char *pval = StringValuePtr(val);
362
+ long len = RSTRING_LEN(val);
337
363
 
338
364
  TypedData_Get_Struct(self, rubyDuckDBAppender, &appender_data_type, ctx);
339
365
 
340
- return state_to_rbool(duckdb_append_varchar(ctx->appender, pval));
366
+ return state_to_rbool(duckdb_append_varchar_length(ctx->appender, pval, (idx_t)len));
341
367
  }
342
368
 
343
369
  /* :nodoc: */
@@ -13,7 +13,7 @@ typedef struct {
13
13
  * the Ruby DuckDB::ArrowArrayStream object is gone, so this context is
14
14
  * freed only by the stream release callback, and it holds a reference on
15
15
  * the result struct (rbduckdb_result_ref) until then. The release callback
16
- * must not call any Ruby API: it can run during GC sweep (via deallocate
16
+ * must not call any Ruby API: it can run during GC finalization (via deallocate
17
17
  * of an unconsumed stream) or from a non-Ruby thread.
18
18
  */
19
19
  typedef struct {
@@ -34,10 +34,14 @@ static int stream_get_next(struct ArrowArrayStream *stream, struct ArrowArray *o
34
34
  static const char *stream_get_last_error(struct ArrowArrayStream *stream);
35
35
  static void stream_release(struct ArrowArrayStream *stream);
36
36
 
37
+ /*
38
+ * No RUBY_TYPED_FREE_IMMEDIATELY: deallocate can block, and must not run
39
+ * during GC sweep: releasing the stream may destroy a streaming result (see result.c).
40
+ */
37
41
  static const rb_data_type_t arrow_array_stream_data_type = {
38
42
  "DuckDB/ArrowArrayStream",
39
43
  {NULL, deallocate, memsize,},
40
- 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
44
+ 0, 0, RUBY_TYPED_WB_PROTECTED
41
45
  };
42
46
 
43
47
  static void deallocate(void *ctx) {
@@ -26,7 +26,7 @@ static VALUE connection__arrow_release(VALUE self, VALUE address);
26
26
  static const rb_data_type_t arrow_converted_schema_data_type = {
27
27
  "DuckDB/ArrowConvertedSchema",
28
28
  {NULL, deallocate, memsize,},
29
- 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
29
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
30
30
  };
31
31
 
32
32
  static void deallocate(void *ctx) {
@@ -10,7 +10,7 @@ static VALUE client_context_connection_id(VALUE self);
10
10
  static const rb_data_type_t client_context_data_type = {
11
11
  "DuckDB/ClientContext",
12
12
  {NULL, deallocate, memsize,},
13
- 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
13
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
14
14
  };
15
15
 
16
16
  static void deallocate(void *ctx) {
data/ext/duckdb/column.c CHANGED
@@ -12,7 +12,7 @@ static VALUE column_name(VALUE oDuckDBColumn);
12
12
  static const rb_data_type_t column_data_type = {
13
13
  "DuckDB/Column",
14
14
  {NULL, deallocate, memsize,},
15
- 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
15
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
16
16
  };
17
17
 
18
18
  static void deallocate(void *ctx) {
data/ext/duckdb/column.h CHANGED
@@ -2,7 +2,6 @@
2
2
  #define RUBY_DUCKDB_COLUMN_H
3
3
 
4
4
  struct _rubyDuckDBColumn {
5
- VALUE result;
6
5
  idx_t col;
7
6
  };
8
7
 
data/ext/duckdb/config.c CHANGED
@@ -13,7 +13,7 @@ static VALUE config_set_config(VALUE self, VALUE key, VALUE value);
13
13
  static const rb_data_type_t config_data_type = {
14
14
  "DuckDB/Config",
15
15
  {NULL, deallocate, memsize,},
16
- 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
16
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
17
17
  };
18
18
 
19
19
  static void deallocate(void * ctx) {
@@ -4,6 +4,7 @@ VALUE cDuckDBConnection;
4
4
 
5
5
  static void deallocate(void *ctx);
6
6
  static void mark(void *ctx);
7
+ static void compact(void *ctx);
7
8
  static VALUE allocate(VALUE klass);
8
9
  static size_t memsize(const void *p);
9
10
  static VALUE connection_disconnect(VALUE self);
@@ -19,10 +20,15 @@ static VALUE connection__register_aggregate_function_set(VALUE self, VALUE aggre
19
20
  static VALUE connection__register_table_function(VALUE self, VALUE table_function);
20
21
  static VALUE connection__get_table_names(VALUE self, VALUE query, VALUE qualified);
21
22
 
23
+ /*
24
+ * No RUBY_TYPED_FREE_IMMEDIATELY: deallocate can block, and must not run
25
+ * during GC sweep: duckdb_disconnect rolls back an open transaction and may drop the
26
+ * last reference to the database (checkpoint on shutdown).
27
+ */
22
28
  static const rb_data_type_t connection_data_type = {
23
29
  "DuckDB/Connection",
24
- {mark, deallocate, memsize,},
25
- 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
30
+ {mark, deallocate, memsize, compact},
31
+ 0, 0, RUBY_TYPED_WB_PROTECTED
26
32
  };
27
33
 
28
34
  static void deallocate(void *ctx) {
@@ -34,16 +40,29 @@ static void deallocate(void *ctx) {
34
40
 
35
41
  static void mark(void *ctx) {
36
42
  rubyDuckDBConnection *p = (rubyDuckDBConnection *)ctx;
37
- rb_gc_mark(p->registered_functions);
43
+ rb_gc_mark_movable(p->database);
44
+ }
45
+
46
+ static void compact(void *ctx) {
47
+ rubyDuckDBConnection *p = (rubyDuckDBConnection *)ctx;
48
+ p->database = rb_gc_location(p->database);
38
49
  }
39
50
 
40
51
  static VALUE allocate(VALUE klass) {
41
52
  rubyDuckDBConnection *ctx = xcalloc((size_t)1, sizeof(rubyDuckDBConnection));
42
- VALUE obj = TypedData_Wrap_Struct(klass, &connection_data_type, ctx);
43
- VALUE registered_functions = rb_ary_new();
44
- ctx->registered_functions = registered_functions;
45
- RB_GC_GUARD(registered_functions);
46
- return obj;
53
+ ctx->database = Qnil;
54
+ return TypedData_Wrap_Struct(klass, &connection_data_type, ctx);
55
+ }
56
+
57
+ /* Anchors obj to this connection's database so it outlives the connection. */
58
+ static void connection_retain_registered(VALUE self, VALUE obj) {
59
+ rubyDuckDBConnection *ctx;
60
+
61
+ TypedData_Get_Struct(self, rubyDuckDBConnection, &connection_data_type, ctx);
62
+ if (NIL_P(ctx->database)) {
63
+ rb_raise(eDuckDBError, "connection is not associated with a database");
64
+ }
65
+ rbduckdb_database_retain(ctx->database, obj);
47
66
  }
48
67
 
49
68
  static size_t memsize(const void *p) {
@@ -69,6 +88,7 @@ VALUE rbduckdb_create_connection(VALUE oDuckDBDatabase) {
69
88
  if (duckdb_connect(ctxdb->db, &(ctxcon->con)) == DuckDBError) {
70
89
  rb_raise(eDuckDBError, "connection error");
71
90
  }
91
+ RB_OBJ_WRITE(obj, &ctxcon->database, oDuckDBDatabase);
72
92
 
73
93
  return obj;
74
94
  }
@@ -79,8 +99,12 @@ static VALUE connection_disconnect(VALUE self) {
79
99
  TypedData_Get_Struct(self, rubyDuckDBConnection, &connection_data_type, ctx);
80
100
  duckdb_disconnect(&(ctx->con));
81
101
 
82
- /* Clear registered functions to release memory */
83
- rb_ary_clear(ctx->registered_functions);
102
+ /*
103
+ * Registered functions are not released here: the catalog entry is still
104
+ * reachable from other connections. They are released with the database.
105
+ * This connection no longer needs the database, so stop retaining it.
106
+ */
107
+ RB_OBJ_WRITE(self, &ctx->database, Qnil);
84
108
 
85
109
  return self;
86
110
  }
@@ -145,9 +169,11 @@ static VALUE connection__connect(VALUE self, VALUE oDuckDBDatabase) {
145
169
  ctxdb = rbduckdb_get_struct_database(oDuckDBDatabase);
146
170
  TypedData_Get_Struct(self, rubyDuckDBConnection, &connection_data_type, ctx);
147
171
 
172
+ /* Set only on success: a failed connect must not re-anchor registrations to the new database. */
148
173
  if (duckdb_connect(ctxdb->db, &(ctx->con)) == DuckDBError) {
149
174
  rb_raise(eDuckDBError, "connection error");
150
175
  }
176
+ RB_OBJ_WRITE(self, &ctx->database, oDuckDBDatabase);
151
177
 
152
178
  return self;
153
179
  }
@@ -239,8 +265,8 @@ static VALUE connection__register_logical_type(VALUE self, VALUE logical_type) {
239
265
  rb_raise(eDuckDBError, "Failed to register logical type");
240
266
  }
241
267
 
242
- /* Keep reference to prevent GC while connection is alive */
243
- rb_ary_push(ctxcon->registered_functions, logical_type);
268
+ /* Keep reference alive as long as the database can call it */
269
+ connection_retain_registered(self, logical_type);
244
270
 
245
271
  return self;
246
272
  }
@@ -260,8 +286,8 @@ static VALUE connection__register_scalar_function(VALUE self, VALUE scalar_funct
260
286
  rb_raise(eDuckDBError, "Failed to register scalar function");
261
287
  }
262
288
 
263
- /* Keep reference to prevent GC while connection is alive */
264
- rb_ary_push(ctxcon->registered_functions, scalar_function);
289
+ /* Keep reference alive as long as the database can call it */
290
+ connection_retain_registered(self, scalar_function);
265
291
 
266
292
  return self;
267
293
  }
@@ -281,8 +307,8 @@ static VALUE connection__register_scalar_function_set(VALUE self, VALUE scalar_f
281
307
  rb_raise(eDuckDBError, "Failed to register scalar function set");
282
308
  }
283
309
 
284
- /* Keep reference to prevent GC while connection is alive */
285
- rb_ary_push(ctxcon->registered_functions, scalar_function_set);
310
+ /* Keep reference alive as long as the database can call it */
311
+ connection_retain_registered(self, scalar_function_set);
286
312
 
287
313
  return self;
288
314
  }
@@ -302,8 +328,8 @@ static VALUE connection__register_aggregate_function(VALUE self, VALUE aggregate
302
328
  rb_raise(eDuckDBError, "Failed to register aggregate function");
303
329
  }
304
330
 
305
- /* Keep reference to prevent GC while connection is alive */
306
- rb_ary_push(ctxcon->registered_functions, aggregate_function);
331
+ /* Keep reference alive as long as the database can call it */
332
+ connection_retain_registered(self, aggregate_function);
307
333
 
308
334
  return self;
309
335
  }
@@ -323,8 +349,8 @@ static VALUE connection__register_aggregate_function_set(VALUE self, VALUE aggre
323
349
  rb_raise(eDuckDBError, "Failed to register aggregate function set");
324
350
  }
325
351
 
326
- /* Keep reference to prevent GC while connection is alive */
327
- rb_ary_push(ctxcon->registered_functions, aggregate_function_set);
352
+ /* Keep reference alive as long as the database can call it */
353
+ connection_retain_registered(self, aggregate_function_set);
328
354
 
329
355
  return self;
330
356
  }
@@ -343,8 +369,8 @@ static VALUE connection__register_table_function(VALUE self, VALUE table_functio
343
369
  rb_raise(eDuckDBError, "Failed to register table function");
344
370
  }
345
371
 
346
- /* Keep reference to prevent GC while connection is alive */
347
- rb_ary_push(ctxcon->registered_functions, table_function);
372
+ /* Keep reference alive as long as the database can call it */
373
+ connection_retain_registered(self, table_function);
348
374
 
349
375
  return self;
350
376
  }
@@ -3,7 +3,8 @@
3
3
 
4
4
  struct _rubyDuckDBConnection {
5
5
  duckdb_connection con;
6
- VALUE registered_functions;
6
+ /* The DuckDB::Database this connection belongs to. Registrations are anchored there. */
7
+ VALUE database;
7
8
  };
8
9
 
9
10
  typedef struct _rubyDuckDBConnection rubyDuckDBConnection;
@@ -16,7 +16,7 @@ static VALUE data_chunk__reset(VALUE self);
16
16
  static const rb_data_type_t data_chunk_data_type = {
17
17
  "DuckDB/DataChunk",
18
18
  {NULL, deallocate, memsize,},
19
- 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
19
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
20
20
  };
21
21
 
22
22
  static void deallocate(void *ctx) {
@@ -4,6 +4,8 @@ VALUE cDuckDBDatabase;
4
4
 
5
5
  static void close_database(rubyDuckDB *p);
6
6
  static void deallocate(void * ctx);
7
+ static void mark(void *ctx);
8
+ static void compact(void *ctx);
7
9
  static VALUE allocate(VALUE klass);
8
10
  static size_t memsize(const void *p);
9
11
  static duckdb_config create_config_with_ruby_api(void);
@@ -11,16 +13,37 @@ static VALUE database__initialize(VALUE self, VALUE file, VALUE config);
11
13
  static VALUE database__connect(VALUE self);
12
14
  static VALUE database_close(VALUE self);
13
15
 
16
+ /*
17
+ * No RUBY_TYPED_FREE_IMMEDIATELY: deallocate can block, and must not run
18
+ * during GC sweep: duckdb_close may checkpoint to disk and joins worker threads.
19
+ */
14
20
  static const rb_data_type_t database_data_type = {
15
21
  "DuckDB/Database",
16
- {NULL, deallocate, memsize,},
17
- 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
22
+ {mark, deallocate, memsize, compact},
23
+ 0, 0, RUBY_TYPED_WB_PROTECTED
18
24
  };
19
25
 
20
26
  static void close_database(rubyDuckDB *p) {
21
27
  duckdb_close(&(p->db));
22
28
  }
23
29
 
30
+ /*
31
+ * duckdb_close joins DuckDB's worker threads, and a worker tearing down a
32
+ * pipeline can still run a UDF callback -- the aggregate destroy callback,
33
+ * say, for a query that failed and left its pipeline queued. That callback is
34
+ * dispatched to the executor thread, which needs the GVL, so closing while
35
+ * holding it deadlocks: the worker waits for the executor, the executor waits
36
+ * for us, and we wait for the worker.
37
+ *
38
+ * The GC free function calls close_database directly and cannot do this;
39
+ * a free function must not release the GVL. A database reaching close via
40
+ * the GC with callbacks still pending would deadlock the same way.
41
+ */
42
+ static void *close_database_without_gvl(void *ctx) {
43
+ close_database((rubyDuckDB *)ctx);
44
+ return NULL;
45
+ }
46
+
24
47
  static void deallocate(void * ctx) {
25
48
  rubyDuckDB *p = (rubyDuckDB *)ctx;
26
49
 
@@ -28,13 +51,57 @@ static void deallocate(void * ctx) {
28
51
  xfree(p);
29
52
  }
30
53
 
54
+ static void mark(void *ctx) {
55
+ rubyDuckDB *p = (rubyDuckDB *)ctx;
56
+
57
+ rb_gc_mark_movable(p->registered_functions);
58
+ rb_gc_mark_movable(p->cached_path);
59
+ rb_gc_mark_movable(p->cache_wrappers);
60
+ }
61
+
62
+ static void compact(void *ctx) {
63
+ rubyDuckDB *p = (rubyDuckDB *)ctx;
64
+
65
+ p->registered_functions = rb_gc_location(p->registered_functions);
66
+ p->cached_path = rb_gc_location(p->cached_path);
67
+ p->cache_wrappers = rb_gc_location(p->cache_wrappers);
68
+ }
69
+
31
70
  static size_t memsize(const void *p) {
32
71
  return sizeof(rubyDuckDB);
33
72
  }
34
73
 
35
74
  static VALUE allocate(VALUE klass) {
36
75
  rubyDuckDB *ctx = xcalloc((size_t)1, sizeof(rubyDuckDB));
37
- return TypedData_Wrap_Struct(klass, &database_data_type, ctx);
76
+ VALUE obj = TypedData_Wrap_Struct(klass, &database_data_type, ctx);
77
+ ctx->cached_path = Qnil;
78
+ ctx->cache_wrappers = Qnil;
79
+ RB_OBJ_WRITE(obj, &ctx->registered_functions, rb_ary_new());
80
+ return obj;
81
+ }
82
+
83
+ /*
84
+ * Keeps obj alive as long as this database is alive.
85
+ * DuckDB registers functions in the database-level catalog and has no
86
+ * unregister API, so releasing them any earlier leaves a dangling extra_info.
87
+ */
88
+ void rbduckdb_database_retain(VALUE database, VALUE obj) {
89
+ rubyDuckDB *ctx;
90
+
91
+ TypedData_Get_Struct(database, rubyDuckDB, &database_data_type, ctx);
92
+ rb_ary_push(ctx->registered_functions, obj);
93
+ }
94
+
95
+ /*
96
+ * Records that this database is the InstanceCache's wrapper for path, so the
97
+ * cache can find it again and #close can remove it from the cache's weak set.
98
+ */
99
+ void rbduckdb_database_set_cache_entry(VALUE database, VALUE path, VALUE wrappers) {
100
+ rubyDuckDB *ctx;
101
+
102
+ TypedData_Get_Struct(database, rubyDuckDB, &database_data_type, ctx);
103
+ RB_OBJ_WRITE(database, &ctx->cached_path, rb_str_new_frozen(path));
104
+ RB_OBJ_WRITE(database, &ctx->cache_wrappers, wrappers);
38
105
  }
39
106
 
40
107
  rubyDuckDB *rbduckdb_get_struct_database(VALUE obj) {
@@ -119,7 +186,16 @@ static VALUE database__connect(VALUE self) {
119
186
  static VALUE database_close(VALUE self) {
120
187
  rubyDuckDB *ctx;
121
188
  TypedData_Get_Struct(self, rubyDuckDB, &database_data_type, ctx);
122
- close_database(ctx);
189
+ /*
190
+ * A closed wrapper must not be handed to the next get_or_create, so drop it
191
+ * from the cache's set first. The next call then opens a fresh wrapper.
192
+ */
193
+ if (!NIL_P(ctx->cache_wrappers)) {
194
+ rb_funcall(ctx->cache_wrappers, rb_intern("delete"), 1, self);
195
+ RB_OBJ_WRITE(self, &ctx->cache_wrappers, Qnil);
196
+ RB_OBJ_WRITE(self, &ctx->cached_path, Qnil);
197
+ }
198
+ rb_thread_call_without_gvl(close_database_without_gvl, ctx, NULL, NULL);
123
199
  return self;
124
200
  }
125
201
 
@@ -3,12 +3,24 @@
3
3
 
4
4
  struct _rubyDuckDB {
5
5
  duckdb_database db;
6
+ /* Functions and logical types registered on any connection to this database */
7
+ VALUE registered_functions;
8
+ /*
9
+ * Set only for a database handed out by DuckDB::InstanceCache: the path it
10
+ * was opened under, and the cache's weak set of live wrappers. Together they
11
+ * let the cache hand back this same wrapper for the same path, and let
12
+ * #close drop this wrapper from the set. Qnil for any other database.
13
+ */
14
+ VALUE cached_path;
15
+ VALUE cache_wrappers;
6
16
  };
7
17
 
8
18
  typedef struct _rubyDuckDB rubyDuckDB;
9
19
 
10
20
  rubyDuckDB *rbduckdb_get_struct_database(VALUE obj);
11
21
  VALUE rbduckdb_create_database_obj(duckdb_database db);
22
+ void rbduckdb_database_retain(VALUE database, VALUE obj);
23
+ void rbduckdb_database_set_cache_entry(VALUE database, VALUE path, VALUE wrappers);
12
24
  void rbduckdb_init_database(void);
13
25
 
14
26
  #endif
@@ -11,7 +11,7 @@ static VALUE expression__fold(VALUE self, VALUE client_context);
11
11
  static const rb_data_type_t expression_data_type = {
12
12
  "DuckDB/Expression",
13
13
  {NULL, deallocate, memsize,},
14
- 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
14
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
15
15
  };
16
16
 
17
17
  static void deallocate(void *ctx) {
@@ -9,7 +9,7 @@ static size_t memsize(const void *p);
9
9
  static const rb_data_type_t extract_statements_data_type = {
10
10
  "DuckDB/ExtractedStatements",
11
11
  {NULL, deallocate, memsize,},
12
- 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
12
+ 0, 0, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
13
13
  };
14
14
 
15
15
  static void deallocate(void *ctx);