Skip to content

Commit c445619

Browse files
authored
Merge pull request #1103 from suketa/feature/table-function-phase5-init-info
feat: Add DuckDB::InitInfo and TableFunction#init callback (Phase 5)
2 parents f2755e3 + 94fc8d3 commit c445619

File tree

10 files changed

+235
-0
lines changed

10 files changed

+235
-0
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ All notable changes to this project will be documented in this file.
2828
- add DuckDB::TableFunction#execute for setting execute callback (Phase 4).
2929
- add DuckDB::FunctionInfo class for table function execution context (Phase 4).
3030
- add DuckDB::FunctionInfo#set_error for reporting execution errors.
31+
- add DuckDB::TableFunction#init for setting init callback (Phase 5).
32+
- add DuckDB::InitInfo class for table function initialization context (Phase 5).
33+
- add DuckDB::InitInfo#set_error for reporting initialization errors.
3134
- bump duckdb to 1.4.4 on CI.
3235
- add inline style to DuckDB::Connection#register_scalar_function (accepts keyword arguments + block).
3336
- add DuckDB::ScalarFunction.create class method for declarative API.

ext/duckdb/duckdb.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Init_duckdb_native(void) {
4343
rbduckdb_init_duckdb_value_impl();
4444
rbduckdb_init_duckdb_scalar_function();
4545
rbduckdb_init_duckdb_bind_info();
46+
rbduckdb_init_duckdb_init_info();
4647
rbduckdb_init_duckdb_function_info();
4748
rbduckdb_init_duckdb_vector();
4849
rbduckdb_init_duckdb_data_chunk();

ext/duckdb/init_info.c

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#include "ruby-duckdb.h"
2+
3+
VALUE cDuckDBInitInfo;
4+
5+
static void deallocate(void *ctx);
6+
static VALUE allocate(VALUE klass);
7+
static size_t memsize(const void *p);
8+
static VALUE rbduckdb_init_info_set_error(VALUE self, VALUE error);
9+
10+
static const rb_data_type_t init_info_data_type = {
11+
"DuckDB/InitInfo",
12+
{NULL, deallocate, memsize,},
13+
0, 0, RUBY_TYPED_FREE_IMMEDIATELY
14+
};
15+
16+
static void deallocate(void *ctx) {
17+
rubyDuckDBInitInfo *p = (rubyDuckDBInitInfo *)ctx;
18+
xfree(p);
19+
}
20+
21+
static VALUE allocate(VALUE klass) {
22+
rubyDuckDBInitInfo *ctx = xcalloc((size_t)1, sizeof(rubyDuckDBInitInfo));
23+
return TypedData_Wrap_Struct(klass, &init_info_data_type, ctx);
24+
}
25+
26+
static size_t memsize(const void *p) {
27+
return sizeof(rubyDuckDBInitInfo);
28+
}
29+
30+
rubyDuckDBInitInfo *get_struct_init_info(VALUE obj) {
31+
rubyDuckDBInitInfo *ctx;
32+
TypedData_Get_Struct(obj, rubyDuckDBInitInfo, &init_info_data_type, ctx);
33+
return ctx;
34+
}
35+
36+
/*
37+
* call-seq:
38+
* init_info.set_error(error_message) -> self
39+
*
40+
* Sets an error message for the init phase.
41+
* This will cause the query to fail with the specified error.
42+
*
43+
* init_info.set_error('Invalid initialization')
44+
*/
45+
static VALUE rbduckdb_init_info_set_error(VALUE self, VALUE error) {
46+
rubyDuckDBInitInfo *ctx;
47+
const char *error_msg;
48+
49+
TypedData_Get_Struct(self, rubyDuckDBInitInfo, &init_info_data_type, ctx);
50+
51+
error_msg = StringValueCStr(error);
52+
duckdb_init_set_error(ctx->info, error_msg);
53+
54+
return self;
55+
}
56+
57+
void rbduckdb_init_duckdb_init_info(void) {
58+
#if 0
59+
VALUE mDuckDB = rb_define_module("DuckDB");
60+
#endif
61+
cDuckDBInitInfo = rb_define_class_under(mDuckDB, "InitInfo", rb_cObject);
62+
rb_define_alloc_func(cDuckDBInitInfo, allocate);
63+
64+
rb_define_method(cDuckDBInitInfo, "set_error", rbduckdb_init_info_set_error, 1);
65+
}

ext/duckdb/init_info.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#ifndef RUBY_DUCKDB_INIT_INFO_H
2+
#define RUBY_DUCKDB_INIT_INFO_H
3+
4+
struct _rubyDuckDBInitInfo {
5+
duckdb_init_info info;
6+
};
7+
8+
typedef struct _rubyDuckDBInitInfo rubyDuckDBInitInfo;
9+
10+
rubyDuckDBInitInfo *get_struct_init_info(VALUE obj);
11+
void rbduckdb_init_duckdb_init_info(void);
12+
13+
#endif

ext/duckdb/ruby-duckdb.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "./value_impl.h"
3232
#include "./scalar_function.h"
3333
#include "./bind_info.h"
34+
#include "./init_info.h"
3435
#include "./function_info.h"
3536
#include "./vector.h"
3637
#include "./data_chunk.h"

ext/duckdb/table_function.c

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
static VALUE cDuckDBTableFunction;
44
extern VALUE cDuckDBBindInfo;
5+
extern VALUE cDuckDBInitInfo;
56
extern VALUE cDuckDBFunctionInfo;
67
extern VALUE cDuckDBDataChunk;
78

@@ -15,6 +16,8 @@ static VALUE rbduckdb_table_function_add_parameter(VALUE self, VALUE logical_typ
1516
static VALUE rbduckdb_table_function_add_named_parameter(VALUE self, VALUE name, VALUE logical_type);
1617
static VALUE rbduckdb_table_function_set_bind(VALUE self);
1718
static void table_function_bind_callback(duckdb_bind_info info);
19+
static VALUE rbduckdb_table_function_set_init(VALUE self);
20+
static void table_function_init_callback(duckdb_init_info info);
1821
static VALUE rbduckdb_table_function_set_execute(VALUE self);
1922
static void table_function_execute_callback(duckdb_function_info info, duckdb_data_chunk output);
2023

@@ -27,6 +30,7 @@ static const rb_data_type_t table_function_data_type = {
2730
static void mark(void *ctx) {
2831
rubyDuckDBTableFunction *p = (rubyDuckDBTableFunction *)ctx;
2932
rb_gc_mark(p->bind_proc);
33+
rb_gc_mark(p->init_proc);
3034
rb_gc_mark(p->execute_proc);
3135
}
3236

@@ -218,6 +222,68 @@ static void table_function_bind_callback(duckdb_bind_info info) {
218222
}
219223
}
220224

225+
/*
226+
* call-seq:
227+
* table_function.init { |init_info| ... } -> table_function
228+
*
229+
* Sets the init callback for the table function.
230+
* The callback is invoked once during query initialization to set up execution state.
231+
*
232+
* table_function.init do |init_info|
233+
* # Initialize execution state
234+
* end
235+
*/
236+
static VALUE rbduckdb_table_function_set_init(VALUE self) {
237+
rubyDuckDBTableFunction *ctx;
238+
239+
if (!rb_block_given_p()) {
240+
rb_raise(rb_eArgError, "block is required for init");
241+
}
242+
243+
TypedData_Get_Struct(self, rubyDuckDBTableFunction, &table_function_data_type, ctx);
244+
245+
if (!ctx->table_function) {
246+
rb_raise(eDuckDBError, "Table function is destroyed");
247+
}
248+
249+
ctx->init_proc = rb_block_proc();
250+
duckdb_table_function_set_init(ctx->table_function, table_function_init_callback);
251+
duckdb_table_function_set_extra_info(ctx->table_function, (void *)self, NULL);
252+
253+
return self;
254+
}
255+
256+
static VALUE call_init_proc(VALUE args_val) {
257+
VALUE *args = (VALUE *)args_val;
258+
return rb_funcall(args[0], rb_intern("call"), 1, args[1]);
259+
}
260+
261+
static void table_function_init_callback(duckdb_init_info info) {
262+
VALUE self = (VALUE)duckdb_init_get_extra_info(info);
263+
rubyDuckDBTableFunction *ctx;
264+
VALUE init_info_obj;
265+
rubyDuckDBInitInfo *init_info_ctx;
266+
int state = 0;
267+
268+
TypedData_Get_Struct(self, rubyDuckDBTableFunction, &table_function_data_type, ctx);
269+
270+
// Create InitInfo wrapper
271+
init_info_obj = rb_funcall(cDuckDBInitInfo, rb_intern("allocate"), 0);
272+
init_info_ctx = get_struct_init_info(init_info_obj);
273+
init_info_ctx->info = info;
274+
275+
// Call Ruby block with exception protection
276+
VALUE call_args[2] = { ctx->init_proc, init_info_obj };
277+
rb_protect(call_init_proc, (VALUE)call_args, &state);
278+
279+
if (state) {
280+
VALUE err = rb_errinfo();
281+
VALUE msg = rb_funcall(err, rb_intern("message"), 0);
282+
duckdb_init_set_error(info, StringValueCStr(msg));
283+
rb_set_errinfo(Qnil); // Clear the error
284+
}
285+
}
286+
221287
/*
222288
* call-seq:
223289
* table_function.execute { |function_info, output| ... } -> table_function
@@ -304,5 +370,6 @@ void rbduckdb_init_duckdb_table_function(void) {
304370
rb_define_method(cDuckDBTableFunction, "add_parameter", rbduckdb_table_function_add_parameter, 1);
305371
rb_define_method(cDuckDBTableFunction, "add_named_parameter", rbduckdb_table_function_add_named_parameter, 2);
306372
rb_define_method(cDuckDBTableFunction, "bind", rbduckdb_table_function_set_bind, 0);
373+
rb_define_method(cDuckDBTableFunction, "init", rbduckdb_table_function_set_init, 0);
307374
rb_define_method(cDuckDBTableFunction, "execute", rbduckdb_table_function_set_execute, 0);
308375
}

ext/duckdb/table_function.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
struct _rubyDuckDBTableFunction {
55
duckdb_table_function table_function;
66
VALUE bind_proc;
7+
VALUE init_proc;
78
VALUE execute_proc;
89
};
910

lib/duckdb.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
require 'duckdb/logical_type'
1717
require 'duckdb/scalar_function'
1818
require 'duckdb/bind_info'
19+
require 'duckdb/init_info'
1920
require 'duckdb/function_info'
2021
require 'duckdb/vector'
2122
require 'duckdb/data_chunk'

lib/duckdb/init_info.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# frozen_string_literal: true
2+
3+
module DuckDB
4+
#
5+
# The DuckDB::InitInfo provides context during table function initialization.
6+
#
7+
# It is passed to the init callback to set up execution state.
8+
#
9+
# Example:
10+
#
11+
# table_function.init do |init_info|
12+
# # Initialize execution state
13+
# # Can report errors if initialization fails
14+
# init_info.set_error('Initialization failed')
15+
# end
16+
#
17+
# rubocop:disable Lint/EmptyClass
18+
class InitInfo
19+
# All methods are defined in C extension (ext/duckdb/init_info.c)
20+
end
21+
# rubocop:enable Lint/EmptyClass
22+
end

test/duckdb_test/init_info_test.rb

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# frozen_string_literal: true
2+
3+
require 'test_helper'
4+
5+
module DuckDBTest
6+
class InitInfoTest < Minitest::Test
7+
def setup
8+
@database = DuckDB::Database.open
9+
@connection = @database.connect
10+
end
11+
12+
def teardown
13+
@connection.disconnect
14+
@database.close
15+
end
16+
17+
# Test 1: Init callback setup
18+
def test_init_callback
19+
table_function = DuckDB::TableFunction.new
20+
table_function.name = 'test_init'
21+
22+
result = table_function.bind do |bind_info|
23+
bind_info.add_result_column('value', DuckDB::LogicalType::BIGINT)
24+
end
25+
26+
result2 = table_function.init do |_init_info|
27+
# Will be tested in integration tests (Phase 6)
28+
end
29+
30+
assert_equal table_function, result
31+
assert_equal table_function, result2
32+
end
33+
34+
# Test 2: Init without block raises error
35+
def test_init_without_block
36+
table_function = DuckDB::TableFunction.new
37+
38+
error = assert_raises(ArgumentError) do
39+
table_function.init
40+
end
41+
42+
assert_equal 'block is required for init', error.message
43+
end
44+
45+
# Test 3: InitInfo set_error method exists
46+
def test_init_info_set_error
47+
table_function = DuckDB::TableFunction.new
48+
table_function.name = 'test_init_error'
49+
50+
result = table_function.init do |init_info|
51+
# Verify set_error method exists and returns self
52+
# NOTE: This block is stored but not invoked here
53+
# Actual execution and error reporting will be tested in Phase 6
54+
init_info.set_error('Test error')
55+
end
56+
57+
# Verify init returns table_function for method chaining
58+
assert_equal table_function, result
59+
end
60+
end
61+
end

0 commit comments

Comments
 (0)