Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
All notable changes to this project will be documented in this file.

# Unreleased
- add DuckDB::Appender.create_query.
- add `DuckDB::LogicalType.boolean`, `DuckDB::LogicalType.tinyint`, `DuckDB::LogicalType.smallint`,
`DuckDB::LogicalType.integer`, `DuckDB::LogicalType.bigint`, `DuckDB::LogicalType.utinyint`,
`DuckDB::LogicalType.usmallint`, `DuckDB::LogicalType.uinteger`, `DuckDB::LogicalType.ubigint`,
Expand Down
71 changes: 71 additions & 0 deletions ext/duckdb/appender.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ static VALUE cDuckDBAppender;
static void deallocate(void *);
static VALUE allocate(VALUE klass);
static size_t memsize(const void *p);

#ifdef HAVE_DUCKDB_H_GE_V1_4_0
static VALUE appender_s_create_query(VALUE klass, VALUE con, VALUE query, VALUE types, VALUE table, VALUE columns);
#endif

static VALUE appender_initialize(VALUE klass, VALUE con, VALUE schema, VALUE table);
static VALUE appender_error_message(VALUE self);
static VALUE appender__append_bool(VALUE self, VALUE val);
Expand Down Expand Up @@ -56,6 +61,69 @@ static size_t memsize(const void *p) {
return sizeof(rubyDuckDBAppender);
}

#ifdef HAVE_DUCKDB_H_GE_V1_4_0
/* call-seq:
* DuckDB::Appender.create_query -> DuckDB::Appender
*
* Returns a new Appender instance created from a query.
*
* require 'duckdb'
* db = DuckDB::Database.open
* con = db.connect
* con.query('CREATE TABLE users (id INTEGER, name VARCHAR)')
* query = 'INSERT OR REPLACE INTO users SELECT i, val FROM my_appended_data'
* types = [DuckDB::LogicalType::INTEGER, DuckDB::LogicalType::VARCHAR]
* appender = DuckDB::Appender.create_query(con, query, types, 'my_appended_data', %w[i val])
*/
static VALUE appender_s_create_query(VALUE klass, VALUE con, VALUE query, VALUE types, VALUE table, VALUE columns) {
rubyDuckDBConnection *ctxcon;
rubyDuckDBAppender *ctx;
char *query_str = StringValuePtr(query);
char *table_name = NULL;
const char **column_names = NULL;
idx_t column_count = 0;
duckdb_logical_type *type_array = NULL;
VALUE appender = Qnil;

if (!rb_obj_is_kind_of(con, cDuckDBConnection)) {
rb_raise(rb_eTypeError, "1st argument should be instance of DackDB::Connection");
}
if (rb_obj_is_kind_of(types, rb_cArray) == Qfalse) {
rb_raise(rb_eTypeError, "2nd argument should be an Array");
}
column_count = RARRAY_LEN(types);
type_array = ALLOCA_N(duckdb_logical_type, (size_t)column_count);
for (idx_t i = 0; i < column_count; i++) {
VALUE type_val = rb_ary_entry(types, i);
rubyDuckDBLogicalType *type_ctx = get_struct_logical_type(type_val);
type_array[i] = type_ctx->logical_type;
}

if (table != Qnil) {
table_name = StringValuePtr(table);
}
if (columns != Qnil) {
if (rb_obj_is_kind_of(columns, rb_cArray) == Qfalse) {
rb_raise(rb_eTypeError, "4th argument should be an Array or nil");
}
idx_t col_count = RARRAY_LEN(columns);
column_names = ALLOCA_N(const char *, (size_t)col_count);
for (idx_t i = 0; i < col_count; i++) {
VALUE col_name_val = rb_ary_entry(columns, i);
column_names[i] = StringValuePtr(col_name_val);
}
}
ctxcon = get_struct_connection(con);
appender = allocate(klass);
TypedData_Get_Struct(appender, rubyDuckDBAppender, &appender_data_type, ctx);
if (duckdb_appender_create_query(ctxcon->con, query_str, column_count, type_array, table_name, column_names, &ctx->appender) == DuckDBError) {
rb_raise(eDuckDBError, "failed to create appender from query");
}

return appender;
}
#endif

