diff --git a/.github/workflows/make_documents.yml b/.github/workflows/make_documents.yml new file mode 100644 index 00000000..65c2954d --- /dev/null +++ b/.github/workflows/make_documents.yml @@ -0,0 +1,34 @@ +name: Deploy RDoc to GitHub Pages + +on: + push: + branches: + - main + +jobs: + deploy: + name: Build and Deploy RDoc + runs-on: ubuntu-latest + + steps: + # リポジトリをクローン + - name: Checkout code + uses: actions/checkout@v3 + + # Ruby をセットアップ + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.4 + + # RDoc ドキュメントを生成 + - name: Generate RDoc + run: rdoc -o docs + + # GitHub Pages 用にデプロイ + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_branch: gh-pages + publish_dir: ./docs diff --git a/.github/workflows/ruby_asan_on_ubuntu.yml b/.github/workflows/ruby_asan_on_ubuntu.yml new file mode 100644 index 00000000..a9eaafb7 --- /dev/null +++ b/.github/workflows/ruby_asan_on_ubuntu.yml @@ -0,0 +1,71 @@ +name: Ubuntu + +on: + push: + branches: + - main + pull_request: + types: + - opened + - synchronize + - reopened + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + ruby: ['asan'] + duckdb: ['1.1.3', '1.1.1', '1.0.0'] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + + # - name: duckdb cache + # id: duckdb-cache + # uses: actions/cache@v4 + # with: + # path: duckdb-v${{ matrix.duckdb }} + # key: ${{ runner.os }}-duckdb-v${{ matrix.duckdb }} + + - name: Build duckdb ${{ matrix.duckdb }} + env: + DUCKDB_VERSION: ${{ matrix.duckdb }} + if: steps.duckdb-cache.outputs.cache-hit != 'true' + run: | + git clone -b v$DUCKDB_VERSION https://github.com/cwida/duckdb.git duckdb-tmp-v$DUCKDB_VERSION + cd duckdb-tmp-v$DUCKDB_VERSION && make debug && cd .. + rm -rf duckdb-v$DUCKDB_VERSION + mkdir -p duckdb-v$DUCKDB_VERSION/build/debug/src duckdb-v$DUCKDB_VERSION/src + cp -rip duckdb-tmp-v$DUCKDB_VERSION/build/debug/src/*.so duckdb-v$DUCKDB_VERSION/build/debug/src/ + cp -rip duckdb-tmp-v$DUCKDB_VERSION/src/include duckdb-v$DUCKDB_VERSION/src/ + + - name: bundle install with Ruby ${{ matrix.ruby }} + env: + DUCKDB_VERSION: ${{ matrix.duckdb }} + run: | + bundle install --jobs 4 --retry 3 + + - name: Build test with DUCKDB_API_NO_DEPRECATED and Ruby ${{ matrix.ruby }} + env: + DUCKDB_VERSION: ${{ matrix.duckdb }} + run: | + env DUCKDB_API_NO_DEPRECATED=1 bundle exec rake build -- --with-duckdb-include=${GITHUB_WORKSPACE}/duckdb-v${DUCKDB_VERSION}/src/include --with-duckdb-lib=${GITHUB_WORKSPACE}/duckdb-v${DUCKDB_VERSION}/build/debug/src/ + bundle exec rake clean + + - name: Build with Ruby ${{ matrix.ruby }} + env: + DUCKDB_VERSION: ${{ matrix.duckdb }} + run: | + bundle exec rake build -- --with-duckdb-include=${GITHUB_WORKSPACE}/duckdb-v${DUCKDB_VERSION}/src/include --with-duckdb-lib=${GITHUB_WORKSPACE}/duckdb-v${DUCKDB_VERSION}/build/debug/src/ + + - name: test with Ruby ${{ matrix.ruby }} + env: + DUCKDB_VERSION: ${{ matrix.duckdb }} + run: | + env RUBYOPT=-W:deprecated ruby -Ilib test/duckdb_test/ruby_asan_test.rb diff --git a/.github/workflows/test_on_macos.yml b/.github/workflows/test_on_macos.yml index 18b0a5af..17da2b4d 100644 --- a/.github/workflows/test_on_macos.yml +++ b/.github/workflows/test_on_macos.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: ruby: ['3.1.6', '3.2.6', '3.3.6', '3.4.1', 'head'] - duckdb: ['1.1.3', '1.1.1', '1.0.0'] + duckdb: ['1.2.0', '1.1.3', '1.1.1', '1.0.0'] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/test_on_ubuntu.yml b/.github/workflows/test_on_ubuntu.yml index dd4334a4..21b0367e 100644 --- a/.github/workflows/test_on_ubuntu.yml +++ b/.github/workflows/test_on_ubuntu.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: ruby: ['3.1.6', '3.2.6', '3.3.6', '3.4.1', 'head'] - duckdb: ['1.1.3', '1.1.1', '1.0.0'] + duckdb: ['1.2.0', '1.1.3', '1.1.1', '1.0.0'] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/test_on_windows.yml b/.github/workflows/test_on_windows.yml index 8ca1b357..2ff5882e 100644 --- a/.github/workflows/test_on_windows.yml +++ b/.github/workflows/test_on_windows.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: ruby: ['3.1.6', '3.2.6', '3.3.6', 'ucrt', 'mingw', 'mswin', 'head'] - duckdb: ['1.1.3', '1.1.1', '1.0.0'] + duckdb: ['1.2.0', '1.1.3', '1.1.1', '1.0.0'] steps: - uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index 6299c232..704f235a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ *.so Makefile ng_test +/docs/ diff --git a/.rdoc_options b/.rdoc_options new file mode 100644 index 00000000..ccd1f6b1 --- /dev/null +++ b/.rdoc_options @@ -0,0 +1,22 @@ +--- +encoding: UTF-8 +static_path: [] +rdoc_include: [] +charset: UTF-8 +exclude: +- "~\\z" +- "\\.orig\\z" +- "\\.rej\\z" +- "\\.bak\\z" +- "\\.gemspec\\z" +- "ext/duckdb/extconf.rb" +- "Gemfile" +- "Gemfile.lock" +- "Rakefile" +- "getduckdb.sh" +- "rdoc.log" +- "tmp" +- "mkmf.log" +- "Dockerfile" +- "bin" +main_page: "README.md" diff --git a/CHANGELOG.md b/CHANGELOG.md index 311f7fa7..0d752e3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,20 @@ All notable changes to this project will be documented in this file. # Unreleased +- bump duckdb to 1.2.0. +- add `DuckDB::LogicalType` class(Thanks to @otegami). + - `DuckDB::LogicalType` class is under construction. `DuckDB::LogicalType#type`, `DuckDB::LogicalType#width`, + `DuckDB::LogicalType#scale`, `DuckDB::LogicalType#child_type` are available. - add `DuckDB::Appender#error_message`. - fix error message when `DuckDB::Appender#flush`, `DuckDB::Appender#close`, `DuckDB::Appender#end_row`, - `DuckDB::Appender#append_bool`, `DuckDB::Appender#append_int8`, `DuckDB::Appender#append_int16` failed. + `DuckDB::Appender#append_bool`, `DuckDB::Appender#append_int8`, `DuckDB::Appender#append_int16`, + `DuckDB::Appender#append_int32`, `DuckDB::Appender#append_int64`, `DuckDB::Appender#append_uint8` failed. - `DuckDB::Appender#begin_row` does nothing. Only returns self. `DuckDB::Appender#end_row` is only required. +## Breaking changes +- `DuckDB::Result#row_count`, `DuckDB::Result#row_size` are deprecated. +- `DuckDB::Result#use_chunk_each?`, `DuckDB::Result#use_chunk_each=` are deprecated. + # 1.1.3.1 - 2024-11-27 - fix to `DuckDB::Connection#query` with multiple SQL statements. Calling PreparedStatement#destroy after each statement executed. - install valgrind in docker development environment. diff --git a/Dockerfile b/Dockerfile index 7f038706..435c1952 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ ARG RUBY_VERSION=3.3.6 FROM ruby:${RUBY_VERSION} -ARG DUCKDB_VERSION=1.1.3 +ARG DUCKDB_VERSION=1.2.0 ARG VALGRIND_VERSION=3.21.0 RUN apt update -qq && \ diff --git a/README.md b/README.md index 70aa0635..8fa158bc 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,8 @@ gem install duckdb -- --with-duckdb-include=/duckdb_header_directory --with-duck ## Usage +The followings are some examples, for more detailed information, please refer to the [documentation](https://suketa.github.io/ruby-duckdb/index.html). + ```ruby require 'duckdb' diff --git a/ext/duckdb/appender.c b/ext/duckdb/appender.c index 1bbcc8bf..4f7be1d0 100644 --- a/ext/duckdb/appender.c +++ b/ext/duckdb/appender.c @@ -9,10 +9,10 @@ static VALUE appender_initialize(VALUE klass, VALUE con, VALUE schema, VALUE tab static VALUE appender_error_message(VALUE self); static VALUE appender__append_bool(VALUE self, VALUE val); static VALUE appender__append_int8(VALUE self, VALUE val); -static VALUE appender__apend_int16(VALUE self, VALUE val); -static VALUE appender_append_int32(VALUE self, VALUE val); -static VALUE appender_append_int64(VALUE self, VALUE val); -static VALUE appender_append_uint8(VALUE self, VALUE val); +static VALUE appender__append_int16(VALUE self, VALUE val); +static VALUE appender__append_int32(VALUE self, VALUE val); +static VALUE appender__append_int64(VALUE self, VALUE val); +static VALUE appender__append_uint8(VALUE self, VALUE val); static VALUE appender_append_uint16(VALUE self, VALUE val); static VALUE appender_append_uint32(VALUE self, VALUE val); static VALUE appender_append_uint64(VALUE self, VALUE val); @@ -138,7 +138,7 @@ static VALUE appender__append_int8(VALUE self, VALUE val) { } /* :nodoc: */ -static VALUE appender__apend_int16(VALUE self, VALUE val) { +static VALUE appender__append_int16(VALUE self, VALUE val) { rubyDuckDBAppender *ctx; int16_t i16val = (int16_t)NUM2INT(val); @@ -147,72 +147,34 @@ static VALUE appender__apend_int16(VALUE self, VALUE val) { return duckdb_state_to_bool_value(duckdb_append_int16(ctx->appender, i16val)); } -/* call-seq: - * appender.append_int32(val) -> self - * - * Appends an int32(INTEGER) value to the current row in the appender. - * - * require 'duckdb' - * db = DuckDB::Database.open - * con = db.connect - * con.query('CREATE TABLE users (id INTEGER, age INTEGER)') - * appender = con.appender('users') - * appender - * .append_int32(1) - * .append_int32(20) - * .end_row - * .flush - */ -static VALUE appender_append_int32(VALUE self, VALUE val) { +/* :nodoc: */ +static VALUE appender__append_int32(VALUE self, VALUE val) { rubyDuckDBAppender *ctx; int32_t i32val = (int32_t)NUM2INT(val); TypedData_Get_Struct(self, rubyDuckDBAppender, &appender_data_type, ctx); - if (duckdb_append_int32(ctx->appender, i32val) == DuckDBError) { - rb_raise(eDuckDBError, "failed to append int32"); - } - return self; + return duckdb_state_to_bool_value(duckdb_append_int32(ctx->appender, i32val)); } -/* call-seq: - * appender.append_int64(val) -> self - * - * Appends an int64(BIGINT) value to the current row in the appender. - * - * require 'duckdb' - * db = DuckDB::Database.open - * con = db.connect - * con.query('CREATE TABLE users (id INTEGER, age BIGINT)') - * appender = con.appender('users') - * appender - * .append_int32(1) - * .append_int64(20) - * .end_row - * .flush - */ -static VALUE appender_append_int64(VALUE self, VALUE val) { +/* :nodoc: */ +static VALUE appender__append_int64(VALUE self, VALUE val) { rubyDuckDBAppender *ctx; int64_t i64val = (int64_t)NUM2LL(val); TypedData_Get_Struct(self, rubyDuckDBAppender, &appender_data_type, ctx); - if (duckdb_append_int64(ctx->appender, i64val) == DuckDBError) { - rb_raise(eDuckDBError, "failed to append int64"); - } - return self; + return duckdb_state_to_bool_value(duckdb_append_int64(ctx->appender, i64val)); } -static VALUE appender_append_uint8(VALUE self, VALUE val) { +/* :nodoc: */ +static VALUE appender__append_uint8(VALUE self, VALUE val) { rubyDuckDBAppender *ctx; - int8_t ui8val = (uint8_t)NUM2UINT(val); + uint8_t ui8val = (uint8_t)NUM2UINT(val); TypedData_Get_Struct(self, rubyDuckDBAppender, &appender_data_type, ctx); - if (duckdb_append_uint8(ctx->appender, ui8val) == DuckDBError) { - rb_raise(eDuckDBError, "failed to append uint8"); - } - return self; + return duckdb_state_to_bool_value(duckdb_append_uint8(ctx->appender, ui8val)); } static VALUE appender_append_uint16(VALUE self, VALUE val) { @@ -458,9 +420,6 @@ void rbduckdb_init_duckdb_appender(void) { rb_define_alloc_func(cDuckDBAppender, allocate); rb_define_method(cDuckDBAppender, "initialize", appender_initialize, 3); rb_define_method(cDuckDBAppender, "error_message", appender_error_message, 0); - rb_define_method(cDuckDBAppender, "append_int32", appender_append_int32, 1); - rb_define_method(cDuckDBAppender, "append_int64", appender_append_int64, 1); - rb_define_method(cDuckDBAppender, "append_uint8", appender_append_uint8, 1); rb_define_method(cDuckDBAppender, "append_uint16", appender_append_uint16, 1); rb_define_method(cDuckDBAppender, "append_uint32", appender_append_uint32, 1); rb_define_method(cDuckDBAppender, "append_uint64", appender_append_uint64, 1); @@ -480,7 +439,10 @@ void rbduckdb_init_duckdb_appender(void) { rb_define_private_method(cDuckDBAppender, "_close", appender__close, 0); rb_define_private_method(cDuckDBAppender, "_append_bool", appender__append_bool, 1); rb_define_private_method(cDuckDBAppender, "_append_int8", appender__append_int8, 1); - rb_define_private_method(cDuckDBAppender, "_append_int16", appender__apend_int16, 1); + rb_define_private_method(cDuckDBAppender, "_append_int16", appender__append_int16, 1); + rb_define_private_method(cDuckDBAppender, "_append_int32", appender__append_int32, 1); + rb_define_private_method(cDuckDBAppender, "_append_int64", appender__append_int64, 1); + rb_define_private_method(cDuckDBAppender, "_append_uint8", appender__append_uint8, 1); rb_define_private_method(cDuckDBAppender, "_append_date", appender__append_date, 3); rb_define_private_method(cDuckDBAppender, "_append_interval", appender__append_interval, 3); rb_define_private_method(cDuckDBAppender, "_append_time", appender__append_time, 4); diff --git a/ext/duckdb/extconf.rb b/ext/duckdb/extconf.rb index 9324095c..cce41d2a 100644 --- a/ext/duckdb/extconf.rb +++ b/ext/duckdb/extconf.rb @@ -61,9 +61,6 @@ def print_message(msg) # check duckdb >= 1.0.0 have_func('duckdb_fetch_chunk', 'duckdb.h') -# check duckdb >= 1.0.0 -have_func('duckdb_fetch_chunk', 'duckdb.h') - # check duckdb >= 1.1.0 have_func('duckdb_result_error_type', 'duckdb.h') diff --git a/ext/duckdb/logical_type.c b/ext/duckdb/logical_type.c index e2254f32..971776c5 100644 --- a/ext/duckdb/logical_type.c +++ b/ext/duckdb/logical_type.c @@ -5,8 +5,10 @@ static VALUE cDuckDBLogicalType; static void deallocate(void *ctx); static VALUE allocate(VALUE klass); static size_t memsize(const void *p); +static VALUE duckdb_logical_type__type(VALUE self); static VALUE duckdb_logical_type_width(VALUE self); static VALUE duckdb_logical_type_scale(VALUE self); +static VALUE duckdb_logical_type_child_type(VALUE self); static const rb_data_type_t logical_type_data_type = { "DuckDB/LogicalType", @@ -33,6 +35,19 @@ static size_t memsize(const void *p) { return sizeof(rubyDuckDBLogicalType); } +/* + * call-seq: + * decimal_col.logical_type.type -> Symbol + * + * Returns the logical type's type symbol. + * + */ +static VALUE duckdb_logical_type__type(VALUE self) { + rubyDuckDBLogicalType *ctx; + TypedData_Get_Struct(self, rubyDuckDBLogicalType, &logical_type_data_type, ctx); + return INT2FIX(duckdb_get_type_id(ctx->logical_type)); +} + /* * call-seq: * decimal_col.logical_type.width -> Integer @@ -59,6 +74,34 @@ static VALUE duckdb_logical_type_scale(VALUE self) { return INT2FIX(duckdb_decimal_scale(ctx->logical_type)); } +/* + * call-seq: + * list_col.logical_type.child_type -> DuckDB::LogicalType + * + * Returns the child logical type for list and map types, otherwise nil. + * + */ +static VALUE duckdb_logical_type_child_type(VALUE self) { + rubyDuckDBLogicalType *ctx; + duckdb_type type_id; + duckdb_logical_type child_logical_type; + VALUE logical_type = Qnil; + + TypedData_Get_Struct(self, rubyDuckDBLogicalType, &logical_type_data_type, ctx); + type_id = duckdb_get_type_id(ctx->logical_type); + + switch(type_id) { + case DUCKDB_TYPE_LIST: + case DUCKDB_TYPE_MAP: + child_logical_type = duckdb_list_type_child_type(ctx->logical_type); + logical_type = rbduckdb_create_logical_type(child_logical_type); + break; + default: + logical_type = Qnil; + } + return logical_type; +} + VALUE rbduckdb_create_logical_type(duckdb_logical_type logical_type) { VALUE obj; rubyDuckDBLogicalType *ctx; @@ -78,6 +121,8 @@ void rbduckdb_init_duckdb_logical_type(void) { cDuckDBLogicalType = rb_define_class_under(mDuckDB, "LogicalType", rb_cObject); rb_define_alloc_func(cDuckDBLogicalType, allocate); + rb_define_private_method(cDuckDBLogicalType, "_type", duckdb_logical_type__type, 0); rb_define_method(cDuckDBLogicalType, "width", duckdb_logical_type_width, 0); rb_define_method(cDuckDBLogicalType, "scale", duckdb_logical_type_scale, 0); + rb_define_method(cDuckDBLogicalType, "child_type", duckdb_logical_type_child_type, 0); } diff --git a/ext/duckdb/prepared_statement.c b/ext/duckdb/prepared_statement.c index edf4b97e..ec6faff0 100644 --- a/ext/duckdb/prepared_statement.c +++ b/ext/duckdb/prepared_statement.c @@ -81,6 +81,8 @@ VALUE rbduckdb_prepared_statement_new(duckdb_connection con, duckdb_extracted_st static VALUE duckdb_prepared_statement_initialize(VALUE self, VALUE con, VALUE query) { rubyDuckDBConnection *ctxcon; rubyDuckDBPreparedStatement *ctx; + duckdb_state state; + const char *error; if (!rb_obj_is_kind_of(con, cDuckDBConnection)) { rb_raise(rb_eTypeError, "1st argument should be instance of DackDB::Connection"); @@ -89,10 +91,20 @@ static VALUE duckdb_prepared_statement_initialize(VALUE self, VALUE con, VALUE q TypedData_Get_Struct(self, rubyDuckDBPreparedStatement, &prepared_statement_data_type, ctx); ctxcon = get_struct_connection(con); - if (duckdb_prepare(ctxcon->con, StringValuePtr(query), &(ctx->prepared_statement)) == DuckDBError) { - const char *error = duckdb_prepare_error(ctx->prepared_statement); + // Initialize to NULL before preparing + ctx->prepared_statement = NULL; + + // Try to prepare the statement and get the state + state = duckdb_prepare(ctxcon->con, StringValuePtr(query), &(ctx->prepared_statement)); + + // Get error message before any exception might be thrown + error = duckdb_prepare_error(ctx->prepared_statement); + + // If preparation failed, raise Ruby exception with the error message + if (state == DuckDBError) { rb_raise(eDuckDBError, "%s", error ? error : "Failed to prepare statement(Database connection closed?)."); } + return self; } diff --git a/ext/duckdb/result.c b/ext/duckdb/result.c index 33a7fd3d..8bdab51c 100644 --- a/ext/duckdb/result.c +++ b/ext/duckdb/result.c @@ -24,7 +24,6 @@ static void deallocate(void *ctx); static VALUE allocate(VALUE klass); static size_t memsize(const void *p); static VALUE duckdb_result_column_count(VALUE oDuckDBResult); -static VALUE duckdb_result_row_count(VALUE oDuckDBResult); static VALUE duckdb_result_rows_changed(VALUE oDuckDBResult); static VALUE duckdb_result_columns(VALUE oDuckDBResult); static VALUE duckdb_result_streaming_p(VALUE oDuckDBResult); @@ -147,36 +146,6 @@ static VALUE duckdb_result_column_count(VALUE oDuckDBResult) { return LL2NUM(duckdb_column_count(&(ctx->result))); } -/* - * call-seq: - * result.row_count -> Integer - * - * Returns the column size of the result. - * - * DuckDB::Database.open do |db| - * db.connect do |con| - * r = con.query('CREATE TABLE t2 (id INT, name VARCHAR(128))') - * r = con.query("INSERT INTO t2 VALUES (1, 'Alice'), (2, 'Bob'), (3, 'Catherine')") - * r = con.query('SELECT * FROM t2') - * r.row_count # => 3 - * r = con.query('SELECT * FROM t2 where id = 1') - * r.row_count # => 1 - * end - * end - * - */ -static VALUE duckdb_result_row_count(VALUE oDuckDBResult) { - -#ifdef DUCKDB_API_NO_DEPRECATED - return Qnil; -#else - rubyDuckDBResult *ctx; - rb_warn("`row_count` will be deprecated in the future."); - TypedData_Get_Struct(oDuckDBResult, rubyDuckDBResult, &result_data_type, ctx); - return LL2NUM(duckdb_row_count(&(ctx->result))); -#endif -} - /* * call-seq: * result.columns -> DuckDB::Column[] @@ -270,11 +239,7 @@ static VALUE duckdb_result__chunk_stream(VALUE oDuckDBResult) { arg.col_count = duckdb_column_count(&(ctx->result)); -#ifdef HAVE_DUCKDB_H_GE_V1_0_0 while((arg.chunk = duckdb_fetch_chunk(ctx->result)) != NULL) { -#else - while((arg.chunk = duckdb_stream_fetch_chunk(ctx->result)) != NULL) { -#endif rb_ensure(yield_rows, (VALUE)&arg, destroy_data_chunk, (VALUE)&arg); } return Qnil; @@ -913,7 +878,6 @@ void rbduckdb_init_duckdb_result(void) { rb_define_alloc_func(cDuckDBResult, allocate); rb_define_method(cDuckDBResult, "column_count", duckdb_result_column_count, 0); - rb_define_method(cDuckDBResult, "row_count", duckdb_result_row_count, 0); /* deprecated */ rb_define_method(cDuckDBResult, "rows_changed", duckdb_result_rows_changed, 0); rb_define_method(cDuckDBResult, "columns", duckdb_result_columns, 0); rb_define_method(cDuckDBResult, "streaming?", duckdb_result_streaming_p, 0); diff --git a/lib/duckdb.rb b/lib/duckdb.rb index 2418f037..839b5995 100644 --- a/lib/duckdb.rb +++ b/lib/duckdb.rb @@ -13,6 +13,7 @@ require 'duckdb/appender' require 'duckdb/config' require 'duckdb/column' +require 'duckdb/logical_type' require 'duckdb/infinity' # DuckDB provides Ruby interface of DuckDB. diff --git a/lib/duckdb/appender.rb b/lib/duckdb/appender.rb index acd02005..46712578 100644 --- a/lib/duckdb/appender.rb +++ b/lib/duckdb/appender.rb @@ -51,7 +51,7 @@ def end_row end # :call-seq: - # flush -> self + # appender.flush -> self # # Flushes the appender to the table, forcing the cache of the appender to be cleared. # If flushing the data triggers a constraint violation or any other error, then all @@ -74,7 +74,7 @@ def flush end # :call-seq: - # close -> self + # appender.close -> self # # Closes the appender by flushing all intermediate states and closing it for further appends. # If flushing the data triggers a constraint violation or any other error, then all data is @@ -160,6 +160,69 @@ def append_int16(value) raise_appender_error('failed to append_int16') end + # call-seq: + # appender.append_int32(val) -> self + # + # Appends an int32(INTEGER) value to the current row in the appender. + # + # require 'duckdb' + # db = DuckDB::Database.open + # con = db.connect + # con.query('CREATE TABLE users (id INTEGER, age INTEGER)') + # appender = con.appender('users') + # appender + # .append_int32(1) + # .append_int32(20) + # .end_row + # .flush + def append_int32(value) + return self if _append_int32(value) + + raise_appender_error('failed to append_int32') + end + + # call-seq: + # appender.append_int64(val) -> self + # + # Appends an int64(BIGINT) value to the current row in the appender. + # + # require 'duckdb' + # db = DuckDB::Database.open + # con = db.connect + # con.query('CREATE TABLE users (id INTEGER, age BIGINT)') + # appender = con.appender('users') + # appender + # .append_int32(1) + # .append_int64(20) + # .end_row + # .flush + def append_int64(value) + return self if _append_int64(value) + + raise_appender_error('failed to append_int64') + end + + # call-seq: + # appender.append_uint8(val) -> self + # + # Appends an uint8 value to the current row in the appender. + # + # require 'duckdb' + # db = DuckDB::Database.open + # con = db.connect + # con.query('CREATE TABLE users (id INTEGER, age UTINYINT)') + # appender = con.appender('users') + # appender + # .append_int32(1) + # .append_uint8(20) + # .end_row + # .flush + def append_uint8(value) + return self if _append_uint8(value) + + raise_appender_error('failed to append_uint8') + end + # appends huge int value. # # require 'duckdb' diff --git a/lib/duckdb/logical_type.rb b/lib/duckdb/logical_type.rb new file mode 100644 index 00000000..fb38b670 --- /dev/null +++ b/lib/duckdb/logical_type.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module DuckDB + class LogicalType + # returns logical type's type symbol + # `:unknown` means that the logical type's type is unknown/unsupported by ruby-duckdb. + # `:invalid` means that the logical type's type is invalid in duckdb. + # + # require 'duckdb' + # db = DuckDB::Database.open + # con = db.connect + # con.query('CREATE TABLE climates (id INTEGER, temperature DECIMAIL)') + # + # users = con.query('SELECT * FROM climates') + # columns = users.columns + # columns.second.logical_type.type #=> :decimal + def type + type_id = _type + DuckDB::Converter::IntToSym.type_to_sym(type_id) + end + end +end diff --git a/lib/duckdb/result.rb b/lib/duckdb/result.rb index 0a1502c9..41ca7d75 100644 --- a/lib/duckdb/result.rb +++ b/lib/duckdb/result.rb @@ -27,25 +27,11 @@ class Result RETURN_TYPES = %i[invalid changed_rows nothing query_result].freeze alias column_size column_count - alias row_size row_count class << self def new raise DuckDB::Error, 'DuckDB::Result cannot be instantiated directly.' end - - def use_chunk_each=(value) # :nodoc: - raise('`changing DuckDB::Result.use_chunk_each to false` was deprecated.') unless value - - warn('`DuckDB::Result.use_chunk_each=` will be deprecated.') - - true - end - - def use_chunk_each? # :nodoc: - warn('`DuckDB::Result.use_chunk_each?` will be deprecated.') - true - end end def each(&) diff --git a/test/duckdb_test/appender_test.rb b/test/duckdb_test/appender_test.rb index 681697ac..4ab91646 100644 --- a/test/duckdb_test/appender_test.rb +++ b/test/duckdb_test/appender_test.rb @@ -5,19 +5,23 @@ module DuckDBTest class AppenderTest < Minitest::Test - def self.con - @db ||= DuckDB::Database.open # FIXME - @db.connect + def setup + @db = DuckDB::Database.open # FIXME + @con = @db.connect end - def setup - @con = AppenderTest.con + def safe_drop_table + @con.execute("DROP TABLE #{table};") + rescue DuckDB::Error + # ignore DuckDB::Error end def teardown - @con.execute("DROP TABLE #{table};") + safe_drop_table + @con.close + @db.close rescue DuckDB::Error - # ignore + # ignore DuckDB::Error end def table @@ -57,7 +61,7 @@ def sub_test_append_column2(method, type, values:, expected:) r = @con.execute("SELECT col FROM #{table}") assert_equal(expected, r.first.first, "in #{caller[0]}") ensure - teardown + safe_drop_table end def sub_assert_equal(expected, actual) @@ -82,7 +86,7 @@ def assert_duckdb_appender(expected, type, &block) r = @con.execute("SELECT col FROM #{table}") sub_assert_equal(expected, r.first.first) ensure - teardown + safe_drop_table end def test_begin_row diff --git a/test/duckdb_test/logical_type_test.rb b/test/duckdb_test/logical_type_test.rb index 9599d6b9..1410a7f1 100644 --- a/test/duckdb_test/logical_type_test.rb +++ b/test/duckdb_test/logical_type_test.rb @@ -4,22 +4,98 @@ module DuckDBTest class LogicalTypeTest < Minitest::Test + CREATE_TYPE_ENUM_SQL = <<~SQL + CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy', '𝘾𝝾օɭ 😎'); + SQL + CREATE_TABLE_SQL = <<~SQL - CREATE TABLE table1 - ( - decimal_col DECIMAL(9, 6) + CREATE TABLE table1( + boolean_col BOOLEAN, + tinyint_col TINYINT, + smallint_col SMALLINT, + integer_col INTEGER, + bigint_col BIGINT, + utinyint_col UTINYINT, + usmallint_col USMALLINT, + uinteger_col UINTEGER, + ubigint_col UBIGINT, + real_col REAL, + double_col DOUBLE, + date_col DATE, + time_col TIME, + timestamp_col timestamp, + interval_col INTERVAL, + hugeint_col HUGEINT, + varchar_col VARCHAR, + decimal_col DECIMAL(9, 6), + enum_col mood, + int_list_col INT[], + varchar_list_col VARCHAR[], + struct_col STRUCT(word VARCHAR, length INTEGER), + uuid_col UUID, + map_col MAP(INTEGER, VARCHAR) ); SQL INSERT_SQL = <<~SQL INSERT INTO table1 VALUES ( - 123.456789 + true, + 1, + 32767, + 2147483647, + 9223372036854775807, + 1, + 32767, + 2147483647, + 9223372036854775807, + 12345.375, + 123.456789, + '2019-11-03', + '12:34:56', + '2019-11-03 12:34:56', + '1 day', + 170141183460469231731687303715884105727, + 'string', + 123.456789, + 'sad', + [1, 2, 3], + ['a', 'b', 'c'], + ROW('Ruby', 4), + '#{SecureRandom.uuid}', + MAP{1: 'foo'} ) SQL SELECT_SQL = 'SELECT * FROM table1' + EXPECTED_TYPES = %i[ + boolean + tinyint + smallint + integer + bigint + utinyint + usmallint + uinteger + ubigint + float + double + date + time + timestamp + interval + hugeint + varchar + decimal + enum + list + list + struct + uuid + map + ].freeze + def setup @db = DuckDB::Database.open @con = @db.connect @@ -28,8 +104,9 @@ def setup @columns = result.columns end - def test_defined_klass - assert(DuckDB.const_defined?(:LogicalType)) + def test_type + logical_types = @columns.map(&:logical_type) + assert_equal(EXPECTED_TYPES, logical_types.map(&:type)) end def test_decimal_width @@ -42,9 +119,26 @@ def test_decimal_scale assert_equal(6, decimal_column.logical_type.scale) end + def test_list_child_type + list_columns = @columns.select { |column| column.type == :list } + child_types = list_columns.map do |list_column| + list_column.logical_type.child_type + end + assert(child_types.all? { |child_type| child_type.is_a?(DuckDB::LogicalType) }) + assert_equal([:integer, :varchar], child_types.map(&:type)) + end + + def test_map_child_type + map_column = @columns.detect { |column| column.type == :map } + child_type = map_column.logical_type.child_type + assert(child_type.is_a?(DuckDB::LogicalType)) + assert_equal(:struct, child_type.type) + end + private def create_data(con) + con.query(CREATE_TYPE_ENUM_SQL) con.query(CREATE_TABLE_SQL) con.query(INSERT_SQL) end diff --git a/test/duckdb_test/result_test.rb b/test/duckdb_test/result_test.rb index 82972b86..35607fd8 100644 --- a/test/duckdb_test/result_test.rb +++ b/test/duckdb_test/result_test.rb @@ -135,15 +135,6 @@ def test_column_count assert_equal(2, r.column_size) end - def test_row_count - r = @@con.query('SELECT * FROM table1') - assert_equal(2, r.row_count) - assert_equal(2, r.row_size) - r = @@con.query('SELECT * FROM table1 WHERE boolean_col = true') - assert_equal(1, r.row_count) - assert_equal(1, r.row_size) - end - def test_columns assert_instance_of(DuckDB::Column, @result.columns.first) end diff --git a/test/duckdb_test/ruby_asan_test.rb b/test/duckdb_test/ruby_asan_test.rb new file mode 100644 index 00000000..3524094b --- /dev/null +++ b/test/duckdb_test/ruby_asan_test.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +require 'duckdb' + +def run_duckdb_asan_test + db = DuckDB::Database.open + con = db.connect + DuckDB::PreparedStatement.new(con, 'INSERT INTO test VALUES (?, "hello")') +rescue StandardError => e + p e +end + +run_duckdb_asan_test