static VALUE appender_initialize(VALUE self, VALUE con, VALUE schema, VALUE table) {

rubyDuckDBConnection *ctxcon;
Expand Down Expand Up @@ -389,6 +457,9 @@ void rbduckdb_init_duckdb_appender(void) {
#endif
cDuckDBAppender = rb_define_class_under(mDuckDB, "Appender", rb_cObject);
rb_define_alloc_func(cDuckDBAppender, allocate);
#ifdef HAVE_DUCKDB_H_GE_V1_4_0
rb_define_singleton_method(cDuckDBAppender, "create_query", appender_s_create_query, 5);
#endif
rb_define_method(cDuckDBAppender, "initialize", appender_initialize, 3);
rb_define_method(cDuckDBAppender, "error_message", appender_error_message, 0);
rb_define_private_method(cDuckDBAppender, "_end_row", appender__end_row, 0);
Expand Down
6 changes: 6 additions & 0 deletions ext/duckdb/logical_type.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ static void deallocate(void *ctx) {
xfree(p);
}

rubyDuckDBLogicalType *get_struct_logical_type(VALUE obj) {
rubyDuckDBLogicalType *ctx;
TypedData_Get_Struct(obj, rubyDuckDBLogicalType, &logical_type_data_type, ctx);
return ctx;
}

static VALUE allocate(VALUE klass) {
rubyDuckDBLogicalType *ctx = xcalloc((size_t)1, sizeof(rubyDuckDBLogicalType));
return TypedData_Wrap_Struct(klass, &logical_type_data_type, ctx);
Expand Down
2 changes: 1 addition & 1 deletion ext/duckdb/logical_type.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ typedef struct _rubyDuckDBLogicalType rubyDuckDBLogicalType;

void rbduckdb_init_duckdb_logical_type(void);
VALUE rbduckdb_create_logical_type(duckdb_logical_type logical_type);

rubyDuckDBLogicalType *get_struct_logical_type(VALUE obj);
#endif
4 changes: 4 additions & 0 deletions lib/duckdb/appender.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ class Appender
private_constant :RANGE_INT16, :RANGE_INT32, :RANGE_INT64
# :startdoc:

class << self
alias from_query create_query if respond_to?(:create_query)
end

# :call-seq:
# appender.begin_row -> self
# A nop method, provided for backwards compatibility reasons.
Expand Down
53 changes: 53 additions & 0 deletions test/duckdb_test/appender_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,59 @@ def test_s_new_with_schema
assert_raises(DuckDB::Error) { appender = DuckDB::Appender.new(@con, 'b', 'b') }
end

def test_s_create_query
unless DuckDB::Appender.respond_to?(:create_query)
skip 'DuckDB::Appender.create_query is not supported in this DuckDB version'
end

query = 'INSERT OR REPLACE INTO t SELECT i, val FROM my_appended_data'
types = [DuckDB::LogicalType::INTEGER, DuckDB::LogicalType::VARCHAR]
appender = DuckDB::Appender.create_query(@con, query, types, 'my_appended_data', %w[i val])

assert_instance_of(DuckDB::Appender, appender)
end

# test for alias of create_query
def test_s_from_query
unless DuckDB::Appender.respond_to?(:create_from_query)
skip 'DuckDB::Appender.create_query is not supported in this DuckDB version'
end

query = 'INSERT OR REPLACE INTO t SELECT i, val FROM my_appended_data'
types = [DuckDB::LogicalType::INTEGER, DuckDB::LogicalType::VARCHAR]
appender = DuckDB::Appender.from_query(@con, query, types, 'my_appended_data', %w[i val])

assert_instance_of(DuckDB::Appender, appender)
end

def test_s_create_query_append_test
unless DuckDB::Appender.respond_to?(:create_query)
skip 'DuckDB::Appender.create_query is not supported in this DuckDB version'
end

@con.query('CREATE TABLE t (i INT PRIMARY KEY, value VARCHAR)')
@con.query("INSERT INTO t VALUES (1, 'hello')")

query = 'INSERT OR REPLACE INTO t SELECT i, val FROM my_appended_data'
types = [DuckDB::LogicalType::INTEGER, DuckDB::LogicalType::VARCHAR]
appender = DuckDB::Appender.create_query(@con, query, types, 'my_appended_data', %w[i val])

appender.begin_row
appender.append_int32(1)
appender.append_varchar('hello world')
appender.end_row
appender.flush

appender.begin_row
appender.append_int32(2)
appender.append_varchar('bye bye')
appender.end_row
appender.flush

r = @con.query('SELECT * FROM t ORDER BY i')
assert_equal([[1, 'hello world'], [2, 'bye bye']], r.to_a)
end

def sub_test_append_column2(method, type, values:, expected:)
create_appender("col #{type}")
@appender.send(method, *values)
Expand Down