├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── make_documents.yml │ ├── test_on_macos.yml │ ├── test_on_ubuntu.yml │ └── test_on_windows.yml ├── .gitignore ├── .rdoc_options ├── CHANGELOG.md ├── CONTRIBUTION.md ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── benchmark ├── async_query.rb ├── converter_hugeint_ips.rb ├── get_converter_module_ips.rb └── to_intern_ips.rb ├── bin ├── console └── setup ├── docker-compose.yml ├── duckdb.gemspec ├── ext └── duckdb │ ├── appender.c │ ├── appender.h │ ├── blob.c │ ├── blob.h │ ├── column.c │ ├── column.h │ ├── config.c │ ├── config.h │ ├── connection.c │ ├── connection.h │ ├── converter.h │ ├── conveter.c │ ├── database.c │ ├── database.h │ ├── duckdb.c │ ├── error.c │ ├── error.h │ ├── extconf.rb │ ├── extracted_statements.c │ ├── extracted_statements.h │ ├── instance_cache.c │ ├── instance_cache.h │ ├── logical_type.c │ ├── logical_type.h │ ├── pending_result.c │ ├── pending_result.h │ ├── prepared_statement.c │ ├── prepared_statement.h │ ├── result.c │ ├── result.h │ ├── ruby-duckdb.h │ ├── util.c │ └── util.h ├── getduckdb.sh ├── lib ├── duckdb.rb └── duckdb │ ├── appender.rb │ ├── column.rb │ ├── config.rb │ ├── connection.rb │ ├── converter.rb │ ├── converter │ └── int_to_sym.rb │ ├── database.rb │ ├── extracted_statements.rb │ ├── infinity.rb │ ├── instance_cache.rb │ ├── interval.rb │ ├── library_version.rb │ ├── logical_type.rb │ ├── pending_result.rb │ ├── prepared_statement.rb │ ├── result.rb │ └── version.rb ├── sample ├── async_query.rb └── async_query_stream.rb └── test ├── duckdb_test.rb ├── duckdb_test ├── appender_test.rb ├── bind_hugeint_internal_test.rb ├── blob_test.rb ├── column_test.rb ├── config_test.rb ├── connection_execute_multiple_sql_test.rb ├── connection_query_test.rb ├── connection_test.rb ├── database_test.rb ├── duckdb_version.rb ├── enum_test.rb ├── extracted_statements_test.rb ├── instance_cache_test.rb ├── interval_test.rb ├── logical_type_test.rb ├── pending_result_test.rb ├── prepared_statement_bind_decimal_test.rb ├── prepared_statement_test.rb ├── result_array_test.rb ├── result_bit_test.rb ├── result_each_test.rb ├── result_list_test.rb ├── result_map_test.rb ├── result_struct_test.rb ├── result_test.rb ├── result_time_tz_test.rb ├── result_timestamp_ns_test.rb ├── result_timestamp_tz_test.rb ├── result_to_decimal_test.rb └── result_union_test.rb ├── ng └── connection_query_ng.rb └── test_helper.rb /.gitattributes: -------------------------------------------------------------------------------- 1 | *.rb diff=ruby 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: suketa 4 | -------------------------------------------------------------------------------- /.github/workflows/make_documents.yml: -------------------------------------------------------------------------------- 1 | name: Deploy RDoc to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy: 10 | name: Build and Deploy RDoc 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | # リポジトリをクローン 15 | - name: Checkout code 16 | uses: actions/checkout@v3 17 | 18 | # Ruby をセットアップ 19 | - name: Set up Ruby 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | ruby-version: 3.4 23 | 24 | # RDoc ドキュメントを生成 25 | - name: Generate RDoc 26 | run: rdoc -o docs 27 | 28 | # GitHub Pages 用にデプロイ 29 | - name: Deploy to GitHub Pages 30 | uses: peaceiris/actions-gh-pages@v3 31 | with: 32 | github_token: ${{ secrets.GITHUB_TOKEN }} 33 | publish_branch: gh-pages 34 | publish_dir: ./docs 35 | -------------------------------------------------------------------------------- /.github/workflows/test_on_macos.yml: -------------------------------------------------------------------------------- 1 | name: MacOS 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: 9 | - opened 10 | - synchronize 11 | - reopened 12 | 13 | jobs: 14 | test: 15 | runs-on: macos-latest 16 | strategy: 17 | matrix: 18 | ruby: ['3.2.7', '3.3.8', '3.4.2', 'head', '3.5.0-preview1'] 19 | duckdb: ['1.2.2', '1.1.3', '1.1.1', '1.3.0'] 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - name: Set up Ruby 25 | uses: ruby/setup-ruby@v1 26 | with: 27 | ruby-version: ${{ matrix.ruby }} 28 | 29 | - name: duckdb cache 30 | id: duckdb-cache 31 | uses: actions/cache@v4 32 | with: 33 | path: duckdb-v${{ matrix.duckdb }} 34 | key: ${{ runner.os }}-duckdb-v${{ matrix.duckdb }} 35 | 36 | - name: Build duckdb ${{ matrix.duckdb }} 37 | env: 38 | DUCKDB_VERSION: ${{ matrix.duckdb }} 39 | if: steps.duckdb-cache.outputs.cache-hit != 'true' 40 | run: | 41 | git clone -b v$DUCKDB_VERSION https://github.com/cwida/duckdb.git duckdb-tmp-v$DUCKDB_VERSION 42 | cd duckdb-tmp-v$DUCKDB_VERSION && make && cd .. 43 | rm -rf duckdb-v$DUCKDB_VERSION 44 | mkdir -p duckdb-v$DUCKDB_VERSION/build/release/src duckdb-v$DUCKDB_VERSION/src 45 | cp -rip duckdb-tmp-v$DUCKDB_VERSION/build/release/src/*.dylib duckdb-v$DUCKDB_VERSION/build/release/src 46 | cp -rip duckdb-tmp-v$DUCKDB_VERSION/src/include duckdb-v$DUCKDB_VERSION/src/ 47 | 48 | - name: bundle install with Ruby ${{ matrix.ruby }} 49 | env: 50 | DUCKDB_VERSION: ${{ matrix.duckdb }} 51 | run: | 52 | bundle install --jobs 4 --retry 3 53 | 54 | - name: Build test with DUCKDB_API_NO_DEPRECATED and Ruby ${{ matrix.ruby }} 55 | env: 56 | DUCKDB_VERSION: ${{ matrix.duckdb }} 57 | run: | 58 | 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/release/src/ 59 | bundle exec rake clean 60 | 61 | - name: Build with Ruby ${{ matrix.ruby }} 62 | env: 63 | DUCKDB_VERSION: ${{ matrix.duckdb }} 64 | run: | 65 | 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/release/src/ 66 | 67 | - name: test with Ruby ${{ matrix.ruby }} 68 | env: 69 | DUCKDB_VERSION: ${{ matrix.duckdb }} 70 | run: | 71 | export DYLD_LIBRARY_PATH=${GITHUB_WORKSPACE}/duckdb-v${DUCKDB_VERSION}/build/release/src:${DYLD_LIBRARY_PATH} 72 | env RUBYOPT=-W:deprecated rake test 73 | 74 | post-test: 75 | name: All tests passed on macos 76 | runs-on: macos-latest 77 | needs: test 78 | steps: 79 | - run: echo ok 80 | -------------------------------------------------------------------------------- /.github/workflows/test_on_ubuntu.yml: -------------------------------------------------------------------------------- 1 | name: Ubuntu 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: 9 | - opened 10 | - synchronize 11 | - reopened 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | ruby: ['3.2.7', '3.3.8', '3.4.2', 'head', '3.5.0-preview1'] 19 | duckdb: ['1.2.2', '1.1.3', '1.1.1', '1.3.0'] 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - name: Set up Ruby 25 | uses: ruby/setup-ruby@v1 26 | with: 27 | ruby-version: ${{ matrix.ruby }} 28 | 29 | - name: duckdb cache 30 | id: duckdb-cache 31 | uses: actions/cache@v4 32 | with: 33 | path: duckdb-v${{ matrix.duckdb }} 34 | key: ${{ runner.os }}-duckdb-v${{ matrix.duckdb }} 35 | 36 | - name: Build duckdb ${{ matrix.duckdb }} 37 | env: 38 | DUCKDB_VERSION: ${{ matrix.duckdb }} 39 | if: steps.duckdb-cache.outputs.cache-hit != 'true' 40 | run: | 41 | git clone -b v$DUCKDB_VERSION https://github.com/cwida/duckdb.git duckdb-tmp-v$DUCKDB_VERSION 42 | cd duckdb-tmp-v$DUCKDB_VERSION && make && cd .. 43 | rm -rf duckdb-v$DUCKDB_VERSION 44 | mkdir -p duckdb-v$DUCKDB_VERSION/build/release/src duckdb-v$DUCKDB_VERSION/src 45 | cp -rip duckdb-tmp-v$DUCKDB_VERSION/build/release/src/*.so duckdb-v$DUCKDB_VERSION/build/release/src 46 | cp -rip duckdb-tmp-v$DUCKDB_VERSION/src/include duckdb-v$DUCKDB_VERSION/src/ 47 | 48 | - name: bundle install with Ruby ${{ matrix.ruby }} 49 | env: 50 | DUCKDB_VERSION: ${{ matrix.duckdb }} 51 | run: | 52 | bundle install --jobs 4 --retry 3 53 | 54 | - name: Build test with DUCKDB_API_NO_DEPRECATED and Ruby ${{ matrix.ruby }} 55 | env: 56 | DUCKDB_VERSION: ${{ matrix.duckdb }} 57 | run: | 58 | 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/release/src/ 59 | bundle exec rake clean 60 | 61 | - name: Build with Ruby ${{ matrix.ruby }} 62 | env: 63 | DUCKDB_VERSION: ${{ matrix.duckdb }} 64 | run: | 65 | 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/release/src/ 66 | 67 | - name: test with Ruby ${{ matrix.ruby }} 68 | env: 69 | DUCKDB_VERSION: ${{ matrix.duckdb }} 70 | run: | 71 | env RUBYOPT=-W:deprecated rake test 72 | 73 | post-test: 74 | name: All tests passed on Ubuntu 75 | runs-on: ubuntu-latest 76 | needs: test 77 | steps: 78 | - run: echo ok 79 | -------------------------------------------------------------------------------- /.github/workflows/test_on_windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: 9 | - opened 10 | - synchronize 11 | - reopened 12 | 13 | jobs: 14 | test: 15 | runs-on: windows-latest 16 | strategy: 17 | matrix: 18 | # ruby: ['3.2.6', '3.3.8', '3.4.1', 'ucrt', 'mingw', 'mswin', 'head'] 19 | ruby: ['3.2.6', '3.3.8', '3.4.1', 'ucrt', 'mingw', 'mswin'] 20 | duckdb: ['1.2.2', '1.1.3', '1.1.1', '1.3.0'] 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Set up Ruby 26 | uses: ruby/setup-ruby@v1 27 | with: 28 | ruby-version: ${{ matrix.ruby }} 29 | 30 | - name: download duckdb binary for windows 64bit 31 | env: 32 | DUCKDB_VERSION: ${{ matrix.duckdb }} 33 | run: | 34 | curl -OL https://github.com/duckdb/duckdb/releases/download/v${env:DUCKDB_VERSION}/libduckdb-windows-amd64.zip 35 | 36 | - name: extract zip file 37 | run: | 38 | unzip libduckdb-windows-amd64.zip 39 | 40 | - name: setup duckdb.dll 41 | run: | 42 | cp duckdb.dll C:/Windows/System32/ 43 | 44 | - name: Build with Rake with Ruby ${{ matrix.ruby }} 45 | run: | 46 | bundle install 47 | bundle exec rake build -- --with-duckdb-include=../../../.. --with-duckdb-lib=../../../.. 48 | 49 | - name: rake test 50 | run: | 51 | rake test 52 | 53 | post-test: 54 | name: All tests passed on Windows 55 | runs-on: windows-latest 56 | needs: test 57 | steps: 58 | - run: echo ok 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | *.a 10 | *.bak 11 | *.bundle 12 | *.dll 13 | *.log 14 | *.o 15 | *.obj 16 | *.so 17 | Makefile 18 | ng_test 19 | /docs/ 20 | -------------------------------------------------------------------------------- /.rdoc_options: -------------------------------------------------------------------------------- 1 | --- 2 | encoding: UTF-8 3 | static_path: [] 4 | rdoc_include: [] 5 | charset: UTF-8 6 | exclude: 7 | - "~\\z" 8 | - "\\.orig\\z" 9 | - "\\.rej\\z" 10 | - "\\.bak\\z" 11 | - "\\.gemspec\\z" 12 | - "ext/duckdb/extconf.rb" 13 | - "Gemfile" 14 | - "Gemfile.lock" 15 | - "Rakefile" 16 | - "getduckdb.sh" 17 | - "rdoc.log" 18 | - "tmp" 19 | - "mkmf.log" 20 | - "Dockerfile" 21 | - "bin" 22 | main_page: "README.md" 23 | -------------------------------------------------------------------------------- /CONTRIBUTION.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | ## Environment setup 4 | 5 | ### With docker 6 | 7 | 1. Fork the repository and `git clone` to your local machine. 8 | 2. Build and access to docker container 9 | 10 | ``` 11 | docker compose build ubuntu 12 | docker compose run --rm ubuntu bash 13 | ``` 14 | 15 | In case you want custom ruby or duckdb versions, use `--build-arg` options 16 | ``` 17 | docker compose build ubuntu --build-arg RUBY_VERSION=3.1.3 --build-arg DUCKDB_VERSION=1.0.0 18 | ``` 19 | 20 | ### Without docker 21 | 22 | 1. Install [Ruby](https://www.ruby-lang.org/) into your local machine. 23 | 2. Install [duckdb](https://duckdb.org/) into your local machine. 24 | 3. Fork the repository and `git clone` to your local machine. 25 | 4. Run `bundle install` 26 | 5. Run `rake build` 27 | or you might run with C duckdb header and library directories: 28 | `rake build -- --with-duckdb-include=/duckdb_header_directory --with-duckdb-lib=/duckdb_library_directory` 29 | 30 | 31 | ## Issue 32 | 33 | If you spot a problem, [search if an issue already exists](https://github.com/suketa/ruby-duckdb/issues). 34 | If a related issue doesn't exist, you can open a [new issue](https://github.com/suketa/ruby-duckdb/issues/new). 35 | 36 | 37 | ## Fix Issues or Add New Features. 38 | 39 | 1. Run `rake test` 40 | 2. Create new branch to change the code. 41 | 3. Change the code. 42 | 4. Write test. 43 | 5. Run `rake test` and confirm all tests pass. 44 | 6. Git push. 45 | 7. Create PR. 46 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG RUBY_VERSION=3.4.4 2 | FROM ruby:${RUBY_VERSION} 3 | 4 | ARG DUCKDB_VERSION=1.3.0 5 | ARG VALGRIND_VERSION=3.21.0 6 | 7 | RUN apt-get update -qq && \ 8 | apt-get install -y --no-install-recommends build-essential curl git wget libc6-dbg && \ 9 | apt-get clean 10 | 11 | COPY getduckdb.sh . 12 | RUN ./getduckdb.sh 13 | 14 | RUN unzip duckdb.zip -d libduckdb && \ 15 | mv libduckdb/duckdb.* /usr/local/include && \ 16 | mv libduckdb/libduckdb.so /usr/local/lib && \ 17 | ldconfig /usr/local/lib 18 | 19 | RUN mkdir valgrind-tmp && \ 20 | cd valgrind-tmp && \ 21 | wget https://sourceware.org/pub/valgrind/valgrind-${VALGRIND_VERSION}.tar.bz2 && \ 22 | tar xf valgrind-${VALGRIND_VERSION}.tar.bz2 && \ 23 | cd valgrind-${VALGRIND_VERSION} && \ 24 | ./configure && \ 25 | make -s && \ 26 | make -s install && \ 27 | cd .. && \ 28 | rm -rf /valgrind-tmp 29 | 30 | COPY . /root/ruby-duckdb 31 | WORKDIR /root/ruby-duckdb 32 | RUN git config --global --add safe.directory /root/ruby-duckdb 33 | RUN bundle install && rake build 34 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in duckdb.gemspec 4 | gemspec 5 | 6 | if /(linux|darwin)/ =~ RUBY_PLATFORM 7 | gem 'benchmark-ips' 8 | gem 'stackprof' 9 | end 10 | 11 | if /linux/ =~ RUBY_PLATFORM 12 | gem 'ruby_memcheck' 13 | end 14 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | duckdb (1.3.0.0) 5 | bigdecimal (>= 3.1.4) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | benchmark-ips (2.14.0) 11 | bigdecimal (3.2.0) 12 | mini_portile2 (2.8.9) 13 | minitest (5.25.5) 14 | nokogiri (1.18.8) 15 | mini_portile2 (~> 2.8.2) 16 | racc (~> 1.4) 17 | nokogiri (1.18.8-aarch64-linux-gnu) 18 | racc (~> 1.4) 19 | nokogiri (1.18.8-arm-linux-gnu) 20 | racc (~> 1.4) 21 | nokogiri (1.18.8-arm64-darwin) 22 | racc (~> 1.4) 23 | nokogiri (1.18.8-x86_64-darwin) 24 | racc (~> 1.4) 25 | nokogiri (1.18.8-x86_64-linux-gnu) 26 | racc (~> 1.4) 27 | racc (1.8.1) 28 | rake (13.3.0) 29 | rake-compiler (1.3.0) 30 | rake 31 | ruby_memcheck (3.0.1) 32 | nokogiri 33 | stackprof (0.2.27) 34 | 35 | PLATFORMS 36 | aarch64-linux 37 | arm-linux 38 | arm64-darwin 39 | x86-linux 40 | x86_64-darwin 41 | x86_64-linux 42 | 43 | DEPENDENCIES 44 | benchmark-ips 45 | bundler (~> 2.3) 46 | duckdb! 47 | minitest (~> 5.0) 48 | rake (~> 13.0) 49 | rake-compiler 50 | ruby_memcheck 51 | stackprof 52 | 53 | BUNDLED WITH 54 | 2.5.22 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 suketa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ruby-duckdb 2 | 3 | [![Ubuntu](https://github.com/suketa/ruby-duckdb/workflows/Ubuntu/badge.svg)](https://github.com/suketa/ruby-duckdb/actions?query=workflow%3AUbuntu) 4 | [![MacOS](https://github.com/suketa/ruby-duckdb/workflows/MacOS/badge.svg)](https://github.com/suketa/ruby-duckdb/actions?query=workflow%3AMacOS) 5 | [![Windows](https://github.com/suketa/ruby-duckdb/workflows/Windows/badge.svg)](https://github.com/suketa/ruby-duckdb/actions?query=workflow%3AWindows) 6 | [![Gem Version](https://badge.fury.io/rb/duckdb.svg)](https://badge.fury.io/rb/duckdb) 7 | 8 | ## Description 9 | 10 | This gem `duckdb` is Ruby client for the [DuckDB](https://www.duckdb.org) database engine. 11 | 12 | ## Requirement 13 | 14 | You must have [DuckDB](https://www.duckdb.org) engine installed in order to use this gem. 15 | 16 | ## Pre-requisite setup (Linux): 17 | 1. Head over to the [DuckDB](https://duckdb.org/) webpage. 18 | 19 | 2. Download the latest C++ package release for DuckDB. 20 | 21 | 3. Move the files to their respective location: 22 | - Extract the `duckdb.h` and `duckdb.hpp` file to `/usr/local/include`. 23 | - Extract the `libduckdb.so` file to `/usr/local/lib`. 24 | 25 | ```sh 26 | unzip libduckdb-linux-amd64.zip -d libduckdb 27 | sudo mv libduckdb/duckdb.* /usr/local/include/ 28 | sudo mv libduckdb/libduckdb.so /usr/local/lib 29 | ``` 30 | 31 | 4. To create the necessary link, run `ldconfig` as root: 32 | 33 | ```sh 34 | sudo ldconfig /usr/local/lib # adding a --verbose flag is optional - but this will let you know if the libduckdb.so library has been linked 35 | ``` 36 | 37 | ## Pre-requisite setup (macOS): 38 | 39 | Using `brew install` is recommended. 40 | 41 | ```sh 42 | brew install duckdb 43 | ``` 44 | 45 | ## Pre-requisite setup (Windows): 46 | 47 | Using [Ruby + Devkit](https://rubyinstaller.org/downloads/) is recommended. 48 | 49 | 1. Download libduckdb-windows-amd64.zip from [DuckDB](https://github.com/duckdb/duckdb/releases) and extract it. 50 | 2. Copy `duckdb.dll` into `C:\Windows\System32` 51 | 52 | ## How to install 53 | 54 | ```sh 55 | gem install duckdb 56 | ``` 57 | 58 | After you've run the above pre-requisite setup, this should work fine. 59 | 60 | If it doesn't, you may have to specify the location of the C header and library files: 61 | 62 | ```sh 63 | gem install duckdb -- --with-duckdb-include=/duckdb_header_directory --with-duckdb-lib=/duckdb_library_directory 64 | ``` 65 | 66 | ## Usage 67 | 68 | The followings are some examples, for more detailed information, please refer to the [documentation](https://suketa.github.io/ruby-duckdb/index.html). 69 | 70 | ```ruby 71 | require 'duckdb' 72 | 73 | db = DuckDB::Database.open # database in memory 74 | con = db.connect 75 | 76 | con.query('CREATE TABLE users (id INTEGER, name VARCHAR(30))') 77 | 78 | con.query("INSERT into users VALUES(1, 'Alice')") 79 | con.query("INSERT into users VALUES(2, 'Bob')") 80 | con.query("INSERT into users VALUES(3, 'Cathy')") 81 | 82 | result = con.query('SELECT * from users') 83 | result.each do |row| 84 | puts row 85 | end 86 | ``` 87 | 88 | Or, you can use block. 89 | 90 | ```ruby 91 | require 'duckdb' 92 | 93 | DuckDB::Database.open do |db| 94 | db.connect do |con| 95 | con.query('CREATE TABLE users (id INTEGER, name VARCHAR(30))') 96 | 97 | con.query("INSERT into users VALUES(1, 'Alice')") 98 | con.query("INSERT into users VALUES(2, 'Bob')") 99 | con.query("INSERT into users VALUES(3, 'Cathy')") 100 | 101 | result = con.query('SELECT * from users') 102 | result.each do |row| 103 | puts row 104 | end 105 | end 106 | end 107 | ``` 108 | 109 | ### Using bind variables 110 | 111 | You can use bind variables. 112 | 113 | ```ruby 114 | con.query('SELECT * FROM users WHERE name = ? AND email = ?', 'Alice', 'alice@example.com') 115 | # or 116 | con.query('SELECT * FROM users WHERE name = $name AND email = $email', name: 'Alice', email: 'alice@example.com') 117 | ``` 118 | ### Using prepared statement 119 | 120 | You can use prepared statement. Prepared statement object is created by `Connection#prepare` method or `DuckDB::PreparedStatement.new`. 121 | 122 | ```ruby 123 | stmt = con.prepare('SELECT * FROM users WHERE name = $name AND email = $email') 124 | # or 125 | # stmt = con.prepared_statement('SELECT * FROM users WHERE name = $name AND email = $email') 126 | # or 127 | # stmt = DuckDB::PreparedStatement.new(con, 'SELECT * FROM users WHERE name = $name AND email = $email') 128 | stmt.bind(name: 'Alice', email: 'alice@example.com') 129 | result = stmt.execute 130 | stmt.destroy 131 | ``` 132 | You must call `PreparedStatement#destroy` method after using prepared statement. Otherwise, automatically destroyed 133 | when the PreparedStatement object is garbage collected. 134 | 135 | Instead of calling `PreparedStatement#destroy`, you can use block. 136 | 137 | ```ruby 138 | result = con.prepare('SELECT * FROM users WHERE name = $name AND email = $email') do |stmt| 139 | stmt.bind(name: 'Alice', email: 'alice@example.com') 140 | stmt.execute 141 | end 142 | ``` 143 | 144 | ### Using async query 145 | 146 | You can use async query. 147 | 148 | ```ruby 149 | pending_result = con.async_query('SLOW QUERY') 150 | pending_result.execute_task while pending_result.state == :not_ready 151 | 152 | result = pending_result.execute_pending 153 | result.each.first 154 | ``` 155 | 156 | Here is [the benchmark](./benchmark/async_query.rb). 157 | 158 | ### Using BLOB column 159 | 160 | Use `DuckDB::Blob.new` or `my_string.force_encoding(Encoding::BINARY)`. 161 | 162 | ```ruby 163 | require 'duckdb' 164 | 165 | DuckDB::Database.open do |db| 166 | db.connect do |con| 167 | con.query('CREATE TABLE blob_table (binary_data BLOB)') 168 | stmt = DuckDB::PreparedStatement.new(con, 'INSERT INTO blob_table VALUES ($1)') 169 | 170 | stmt.bind(1, DuckDB::Blob.new("\0\1\2\3\4\5")) 171 | # or 172 | # stmt.bind(1, "\0\1\2\3\4\5".force_encoding(Encoding::BINARY)) 173 | stmt.execute 174 | 175 | result = con.query('SELECT binary_data FROM blob_table') 176 | puts result.first.first 177 | end 178 | end 179 | ``` 180 | 181 | ### Appender 182 | 183 | Appender class provides Ruby interface of [DuckDB Appender](https://duckdb.org/docs/data/appender) 184 | 185 | ```ruby 186 | require 'duckdb' 187 | require 'benchmark' 188 | 189 | def insert 190 | DuckDB::Database.open do |db| 191 | db.connect do |con| 192 | con.query('CREATE TABLE users (id INTEGER, name VARCHAR(30))') 193 | 194 | 10000.times do 195 | con.query("INSERT into users VALUES(1, 'Alice')") 196 | end 197 | end 198 | end 199 | end 200 | 201 | def prepare 202 | DuckDB::Database.open do |db| 203 | db.connect do |con| 204 | con.query('CREATE TABLE users (id INTEGER, name VARCHAR(30))') 205 | stmt = con.prepared_statement('INSERT INTO users VALUES($1, $2)') 206 | 207 | 10000.times do 208 | stmt.bind(1, 1) 209 | stmt.bind(2, 'Alice') 210 | stmt.execute 211 | end 212 | end 213 | end 214 | end 215 | 216 | def append 217 | DuckDB::Database.open do |db| 218 | db.connect do |con| 219 | con.query('CREATE TABLE users (id INTEGER, name VARCHAR(30))') 220 | appender = con.appender('users') 221 | 222 | 10000.times do 223 | appender.append(1) 224 | appender.append('Alice') 225 | appender.end_row 226 | end 227 | 228 | appender.flush 229 | end 230 | end 231 | end 232 | 233 | Benchmark.bm(8) do |x| 234 | x.report('insert') { insert } 235 | x.report('prepare') { prepare } 236 | x.report('append') { append } 237 | end 238 | 239 | # => 240 | # user system total real 241 | # insert 0.637439 0.000000 0.637439 ( 0.637486 ) 242 | # prepare 0.230457 0.000000 0.230457 ( 0.230460 ) 243 | # append 0.012666 0.000000 0.012666 ( 0.012670 ) 244 | ``` 245 | 246 | ### Configuration 247 | 248 | Config class provides Ruby interface of [DuckDB configuration](https://duckdb.org/docs/api/c/config). 249 | 250 | ```ruby 251 | require 'duckdb' 252 | 253 | config = DuckDB::Config.new 254 | config['default_order'] = 'DESC' 255 | 256 | db = DuckDB::Database.open(nil, config) 257 | 258 | con = db.connect 259 | con.query('CREATE TABLE numbers (number INTEGER)') 260 | con.query('INSERT INTO numbers VALUES (2), (1), (4), (3)') 261 | 262 | # number is ordered by descending 263 | res = con.query('SELECT number FROM numbers ORDER BY number') 264 | res.first.first # => 4 265 | ``` 266 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake/testtask' 3 | ruby_memcheck_avaiable = begin 4 | require 'ruby_memcheck' 5 | true 6 | rescue LoadError 7 | false 8 | end 9 | 10 | 11 | if ruby_memcheck_avaiable 12 | RubyMemcheck.config( 13 | binary_name: 'duckdb/duckdb_native', 14 | valgrind_options: ['--max-threads=1000'] 15 | ) 16 | end 17 | 18 | test_config = lambda do |t| 19 | t.libs << 'test' 20 | t.libs << 'lib' 21 | t.test_files = FileList['test/**/*_test.rb'] 22 | end 23 | 24 | Rake::TestTask.new(test: :compile, &test_config) 25 | 26 | if ruby_memcheck_avaiable 27 | namespace :test do 28 | RubyMemcheck::TestTask.new(valgrind: :compile, &test_config) 29 | end 30 | end 31 | 32 | require 'rake/extensiontask' 33 | 34 | task build: :compile 35 | 36 | Rake::ExtensionTask.new('duckdb_native') do |ext| 37 | ext.ext_dir = 'ext/duckdb' 38 | ext.lib_dir = 'lib/duckdb' 39 | end 40 | 41 | task default: %i[clobber compile test] 42 | -------------------------------------------------------------------------------- /benchmark/async_query.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'duckdb' 3 | require 'benchmark/ips' 4 | 5 | DuckDB::Database.open do |db| 6 | db.connect do |con| 7 | con.query('SET threads=1') 8 | con.query('CREATE TABLE tbl as SELECT range a, mod(range, 10) b FROM range(100000)') 9 | con.query('CREATE TABLE tbl2 as SELECT range a, mod(range, 10) b FROM range(100000)') 10 | query_sql = 'SELECT * FROM tbl where b = (SELECT min(b) FROM tbl2)' 11 | print <<~END_OF_HEAD 12 | 13 | Benchmark: Get first record ====================================== 14 | END_OF_HEAD 15 | 16 | Benchmark.ips do |x| 17 | x.report('async_query') do 18 | pending_result = con.async_query(query_sql) 19 | 20 | pending_result.execute_task while pending_result.state == :not_ready 21 | result = pending_result.execute_pending 22 | result.each.first 23 | end 24 | x.report('query') do 25 | result = con.query(query_sql) 26 | result.each.first 27 | end 28 | x.report('async_query_stream') do 29 | pending_result = con.async_query_stream(query_sql) 30 | 31 | pending_result.execute_task while pending_result.state == :not_ready 32 | result = pending_result.execute_pending 33 | result.each.first 34 | end 35 | end 36 | 37 | print <<~END_OF_HEAD 38 | 39 | 40 | Benchmark: Get all records ====================================== 41 | END_OF_HEAD 42 | 43 | Benchmark.ips do |x| 44 | x.report('async_query') do 45 | pending_result = con.async_query(query_sql) 46 | 47 | pending_result.execute_task while pending_result.state == :not_ready 48 | result = pending_result.execute_pending 49 | result.each.to_a 50 | end 51 | x.report('query') do 52 | result = con.query(query_sql) 53 | result.each.to_a 54 | end 55 | x.report('async_query_stream') do 56 | pending_result = con.async_query_stream(query_sql) 57 | 58 | pending_result.execute_task while pending_result.state == :not_ready 59 | result = pending_result.execute_pending 60 | result.each.to_a 61 | end 62 | end 63 | end 64 | end 65 | 66 | __END__ 67 | 68 | results: 69 | Benchmark: Get first record ====================================== 70 | Warming up -------------------------------------- 71 | async_query 70.000 i/100ms 72 | query 88.000 i/100ms 73 | async_query_stream 188.000 i/100ms 74 | Calculating ------------------------------------- 75 | async_query 847.191 (± 4.6%) i/s - 4.270k in 5.051650s 76 | query 850.509 (± 3.8%) i/s - 4.312k in 5.078167s 77 | async_query_stream 1.757k (± 7.3%) i/s - 8.836k in 5.057142s 78 | 79 | 80 | Benchmark: Get all records ====================================== 81 | Warming up -------------------------------------- 82 | async_query 40.000 i/100ms 83 | query 40.000 i/100ms 84 | async_query_stream 39.000 i/100ms 85 | Calculating ------------------------------------- 86 | async_query 402.567 (± 0.5%) i/s - 2.040k in 5.067639s 87 | query 406.632 (± 0.7%) i/s - 2.040k in 5.017079s 88 | async_query_stream 395.532 (± 0.8%) i/s - 1.989k in 5.028955s 89 | -------------------------------------------------------------------------------- /benchmark/converter_hugeint_ips.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | require 'duckdb' 5 | require 'benchmark/ips' 6 | 7 | Benchmark.ips do |x| 8 | x.report('hugeint_convert') { DuckDB::Converter._to_hugeint_from_vector(123_456_789, 123_456_789) } 9 | end 10 | 11 | __END__ 12 | 13 | ## before 14 | ``` 15 | ✦ ❯ ruby benchmark/converter_hugeint_ips.rb 16 | Warming up -------------------------------------- 17 | hugeint_convert 318.524k i/100ms 18 | Calculating ------------------------------------- 19 | hugeint_convert 3.940M (± 0.7%) i/s - 19.748M in 5.012440s 20 | ``` 21 | 22 | ## after (use bit shift) 23 | ✦ ❯ ruby benchmark/converter_hugeint_ips.rb 24 | Warming up -------------------------------------- 25 | hugeint_convert 347.419k i/100ms 26 | Calculating ------------------------------------- 27 | hugeint_convert 4.171M (± 0.3%) i/s - 21.193M in 5.081131s 28 | -------------------------------------------------------------------------------- /benchmark/get_converter_module_ips.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | require 'duckdb' 5 | require 'benchmark/ips' 6 | 7 | db = DuckDB::Database.open 8 | con = db.connect 9 | con.query('CREATE TABLE hugeints (hugeint_val HUGEINT)') 10 | con.query('INSERT INTO hugeints VALUES (1234567890123456789012345678901234)') 11 | result = con.query('SELECT hugeint_val FROM hugeints') 12 | 13 | Benchmark.ips do |x| 14 | x.report('hugeint_convert') { result.each.to_a[0][0] } 15 | end 16 | 17 | __END__ 18 | 19 | ## before 20 | ``` 21 | ✦ ❯ ruby benchmark/get_converter_module_ips.rb 22 | Warming up -------------------------------------- 23 | hugeint_convert 45.376k i/100ms 24 | Calculating ------------------------------------- 25 | hugeint_convert 552.127k (± 0.7%) i/s - 2.768M in 5.013483s 26 | ``` 27 | -------------------------------------------------------------------------------- /benchmark/to_intern_ips.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | require 'duckdb' 5 | require 'benchmark/ips' 6 | 7 | db = DuckDB::Database.open 8 | con = db.connect 9 | con.query(<<~SQL 10 | CREATE TABLE t ( 11 | date_value DATE, 12 | time_value TIME, 13 | timestamp_value TIMESTAMP, 14 | interval_value INTERVAL, 15 | hugeint_value HUGEINT, 16 | uuid_value UUID, 17 | decimal_value DECIMAL(4, 2) 18 | ) 19 | SQL 20 | ) 21 | con.query(<<~SQL 22 | INSERT INTO t VALUES 23 | ( 24 | '2019-01-01', 25 | '12:00:00', 26 | '2019-01-01 12:00:00', 27 | '1 day', 28 | 12345678901234567890, 29 | 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 30 | 0.12 31 | ), 32 | ( 33 | '2019-01-01', 34 | '12:00:00', 35 | '2019-01-01 12:00:00', 36 | '1 day', 37 | 12345678901234567890, 38 | 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 39 | 0.12 40 | ), 41 | ( 42 | '2019-01-01', 43 | '12:00:00', 44 | '2019-01-01 12:00:00', 45 | '1 day', 46 | 12345678901234567890, 47 | 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', 48 | 2.12 49 | ) 50 | SQL 51 | ) 52 | result = con.query('SELECT * FROM t') 53 | 54 | Benchmark.ips do |x| 55 | x.report('_to_date') { result.each.to_a } 56 | end 57 | 58 | __END__ 59 | ``` 60 | ## before 61 | ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [x86_64-linux] 62 | Warming up -------------------------------------- 63 | _to_date 30.790k i/100ms 64 | Calculating ------------------------------------- 65 | _to_date 365.254k (± 0.2%) i/s - 1.847M in 5.057875s 66 | 67 | ## after 68 | ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [x86_64-linux] 69 | Warming up -------------------------------------- 70 | _to_date 36.047k i/100ms 71 | Calculating ------------------------------------- 72 | _to_date 383.760k (± 3.3%) i/s - 1.947M in 5.077849s 73 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "duckdb" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | ubuntu: 3 | build: 4 | context: . 5 | dockerfile: ./Dockerfile 6 | args: 7 | - RUBY_VERSION 8 | - DUCKDB_VERSION 9 | working_dir: /root/ruby-duckdb 10 | volumes: 11 | - .:/root/ruby-duckdb 12 | -------------------------------------------------------------------------------- /duckdb.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'duckdb/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'duckdb' 9 | spec.version = DuckDB::VERSION 10 | spec.authors = ['Masaki Suketa'] 11 | spec.email = ['masaki.suketa@nifty.ne.jp'] 12 | 13 | spec.summary = 'This module is Ruby binding for DuckDB database engine.' 14 | spec.description = 'This module is Ruby binding for DuckDB database engine. You must have the DuckDB engine installed to build/use this module.' 15 | spec.homepage = 'https://github.com/suketa/ruby-duckdb' 16 | spec.license = 'MIT' 17 | 18 | spec.metadata['rubygems_mfa_required'] = 'true' 19 | spec.metadata['homepage_uri'] = spec.homepage 20 | spec.metadata['source_code_uri'] = 'https://github.com/suketa/ruby-duckdb' 21 | spec.metadata['changelog_uri'] = 'https://github.com/suketa/ruby-duckdb/blob/master/CHANGELOG.md' 22 | 23 | # Specify which files should be added to the gem when it is released. 24 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 25 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 26 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 27 | end 28 | spec.require_paths = ['lib'] 29 | spec.extensions = ['ext/duckdb/extconf.rb'] 30 | spec.required_ruby_version = '>= 3.1.0' 31 | spec.add_dependency 'bigdecimal', '>= 3.1.4' 32 | 33 | spec.add_development_dependency 'bundler', '~> 2.3' 34 | spec.add_development_dependency 'minitest', '~> 5.0' 35 | spec.add_development_dependency 'rake', '~> 13.0' 36 | spec.add_development_dependency 'rake-compiler' 37 | end 38 | -------------------------------------------------------------------------------- /ext/duckdb/appender.h: -------------------------------------------------------------------------------- 1 | #ifndef RUBY_DUCKDB_APPENDER_H 2 | #define RUBY_DUCKDB_APPENDER_H 3 | 4 | struct _rubyDuckDBAppender { 5 | duckdb_appender appender; 6 | }; 7 | 8 | typedef struct _rubyDuckDBAppender rubyDuckDBAppender; 9 | 10 | void rbduckdb_init_duckdb_appender(void); 11 | 12 | #endif 13 | -------------------------------------------------------------------------------- /ext/duckdb/blob.c: -------------------------------------------------------------------------------- 1 | #include "ruby-duckdb.h" 2 | 3 | VALUE cDuckDBBlob; 4 | 5 | void rbduckdb_init_duckdb_blob(void) { 6 | #if 0 7 | VALUE mDuckDB = rb_define_module("DuckDB"); 8 | #endif 9 | cDuckDBBlob = rb_define_class_under(mDuckDB, "Blob", rb_cString); 10 | } 11 | -------------------------------------------------------------------------------- /ext/duckdb/blob.h: -------------------------------------------------------------------------------- 1 | #ifndef RUBY_DUCKDB_BLOB_H 2 | #define RUBY_DUCKDB_BLOB_H 3 | 4 | void rbduckdb_init_duckdb_blob(void); 5 | 6 | #endif 7 | 8 | -------------------------------------------------------------------------------- /ext/duckdb/column.c: -------------------------------------------------------------------------------- 1 | #include "ruby-duckdb.h" 2 | 3 | static VALUE cDuckDBColumn; 4 | 5 | static void deallocate(void *ctx); 6 | static VALUE allocate(VALUE klass); 7 | static size_t memsize(const void *p); 8 | static VALUE duckdb_column__type(VALUE oDuckDBColumn); 9 | static VALUE duckdb_column__logical_type(VALUE oDuckDBColumn); 10 | static VALUE duckdb_column_get_name(VALUE oDuckDBColumn); 11 | 12 | static const rb_data_type_t column_data_type = { 13 | "DuckDB/Column", 14 | {NULL, deallocate, memsize,}, 15 | 0, 0, RUBY_TYPED_FREE_IMMEDIATELY 16 | }; 17 | 18 | static void deallocate(void *ctx) { 19 | rubyDuckDBColumn *p = (rubyDuckDBColumn *)ctx; 20 | 21 | xfree(p); 22 | } 23 | 24 | static VALUE allocate(VALUE klass) { 25 | rubyDuckDBColumn *ctx = xcalloc((size_t)1, sizeof(rubyDuckDBColumn)); 26 | return TypedData_Wrap_Struct(klass, &column_data_type, ctx); 27 | } 28 | 29 | static size_t memsize(const void *p) { 30 | return sizeof(rubyDuckDBColumn); 31 | } 32 | 33 | /* :nodoc: */ 34 | VALUE duckdb_column__type(VALUE oDuckDBColumn) { 35 | rubyDuckDBColumn *ctx; 36 | rubyDuckDBResult *ctxresult; 37 | VALUE result; 38 | duckdb_type type; 39 | 40 | TypedData_Get_Struct(oDuckDBColumn, rubyDuckDBColumn, &column_data_type, ctx); 41 | 42 | result = rb_ivar_get(oDuckDBColumn, rb_intern("result")); 43 | ctxresult = get_struct_result(result); 44 | type = duckdb_column_type(&(ctxresult->result), ctx->col); 45 | 46 | return INT2FIX(type); 47 | } 48 | 49 | /* 50 | * call-seq: 51 | * column.logical_type -> DuckDB::LogicalType 52 | * 53 | * Returns the logical type class. 54 | * 55 | */ 56 | VALUE duckdb_column__logical_type(VALUE oDuckDBColumn) { 57 | rubyDuckDBColumn *ctx; 58 | rubyDuckDBResult *ctxresult; 59 | VALUE result; 60 | duckdb_logical_type _logical_type; 61 | VALUE logical_type = Qnil; 62 | 63 | TypedData_Get_Struct(oDuckDBColumn, rubyDuckDBColumn, &column_data_type, ctx); 64 | 65 | result = rb_ivar_get(oDuckDBColumn, rb_intern("result")); 66 | ctxresult = get_struct_result(result); 67 | _logical_type = duckdb_column_logical_type(&(ctxresult->result), ctx->col); 68 | 69 | if (_logical_type) { 70 | logical_type = rbduckdb_create_logical_type(_logical_type); 71 | } 72 | 73 | return logical_type; 74 | } 75 | 76 | /* 77 | * call-seq: 78 | * column.name -> string. 79 | * 80 | * Returns the column name. 81 | * 82 | */ 83 | VALUE duckdb_column_get_name(VALUE oDuckDBColumn) { 84 | rubyDuckDBColumn *ctx; 85 | VALUE result; 86 | rubyDuckDBResult *ctxresult; 87 | 88 | TypedData_Get_Struct(oDuckDBColumn, rubyDuckDBColumn, &column_data_type, ctx); 89 | 90 | result = rb_ivar_get(oDuckDBColumn, rb_intern("result")); 91 | 92 | ctxresult = get_struct_result(result); 93 | 94 | return rb_utf8_str_new_cstr(duckdb_column_name(&(ctxresult->result), ctx->col)); 95 | } 96 | 97 | VALUE rbduckdb_create_column(VALUE oDuckDBResult, idx_t col) { 98 | VALUE obj; 99 | rubyDuckDBColumn *ctx; 100 | 101 | obj = allocate(cDuckDBColumn); 102 | TypedData_Get_Struct(obj, rubyDuckDBColumn, &column_data_type, ctx); 103 | 104 | rb_ivar_set(obj, rb_intern("result"), oDuckDBResult); 105 | ctx->col = col; 106 | 107 | return obj; 108 | } 109 | 110 | void rbduckdb_init_duckdb_column(void) { 111 | #if 0 112 | VALUE mDuckDB = rb_define_module("DuckDB"); 113 | #endif 114 | cDuckDBColumn = rb_define_class_under(mDuckDB, "Column", rb_cObject); 115 | rb_define_alloc_func(cDuckDBColumn, allocate); 116 | 117 | rb_define_private_method(cDuckDBColumn, "_type", duckdb_column__type, 0); 118 | rb_define_method(cDuckDBColumn, "logical_type", duckdb_column__logical_type, 0); 119 | rb_define_method(cDuckDBColumn, "name", duckdb_column_get_name, 0); 120 | } 121 | -------------------------------------------------------------------------------- /ext/duckdb/column.h: -------------------------------------------------------------------------------- 1 | #ifndef RUBY_DUCKDB_COLUMN_H 2 | #define RUBY_DUCKDB_COLUMN_H 3 | 4 | struct _rubyDuckDBColumn { 5 | VALUE result; 6 | idx_t col; 7 | }; 8 | 9 | typedef struct _rubyDuckDBColumn rubyDuckDBColumn; 10 | 11 | void rbduckdb_init_duckdb_column(void); 12 | VALUE rbduckdb_create_column(VALUE oDuckDBResult, idx_t col); 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /ext/duckdb/config.c: -------------------------------------------------------------------------------- 1 | #include "ruby-duckdb.h" 2 | 3 | VALUE cDuckDBConfig; 4 | 5 | static void deallocate(void *); 6 | static VALUE allocate(VALUE klass); 7 | static size_t memsize(const void *p); 8 | static VALUE config_s_size(VALUE klass); 9 | static VALUE config_s_get_config_flag(VALUE self, VALUE value); 10 | static VALUE config_initialize(VALUE self); 11 | static VALUE config_set_config(VALUE self, VALUE key, VALUE value); 12 | 13 | static const rb_data_type_t config_data_type = { 14 | "DuckDB/Config", 15 | {NULL, deallocate, memsize,}, 16 | 0, 0, RUBY_TYPED_FREE_IMMEDIATELY 17 | }; 18 | 19 | static void deallocate(void * ctx) { 20 | rubyDuckDBConfig *p = (rubyDuckDBConfig *)ctx; 21 | 22 | duckdb_destroy_config(&(p->config)); 23 | xfree(p); 24 | } 25 | 26 | static VALUE allocate(VALUE klass) { 27 | rubyDuckDBConfig *ctx = xcalloc((size_t)1, sizeof(rubyDuckDBConfig)); 28 | return TypedData_Wrap_Struct(klass, &config_data_type, ctx); 29 | } 30 | 31 | static size_t memsize(const void *p) { 32 | return sizeof(rubyDuckDBConfig); 33 | } 34 | 35 | rubyDuckDBConfig *get_struct_config(VALUE obj) { 36 | rubyDuckDBConfig *ctx; 37 | TypedData_Get_Struct(obj, rubyDuckDBConfig, &config_data_type, ctx); 38 | return ctx; 39 | } 40 | 41 | static VALUE config_initialize(VALUE self) { 42 | rubyDuckDBConfig *ctx; 43 | 44 | TypedData_Get_Struct(self, rubyDuckDBConfig, &config_data_type, ctx); 45 | 46 | if (duckdb_create_config(&(ctx->config)) == DuckDBError) { 47 | rb_raise(eDuckDBError, "failed to create config"); 48 | } 49 | return self; 50 | } 51 | 52 | static VALUE config_s_size(VALUE self) { 53 | return ULONG2NUM(duckdb_config_count()); 54 | } 55 | 56 | static VALUE config_s_get_config_flag(VALUE klass, VALUE value) { 57 | const char *pkey; 58 | const char *pdesc; 59 | 60 | size_t i = NUM2INT(value); 61 | 62 | if (duckdb_get_config_flag(i, &pkey, &pdesc) == DuckDBError) { 63 | rb_raise(eDuckDBError, "failed to get config information of index %ld", i); 64 | } 65 | 66 | return rb_ary_new3(2, rb_utf8_str_new_cstr(pkey), rb_utf8_str_new_cstr(pdesc)); 67 | } 68 | 69 | static VALUE config_set_config(VALUE self, VALUE key, VALUE value) { 70 | const char *pkey = StringValuePtr(key); 71 | const char *pval = StringValuePtr(value); 72 | 73 | rubyDuckDBConfig *ctx; 74 | TypedData_Get_Struct(self, rubyDuckDBConfig, &config_data_type, ctx); 75 | 76 | if (duckdb_set_config(ctx->config, pkey, pval) == DuckDBError) { 77 | rb_raise(eDuckDBError, "failed to set config %s => %s", pkey, pval); 78 | } 79 | return self; 80 | } 81 | 82 | void rbduckdb_init_duckdb_config(void) { 83 | #if 0 84 | VALUE mDuckDB = rb_define_module("DuckDB"); 85 | #endif 86 | cDuckDBConfig = rb_define_class_under(mDuckDB, "Config", rb_cObject); 87 | rb_define_alloc_func(cDuckDBConfig, allocate); 88 | rb_define_singleton_method(cDuckDBConfig, "size", config_s_size, 0); 89 | rb_define_singleton_method(cDuckDBConfig, "get_config_flag", config_s_get_config_flag, 1); 90 | 91 | rb_define_method(cDuckDBConfig, "initialize", config_initialize, 0); 92 | rb_define_method(cDuckDBConfig, "set_config", config_set_config, 2); 93 | } 94 | -------------------------------------------------------------------------------- /ext/duckdb/config.h: -------------------------------------------------------------------------------- 1 | #ifndef RUBY_DUCKDB_CONFIG_H 2 | #define RUBY_DUCKDB_CONFIG_H 3 | 4 | struct _rubyDuckDBConfig { 5 | duckdb_config config; 6 | }; 7 | 8 | typedef struct _rubyDuckDBConfig rubyDuckDBConfig; 9 | 10 | rubyDuckDBConfig *get_struct_config(VALUE obj); 11 | 12 | void rbduckdb_init_duckdb_config(void); 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /ext/duckdb/connection.c: -------------------------------------------------------------------------------- 1 | #include "ruby-duckdb.h" 2 | 3 | VALUE cDuckDBConnection; 4 | 5 | static void deallocate(void *ctx); 6 | static VALUE allocate(VALUE klass); 7 | static size_t memsize(const void *p); 8 | static VALUE duckdb_connection_disconnect(VALUE self); 9 | static VALUE duckdb_connection_interrupt(VALUE self); 10 | static VALUE duckdb_connection_query_progress(VALUE self); 11 | static VALUE duckdb_connection_connect(VALUE self, VALUE oDuckDBDatabase); 12 | static VALUE duckdb_connection_query_sql(VALUE self, VALUE str); 13 | 14 | static const rb_data_type_t connection_data_type = { 15 | "DuckDB/Connection", 16 | {NULL, deallocate, memsize,}, 17 | 0, 0, RUBY_TYPED_FREE_IMMEDIATELY 18 | }; 19 | 20 | static void deallocate(void *ctx) { 21 | rubyDuckDBConnection *p = (rubyDuckDBConnection *)ctx; 22 | 23 | duckdb_disconnect(&(p->con)); 24 | xfree(p); 25 | } 26 | 27 | static VALUE allocate(VALUE klass) { 28 | rubyDuckDBConnection *ctx = xcalloc((size_t)1, sizeof(rubyDuckDBConnection)); 29 | return TypedData_Wrap_Struct(klass, &connection_data_type, ctx); 30 | } 31 | 32 | static size_t memsize(const void *p) { 33 | return sizeof(rubyDuckDBConnection); 34 | } 35 | 36 | rubyDuckDBConnection *get_struct_connection(VALUE obj) { 37 | rubyDuckDBConnection *ctx; 38 | TypedData_Get_Struct(obj, rubyDuckDBConnection, &connection_data_type, ctx); 39 | return ctx; 40 | } 41 | 42 | VALUE rbduckdb_create_connection(VALUE oDuckDBDatabase) { 43 | rubyDuckDB *ctxdb; 44 | rubyDuckDBConnection *ctxcon; 45 | VALUE obj; 46 | 47 | ctxdb = rbduckdb_get_struct_database(oDuckDBDatabase); 48 | 49 | obj = allocate(cDuckDBConnection); 50 | TypedData_Get_Struct(obj, rubyDuckDBConnection, &connection_data_type, ctxcon); 51 | 52 | if (duckdb_connect(ctxdb->db, &(ctxcon->con)) == DuckDBError) { 53 | rb_raise(eDuckDBError, "connection error"); 54 | } 55 | 56 | return obj; 57 | } 58 | 59 | static VALUE duckdb_connection_disconnect(VALUE self) { 60 | rubyDuckDBConnection *ctx; 61 | 62 | TypedData_Get_Struct(self, rubyDuckDBConnection, &connection_data_type, ctx); 63 | duckdb_disconnect(&(ctx->con)); 64 | 65 | return self; 66 | } 67 | 68 | /* 69 | * call-seq: 70 | * connection.interrupt -> nil 71 | * 72 | * Interrupts the currently running query. 73 | * 74 | * db = DuckDB::Database.open 75 | * conn = db.connect 76 | * con.query('SET ENABLE_PROGRESS_BAR=true') 77 | * con.query('SET ENABLE_PROGRESS_BAR_PRINT=false') 78 | * pending_result = con.async_query('slow query') 79 | * 80 | * pending_result.execute_task 81 | * con.interrupt # => nil 82 | */ 83 | static VALUE duckdb_connection_interrupt(VALUE self) { 84 | rubyDuckDBConnection *ctx; 85 | 86 | TypedData_Get_Struct(self, rubyDuckDBConnection, &connection_data_type, ctx); 87 | duckdb_interrupt(ctx->con); 88 | 89 | return Qnil; 90 | } 91 | 92 | /* 93 | * Returns the progress of the currently running query. 94 | * 95 | * require 'duckdb' 96 | * 97 | * db = DuckDB::Database.open 98 | * conn = db.connect 99 | * con.query('SET ENABLE_PROGRESS_BAR=true') 100 | * con.query('SET ENABLE_PROGRESS_BAR_PRINT=false') 101 | * con.query_progress # => -1.0 102 | * pending_result = con.async_query('slow query') 103 | * con.query_progress # => 0.0 104 | * pending_result.execute_task 105 | * con.query_progress # => Float 106 | */ 107 | static VALUE duckdb_connection_query_progress(VALUE self) { 108 | rubyDuckDBConnection *ctx; 109 | duckdb_query_progress_type progress; 110 | 111 | TypedData_Get_Struct(self, rubyDuckDBConnection, &connection_data_type, ctx); 112 | progress = duckdb_query_progress(ctx->con); 113 | 114 | return rb_funcall(mDuckDBConverter, rb_intern("_to_query_progress"), 3, DBL2NUM(progress.percentage), ULL2NUM(progress.rows_processed), ULL2NUM(progress.total_rows_to_process)); 115 | } 116 | 117 | /* :nodoc: */ 118 | static VALUE duckdb_connection_connect(VALUE self, VALUE oDuckDBDatabase) { 119 | rubyDuckDBConnection *ctx; 120 | rubyDuckDB *ctxdb; 121 | 122 | if (!rb_obj_is_kind_of(oDuckDBDatabase, cDuckDBDatabase)) { 123 | rb_raise(rb_eTypeError, "The first argument must be DuckDB::Database object."); 124 | } 125 | ctxdb = rbduckdb_get_struct_database(oDuckDBDatabase); 126 | TypedData_Get_Struct(self, rubyDuckDBConnection, &connection_data_type, ctx); 127 | 128 | if (duckdb_connect(ctxdb->db, &(ctx->con)) == DuckDBError) { 129 | rb_raise(eDuckDBError, "connection error"); 130 | } 131 | 132 | return self; 133 | } 134 | 135 | /* :nodoc: */ 136 | static VALUE duckdb_connection_query_sql(VALUE self, VALUE str) { 137 | rubyDuckDBConnection *ctx; 138 | rubyDuckDBResult *ctxr; 139 | 140 | VALUE result = rbduckdb_create_result(); 141 | 142 | TypedData_Get_Struct(self, rubyDuckDBConnection, &connection_data_type, ctx); 143 | ctxr = get_struct_result(result); 144 | 145 | if (!(ctx->con)) { 146 | rb_raise(eDuckDBError, "Database connection closed"); 147 | } 148 | 149 | if (duckdb_query(ctx->con, StringValueCStr(str), &(ctxr->result)) == DuckDBError) { 150 | rb_raise(eDuckDBError, "%s", duckdb_result_error(&(ctxr->result))); 151 | } 152 | return result; 153 | } 154 | 155 | void rbduckdb_init_duckdb_connection(void) { 156 | #if 0 157 | VALUE mDuckDB = rb_define_module("DuckDB"); 158 | #endif 159 | cDuckDBConnection = rb_define_class_under(mDuckDB, "Connection", rb_cObject); 160 | rb_define_alloc_func(cDuckDBConnection, allocate); 161 | 162 | rb_define_method(cDuckDBConnection, "disconnect", duckdb_connection_disconnect, 0); 163 | rb_define_method(cDuckDBConnection, "interrupt", duckdb_connection_interrupt, 0); 164 | rb_define_method(cDuckDBConnection, "query_progress", duckdb_connection_query_progress, 0); 165 | rb_define_private_method(cDuckDBConnection, "_connect", duckdb_connection_connect, 1); 166 | /* TODO: query_sql => _query_sql */ 167 | rb_define_private_method(cDuckDBConnection, "query_sql", duckdb_connection_query_sql, 1); 168 | } 169 | -------------------------------------------------------------------------------- /ext/duckdb/connection.h: -------------------------------------------------------------------------------- 1 | #ifndef RUBY_DUCKDB_CONNECTION_H 2 | #define RUBY_DUCKDB_CONNECTION_H 3 | 4 | struct _rubyDuckDBConnection { 5 | duckdb_connection con; 6 | }; 7 | 8 | typedef struct _rubyDuckDBConnection rubyDuckDBConnection; 9 | 10 | rubyDuckDBConnection *get_struct_connection(VALUE obj); 11 | void rbduckdb_init_duckdb_connection(void); 12 | VALUE rbduckdb_create_connection(VALUE oDuckDBDatabase); 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /ext/duckdb/converter.h: -------------------------------------------------------------------------------- 1 | #ifndef RUBY_DUCKDB_CONVERTER_H 2 | #define RUBY_DUCKDB_CONVERTER_H 3 | 4 | void rbduckdb_init_duckdb_converter(void); 5 | 6 | #endif 7 | -------------------------------------------------------------------------------- /ext/duckdb/conveter.c: -------------------------------------------------------------------------------- 1 | #include "ruby-duckdb.h" 2 | 3 | VALUE mDuckDBConverter; 4 | 5 | void rbduckdb_init_duckdb_converter(void) { 6 | mDuckDBConverter = rb_define_module_under(mDuckDB, "Converter"); 7 | } 8 | -------------------------------------------------------------------------------- /ext/duckdb/database.c: -------------------------------------------------------------------------------- 1 | #include "ruby-duckdb.h" 2 | 3 | VALUE cDuckDBDatabase; 4 | 5 | static void close_database(rubyDuckDB *p); 6 | static void deallocate(void * ctx); 7 | static VALUE allocate(VALUE klass); 8 | static size_t memsize(const void *p); 9 | static VALUE duckdb_database_s_open(int argc, VALUE *argv, VALUE cDuckDBDatabase); 10 | static VALUE duckdb_database_s_open_ext(int argc, VALUE *argv, VALUE cDuckDBDatabase); 11 | static VALUE duckdb_database_connect(VALUE self); 12 | static VALUE duckdb_database_close(VALUE self); 13 | 14 | static const rb_data_type_t database_data_type = { 15 | "DuckDB/Database", 16 | {NULL, deallocate, memsize,}, 17 | 0, 0, RUBY_TYPED_FREE_IMMEDIATELY 18 | }; 19 | 20 | static void close_database(rubyDuckDB *p) { 21 | duckdb_close(&(p->db)); 22 | } 23 | 24 | static void deallocate(void * ctx) { 25 | rubyDuckDB *p = (rubyDuckDB *)ctx; 26 | 27 | close_database(p); 28 | xfree(p); 29 | } 30 | 31 | static size_t memsize(const void *p) { 32 | return sizeof(rubyDuckDB); 33 | } 34 | 35 | static VALUE allocate(VALUE klass) { 36 | rubyDuckDB *ctx = xcalloc((size_t)1, sizeof(rubyDuckDB)); 37 | return TypedData_Wrap_Struct(klass, &database_data_type, ctx); 38 | } 39 | 40 | rubyDuckDB *rbduckdb_get_struct_database(VALUE obj) { 41 | rubyDuckDB *ctx; 42 | TypedData_Get_Struct(obj, rubyDuckDB, &database_data_type, ctx); 43 | return ctx; 44 | } 45 | 46 | /* :nodoc: */ 47 | static VALUE duckdb_database_s_open(int argc, VALUE *argv, VALUE cDuckDBDatabase) { 48 | rubyDuckDB *ctx; 49 | VALUE obj; 50 | 51 | char *pfile = NULL; 52 | VALUE file = Qnil; 53 | 54 | rb_scan_args(argc, argv, "01", &file); 55 | 56 | if (!NIL_P(file)) { 57 | pfile = StringValuePtr(file); 58 | } 59 | 60 | obj = allocate(cDuckDBDatabase); 61 | TypedData_Get_Struct(obj, rubyDuckDB, &database_data_type, ctx); 62 | if (duckdb_open(pfile, &(ctx->db)) == DuckDBError) { 63 | rb_raise(eDuckDBError, "Failed to open database"); /* FIXME */ 64 | } 65 | return obj; 66 | } 67 | 68 | /* :nodoc: */ 69 | static VALUE duckdb_database_s_open_ext(int argc, VALUE *argv, VALUE cDuckDBDatabase) { 70 | rubyDuckDB *ctx; 71 | VALUE obj; 72 | rubyDuckDBConfig *ctx_config; 73 | char *perror; 74 | 75 | char *pfile = NULL; 76 | VALUE file = Qnil; 77 | VALUE config = Qnil; 78 | 79 | rb_scan_args(argc, argv, "02", &file, &config); 80 | 81 | if (!NIL_P(file)) { 82 | pfile = StringValuePtr(file); 83 | } 84 | 85 | obj = allocate(cDuckDBDatabase); 86 | TypedData_Get_Struct(obj, rubyDuckDB, &database_data_type, ctx); 87 | if (!NIL_P(config)) { 88 | if (!rb_obj_is_kind_of(config, cDuckDBConfig)) { 89 | rb_raise(rb_eTypeError, "The second argument must be DuckDB::Config object."); 90 | } 91 | ctx_config = get_struct_config(config); 92 | if (duckdb_open_ext(pfile, &(ctx->db), ctx_config->config, &perror) == DuckDBError) { 93 | rb_raise(eDuckDBError, "Failed to open database %s", perror); 94 | } 95 | } else { 96 | if (duckdb_open(pfile, &(ctx->db)) == DuckDBError) { 97 | rb_raise(eDuckDBError, "Failed to open database"); /* FIXME */ 98 | } 99 | } 100 | return obj; 101 | } 102 | 103 | /* :nodoc: */ 104 | static VALUE duckdb_database_connect(VALUE self) { 105 | return rbduckdb_create_connection(self); 106 | } 107 | 108 | /* 109 | * call-seq: 110 | * duckdb.close -> DuckDB::Database 111 | * 112 | * closes DuckDB database. 113 | */ 114 | static VALUE duckdb_database_close(VALUE self) { 115 | rubyDuckDB *ctx; 116 | TypedData_Get_Struct(self, rubyDuckDB, &database_data_type, ctx); 117 | close_database(ctx); 118 | return self; 119 | } 120 | 121 | VALUE rbduckdb_create_database_obj(duckdb_database db) { 122 | VALUE obj = allocate(cDuckDBDatabase); 123 | rubyDuckDB *ctx; 124 | TypedData_Get_Struct(obj, rubyDuckDB, &database_data_type, ctx); 125 | ctx->db = db; 126 | return obj; 127 | } 128 | 129 | void rbduckdb_init_duckdb_database(void) { 130 | #if 0 131 | VALUE mDuckDB = rb_define_module("DuckDB"); 132 | #endif 133 | cDuckDBDatabase = rb_define_class_under(mDuckDB, "Database", rb_cObject); 134 | rb_define_alloc_func(cDuckDBDatabase, allocate); 135 | rb_define_singleton_method(cDuckDBDatabase, "_open", duckdb_database_s_open, -1); 136 | rb_define_singleton_method(cDuckDBDatabase, "_open_ext", duckdb_database_s_open_ext, -1); 137 | rb_define_private_method(cDuckDBDatabase, "_connect", duckdb_database_connect, 0); 138 | rb_define_method(cDuckDBDatabase, "close", duckdb_database_close, 0); 139 | } 140 | -------------------------------------------------------------------------------- /ext/duckdb/database.h: -------------------------------------------------------------------------------- 1 | #ifndef RUBY_DUCKDB_DATABASE_H 2 | #define RUBY_DUCKDB_DATABASE_H 3 | 4 | struct _rubyDuckDB { 5 | duckdb_database db; 6 | }; 7 | 8 | typedef struct _rubyDuckDB rubyDuckDB; 9 | 10 | rubyDuckDB *rbduckdb_get_struct_database(VALUE obj); 11 | VALUE rbduckdb_create_database_obj(duckdb_database db); 12 | void rbduckdb_init_duckdb_database(void); 13 | 14 | #endif 15 | -------------------------------------------------------------------------------- /ext/duckdb/duckdb.c: -------------------------------------------------------------------------------- 1 | #include "ruby-duckdb.h" 2 | 3 | VALUE mDuckDB; 4 | VALUE PositiveInfinity; 5 | VALUE NegativeInfinity; 6 | 7 | static VALUE duckdb_s_library_version(VALUE self); 8 | 9 | /* 10 | * call-seq: 11 | * DuckDB.library_version -> String 12 | * 13 | * Returns the version of the DuckDB library. 14 | * 15 | * DuckDB.library_version # => "0.2.0" 16 | */ 17 | static VALUE duckdb_s_library_version(VALUE self) { 18 | return rb_str_new2(duckdb_library_version()); 19 | } 20 | 21 | void 22 | Init_duckdb_native(void) { 23 | mDuckDB = rb_define_module("DuckDB"); 24 | PositiveInfinity = rb_str_new_literal("infinity"); 25 | NegativeInfinity = rb_str_new_literal("-infinity"); 26 | 27 | rb_define_singleton_method(mDuckDB, "library_version", duckdb_s_library_version, 0); 28 | 29 | rbduckdb_init_duckdb_error(); 30 | rbduckdb_init_duckdb_database(); 31 | rbduckdb_init_duckdb_connection(); 32 | rbduckdb_init_duckdb_result(); 33 | rbduckdb_init_duckdb_column(); 34 | rbduckdb_init_duckdb_logical_type(); 35 | rbduckdb_init_duckdb_prepared_statement(); 36 | rbduckdb_init_duckdb_pending_result(); 37 | rbduckdb_init_duckdb_blob(); 38 | rbduckdb_init_duckdb_appender(); 39 | rbduckdb_init_duckdb_config(); 40 | rbduckdb_init_duckdb_converter(); 41 | rbduckdb_init_duckdb_extracted_statements(); 42 | #ifdef HAVE_DUCKDB_H_GE_V1_2_0 43 | rbduckdb_init_duckdb_instance_cache(); 44 | #endif 45 | } 46 | -------------------------------------------------------------------------------- /ext/duckdb/error.c: -------------------------------------------------------------------------------- 1 | #include "ruby-duckdb.h" 2 | 3 | VALUE eDuckDBError; 4 | 5 | void rbduckdb_init_duckdb_error(void) { 6 | #if 0 7 | VALUE mDuckDB = rb_define_module("DuckDB"); 8 | #endif 9 | eDuckDBError = rb_define_class_under(mDuckDB, "Error", rb_eStandardError); 10 | } 11 | -------------------------------------------------------------------------------- /ext/duckdb/error.h: -------------------------------------------------------------------------------- 1 | #ifndef RUBY_DUCKDB_ERROR_H 2 | #define RUBY_DUCKDB_ERROR_H 3 | 4 | void rbduckdb_init_duckdb_error(void); 5 | 6 | #endif 7 | 8 | 9 | -------------------------------------------------------------------------------- /ext/duckdb/extconf.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'mkmf' 4 | 5 | DUCKDB_REQUIRED_VERSION = '1.1.0' 6 | 7 | def check_duckdb_header(header, version) 8 | found = find_header( 9 | header, 10 | '/opt/homebrew/include', 11 | '/opt/homebrew/opt/duckdb/include', 12 | '/opt/local/include' 13 | ) 14 | return if found 15 | 16 | msg = "#{header} is not found. Install #{header} of duckdb >= #{version}." 17 | print_message(msg) 18 | raise msg 19 | end 20 | 21 | def check_duckdb_library(library, func, version) 22 | found = find_library( 23 | library, 24 | func, 25 | '/opt/homebrew/lib', 26 | '/opt/homebrew/opt/duckdb/lib', 27 | '/opt/local/lib' 28 | ) 29 | have_func(func, 'duckdb.h') 30 | return if found 31 | 32 | raise_not_found_library(library, version) 33 | end 34 | 35 | def raise_not_found_library(library, version) 36 | library_name = duckdb_library_name(library) 37 | msg = "#{library_name} is not found. Install #{library_name} of duckdb >= #{version}." 38 | print_message(msg) 39 | raise msg 40 | end 41 | 42 | def duckdb_library_name(library) 43 | "lib#{library}.#{RbConfig::CONFIG['DLEXT']}" 44 | end 45 | 46 | def print_message(msg) 47 | print <<~END_OF_MESSAGE 48 | 49 | #{'*' * 80} 50 | #{msg} 51 | #{'*' * 80} 52 | 53 | END_OF_MESSAGE 54 | end 55 | 56 | dir_config('duckdb') 57 | 58 | check_duckdb_header('duckdb.h', DUCKDB_REQUIRED_VERSION) 59 | check_duckdb_library('duckdb', 'duckdb_result_error_type', DUCKDB_REQUIRED_VERSION) 60 | 61 | # check duckdb >= 1.1.0 62 | have_func('duckdb_result_error_type', 'duckdb.h') 63 | 64 | # check duckdb >= 1.2.0 65 | have_func('duckdb_create_instance_cache', 'duckdb.h') 66 | 67 | # check duckdb >= 1.3.0 68 | have_func('duckdb_get_table_names', 'duckdb.h') 69 | 70 | # Building with enabled DUCKDB_API_NO_DEPRECATED is failed with DuckDB v1.1.0 only. 71 | # DuckDB v1.1.1 is fixed this issue https://github.com/duckdb/duckdb/issues/13872. 72 | have_const('DUCKDB_TYPE_SQLNULL', 'duckdb.h') 73 | 74 | $CFLAGS << ' -DDUCKDB_API_NO_DEPRECATED' if ENV['DUCKDB_API_NO_DEPRECATED'] 75 | 76 | create_makefile('duckdb/duckdb_native') 77 | -------------------------------------------------------------------------------- /ext/duckdb/extracted_statements.c: -------------------------------------------------------------------------------- 1 | #include "ruby-duckdb.h" 2 | 3 | static VALUE cDuckDBExtractedStatements; 4 | 5 | static void deallocate(void *ctx); 6 | static VALUE allocate(VALUE klass); 7 | static size_t memsize(const void *p); 8 | 9 | static const rb_data_type_t extract_statements_data_type = { 10 | "DuckDB/ExtractedStatements", 11 | {NULL, deallocate, memsize,}, 12 | 0, 0, RUBY_TYPED_FREE_IMMEDIATELY 13 | }; 14 | 15 | static void deallocate(void *ctx); 16 | static VALUE allocate(VALUE klass); 17 | static size_t memsize(const void *p); 18 | 19 | static VALUE duckdb_extracted_statements_initialize(VALUE self, VALUE con, VALUE query); 20 | static VALUE duckdb_extracted_statements_destroy(VALUE self); 21 | static VALUE duckdb_extracted_statements_size(VALUE self); 22 | static VALUE duckdb_extracted_statements_prepared_statement(VALUE self, VALUE con, VALUE index); 23 | 24 | static void deallocate(void *ctx) { 25 | rubyDuckDBExtractedStatements *p = (rubyDuckDBExtractedStatements *)ctx; 26 | 27 | duckdb_destroy_extracted(&(p->extracted_statements)); 28 | xfree(p); 29 | } 30 | 31 | static VALUE allocate(VALUE klass) { 32 | rubyDuckDBExtractedStatements *ctx = xcalloc((size_t)1, sizeof(rubyDuckDBExtractedStatements)); 33 | ctx->num_statements = 0; 34 | 35 | return TypedData_Wrap_Struct(klass, &extract_statements_data_type, ctx); 36 | } 37 | 38 | static size_t memsize(const void *p) { 39 | return sizeof(rubyDuckDBExtractedStatements); 40 | } 41 | 42 | static VALUE duckdb_extracted_statements_initialize(VALUE self, VALUE con, VALUE query) { 43 | rubyDuckDBConnection *pcon; 44 | rubyDuckDBExtractedStatements *ctx; 45 | char *pquery; 46 | const char *error; 47 | 48 | if (rb_obj_is_kind_of(con, cDuckDBConnection) != Qtrue) { 49 | rb_raise(rb_eTypeError, "1st argument must be DuckDB::Connection"); 50 | } 51 | 52 | pquery = StringValuePtr(query); 53 | pcon = get_struct_connection(con); 54 | TypedData_Get_Struct(self, rubyDuckDBExtractedStatements, &extract_statements_data_type, ctx); 55 | 56 | ctx->num_statements = duckdb_extract_statements(pcon->con, pquery, &(ctx->extracted_statements)); 57 | 58 | if (ctx->num_statements == 0) { 59 | error = duckdb_extract_statements_error(ctx->extracted_statements); 60 | rb_raise(eDuckDBError, "%s", error ? error : "Failed to extract statements(Database connection closed?)."); 61 | } 62 | 63 | return self; 64 | } 65 | 66 | static VALUE duckdb_extracted_statements_destroy(VALUE self) { 67 | rubyDuckDBExtractedStatements *ctx; 68 | 69 | TypedData_Get_Struct(self, rubyDuckDBExtractedStatements, &extract_statements_data_type, ctx); 70 | 71 | duckdb_destroy_extracted(&(ctx->extracted_statements)); 72 | 73 | return Qnil; 74 | } 75 | 76 | static VALUE duckdb_extracted_statements_size(VALUE self) { 77 | rubyDuckDBExtractedStatements *ctx; 78 | 79 | TypedData_Get_Struct(self, rubyDuckDBExtractedStatements, &extract_statements_data_type, ctx); 80 | 81 | return ULL2NUM(ctx->num_statements); 82 | } 83 | 84 | static VALUE duckdb_extracted_statements_prepared_statement(VALUE self, VALUE con, VALUE index) { 85 | rubyDuckDBConnection *pcon; 86 | rubyDuckDBExtractedStatements *ctx; 87 | 88 | if (rb_obj_is_kind_of(con, cDuckDBConnection) != Qtrue) { 89 | rb_raise(rb_eTypeError, "1st argument must be DuckDB::Connection"); 90 | } 91 | pcon = get_struct_connection(con); 92 | TypedData_Get_Struct(self, rubyDuckDBExtractedStatements, &extract_statements_data_type, ctx); 93 | 94 | return rbduckdb_prepared_statement_new(pcon->con, ctx->extracted_statements, NUM2ULL(index)); 95 | } 96 | 97 | void rbduckdb_init_duckdb_extracted_statements(void) { 98 | #if 0 99 | VALUE mDuckDB = rb_define_module("DuckDB"); 100 | #endif 101 | cDuckDBExtractedStatements = rb_define_class_under(mDuckDB, "ExtractedStatementsImpl", rb_cObject); 102 | 103 | rb_define_alloc_func(cDuckDBExtractedStatements, allocate); 104 | rb_define_method(cDuckDBExtractedStatements, "initialize", duckdb_extracted_statements_initialize, 2); 105 | rb_define_method(cDuckDBExtractedStatements, "destroy", duckdb_extracted_statements_destroy, 0); 106 | rb_define_method(cDuckDBExtractedStatements, "size", duckdb_extracted_statements_size, 0); 107 | rb_define_method(cDuckDBExtractedStatements, "prepared_statement", duckdb_extracted_statements_prepared_statement, 2); 108 | } 109 | -------------------------------------------------------------------------------- /ext/duckdb/extracted_statements.h: -------------------------------------------------------------------------------- 1 | #ifndef RUBY_DUCKDB_EXTRACTED_STATEMENTS_H 2 | #define RUBY_DUCKDB_EXTRACTED_STATEMENTS_H 3 | 4 | struct _rubyDuckDBExtractedStatements { 5 | duckdb_extracted_statements extracted_statements; 6 | idx_t num_statements; 7 | }; 8 | 9 | typedef struct _rubyDuckDBExtractedStatements rubyDuckDBExtractedStatements; 10 | 11 | void rbduckdb_init_duckdb_extracted_statements(void); 12 | #endif 13 | -------------------------------------------------------------------------------- /ext/duckdb/instance_cache.c: -------------------------------------------------------------------------------- 1 | #include "ruby-duckdb.h" 2 | 3 | #ifdef HAVE_DUCKDB_H_GE_V1_2_0 4 | VALUE cDuckDBInstanceCache; 5 | 6 | static void deallocate(void * ctx); 7 | static VALUE allocate(VALUE klass); 8 | static size_t memsize(const void *p); 9 | static VALUE duckdb_instance_cache_initialize(VALUE self); 10 | static VALUE duckdb_instance_cache_get_or_create(int argc, VALUE *argv, VALUE self); 11 | static VALUE duckdb_instance_cache_destroy(VALUE self); 12 | 13 | static const rb_data_type_t instance_cache_data_type = { 14 | "DuckDB/InstanceCache", 15 | {NULL, deallocate, memsize,}, 16 | 0, 0, RUBY_TYPED_FREE_IMMEDIATELY 17 | }; 18 | 19 | static void deallocate(void * ctx) { 20 | rubyDuckDBInstanceCache *p = (rubyDuckDBInstanceCache *)ctx; 21 | 22 | if (p->instance_cache) { 23 | duckdb_destroy_instance_cache(&(p->instance_cache)); 24 | } 25 | xfree(p); 26 | } 27 | 28 | static size_t memsize(const void *p) { 29 | return sizeof(rubyDuckDBInstanceCache); 30 | } 31 | 32 | static VALUE allocate(VALUE klass) { 33 | rubyDuckDBInstanceCache *ctx = xcalloc((size_t)1, sizeof(rubyDuckDBInstanceCache)); 34 | return TypedData_Wrap_Struct(klass, &instance_cache_data_type, ctx); 35 | } 36 | 37 | static VALUE duckdb_instance_cache_initialize(VALUE self) { 38 | rubyDuckDBInstanceCache *ctx; 39 | 40 | TypedData_Get_Struct(self, rubyDuckDBInstanceCache, &instance_cache_data_type, ctx); 41 | 42 | ctx->instance_cache = duckdb_create_instance_cache(); 43 | if (ctx->instance_cache == NULL) { 44 | rb_raise(eDuckDBError, "Failed to create instance cache"); 45 | } 46 | 47 | return self; 48 | } 49 | 50 | /* :nodoc: */ 51 | static VALUE duckdb_instance_cache_get_or_create(int argc, VALUE *argv, VALUE self) { 52 | VALUE vpath = Qnil; 53 | VALUE vconfig = Qnil; 54 | const char *path = NULL; 55 | char *error = NULL; 56 | duckdb_config config = NULL; 57 | duckdb_database db; 58 | rubyDuckDBInstanceCache *ctx; 59 | 60 | rb_scan_args(argc, argv, "02", &vpath, &vconfig); 61 | if (!NIL_P(vpath)) { 62 | path = StringValuePtr(vpath); 63 | } 64 | if (!NIL_P(vconfig)) { 65 | if (!rb_obj_is_kind_of(vconfig, cDuckDBConfig)) { 66 | rb_raise(rb_eTypeError, "The second argument must be DuckDB::Config object."); 67 | } 68 | rubyDuckDBConfig *ctx_config = get_struct_config(vconfig); 69 | config = ctx_config->config; 70 | } 71 | 72 | TypedData_Get_Struct(self, rubyDuckDBInstanceCache, &instance_cache_data_type, ctx); 73 | 74 | if (duckdb_get_or_create_from_cache(ctx->instance_cache, path, &db, config, &error) == DuckDBError) { 75 | if (error) { 76 | VALUE message = rb_str_new_cstr(error); 77 | duckdb_free(error); 78 | rb_raise(eDuckDBError, "%s", StringValuePtr(message)); 79 | } else { 80 | rb_raise(eDuckDBError, "Failed to get or create database from instance cache"); 81 | } 82 | } 83 | return rbduckdb_create_database_obj(db); 84 | } 85 | 86 | static VALUE duckdb_instance_cache_destroy(VALUE self) { 87 | rubyDuckDBInstanceCache *ctx; 88 | TypedData_Get_Struct(self, rubyDuckDBInstanceCache, &instance_cache_data_type, ctx); 89 | 90 | if (ctx->instance_cache) { 91 | duckdb_destroy_instance_cache(&(ctx->instance_cache)); 92 | ctx->instance_cache = NULL; 93 | } 94 | 95 | return Qnil; 96 | } 97 | 98 | void rbduckdb_init_duckdb_instance_cache(void) { 99 | #if 0 100 | VALUE mDuckDB = rb_define_module("DuckDB"); 101 | #endif 102 | cDuckDBInstanceCache = rb_define_class_under(mDuckDB, "InstanceCache", rb_cObject); 103 | rb_define_method(cDuckDBInstanceCache, "initialize", duckdb_instance_cache_initialize, 0); 104 | rb_define_method(cDuckDBInstanceCache, "get_or_create", duckdb_instance_cache_get_or_create, -1); 105 | rb_define_method(cDuckDBInstanceCache, "destroy", duckdb_instance_cache_destroy, 0); 106 | rb_define_alloc_func(cDuckDBInstanceCache, allocate); 107 | } 108 | #endif 109 | -------------------------------------------------------------------------------- /ext/duckdb/instance_cache.h: -------------------------------------------------------------------------------- 1 | #ifndef RUBY_DUCKDB_INSTANCE_CACHE_H 2 | #define RUBY_DUCKDB_INSTANCE_CACHE_H 3 | 4 | #ifdef HAVE_DUCKDB_H_GE_V1_2_0 5 | 6 | struct _rubyDuckDBInstanceCache { 7 | duckdb_instance_cache instance_cache; 8 | }; 9 | 10 | typedef struct _rubyDuckDBInstanceCache rubyDuckDBInstanceCache; 11 | 12 | void rbduckdb_init_duckdb_instance_cache(void); 13 | 14 | #endif 15 | 16 | #endif 17 | 18 | -------------------------------------------------------------------------------- /ext/duckdb/logical_type.h: -------------------------------------------------------------------------------- 1 | #ifndef RUBY_DUCKDB_LOGICAL_TYPE_H 2 | #define RUBY_DUCKDB_LOGICAL_TYPE_H 3 | 4 | struct _rubyDuckDBLogicalType { 5 | duckdb_logical_type logical_type; 6 | }; 7 | 8 | typedef struct _rubyDuckDBLogicalType rubyDuckDBLogicalType; 9 | 10 | void rbduckdb_init_duckdb_logical_type(void); 11 | VALUE rbduckdb_create_logical_type(duckdb_logical_type logical_type); 12 | 13 | #endif 14 | -------------------------------------------------------------------------------- /ext/duckdb/pending_result.c: -------------------------------------------------------------------------------- 1 | #include "ruby-duckdb.h" 2 | 3 | static VALUE cDuckDBPendingResult; 4 | 5 | static void deallocate(void *ctx); 6 | static VALUE allocate(VALUE klass); 7 | static size_t memsize(const void *p); 8 | static VALUE duckdb_pending_result_initialize(int argc, VALUE *args, VALUE self); 9 | static VALUE duckdb_pending_result_execute_task(VALUE self); 10 | static VALUE duckdb_pending_result_execute_pending(VALUE self); 11 | static VALUE duckdb_pending_result_execution_finished_p(VALUE self); 12 | static VALUE duckdb_pending_result__state(VALUE self); 13 | static VALUE duckdb_pending_result__execute_check_state(VALUE self); 14 | 15 | static const rb_data_type_t pending_result_data_type = { 16 | "DuckDB/PendingResult", 17 | {NULL, deallocate, memsize,}, 18 | 0, 0, RUBY_TYPED_FREE_IMMEDIATELY 19 | }; 20 | 21 | static void deallocate(void *ctx) { 22 | rubyDuckDBPendingResult *p = (rubyDuckDBPendingResult *)ctx; 23 | 24 | duckdb_destroy_pending(&(p->pending_result)); 25 | xfree(p); 26 | } 27 | 28 | static VALUE allocate(VALUE klass) { 29 | rubyDuckDBPendingResult *ctx = xcalloc((size_t)1, sizeof(rubyDuckDBPendingResult)); 30 | ctx->state = DUCKDB_PENDING_RESULT_NOT_READY; 31 | return TypedData_Wrap_Struct(klass, &pending_result_data_type, ctx); 32 | } 33 | 34 | static size_t memsize(const void *p) { 35 | return sizeof(rubyDuckDBPendingResult); 36 | } 37 | 38 | static VALUE duckdb_pending_result_initialize(int argc, VALUE *argv, VALUE self) { 39 | VALUE oDuckDBPreparedStatement; 40 | VALUE streaming_p = Qfalse; 41 | duckdb_state state; 42 | 43 | if (argc == 2) { 44 | rb_warn("The second argument of `DuckDB::PendingResult#new` is now meaningless. The result is the same when it is set to true."); 45 | } 46 | rb_scan_args(argc, argv, "11", &oDuckDBPreparedStatement, &streaming_p); 47 | 48 | if (rb_obj_is_kind_of(oDuckDBPreparedStatement, cDuckDBPreparedStatement) != Qtrue) { 49 | rb_raise(rb_eTypeError, "1st argument must be DuckDB::PreparedStatement"); 50 | } 51 | 52 | rubyDuckDBPendingResult *ctx = get_struct_pending_result(self); 53 | rubyDuckDBPreparedStatement *stmt = get_struct_prepared_statement(oDuckDBPreparedStatement); 54 | 55 | state = duckdb_pending_prepared(stmt->prepared_statement, &(ctx->pending_result)); 56 | 57 | if (state == DuckDBError) { 58 | rb_raise(eDuckDBError, "%s", duckdb_pending_error(ctx->pending_result)); 59 | } 60 | return self; 61 | } 62 | 63 | /* 64 | * call-seq: 65 | * pending_result.execute_task -> nil 66 | * 67 | * Executes the task in the pending result. 68 | * 69 | * db = DuckDB::Database.open 70 | * conn = db.connect 71 | * pending_result = conn.async_query("slow query") 72 | * pending_result.execute_task 73 | */ 74 | static VALUE duckdb_pending_result_execute_task(VALUE self) { 75 | rubyDuckDBPendingResult *ctx = get_struct_pending_result(self); 76 | ctx->state = duckdb_pending_execute_task(ctx->pending_result); 77 | return Qnil; 78 | } 79 | 80 | static VALUE duckdb_pending_result_execution_finished_p(VALUE self) { 81 | rubyDuckDBPendingResult *ctx = get_struct_pending_result(self); 82 | return duckdb_pending_execution_is_finished(ctx->state) ? Qtrue : Qfalse; 83 | } 84 | 85 | /* 86 | * call-seq: 87 | * pending_result.execute_pending -> DuckDB::Result 88 | * 89 | * Get DuckDB::Result object after query execution finished. 90 | * 91 | * db = DuckDB::Database.open 92 | * conn = db.connect 93 | * pending_result = conn.async_query("slow query") 94 | * pending_result.execute_task while pending_result.state != :ready 95 | * result = pending_result.execute_pending # => DuckDB::Result 96 | */ 97 | static VALUE duckdb_pending_result_execute_pending(VALUE self) { 98 | rubyDuckDBPendingResult *ctx; 99 | rubyDuckDBResult *ctxr; 100 | VALUE result = rbduckdb_create_result(); 101 | 102 | TypedData_Get_Struct(self, rubyDuckDBPendingResult, &pending_result_data_type, ctx); 103 | ctxr = get_struct_result(result); 104 | if (duckdb_execute_pending(ctx->pending_result, &(ctxr->result)) == DuckDBError) { 105 | rb_raise(eDuckDBError, "%s", duckdb_pending_error(ctx->pending_result)); 106 | } 107 | return result; 108 | } 109 | 110 | /* :nodoc: */ 111 | static VALUE duckdb_pending_result__state(VALUE self) { 112 | rubyDuckDBPendingResult *ctx = get_struct_pending_result(self); 113 | return INT2FIX(ctx->state); 114 | } 115 | 116 | /* :nodoc: */ 117 | static VALUE duckdb_pending_result__execute_check_state(VALUE self) { 118 | rubyDuckDBPendingResult *ctx = get_struct_pending_result(self); 119 | return INT2FIX(duckdb_pending_execute_check_state(ctx->pending_result)); 120 | } 121 | 122 | rubyDuckDBPendingResult *get_struct_pending_result(VALUE obj) { 123 | rubyDuckDBPendingResult *ctx; 124 | TypedData_Get_Struct(obj, rubyDuckDBPendingResult, &pending_result_data_type, ctx); 125 | return ctx; 126 | } 127 | 128 | void rbduckdb_init_duckdb_pending_result(void) { 129 | #if 0 130 | VALUE mDuckDB = rb_define_module("DuckDB"); 131 | #endif 132 | cDuckDBPendingResult = rb_define_class_under(mDuckDB, "PendingResult", rb_cObject); 133 | rb_define_alloc_func(cDuckDBPendingResult, allocate); 134 | 135 | rb_define_method(cDuckDBPendingResult, "initialize", duckdb_pending_result_initialize, -1); 136 | rb_define_method(cDuckDBPendingResult, "execute_task", duckdb_pending_result_execute_task, 0); 137 | rb_define_method(cDuckDBPendingResult, "execute_pending", duckdb_pending_result_execute_pending, 0); 138 | rb_define_method(cDuckDBPendingResult, "execution_finished?", duckdb_pending_result_execution_finished_p, 0); 139 | rb_define_private_method(cDuckDBPendingResult, "_state", duckdb_pending_result__state, 0); 140 | rb_define_private_method(cDuckDBPendingResult, "_execute_check_state", duckdb_pending_result__execute_check_state, 0); 141 | } 142 | -------------------------------------------------------------------------------- /ext/duckdb/pending_result.h: -------------------------------------------------------------------------------- 1 | #ifndef RUBY_DUCKDB_PENDING_RESULT_H 2 | #define RUBY_DUCKDB_PENDING_RESULT_H 3 | 4 | struct _rubyDuckDBPendingResult { 5 | duckdb_pending_result pending_result; 6 | duckdb_pending_state state; 7 | }; 8 | 9 | typedef struct _rubyDuckDBPendingResult rubyDuckDBPendingResult; 10 | 11 | rubyDuckDBPendingResult *get_struct_pending_result(VALUE obj); 12 | void rbduckdb_init_duckdb_pending_result(void); 13 | #endif 14 | -------------------------------------------------------------------------------- /ext/duckdb/prepared_statement.h: -------------------------------------------------------------------------------- 1 | #ifndef RUBY_DUCKDB_PREPARED_STATEMENT_H 2 | #define RUBY_DUCKDB_PREPARED_STATEMENT_H 3 | 4 | struct _rubyDuckDBPreparedStatement { 5 | duckdb_prepared_statement prepared_statement; 6 | idx_t nparams; 7 | }; 8 | 9 | typedef struct _rubyDuckDBPreparedStatement rubyDuckDBPreparedStatement; 10 | 11 | VALUE rbduckdb_prepared_statement_new(duckdb_connection con, duckdb_extracted_statements extracted_statements, idx_t index); 12 | rubyDuckDBPreparedStatement *get_struct_prepared_statement(VALUE self); 13 | void rbduckdb_init_duckdb_prepared_statement(void); 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /ext/duckdb/result.h: -------------------------------------------------------------------------------- 1 | #ifndef RUBY_DUCKDB_RESULT_H 2 | #define RUBY_DUCKDB_RESULT_H 3 | 4 | struct _rubyDuckDBResult { 5 | duckdb_result result; 6 | }; 7 | 8 | typedef struct _rubyDuckDBResult rubyDuckDBResult; 9 | 10 | rubyDuckDBResult *get_struct_result(VALUE obj); 11 | void rbduckdb_init_duckdb_result(void); 12 | VALUE rbduckdb_create_result(void); 13 | 14 | #endif 15 | 16 | -------------------------------------------------------------------------------- /ext/duckdb/ruby-duckdb.h: -------------------------------------------------------------------------------- 1 | #ifndef RUBY_DUCKDB_H 2 | #define RUBY_DUCKDB_H 3 | 4 | // #define DUCKDB_API_NO_DEPRECATED 5 | #define DUCKDB_NO_EXTENSION_FUNCTIONS // disable extension C-functions 6 | 7 | #include "ruby.h" 8 | #include "ruby/thread.h" 9 | #include 10 | 11 | #ifdef HAVE_CONST_DUCKDB_TYPE_SQLNULL 12 | #define HAVE_DUCKDB_H_GE_V1_1_1 1 13 | #endif 14 | 15 | #ifdef HAVE_DUCKDB_CREATE_INSTANCE_CACHE 16 | #define HAVE_DUCKDB_H_GE_V1_2_0 1 17 | #endif 18 | 19 | #ifdef HAVE_DUCKDB_GET_TABLE_NAMES 20 | #define HAVE_DUCKDB_H_GE_V1_3_0 1 21 | #endif 22 | 23 | #include "./error.h" 24 | #include "./database.h" 25 | #include "./connection.h" 26 | #include "./result.h" 27 | #include "./column.h" 28 | #include "./logical_type.h" 29 | #include "./prepared_statement.h" 30 | #include "./extracted_statements.h" 31 | #include "./pending_result.h" 32 | #include "./util.h" 33 | #include "./converter.h" 34 | 35 | #include "./blob.h" 36 | #include "./appender.h" 37 | #include "./config.h" 38 | 39 | #ifdef HAVE_DUCKDB_H_GE_V1_2_0 40 | #include "./instance_cache.h" 41 | #endif 42 | 43 | extern VALUE mDuckDB; 44 | extern VALUE cDuckDBDatabase; 45 | extern VALUE cDuckDBConnection; 46 | extern VALUE cDuckDBBlob; 47 | extern VALUE cDuckDBConfig; 48 | extern VALUE eDuckDBError; 49 | extern VALUE mDuckDBConverter; 50 | extern VALUE cDuckDBPreparedStatement; 51 | extern VALUE PositiveInfinity; 52 | extern VALUE NegativeInfinity; 53 | 54 | #ifdef HAVE_DUCKDB_H_GE_V1_2_0 55 | extern VALUE cDuckDBInstanceCache; 56 | #endif 57 | 58 | #endif 59 | -------------------------------------------------------------------------------- /ext/duckdb/util.c: -------------------------------------------------------------------------------- 1 | #include "ruby-duckdb.h" 2 | 3 | duckdb_date rbduckdb_to_duckdb_date_from_value(VALUE year, VALUE month, VALUE day) { 4 | duckdb_date_struct dt_struct; 5 | 6 | dt_struct.year = NUM2INT(year); 7 | dt_struct.month = NUM2INT(month); 8 | dt_struct.day = NUM2INT(day); 9 | 10 | return duckdb_to_date(dt_struct); 11 | } 12 | 13 | duckdb_time rbduckdb_to_duckdb_time_from_value(VALUE hour, VALUE min, VALUE sec, VALUE micros) { 14 | duckdb_time_struct time_st; 15 | 16 | time_st.hour = NUM2INT(hour); 17 | time_st.min = NUM2INT(min); 18 | time_st.sec = NUM2INT(sec); 19 | time_st.micros = NUM2INT(micros); 20 | 21 | return duckdb_to_time(time_st); 22 | } 23 | 24 | duckdb_timestamp rbduckdb_to_duckdb_timestamp_from_value(VALUE year, VALUE month, VALUE day, VALUE hour, VALUE min, VALUE sec, VALUE micros) { 25 | duckdb_timestamp_struct timestamp_st; 26 | 27 | timestamp_st.date.year = NUM2INT(year); 28 | timestamp_st.date.month = NUM2INT(month); 29 | timestamp_st.date.day = NUM2INT(day); 30 | timestamp_st.time.hour = NUM2INT(hour); 31 | timestamp_st.time.min = NUM2INT(min); 32 | timestamp_st.time.sec = NUM2INT(sec); 33 | timestamp_st.time.micros = NUM2INT(micros); 34 | 35 | return duckdb_to_timestamp(timestamp_st); 36 | } 37 | 38 | void rbduckdb_to_duckdb_interval_from_value(duckdb_interval* interval, VALUE months, VALUE days, VALUE micros) { 39 | interval->months = NUM2INT(months); 40 | interval->days = NUM2INT(days); 41 | interval->micros = NUM2LL(micros); 42 | } 43 | -------------------------------------------------------------------------------- /ext/duckdb/util.h: -------------------------------------------------------------------------------- 1 | #ifndef RUBY_DUCKDB_UTIL_H 2 | #define RUBY_DUCKDB_UTIL_H 3 | 4 | duckdb_date rbduckdb_to_duckdb_date_from_value(VALUE year, VALUE month, VALUE day); 5 | duckdb_time rbduckdb_to_duckdb_time_from_value(VALUE hour, VALUE min, VALUE sec, VALUE micros); 6 | duckdb_timestamp rbduckdb_to_duckdb_timestamp_from_value(VALUE year, VALUE month, VALUE day, VALUE hour, VALUE min, VALUE sec, VALUE micros); 7 | void rbduckdb_to_duckdb_interval_from_value(duckdb_interval* interval, VALUE months, VALUE days, VALUE micros); 8 | 9 | #endif 10 | -------------------------------------------------------------------------------- /getduckdb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | MACHINE=`uname -m` 4 | 5 | case "$MACHINE" in 6 | "x86_64" ) ARC=amd64 ;; 7 | "aarch64" ) ARC=aarch64 ;; 8 | esac 9 | 10 | wget -O duckdb.zip "https://github.com/duckdb/duckdb/releases/download/v$DUCKDB_VERSION/libduckdb-linux-$ARC.zip" 11 | -------------------------------------------------------------------------------- /lib/duckdb.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'duckdb/duckdb_native' 4 | require 'duckdb/library_version' 5 | require 'duckdb/version' 6 | require 'duckdb/converter' 7 | require 'duckdb/database' 8 | require 'duckdb/connection' 9 | require 'duckdb/extracted_statements' 10 | require 'duckdb/result' 11 | require 'duckdb/prepared_statement' 12 | require 'duckdb/pending_result' 13 | require 'duckdb/appender' 14 | require 'duckdb/config' 15 | require 'duckdb/column' 16 | require 'duckdb/logical_type' 17 | require 'duckdb/infinity' 18 | require 'duckdb/instance_cache' 19 | 20 | # DuckDB provides Ruby interface of DuckDB. 21 | module DuckDB 22 | end 23 | -------------------------------------------------------------------------------- /lib/duckdb/column.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DuckDB 4 | class Column 5 | # returns column type symbol 6 | # `:unknown` means that the column type is unknown/unsupported by ruby-duckdb. 7 | # `:invalid` means that the column type is invalid in duckdb. 8 | # 9 | # require 'duckdb' 10 | # db = DuckDB::Database.open 11 | # con = db.connect 12 | # con.query('CREATE TABLE users (id INTEGER, name VARCHAR(30))') 13 | # 14 | # users = con.query('SELECT * FROM users') 15 | # columns = users.columns 16 | # columns.first.type #=> :integer 17 | def type 18 | type_id = _type 19 | DuckDB::Converter::IntToSym.type_to_sym(type_id) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/duckdb/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DuckDB 4 | # The DuckDB::Config encapsulates DuckDB Configuration. 5 | # 6 | # require 'duckdb' 7 | # config = DuckDB::Config.new 8 | # config['default_order'] = 'DESC' 9 | # db = DuckDB::Database.open(nil, config) 10 | # con = db.connect 11 | # con.query('CREATE TABLE numbers (number INTEGER)') 12 | # con.query('INSERT INTO numbers VALUES (2), (1), (4), (3)') 13 | # 14 | # # number is ordered by descending. 15 | # r = con.query('SELECT number FROM numbers ORDER BY number) 16 | # r.first.first # => 4 17 | class Config 18 | class << self 19 | # 20 | # returns available configuration name and the description. 21 | # The return value is array object. The first element is the configuration 22 | # name. The second is the description. 23 | # 24 | # key, desc = DuckDB::Config.key_description(0) 25 | # key # => "access_mode" 26 | # desc # => "Access mode of the database ([AUTOMATIC], READ_ONLY or READ_WRITE)" 27 | # 28 | alias key_description get_config_flag 29 | 30 | # 31 | # returns the Hash object of all available configuration names and 32 | # the descriptions. 33 | # 34 | # configs = DuckDB::Config.key_descriptions 35 | # configs['default_order'] # => "The order type used when none is specified ([ASC] or DESC)" 36 | # 37 | def key_descriptions 38 | @key_descriptions ||= (0...size).each_with_object({}) do |i, hash| 39 | key, description = key_description(i) 40 | hash[key] = description 41 | end 42 | end 43 | end 44 | 45 | # 46 | # set configuration value 47 | # 48 | # config = DuckDB::Config.new 49 | # # config.set_config('default_order', 'DESC') 50 | # config['default_order'] = 'DESC' 51 | # 52 | # db = DuckDB::Database.open(nil, config) 53 | # con = db.connect 54 | # con.query('CREATE TABLE numbers (number INTEGER)') 55 | # con.query('INSERT INTO numbers VALUES (2), (1), (4), (3)') 56 | # 57 | # # numbers are ordered by descending. 58 | # r = con.query('SELECT number FROM numbers ORDER BY number) 59 | # r.first.first # => 4 60 | alias []= set_config 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/duckdb/connection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DuckDB 4 | # The DuckDB::Connection encapsulates connection with DuckDB database. 5 | # 6 | # require 'duckdb' 7 | # db = DuckDB::Database.open 8 | # con = db.connect 9 | # con.query(sql) 10 | class Connection 11 | # executes sql with args. 12 | # The first argument sql must be SQL string. 13 | # The rest arguments are parameters of SQL string. 14 | # 15 | # require 'duckdb' 16 | # db = DuckDB::Database.open('duckdb_file') 17 | # con = db.connect 18 | # users = con.query('SELECT * FROM users') 19 | # sql = 'SELECT * FROM users WHERE name = ? AND email = ?' 20 | # dave = con.query(sql, 'Dave', 'dave@example.com') 21 | # 22 | # # or You can use named parameter. 23 | # 24 | # sql = 'SELECT * FROM users WHERE name = $name AND email = $email' 25 | # dave = con.query(sql, name: 'Dave', email: 'dave@example.com') 26 | def query(sql, *args, **kwargs) 27 | return query_multi_sql(sql) if args.empty? && kwargs.empty? 28 | 29 | prepare(sql) do |stmt| 30 | stmt.bind_args(*args, **kwargs) 31 | stmt.execute 32 | end 33 | end 34 | 35 | def query_multi_sql(sql) 36 | stmts = ExtractedStatements.new(self, sql) 37 | result = nil 38 | stmts.each do |stmt| 39 | result = stmt.execute 40 | stmt.destroy 41 | end 42 | result 43 | ensure 44 | stmts&.destroy 45 | end 46 | 47 | # executes sql with args asynchronously. 48 | # The first argument sql must be SQL string. 49 | # The rest arguments are parameters of SQL string. 50 | # This method returns DuckDB::PendingResult object. 51 | # 52 | # require 'duckdb' 53 | # db = DuckDB::Database.open('duckdb_file') 54 | # con = db.connect 55 | # 56 | # sql = 'SELECT * FROM users WHERE name = $name AND email = $email' 57 | # pending_result = con.async_query(sql, name: 'Dave', email: 'dave@example.com') 58 | # pending_result.execute_task while pending_result.state == :not_ready 59 | # result = pending_result.execute_pending 60 | # result.each.first 61 | def async_query(sql, *args, **kwargs) 62 | prepare(sql) do |stmt| 63 | stmt.bind_args(*args, **kwargs) 64 | stmt.pending_prepared 65 | end 66 | end 67 | 68 | # executes sql with args asynchronously and provides streaming result. 69 | # The first argument sql must be SQL string. 70 | # The rest arguments are parameters of SQL string. 71 | # This method returns DuckDB::PendingResult object. 72 | # 73 | # require 'duckdb' 74 | # db = DuckDB::Database.open('duckdb_file') 75 | # con = db.connect 76 | # 77 | # sql = 'SELECT * FROM users WHERE name = $name AND email = $email' 78 | # pending_result = con.async_query_stream(sql, name: 'Dave', email: 'dave@example.com') 79 | # 80 | # pending_result.execute_task while pending_result.state == :not_ready 81 | # result = pending_result.execute_pending 82 | # result.each.first 83 | def async_query_stream(sql, *args, **kwargs) 84 | warn("`#{self.class}#{__method__}` will be deprecated. Use `#{self.class}#async_query` instead.") 85 | 86 | async_query(sql, *args, **kwargs) 87 | end 88 | 89 | # connects DuckDB database 90 | # The first argument is DuckDB::Database object 91 | def connect(db) 92 | conn = _connect(db) 93 | return conn unless block_given? 94 | 95 | begin 96 | yield conn 97 | ensure 98 | conn.disconnect 99 | end 100 | end 101 | 102 | # returns PreparedStatement object. 103 | # The first argument is SQL string. 104 | # If block is given, the block is executed with PreparedStatement object 105 | # and the object is cleaned up immediately. 106 | # 107 | # require 'duckdb' 108 | # db = DuckDB::Database.open('duckdb_file') 109 | # con = db.connect 110 | # 111 | # sql = 'SELECT * FROM users WHERE name = $name AND email = $email' 112 | # stmt = con.prepared_statement(sql) 113 | # stmt.bind_args(name: 'Dave', email: 'dave@example.com') 114 | # result = stmt.execute 115 | # 116 | # # or 117 | # result = con.prepared_statement(sql) do |stmt| 118 | # stmt.bind_args(name: 'Dave', email: 'dave@example.com') 119 | # stmt.execute 120 | # end 121 | def prepared_statement(str, &) 122 | return PreparedStatement.new(self, str) unless block_given? 123 | 124 | PreparedStatement.prepare(self, str, &) 125 | end 126 | 127 | # returns Appender object. 128 | # The first argument is table name 129 | def appender(table) 130 | appender = create_appender(table) 131 | 132 | return appender unless block_given? 133 | 134 | yield appender 135 | appender.flush 136 | appender.close 137 | end 138 | 139 | private 140 | 141 | def create_appender(table) 142 | t1, t2 = table.split('.') 143 | t2 ? Appender.new(self, t1, t2) : Appender.new(self, t2, t1) 144 | end 145 | 146 | alias execute query 147 | alias async_execute async_query 148 | alias open connect 149 | alias close disconnect 150 | alias prepare prepared_statement 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /lib/duckdb/converter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'date' 4 | require_relative 'interval' 5 | require_relative 'converter/int_to_sym' 6 | 7 | module DuckDB 8 | QueryProgress = Struct.new(:percentage, :rows_processed, :total_rows_to_process) 9 | 10 | module Converter # :nodoc: all 11 | HALF_HUGEINT_BIT = 64 12 | HALF_HUGEINT = 1 << HALF_HUGEINT_BIT 13 | FLIP_HUGEINT = 1 << 63 14 | EPOCH = Time.local(1970, 1, 1) 15 | EPOCH_UTC = Time.new(1970, 1, 1, 0, 0, 0, 0) 16 | 17 | module_function 18 | 19 | def _to_infinity(value) 20 | if value.positive? 21 | DuckDB::Infinity::POSITIVE 22 | else 23 | DuckDB::Infinity::NEGATIVE 24 | end 25 | end 26 | 27 | def _to_date(year, month, day) 28 | Date.new(year, month, day) 29 | end 30 | 31 | def _to_time(year, month, day, hour, minute, second, microsecond) 32 | Time.local(year, month, day, hour, minute, second, microsecond) 33 | end 34 | 35 | def _to_time_from_duckdb_time(hour, minute, second, microsecond) 36 | Time.parse( 37 | format( 38 | '%02d:%02d:%02d.%06d', 39 | hour: hour, 40 | minute: minute, 41 | second: second, 42 | microsecond: microsecond 43 | ) 44 | ) 45 | end 46 | 47 | def _to_time_from_duckdb_timestamp_s(time) 48 | EPOCH + time 49 | end 50 | 51 | def _to_time_from_duckdb_timestamp_ms(time) 52 | tm = EPOCH + (time / 1000) 53 | Time.local(tm.year, tm.month, tm.day, tm.hour, tm.min, tm.sec, time % 1000 * 1000) 54 | end 55 | 56 | def _to_time_from_duckdb_timestamp_ns(time) 57 | tm = EPOCH + (time / 1_000_000_000) 58 | Time.local(tm.year, tm.month, tm.day, tm.hour, tm.min, tm.sec, time % 1_000_000_000 / 1000) 59 | end 60 | 61 | def _to_time_from_duckdb_time_tz(hour, min, sec, micro, timezone) 62 | sign = '+' 63 | if timezone.negative? 64 | timezone = -timezone 65 | sign = '-' 66 | end 67 | 68 | tzhour = timezone / 3600 69 | tzmin = (timezone % 3600) / 60 70 | 71 | Time.parse( 72 | format( 73 | '%02d:%02d:%02d.%06d%s%02d:%02d', 74 | hour: hour, 75 | min: min, 76 | sec: sec, 77 | micro: micro, 78 | sign: sign, 79 | tzhour: tzhour, 80 | tzmin: tzmin 81 | ) 82 | ) 83 | end 84 | 85 | def _to_time_from_duckdb_timestamp_tz(bits) 86 | micro = bits % 1_000_000 87 | sec = (bits / 1_000_000) 88 | time = EPOCH_UTC + sec 89 | 90 | Time.parse( 91 | format( 92 | '%04d-%02d-%02d %02d:%02d:%02d.%06d +0000', 93 | year: time.year, 94 | mon: time.month, 95 | day: time.day, 96 | hour: time.hour, 97 | min: time.min, 98 | sec: time.sec, 99 | micro: micro 100 | ) 101 | ) 102 | end 103 | 104 | def _to_hugeint_from_vector(lower, upper) 105 | (upper << HALF_HUGEINT_BIT) + lower 106 | end 107 | 108 | def _to_decimal_from_hugeint(width, scale, upper, lower = nil) 109 | v = lower.nil? ? upper : _to_hugeint_from_vector(lower, upper) 110 | _to_decimal_from_value(width, scale, v) 111 | end 112 | 113 | def _to_decimal_from_value(_width, scale, value) 114 | v = value.to_s 115 | v = v.rjust(scale + 1, '0') if v.length < scale 116 | v[-scale, 0] = '.' if scale.positive? 117 | BigDecimal(v) 118 | end 119 | 120 | def _to_interval_from_vector(months, days, micros) 121 | Interval.new(interval_months: months, interval_days: days, interval_micros: micros) 122 | end 123 | 124 | def _to_uuid_from_vector(lower, upper) 125 | upper ^= FLIP_HUGEINT 126 | upper += HALF_HUGEINT if upper.negative? 127 | 128 | str = _to_hugeint_from_vector(lower, upper).to_s(16).rjust(32, '0') 129 | "#{str[0, 8]}-#{str[8, 4]}-#{str[12, 4]}-#{str[16, 4]}-#{str[20, 12]}" 130 | end 131 | 132 | def _parse_date(value) 133 | case value 134 | when Date, Time 135 | value 136 | else 137 | begin 138 | Date.parse(value) 139 | rescue StandardError => e 140 | raise(ArgumentError, "Cannot parse `#{value.inspect}` to Date object. #{e.message}") 141 | end 142 | end 143 | end 144 | 145 | def _parse_time(value) 146 | case value 147 | when Time 148 | value 149 | else 150 | begin 151 | Time.parse(value) 152 | rescue StandardError => e 153 | raise(ArgumentError, "Cannot parse `#{value.inspect}` to Time object. #{e.message}") 154 | end 155 | end 156 | end 157 | 158 | def _parse_deciaml(value) 159 | case value 160 | when BigDecimal 161 | value 162 | else 163 | begin 164 | BigDecimal(value.to_s) 165 | rescue StandardError => e 166 | raise(ArgumentError, "Cannot parse `#{value.inspect}` to BigDecimal object. #{e.message}") 167 | end 168 | end 169 | end 170 | 171 | def _to_query_progress(percentage, rows_processed, total_rows_to_process) 172 | DuckDB::QueryProgress.new(percentage, rows_processed, total_rows_to_process).freeze 173 | end 174 | 175 | private 176 | 177 | def integer_to_hugeint(value) 178 | case value 179 | when Integer 180 | upper = value >> HALF_HUGEINT_BIT 181 | lower = value - (upper << HALF_HUGEINT_BIT) 182 | [lower, upper] 183 | else 184 | raise(ArgumentError, "The argument `#{value.inspect}` must be Integer.") 185 | end 186 | end 187 | 188 | def decimal_to_hugeint(value) 189 | integer_value = (value * (10 ** value.scale)).to_i 190 | integer_to_hugeint(integer_value) 191 | rescue FloatDomainError => e 192 | raise(ArgumentError, "The argument `#{value.inspect}` must be converted to Integer. #{e.message}") 193 | end 194 | end 195 | end 196 | -------------------------------------------------------------------------------- /lib/duckdb/converter/int_to_sym.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DuckDB 4 | module Converter 5 | module IntToSym # :nodoc: all 6 | STATEMENT_TYPES = %i[ 7 | invalid 8 | select 9 | insert 10 | update 11 | explain 12 | delete 13 | prepare 14 | create 15 | execute 16 | alter 17 | transaction 18 | copy 19 | analyze 20 | variable_set 21 | create_func 22 | drop 23 | export 24 | pragma 25 | vacuum 26 | call 27 | set 28 | load 29 | relation 30 | extension 31 | logical_plan 32 | attach 33 | detach 34 | multi 35 | ].freeze 36 | 37 | HASH_TYPES = { 38 | 0 => :invalid, 39 | 1 => :boolean, 40 | 2 => :tinyint, 41 | 3 => :smallint, 42 | 4 => :integer, 43 | 5 => :bigint, 44 | 6 => :utinyint, 45 | 7 => :usmallint, 46 | 8 => :uinteger, 47 | 9 => :ubigint, 48 | 10 => :float, 49 | 11 => :double, 50 | 12 => :timestamp, 51 | 13 => :date, 52 | 14 => :time, 53 | 15 => :interval, 54 | 16 => :hugeint, 55 | 32 => :uhugeint, 56 | 17 => :varchar, 57 | 18 => :blob, 58 | 19 => :decimal, 59 | 20 => :timestamp_s, 60 | 21 => :timestamp_ms, 61 | 22 => :timestamp_ns, 62 | 23 => :enum, 63 | 24 => :list, 64 | 25 => :struct, 65 | 26 => :map, 66 | 33 => :array, 67 | 27 => :uuid, 68 | 28 => :union, 69 | 29 => :bit, 70 | 30 => :time_tz, 71 | 31 => :timestamp_tz 72 | }.freeze 73 | 74 | module_function 75 | 76 | def statement_type_to_sym(val) # :nodoc: 77 | raise DuckDB::Error, "Unknown statement type: #{val}" if val >= STATEMENT_TYPES.size 78 | 79 | STATEMENT_TYPES[val] 80 | end 81 | 82 | def type_to_sym(val) # :nodoc: 83 | raise DuckDB::Error, "Unknown type: #{val}" unless HASH_TYPES.key?(val) 84 | 85 | HASH_TYPES[val] 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/duckdb/database.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DuckDB 4 | # The Database class encapsulates a DuckDB database. 5 | # 6 | # The usage is as follows: 7 | # 8 | # require 'duckdb' 9 | # 10 | # db = DuckDB::Database.open # database in memory 11 | # con = db.connect 12 | # 13 | # con.query('CREATE TABLE users (id INTEGER, name VARCHAR(30))') 14 | # 15 | # con.query("INSERT into users VALUES(1, 'Alice')") 16 | # con.query("INSERT into users VALUES(2, 'Bob')") 17 | # con.query("INSERT into users VALUES(3, 'Cathy')") 18 | # 19 | # result = con.query('SELECT * from users') 20 | # result.each do |row| 21 | # p row 22 | # end 23 | class Database 24 | private_class_method :_open 25 | private_class_method :_open_ext 26 | 27 | class << self 28 | # Opens database. 29 | # The first argument is DuckDB database file path to open. 30 | # If there is no argument, the method opens DuckDB database in memory. 31 | # The method yields block if block is given. 32 | # 33 | # DuckDB::Database.open('duckdb_database.db') #=> DuckDB::Database 34 | # 35 | # DuckDB::Database.open #=> opens DuckDB::Database in memory. 36 | # 37 | # DuckDB::Database.open do |db| 38 | # con = db.connect 39 | # con.query('CREATE TABLE users (id INTEGER, name VARCHAR(30))') 40 | # end 41 | def open(dbpath = nil, config = nil) 42 | db = _db_open(dbpath, config) 43 | return db unless block_given? 44 | 45 | begin 46 | yield db 47 | ensure 48 | db.close 49 | end 50 | end 51 | 52 | private 53 | 54 | def _db_open(dbpath, config) # :nodoc: 55 | if config 56 | _open_ext(dbpath, config) 57 | else 58 | _open(dbpath) 59 | end 60 | end 61 | end 62 | 63 | # connects database. 64 | # 65 | # The method yields block and disconnects the database if block given 66 | # 67 | # db = DuckDB::Database.open 68 | # 69 | # con = db.connect # => DuckDB::Connection 70 | # 71 | # db.connect do |con| 72 | # con.query('CREATE TABLE users (id INTEGER, name VARCHAR(30))') 73 | # end 74 | def connect 75 | conn = _connect 76 | return conn unless block_given? 77 | 78 | begin 79 | yield conn 80 | ensure 81 | conn.disconnect 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/duckdb/extracted_statements.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DuckDB 4 | class ExtractedStatements < ExtractedStatementsImpl 5 | include Enumerable 6 | 7 | def initialize(con, sql) 8 | @con = con 9 | super(con, sql) 10 | end 11 | 12 | def each 13 | return to_enum(__method__) { size } unless block_given? 14 | 15 | size.times do |i| 16 | yield prepared_statement(@con, i) 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/duckdb/infinity.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DuckDB 4 | module Infinity 5 | POSITIVE = 'infinity' 6 | NEGATIVE = '-infinity' 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/duckdb/instance_cache.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if defined?(DuckDB::InstanceCache) 4 | 5 | module DuckDB 6 | # The DuckDB::InstanceCache is necessary if a client/program (re)opens 7 | # multiple databases to the same file within the same statement. 8 | # 9 | # require 'duckdb' 10 | # cache = DuckDB::InstanceCache.new 11 | # db1 = cache.get(path: 'db.duckdb') 12 | # db2 = cache.get(path: 'db.duckdb') 13 | class InstanceCache 14 | # :call-seq: 15 | # instance_cache.get(path:, config:) -> self 16 | # 17 | # Returns a DuckDB::Database object for the given path and config. 18 | # db1 = cache.get(path: 'db.duckdb') 19 | # db2 = cache.get(path: 'db.duckdb') 20 | def get(path: nil, config: nil) 21 | get_or_create(path, config) 22 | end 23 | end 24 | end 25 | 26 | end 27 | -------------------------------------------------------------------------------- /lib/duckdb/interval.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DuckDB 4 | # Interval class represents DuckDB's interval type value. 5 | # 6 | # The usage is as follows: 7 | # 8 | # require 'duckdb' 9 | # 10 | # interval = DuckDB::Interval.new(interval_months: 14, interval_days: 3, interval_micros: 14706123456) 11 | # # or 12 | # # interval = DuckDB::Interval.mk_interval(year: 1, month: 2, day: 3, hour: 4, min: 5, sec: 6, usec: 123456) 13 | # # or 14 | # # interval = DuckDB::Interval.iso8601_parse('P1Y2M3DT4H5M6.123456S') 15 | # 16 | # db = DuckDB::Database.open # database in memory 17 | # con = db.connect 18 | # 19 | # con.execute('CREATE TABLE users (value INTERVAL)') 20 | # 21 | # require 'duckdb' 22 | # db = DuckDB::Database.open 23 | # con = db.connect 24 | # con.query('CREATE TABLE intervals (interval_value INTERVAL)') 25 | # appender = con.appender('intervals') 26 | # appender 27 | # .append_interval(interval) 28 | # .end_row 29 | # .flush 30 | class Interval 31 | # :stopdoc: 32 | ISO8601_REGEXP = Regexp.compile( 33 | '(?-{0,1})P 34 | (?-{0,1}\d+Y){0,1} 35 | (?-{0,1}\d+M){0,1} 36 | (?-{0,1}\d+D){0,1} 37 | T{0,1} 38 | (?-{0,1}\d+H){0,1} 39 | (?-{0,1}\d+M){0,1} 40 | ((?-{0,1}\d+)\.{0,1}(?\d*)S){0,1}', 41 | Regexp::EXTENDED 42 | ) 43 | private_constant :ISO8601_REGEXP 44 | # :startdoc: 45 | 46 | class << self 47 | # parses the ISO8601 format string and return the Interval object. 48 | # 49 | # DuckDB::Interval.iso8601_parse('P1Y2M3DT4H5M6.123456S') 50 | # => # 51 | def iso8601_parse(value) 52 | m = ISO8601_REGEXP.match(value) 53 | 54 | raise ArgumentError, "The argument `#{value}` can't be parse." if m.nil? 55 | 56 | year, month, day, hour, min, sec, usec = matched_to_i(m) 57 | 58 | mk_interval(year: year, month: month, day: day, hour: hour, min: min, sec: sec, usec: usec) 59 | end 60 | 61 | # creates the Interval object. 62 | # 63 | # DuckDB::Interval.mk_interval(year: 1, month: 2, day: 3, hour: 4, min: 5, sec: 6, usec: 123456) 64 | # => # 65 | def mk_interval(year: 0, month: 0, day: 0, hour: 0, min: 0, sec: 0, usec: 0) 66 | Interval.new( 67 | interval_months: (year * 12) + month, 68 | interval_days: day, 69 | interval_micros: (((hour * 3600) + (min * 60) + sec) * 1_000_000) + usec 70 | ) 71 | end 72 | 73 | # Convert the value to the Interval object. 74 | # The value can be String or Interval object. 75 | # If the value is String, it is parsed as ISO8601 format. 76 | # If the value is Interval object, it is returned as is. 77 | # Otherwise, ArgumentError is raised. 78 | # 79 | # DuckDB::Interval.to_interval('P1Y2M3DT4H5M6.123456S') 80 | # => # 81 | # 82 | # interval = DuckDB::Interval.to_interval('P1Y2M3DT4H5M6.123456S') 83 | # DuckDB::Interval.to_interval(interval) 84 | # => # 85 | def to_interval(value) 86 | case value 87 | when String 88 | iso8601_parse(value) 89 | when Interval 90 | value 91 | else 92 | raise ArgumentError, "The argument `#{value}` can't be parse." 93 | end 94 | end 95 | 96 | private 97 | 98 | def matched_to_i(matched) # :nodoc: 99 | sign = to_sign(matched) 100 | sec = to_sec(matched) 101 | usec = to_usec(matched) 102 | usec *= -1 if sec.negative? 103 | value = [ 104 | to_year(matched), to_month(matched), to_day(matched), to_hour(matched), to_min(matched), sec, usec 105 | ] 106 | sign.positive? ? value : value.map { |v| v * sign } 107 | end 108 | 109 | def to_sign(matched) # :nodoc: 110 | matched[:negativ] == '-' ? -1 : 1 111 | end 112 | 113 | def to_year(matched) # :nodoc: 114 | matched[:year].to_i 115 | end 116 | 117 | def to_month(matched) # :nodoc: 118 | matched[:month].to_i 119 | end 120 | 121 | def to_day(matched) # :nodoc: 122 | matched[:day].to_i 123 | end 124 | 125 | def to_hour(matched) # :nodoc: 126 | matched[:hour].to_i 127 | end 128 | 129 | def to_min(matched) # :nodoc: 130 | matched[:min].to_i 131 | end 132 | 133 | def to_sec(matched) # :nodoc: 134 | matched[:sec].to_i 135 | end 136 | 137 | def to_usec(matched) # :nodoc: 138 | matched[:usec].to_s.ljust(6, '0')[0, 6].to_i 139 | end 140 | end 141 | 142 | attr_reader :interval_months, :interval_days, :interval_micros 143 | 144 | # creates the Interval object. 145 | # The arguments are the number of months, days, and microseconds. 146 | # The default value is 0. 147 | # 148 | # DuckDB::Interval.new(interval_months: 1, interval_days: 2, interval_micros: 3) 149 | # => # 150 | def initialize(interval_months: 0, interval_days: 0, interval_micros: 0) 151 | @interval_months = interval_months 152 | @interval_days = interval_days 153 | @interval_micros = interval_micros 154 | end 155 | 156 | def ==(other) 157 | other.is_a?(Interval) && 158 | @interval_months == other.interval_months && 159 | @interval_days == other.interval_days && 160 | @interval_micros == other.interval_micros 161 | end 162 | 163 | def eql?(other) 164 | self == other 165 | end 166 | end 167 | end 168 | -------------------------------------------------------------------------------- /lib/duckdb/library_version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DuckDB 4 | # represents the version of the DuckDB library. 5 | # If DuckDB.library_version is v0.2.0, then DuckDB::LIBRARY_VERSION is 0.2.0. 6 | LIBRARY_VERSION = library_version[1..] 7 | end 8 | -------------------------------------------------------------------------------- /lib/duckdb/logical_type.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DuckDB 4 | class LogicalType 5 | alias :alias get_alias 6 | alias :alias= set_alias 7 | 8 | # returns logical type's type symbol 9 | # `:unknown` means that the logical type's type is unknown/unsupported by ruby-duckdb. 10 | # `:invalid` means that the logical type's type is invalid in duckdb. 11 | # 12 | # require 'duckdb' 13 | # db = DuckDB::Database.open 14 | # con = db.connect 15 | # con.query('CREATE TABLE climates (id INTEGER, temperature DECIMAIL)') 16 | # 17 | # users = con.query('SELECT * FROM climates') 18 | # columns = users.columns 19 | # columns.second.logical_type.type #=> :decimal 20 | def type 21 | type_id = _type 22 | DuckDB::Converter::IntToSym.type_to_sym(type_id) 23 | end 24 | 25 | # returns logical type's internal type symbol for Decimal or Enum types 26 | # `:unknown` means that the logical type's type is unknown/unsupported by ruby-duckdb. 27 | # `:invalid` means that the logical type's type is invalid in duckdb. 28 | # 29 | # require 'duckdb' 30 | # db = DuckDB::Database.open 31 | # con = db.connect 32 | # con.query("CREATE TYPE mood AS ENUM ('happy', 'sad')") 33 | # con.query("CREATE TABLE emotions (id INTEGER, enum_col mood)") 34 | # 35 | # users = con.query('SELECT * FROM emotions') 36 | # ernum_col = users.columns.find { |col| col.name == 'enum_col' } 37 | # enum_col.logical_type.internal_type #=> :utinyint 38 | def internal_type 39 | type_id = _internal_type 40 | DuckDB::Converter::IntToSym.type_to_sym(type_id) 41 | end 42 | 43 | # Iterates over each union member name. 44 | # 45 | # When a block is provided, this method yields each union member name in 46 | # order. It also returns the total number of members yielded. 47 | # 48 | # union_logical_type.each_member_name do |name| 49 | # puts "Union member: #{name}" 50 | # end 51 | # 52 | # If no block is given, an Enumerator is returned, which can be used to 53 | # retrieve all member names. 54 | # 55 | # names = union_logical_type.each_member_name.to_a 56 | # # => ["member1", "member2"] 57 | def each_member_name 58 | return to_enum(__method__) {member_count} unless block_given? 59 | 60 | member_count.times do |i| 61 | yield member_name_at(i) 62 | end 63 | end 64 | 65 | # Iterates over each union member type. 66 | # 67 | # When a block is provided, this method yields each union member logical 68 | # type in order. It also returns the total number of members yielded. 69 | # 70 | # union_logical_type.each_member_type do |logical_type| 71 | # puts "Union member: #{logical_type.type}" 72 | # end 73 | # 74 | # If no block is given, an Enumerator is returned, which can be used to 75 | # retrieve all member logical types. 76 | # 77 | # names = union_logical_type.each_member_type.map(&:type) 78 | # # => [:varchar, :integer] 79 | def each_member_type 80 | return to_enum(__method__) {member_count} unless block_given? 81 | 82 | member_count.times do |i| 83 | yield member_type_at(i) 84 | end 85 | end 86 | 87 | # Iterates over each struct child name. 88 | # 89 | # When a block is provided, this method yields each struct child name in 90 | # order. It also returns the total number of children yielded. 91 | # 92 | # struct_logical_type.each_child_name do |name| 93 | # puts "Struct child: #{name}" 94 | # end 95 | # 96 | # If no block is given, an Enumerator is returned, which can be used to 97 | # retrieve all child names. 98 | # 99 | # names = struct_logical_type.each_child_name.to_a 100 | # # => ["child1", "child2"] 101 | def each_child_name 102 | return to_enum(__method__) {child_count} unless block_given? 103 | 104 | child_count.times do |i| 105 | yield child_name_at(i) 106 | end 107 | end 108 | 109 | # Iterates over each struct child type. 110 | # 111 | # When a block is provided, this method yields each struct child type in 112 | # order. It also returns the total number of children yielded. 113 | # 114 | # struct_logical_type.each_child_type do |logical_type| 115 | # puts "Struct child type: #{logical_type.type}" 116 | # end 117 | # 118 | # If no block is given, an Enumerator is returned, which can be used to 119 | # retrieve all child logical types. 120 | # 121 | # types = struct_logical_type.each_child_type.map(&:type) 122 | # # => [:integer, :varchar] 123 | def each_child_type 124 | return to_enum(__method__) {child_count} unless block_given? 125 | 126 | child_count.times do |i| 127 | yield child_type_at(i) 128 | end 129 | end 130 | 131 | # Iterates over each enum dictionary value. 132 | # 133 | # When a block is provided, this method yields each enum dictionary value 134 | # in order. It also returns the total number of dictionary values yielded. 135 | # 136 | # enum_logical_type.each_value do |value| 137 | # puts "Enum value: #{value}" 138 | # end 139 | # 140 | # If no block is given, an Enumerator is returned, which can be used to 141 | # retrieve all enum dictionary values. 142 | # 143 | # values = enum_logical_type.each_value.to_a 144 | # # => ["happy", "sad"] 145 | def each_dictionary_value 146 | return to_enum(__method__) {dictionary_size} unless block_given? 147 | 148 | dictionary_size.times do |i| 149 | yield dictionary_value_at(i) 150 | end 151 | end 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /lib/duckdb/pending_result.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DuckDB 4 | # The DuckDB::PendingResult encapsulates connection with DuckDB pending 5 | # result. 6 | # PendingResult provides methods to execute SQL asynchronousely and check 7 | # if the result is ready and to get the result. 8 | # 9 | # require 'duckdb' 10 | # 11 | # db = DuckDB::Database.open 12 | # con = db.connect 13 | # stmt = con.prepared_statement(VERY_SLOW_QUERY) 14 | # pending_result = stmt.pending_prepared 15 | # while pending_result.state == :not_ready 16 | # print '.' 17 | # sleep(0.01) 18 | # pending_result.execute_task 19 | # end 20 | # result = pending_result.execute_pending 21 | class PendingResult 22 | STATES = %i[ready not_ready error no_tasks].freeze # :nodoc: 23 | 24 | # returns the state of the pending result. 25 | # the result can be :ready, :not_ready, :error, :no_tasks. 26 | # 27 | # :ready means the result is ready to be fetched, and 28 | # you can call `execute_pending` to get the result. 29 | # 30 | # :not_ready means the result is not ready yet, so 31 | # you need to call `execute_task`. 32 | # 33 | # @return [symbol] :ready, :not_ready, :error, :no_tasks 34 | def state 35 | STATES[_state] 36 | end 37 | 38 | # returns the state of the pending result. 39 | # the result can be :ready, :not_ready, :error, :no_tasks. 40 | # 41 | # :ready means the result is ready to be fetched, and 42 | # you can call `execute_pending` to get the result. 43 | # 44 | # :not_ready or :no_tasks might mean the pending result 45 | # is not executed yet, so you need to call `execute_task`. 46 | # 47 | # @return [symbol] :ready, :not_ready, :error, :no_tasks 48 | def execute_check_state 49 | STATES[_execute_check_state] 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/duckdb/prepared_statement.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'date' 4 | require 'bigdecimal' 5 | require_relative 'converter' 6 | 7 | module DuckDB 8 | # The DuckDB::PreparedStatement encapsulates connection with DuckDB prepared 9 | # statement. 10 | # 11 | # require 'duckdb' 12 | # db = DuckDB::Database.open('duckdb_database') 13 | # con = db.connect 14 | # sql ='SELECT name, email FROM users WHERE email = ?' 15 | # stmt = PreparedStatement.new(con, sql) 16 | # stmt.bind(1, 'email@example.com') 17 | # stmt.execute 18 | class PreparedStatement 19 | include DuckDB::Converter 20 | 21 | RANGE_INT16 = -32_768..32_767 22 | RANGE_INT32 = -2_147_483_648..2_147_483_647 23 | RANGE_INT64 = -9_223_372_036_854_775_808..9_223_372_036_854_775_807 24 | 25 | class << self 26 | # return DuckDB::PreparedStatement object. 27 | # The first argument is DuckDB::Connection object. 28 | # The second argument is SQL string. 29 | # If block is given, the block is executed and the statement is destroyed. 30 | # 31 | # require 'duckdb' 32 | # db = DuckDB::Database.open('duckdb_database') 33 | # con = db.connection 34 | # DuckDB::PreparedStatement.prepare(con, 'SELECT * FROM users WHERE id = ?') do |stmt| 35 | # stmt.bind(1, 1) 36 | # stmt.execute 37 | # end 38 | def prepare(con, sql) 39 | stmt = new(con, sql) 40 | return stmt unless block_given? 41 | 42 | begin 43 | yield stmt 44 | ensure 45 | stmt.destroy 46 | end 47 | end 48 | end 49 | 50 | def pending_prepared 51 | PendingResult.new(self) 52 | end 53 | 54 | def pending_prepared_stream 55 | warn("`#{self.class}##{__method__}` will be deprecated. use `#{self.class}#pending_prepared` instead") 56 | pending_prepared 57 | end 58 | 59 | # returns statement type. The return value is one of the following symbols: 60 | # :invalid, :select, :insert, :update, :explain, :delete, :prepare, :create, 61 | # :execute, :alter, :transaction, :copy, :analyze, :variable_set, :create_func, 62 | # :drop, :export, :pragma, :vacuum, :call, :set, :load, :relation, :extension, 63 | # :logical_plan, :attach, :detach, :multi 64 | # 65 | # require 'duckdb' 66 | # db = DuckDB::Database.open('duckdb_database') 67 | # con = db.connect 68 | # stmt = con.prepared_statement('SELECT * FROM users') 69 | # stmt.statement_type # => :select 70 | def statement_type 71 | i = _statement_type 72 | Converter::IntToSym.statement_type_to_sym(i) 73 | end 74 | 75 | # returns parameter type. The argument must be index of parameter. 76 | # 77 | # require 'duckdb' 78 | # db = DuckDB::Database.open 79 | # con = db.connect 80 | # con.execute('CREATE TABLE users (id INTEGER, name VARCHAR(255))') 81 | # stmt = con.prepared_statement('SELECT * FROM users WHERE id = ?') 82 | # stmt.param_type(1) # => :integer 83 | def param_type(index) 84 | i = _param_type(index) 85 | Converter::IntToSym.type_to_sym(i) 86 | end 87 | 88 | # binds all parameters with SQL prepared statement. 89 | # 90 | # require 'duckdb' 91 | # db = DuckDB::Database.open('duckdb_database') 92 | # con = db.connect 93 | # sql ='SELECT name FROM users WHERE id = ?' 94 | # # or 95 | # # sql ='SELECT name FROM users WHERE id = $id' 96 | # stmt = PreparedStatement.new(con, sql) 97 | # stmt.bind_args([1]) 98 | # # or 99 | # # stmt.bind_args(id: 1) 100 | def bind_args(*args, **kwargs) 101 | args.each.with_index(1) do |arg, i| 102 | bind(i, arg) 103 | end 104 | kwargs.each do |key, value| 105 | bind(key, value) 106 | end 107 | end 108 | 109 | # binds i-th parameter with SQL prepared statement. 110 | # The first argument is index of parameter. 111 | # The index of first parameter is 1 not 0. 112 | # The second argument value is to expected Integer value between 0 to 255. 113 | def bind_uint8(index, val) 114 | return _bind_uint8(index, val) if val.between?(0, 255) 115 | 116 | raise DuckDB::Error, "can't bind uint8(bind_uint8) to `#{val}`. The `#{val}` is out of range 0..255." 117 | end 118 | 119 | # binds i-th parameter with SQL prepared statement. 120 | # The first argument is index of parameter. 121 | # The index of first parameter is 1 not 0. 122 | # The second argument value is to expected Integer value between 0 to 65535. 123 | def bind_uint16(index, val) 124 | return _bind_uint16(index, val) if val.between?(0, 65_535) 125 | 126 | raise DuckDB::Error, "can't bind uint16(bind_uint16) to `#{val}`. The `#{val}` is out of range 0..65535." 127 | end 128 | 129 | # binds i-th parameter with SQL prepared statement. 130 | # The first argument is index of parameter. 131 | # The index of first parameter is 1 not 0. 132 | # The second argument value is to expected Integer value between 0 to 4294967295. 133 | def bind_uint32(index, val) 134 | return _bind_uint32(index, val) if val.between?(0, 4_294_967_295) 135 | 136 | raise DuckDB::Error, "can't bind uint32(bind_uint32) to `#{val}`. The `#{val}` is out of range 0..4294967295." 137 | end 138 | 139 | # binds i-th parameter with SQL prepared statement. 140 | # The first argument is index of parameter. 141 | # The index of first parameter is 1 not 0. 142 | # The second argument value is to expected Integer value between 0 to 18446744073709551615. 143 | def bind_uint64(index, val) 144 | return _bind_uint64(index, val) if val.between?(0, 18_446_744_073_709_551_615) 145 | 146 | raise DuckDB::Error, "can't bind uint64(bind_uint64) to `#{val}`. The `#{val}` is out of range 0..18446744073709551615." 147 | end 148 | 149 | # binds i-th parameter with SQL prepared statement. 150 | # The first argument is index of parameter. 151 | # The index of first parameter is 1 not 0. 152 | # The second argument value is to expected Integer value. 153 | # This method uses bind_varchar internally. 154 | # 155 | # require 'duckdb' 156 | # db = DuckDB::Database.open('duckdb_database') 157 | # con = db.connect 158 | # sql ='SELECT name FROM users WHERE bigint_col = ?' 159 | # stmt = PreparedStatement.new(con, sql) 160 | # stmt.bind_hugeint(1, 1_234_567_890_123_456_789_012_345) 161 | def bind_hugeint(index, value) 162 | case value 163 | when Integer 164 | bind_varchar(index, value.to_s) 165 | else 166 | raise(ArgumentError, "2nd argument `#{value}` must be Integer.") 167 | end 168 | end 169 | 170 | # binds i-th parameter with SQL prepared statement. 171 | # The first argument is index of parameter. 172 | # The index of first parameter is 1 not 0. 173 | # The second argument value must be Integer value. 174 | # This method uses duckdb_bind_hugeint internally. 175 | # 176 | # require 'duckdb' 177 | # db = DuckDB::Database.open('duckdb_database') 178 | # con = db.connect 179 | # sql ='SELECT name FROM users WHERE hugeint_col = ?' 180 | # stmt = PreparedStatement.new(con, sql) 181 | # stmt.bind_hugeint_internal(1, 1_234_567_890_123_456_789_012_345) 182 | def bind_hugeint_internal(index, value) 183 | lower, upper = integer_to_hugeint(value) 184 | _bind_hugeint(index, lower, upper) 185 | end 186 | 187 | # binds i-th parameter with SQL prepared statement. 188 | # The first argument is index of parameter. 189 | # The index of first parameter is 1 not 0. 190 | # The second argument value must be Integer value. 191 | # This method uses duckdb_bind_uhugeint internally. 192 | # 193 | # require 'duckdb' 194 | # db = DuckDB::Database.open('duckdb_database') 195 | # con = db.connect 196 | # sql ='SELECT name FROM users WHERE uhugeint_col = ?' 197 | # stmt = PreparedStatement.new(con, sql) 198 | # stmt.bind_uhugeint(1, (2**128) - 1) 199 | def bind_uhugeint(index, value) 200 | lower, upper = integer_to_hugeint(value) 201 | _bind_uhugeint(index, lower, upper) 202 | end 203 | 204 | # binds i-th parameter with SQL prepared statement. 205 | # The first argument is index of parameter. 206 | # The index of first parameter is 1 not 0. 207 | # The second argument value is to expected date. 208 | # 209 | # require 'duckdb' 210 | # db = DuckDB::Database.open('duckdb_database') 211 | # con = db.connect 212 | # sql ='SELECT name FROM users WHERE birth_day = ?' 213 | # stmt = PreparedStatement.new(con, sql) 214 | # stmt.bind(1, Date.today) 215 | # # or you can specify date string. 216 | # # stmt.bind(1, '2021-02-23') 217 | def bind_date(index, value) 218 | date = _parse_date(value) 219 | 220 | _bind_date(index, date.year, date.month, date.day) 221 | end 222 | 223 | # binds i-th parameter with SQL prepared statement. 224 | # The first argument is index of parameter. 225 | # The index of first parameter is 1 not 0. 226 | # The second argument value is to expected time value. 227 | # 228 | # require 'duckdb' 229 | # db = DuckDB::Database.open('duckdb_database') 230 | # con = db.connect 231 | # sql ='SELECT name FROM users WHERE birth_time = ?' 232 | # stmt = PreparedStatement.new(con, sql) 233 | # stmt.bind(1, Time.now) 234 | # # or you can specify time string. 235 | # # stmt.bind(1, '07:39:45') 236 | def bind_time(index, value) 237 | time = _parse_time(value) 238 | 239 | _bind_time(index, time.hour, time.min, time.sec, time.usec) 240 | end 241 | 242 | # binds i-th parameter with SQL prepared statement. 243 | # The first argument is index of parameter. 244 | # The index of first parameter is 1 not 0. 245 | # The second argument value is to expected time value. 246 | # 247 | # require 'duckdb' 248 | # db = DuckDB::Database.open('duckdb_database') 249 | # con = db.connect 250 | # sql ='SELECT name FROM users WHERE created_at = ?' 251 | # stmt = PreparedStatement.new(con, sql) 252 | # stmt.bind(1, Time.now) 253 | # # or you can specify timestamp string. 254 | # # stmt.bind(1, '2022-02-23 07:39:45') 255 | def bind_timestamp(index, value) 256 | time = _parse_time(value) 257 | 258 | _bind_timestamp(index, time.year, time.month, time.day, time.hour, time.min, time.sec, time.usec) 259 | end 260 | 261 | # binds i-th parameter with SQL prepared statement. 262 | # The first argument is index of parameter. 263 | # The index of first parameter is 1 not 0. 264 | # The second argument value is to expected ISO8601 time interval string. 265 | # 266 | # require 'duckdb' 267 | # db = DuckDB::Database.open('duckdb_database') 268 | # con = db.connect 269 | # sql ='SELECT value FROM intervals WHERE interval = ?' 270 | # stmt = PreparedStatement.new(con, sql) 271 | # stmt.bind(1, 'P1Y2D') 272 | def bind_interval(index, value) 273 | value = Interval.to_interval(value) 274 | _bind_interval(index, value.interval_months, value.interval_days, value.interval_micros) 275 | end 276 | 277 | # binds i-th parameter with SQL prepared statement. 278 | # The first argument is index of parameter. 279 | # The index of first parameter is 1 not 0. 280 | # The second argument value is to expected BigDecimal value or any value 281 | # that can be parsed into a BigDecimal. 282 | # 283 | # require 'duckdb' 284 | # db = DuckDB::Database.open('duckdb_database') 285 | # con = db.connect 286 | # sql ='SELECT value FROM decimals WHERE decimal = ?' 287 | # stmt = PreparedStatement.new(con, sql) 288 | # stmt.bind_decimal(1, BigDecimal('987654.321')) 289 | def bind_decimal(index, value) 290 | decimal = _parse_deciaml(value) 291 | lower, upper = decimal_to_hugeint(decimal) 292 | width = decimal.to_s('F').gsub(/[^0-9]/, '').length 293 | _bind_decimal(index, lower, upper, width, decimal.scale) 294 | end 295 | 296 | # binds i-th parameter with SQL prepared statement. 297 | # The first argument is index of parameter. 298 | # The index of first parameter is 1 not 0. 299 | # The second argument value is the value of prepared statement parameter. 300 | # 301 | # require 'duckdb' 302 | # db = DuckDB::Database.open('duckdb_database') 303 | # con = db.connect 304 | # sql ='SELECT name, email FROM users WHERE email = ?' 305 | # stmt = PreparedStatement.new(con, sql) 306 | # stmt.bind(1, 'email@example.com') 307 | def bind(index, value) 308 | case index 309 | when Integer 310 | bind_with_index(index, value) 311 | when String 312 | bind_with_name(index, value) 313 | when Symbol 314 | bind_with_name(index.to_s, value) 315 | else 316 | raise(ArgumentError, "1st argument `#{index}` must be Integer or String or Symbol.") 317 | end 318 | end 319 | 320 | private 321 | 322 | def bind_with_index(index, value) 323 | case value 324 | when NilClass 325 | bind_null(index) 326 | when Float 327 | bind_double(index, value) 328 | when Integer 329 | case value 330 | when RANGE_INT64 331 | bind_int64(index, value) 332 | else 333 | bind_varchar(index, value.to_s) 334 | end 335 | when String 336 | blob?(value) ? bind_blob(index, value) : bind_varchar(index, value) 337 | when TrueClass, FalseClass 338 | bind_bool(index, value) 339 | when Time 340 | bind_varchar(index, value.strftime('%Y-%m-%d %H:%M:%S.%N')) 341 | when Date 342 | bind_varchar(index, value.strftime('%Y-%m-%d')) 343 | when BigDecimal 344 | bind_decimal(index, value) 345 | else 346 | raise(DuckDB::Error, "not supported type `#{value}` (#{value.class})") 347 | end 348 | end 349 | 350 | def bind_with_name(name, value) 351 | raise DuckDB::Error, 'not supported binding with name' unless respond_to?(:bind_parameter_index) 352 | 353 | i = bind_parameter_index(name) 354 | bind_with_index(i, value) 355 | end 356 | 357 | def blob?(value) 358 | value.instance_of?(DuckDB::Blob) || value.encoding == Encoding::BINARY 359 | end 360 | end 361 | end 362 | -------------------------------------------------------------------------------- /lib/duckdb/result.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bigdecimal' 4 | 5 | module DuckDB 6 | # The Result class encapsulates a execute result of DuckDB database. 7 | # 8 | # The usage is as follows: 9 | # 10 | # require 'duckdb' 11 | # 12 | # db = DuckDB::Database.open # database in memory 13 | # con = db.connect 14 | # 15 | # con.execute('CREATE TABLE users (id INTEGER, name VARCHAR(30))') 16 | # 17 | # con.execute("INSERT into users VALUES(1, 'Alice')") 18 | # con.execute("INSERT into users VALUES(2, 'Bob')") 19 | # con.execute("INSERT into users VALUES(3, 'Cathy')") 20 | # 21 | # result = con.execute('SELECT * from users') 22 | # result.each do |row| 23 | # p row 24 | # end 25 | class Result 26 | include Enumerable 27 | RETURN_TYPES = %i[invalid changed_rows nothing query_result].freeze 28 | 29 | alias column_size column_count 30 | 31 | class << self 32 | def new 33 | raise DuckDB::Error, 'DuckDB::Result cannot be instantiated directly.' 34 | end 35 | end 36 | 37 | def each(&) 38 | return _chunk_stream unless block_given? 39 | 40 | _chunk_stream(&) 41 | end 42 | 43 | # returns return type. The return value is one of the following symbols: 44 | # :invalid, :changed_rows, :nothing, :query_result 45 | # 46 | # require 'duckdb' 47 | # db = DuckDB::Database.open('duckdb_database') 48 | # con = db.connect 49 | # result = con.execute('CREATE TABLE users (id INTEGER, name VARCHAR(30))') 50 | # result.return_type # => :nothing 51 | def return_type 52 | i = _return_type 53 | raise DuckDB::Error, "Unknown return type: #{i}" if i >= RETURN_TYPES.size 54 | 55 | RETURN_TYPES[i] 56 | end 57 | 58 | # returns statement type. The return value is one of the following symbols: 59 | # :invalid, :select, :insert, :update, :explain, :delete, :prepare, :create, 60 | # :execute, :alter, :transaction, :copy, :analyze, :variable_set, :create_func, 61 | # :drop, :export, :pragma, :vacuum, :call, :set, :load, :relation, :extension, 62 | # :logical_plan, :attach, :detach, :multi 63 | # 64 | # require 'duckdb' 65 | # db = DuckDB::Database.open('duckdb_database') 66 | # con = db.connect 67 | # result = con.execute('CREATE TABLE users (id INTEGER, name VARCHAR(30))') 68 | # result.statement_type # => :create 69 | def statement_type 70 | i = _statement_type 71 | Converter::IntToSym.statement_type_to_sym(i) 72 | end 73 | 74 | # returns all available ENUM type values of the specified column index. 75 | # require 'duckdb' 76 | # db = DuckDB::Database.open('duckdb_database') 77 | # con = db.connect 78 | # con.execute("CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy', '𝘾𝝾օɭ 😎')") 79 | # con.execute("CREATE TABLE enums (id INTEGER, mood mood)") 80 | # result = con.query('SELECT * FROM enums') 81 | # result.enum_dictionary_values(1) # => ['sad', 'ok', 'happy', '𝘾𝝾օɭ 😎'] 82 | def enum_dictionary_values(col_index) 83 | values = [] 84 | _enum_dictionary_size(col_index).times do |i| 85 | values << _enum_dictionary_value(col_index, i) 86 | end 87 | values 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/duckdb/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DuckDB 4 | # The version string of ruby-duckdb. 5 | # Currently, ruby-duckdb is NOT semantic versioning. 6 | VERSION = '1.3.0.0' 7 | end 8 | -------------------------------------------------------------------------------- /sample/async_query.rb: -------------------------------------------------------------------------------- 1 | require 'duckdb' 2 | 3 | DuckDB::Database.open do |db| 4 | db.connect do |con| 5 | con.query('SET threads=1') 6 | con.query('CREATE TABLE tbl as SELECT range a, mod(range, 10) b FROM range(10000)') 7 | con.query('CREATE TABLE tbl2 as SELECT range a, mod(range, 10) b FROM range(10000)') 8 | # con.query('SET ENABLE_PROGRESS_BAR=true') 9 | # con.query('SET ENABLE_PROGRESS_BAR_PRINT=false') 10 | pending_result = con.async_query('SELECT * FROM tbl where b = (SELECT min(b) FROM tbl2)') 11 | 12 | # con.interrupt 13 | while pending_result.state == :not_ready 14 | pending_result.execute_task 15 | print '.' 16 | $stdout.flush 17 | sleep 0.01 18 | end 19 | result = pending_result.execute_pending 20 | puts 21 | p result.each.first 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /sample/async_query_stream.rb: -------------------------------------------------------------------------------- 1 | require 'duckdb' 2 | 3 | DuckDB::Database.open do |db| 4 | db.connect do |con| 5 | con.query('SET threads=1') 6 | con.query('CREATE TABLE tbl as SELECT range a, mod(range, 10) b FROM range(10000)') 7 | con.query('CREATE TABLE tbl2 as SELECT range a, mod(range, 10) b FROM range(10000)') 8 | # con.query('SET ENABLE_PROGRESS_BAR=true') 9 | # con.query('SET ENABLE_PROGRESS_BAR_PRINT=false') 10 | pending_result = con.async_query_stream('SELECT * FROM tbl where b = (SELECT min(b) FROM tbl2)') 11 | 12 | # con.interrupt 13 | while pending_result.state == :not_ready 14 | pending_result.execute_task 15 | print '.' 16 | $stdout.flush 17 | sleep 0.01 18 | end 19 | result = pending_result.execute_pending 20 | puts 21 | p result.each.first 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/duckdb_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module DuckDBTest 6 | class DuckDBTest < Minitest::Test 7 | def test_that_it_has_a_version_number 8 | refute_nil ::DuckDB::VERSION 9 | end 10 | 11 | def test_that_it_has_a_library_version_number 12 | refute_nil ::DuckDB::LIBRARY_VERSION 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/duckdb_test/bind_hugeint_internal_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module DuckDBTest 6 | class DuckDBBindHugeintInternalTest < Minitest::Test 7 | def setup 8 | @db = DuckDB::Database.open 9 | @con = @db.connect 10 | @con.query('CREATE TABLE hugeints (hugeint_value HUGEINT)') 11 | end 12 | 13 | def prepare_test_value(value) 14 | @con.query("INSERT INTO hugeints VALUES (#{value})") 15 | end 16 | 17 | def do_bind_internal_test(value) 18 | prepare_test_value(value) 19 | stmt = @con.prepared_statement('SELECT hugeint_value FROM hugeints WHERE hugeint_value = ?') 20 | stmt.bind_hugeint_internal(1, value) 21 | result = stmt.execute 22 | assert_equal(value, result.first[0]) 23 | end 24 | 25 | def test_bind_internal_positive1 26 | do_bind_internal_test(1) 27 | end 28 | 29 | def test_bind_internal_zero 30 | do_bind_internal_test(0) 31 | end 32 | 33 | def test_bind_internal_negative1 34 | do_bind_internal_test(-1) 35 | end 36 | 37 | def test_bind_internal_positive100 38 | do_bind_internal_test(100) 39 | end 40 | 41 | def test_bind_internal_val_negative100 42 | do_bind_internal_test(-100) 43 | end 44 | 45 | def test_bind_internal_val_max 46 | do_bind_internal_test(170_141_183_460_469_231_731_687_303_715_884_105_727) 47 | end 48 | 49 | def test_bind_internal_val_min 50 | do_bind_internal_test(-170_141_183_460_469_231_731_687_303_715_884_105_727) 51 | end 52 | 53 | def test_bind_internal_raises_error 54 | exception = assert_raises(ArgumentError) do 55 | do_bind_internal_test('170141183460469231731687303715884105727') 56 | end 57 | assert_equal('The argument `"170141183460469231731687303715884105727"` must be Integer.', exception.message) 58 | end 59 | 60 | def teardown 61 | @con.close 62 | @db.close 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/duckdb_test/blob_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module DuckDBTest 6 | class BlobTest < Minitest::Test 7 | def test_initialize 8 | assert_instance_of(DuckDB::Blob, DuckDB::Blob.new('str')) 9 | end 10 | 11 | def test_superclass 12 | assert_equal(String, DuckDB::Blob.superclass) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/duckdb_test/column_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'securerandom' 5 | 6 | module DuckDBTest 7 | class ColumnTest < Minitest::Test 8 | CREATE_TYPE_ENUM_SQL = <<~SQL 9 | CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy', '𝘾𝝾օɭ 😎'); 10 | SQL 11 | 12 | CREATE_TABLE_SQL = <<~SQL 13 | CREATE TABLE table1( 14 | boolean_col BOOLEAN, 15 | tinyint_col TINYINT, 16 | smallint_col SMALLINT, 17 | integer_col INTEGER, 18 | bigint_col BIGINT, 19 | utinyint_col UTINYINT, 20 | usmallint_col USMALLINT, 21 | uinteger_col UINTEGER, 22 | ubigint_col UBIGINT, 23 | real_col REAL, 24 | double_col DOUBLE, 25 | date_col DATE, 26 | time_col TIME, 27 | timestamp_col timestamp, 28 | interval_col INTERVAL, 29 | hugeint_col HUGEINT, 30 | varchar_col VARCHAR, 31 | ủȵȋɕṓ𝓭е_𝒄𝗈ł VARCHAR, 32 | decimal_col DECIMAL, 33 | enum_col mood, 34 | int_list_col INT[], 35 | varchar_list_col VARCHAR[], 36 | struct_col STRUCT(word VARCHAR, length INTEGER), 37 | uuid_col UUID, 38 | map_col MAP(INTEGER, VARCHAR) 39 | ); 40 | SQL 41 | 42 | INSERT_SQL = <<~SQL 43 | INSERT INTO table1 VALUES 44 | ( 45 | true, 46 | 1, 47 | 32767, 48 | 2147483647, 49 | 9223372036854775807, 50 | 1, 51 | 32767, 52 | 2147483647, 53 | 9223372036854775807, 54 | 12345.375, 55 | 123.456789, 56 | '2019-11-03', 57 | '12:34:56', 58 | '2019-11-03 12:34:56', 59 | '1 day', 60 | 170141183460469231731687303715884105727, 61 | 'string', 62 | 'ȕɲᎥᴄⲟ𝑑ẽ 𝑠τᵲïņ𝕘 😃', 63 | 1, 64 | 'sad', 65 | [1, 2, 3], 66 | ['a', 'b', 'c'], 67 | ROW('Ruby', 4), 68 | '#{SecureRandom.uuid}', 69 | MAP{1: 'foo'} 70 | ) 71 | SQL 72 | 73 | SELECT_SQL = 'SELECT * FROM table1' 74 | 75 | EXPECTED_TYPES = %i[ 76 | boolean 77 | tinyint 78 | smallint 79 | integer 80 | bigint 81 | utinyint 82 | usmallint 83 | uinteger 84 | ubigint 85 | float 86 | double 87 | date 88 | time 89 | timestamp 90 | interval 91 | hugeint 92 | varchar 93 | varchar 94 | decimal 95 | enum 96 | list 97 | list 98 | struct 99 | uuid 100 | map 101 | ].freeze 102 | 103 | EXPECTED_NAMES = %w[ 104 | boolean_col 105 | tinyint_col 106 | smallint_col 107 | integer_col 108 | bigint_col 109 | utinyint_col 110 | usmallint_col 111 | uinteger_col 112 | ubigint_col 113 | real_col 114 | double_col 115 | date_col 116 | time_col 117 | timestamp_col 118 | interval_col 119 | hugeint_col 120 | varchar_col 121 | ủȵȋɕṓ𝓭е_𝒄𝗈ł 122 | decimal_col 123 | enum_col 124 | int_list_col 125 | varchar_list_col 126 | struct_col 127 | uuid_col 128 | map_col 129 | ].freeze 130 | 131 | def setup 132 | @db = DuckDB::Database.open 133 | @con = @db.connect 134 | create_data(@con) 135 | result = @con.query(SELECT_SQL) 136 | @columns = result.columns 137 | end 138 | 139 | def test_type 140 | assert_equal(EXPECTED_TYPES, @columns.map(&:type)) 141 | end 142 | 143 | def test_logical_type 144 | logical_types = @columns.map(&:logical_type) 145 | assert(logical_types.all? { |logical_type| logical_type.is_a?(DuckDB::LogicalType) }) 146 | end 147 | 148 | def test_name 149 | assert_equal(EXPECTED_NAMES, @columns.map(&:name)) 150 | end 151 | 152 | private 153 | 154 | def create_data(con) 155 | con.query(CREATE_TYPE_ENUM_SQL) 156 | con.query(CREATE_TABLE_SQL) 157 | con.query(INSERT_SQL) 158 | end 159 | end 160 | end 161 | -------------------------------------------------------------------------------- /test/duckdb_test/config_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module DuckDBTest 6 | class ConfigTest < Minitest::Test 7 | def test_s_new 8 | config = DuckDB::Config.new 9 | assert_instance_of(DuckDB::Config, config) 10 | end 11 | 12 | def test_s_size 13 | assert_operator(0, :<=, DuckDB::Config.size) 14 | end 15 | 16 | def test_s_get_config_flag 17 | key, value = DuckDB::Config.get_config_flag(0) 18 | assert_equal('access_mode', key) 19 | assert_match(/\AAccess mode of the database/, value) 20 | 21 | assert_raises(TypeError) do 22 | DuckDB::Config.get_config_flag('foo') 23 | end 24 | 25 | assert_raises(DuckDB::Error) do 26 | DuckDB::Config.get_config_flag(DuckDB::Config.size) 27 | end 28 | end 29 | 30 | def test_s_key_description 31 | key, value = DuckDB::Config.key_description(0) 32 | assert_equal('access_mode', key) 33 | assert_match(/\AAccess mode of the database/, value) 34 | end 35 | 36 | def test_s_key_descriptions 37 | h = DuckDB::Config.key_descriptions 38 | assert_instance_of(Hash, h) 39 | assert_match(/\AAccess mode of the database/, h['access_mode']) 40 | end 41 | 42 | def test_set_config 43 | config = DuckDB::Config.new 44 | assert_instance_of(DuckDB::Config, config.set_config('access_mode', 'READ_ONLY')) 45 | 46 | assert_raises(DuckDB::Error) do 47 | config.set_config('access_mode', 'INVALID_VALUE') 48 | end 49 | end 50 | 51 | def test_set_invalid_option 52 | config = DuckDB::Config.new 53 | assert_instance_of(DuckDB::Config, config.set_config('aaa_invalid_option', 'READ_ONLY')) 54 | assert_raises(DuckDB::Error) do 55 | DuckDB::Database.open(nil, config) 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/duckdb_test/connection_execute_multiple_sql_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module DuckDBTest 6 | class ConnectionExecuteMultipleSqlTest < Minitest::Test 7 | def setup 8 | @db = DuckDB::Database.open 9 | @con = @db.connect 10 | end 11 | 12 | def test_multiple_sql 13 | exception = assert_raises(DuckDB::Error) do 14 | @con.execute('CREATE TABLE test (v VARCHAR); CREATE TABLE test (v VARCHAR); SELECT 42;') 15 | end 16 | assert_match(/Table with name "test" already exists/, exception.message) 17 | end 18 | 19 | def test_multiple_select_sql 20 | @con.execute('CREATE TABLE test (i INTEGER)') 21 | result = @con.execute(<<-SQL) 22 | INSERT INTO test VALUES (1), (2); 23 | SELECT * FROM test; 24 | INSERT INTO test VALUES (3), (4); 25 | SELECT * FROM test; 26 | SQL 27 | assert_equal([[1], [2], [3], [4]], result.to_a) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/duckdb_test/connection_query_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DuckDBTest 4 | class ConnectionQueryTest < Minitest::Test 5 | def teardown 6 | File.delete(@file) if File.exist?(@file) 7 | end 8 | 9 | def test_prepared_statement_destroy_in_query 10 | outputs = `ruby -Ilib test/ng/connection_query_ng.rb` 11 | @file, before, after = outputs.split("\n") 12 | assert_equal(before, after) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/duckdb_test/connection_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module DuckDBTest 6 | class ConnectionTest < Minitest::Test 7 | def setup 8 | @db = DuckDB::Database.open 9 | @con = @db.connect 10 | end 11 | 12 | def test_query 13 | assert_instance_of(DuckDB::Result, @con.query('CREATE TABLE table1 (id INTEGER)')) 14 | end 15 | 16 | def test_query_with_valid_params 17 | @con.query('CREATE TABLE t (col1 INTEGER, col2 STRING)') 18 | assert_instance_of(DuckDB::Result, @con.query('INSERT INTO t VALUES(?, ?)', 1, 'a')) 19 | r = @con.query('SELECT col1, col2 FROM t WHERE col1 = ? and col2 = ?', 1, 'a') 20 | 21 | r = r.to_a # fix for using duckdb_fetch_chunk in Result#chunk_each 22 | 23 | assert_equal(1, r.each.first[0]) 24 | assert_equal('a', r.each.first[1]) 25 | end 26 | 27 | def test_query_with_valid_hash_params 28 | @con.query('CREATE TABLE t (col1 INTEGER, col2 STRING)') 29 | assert_instance_of(DuckDB::Result, @con.query('INSERT INTO t VALUES($col1, $col2)', col2: 'a', col1: 1)) 30 | r = @con.query('SELECT col1, col2 FROM t WHERE col1 = $col1 and col2 = $col2', col2: 'a', col1: 1) 31 | 32 | r = r.to_a # fix for using duckdb_fetch_chunk in Result#chunk_each 33 | 34 | assert_equal(1, r.each.first[0]) 35 | assert_equal('a', r.each.first[1]) 36 | end 37 | 38 | def test_query_with_invalid_params 39 | assert_raises(DuckDB::Error) { @con.query('foo', 'bar') } 40 | 41 | assert_raises(ArgumentError) { @con.query } 42 | 43 | assert_raises(TypeError) { @con.query(1) } 44 | 45 | assert_raises(DuckDB::Error) do 46 | invalid_sql = 'CREATE TABLE table1 (' 47 | @con.query(invalid_sql) 48 | end 49 | end 50 | 51 | def test_async_query 52 | pending_result = @con.async_query('CREATE TABLE table1 (id INTEGER)') 53 | assert_instance_of(DuckDB::PendingResult, pending_result) 54 | end 55 | 56 | def test_async_query_with_valid_params 57 | @con.query('CREATE TABLE t (col1 INTEGER, col2 STRING)') 58 | @con.query('INSERT INTO t VALUES(?, ?)', 1, 'a') 59 | pending_result = @con.async_query('SELECT col1, col2 FROM t WHERE col1 = ? and col2 = ?', 1, 'a') 60 | pending_result.execute_task 61 | sleep 0.1 62 | result = pending_result.execute_pending 63 | assert_equal([1, 'a'], result.each.first) 64 | end 65 | 66 | def test_async_query_with_valid_hash_params 67 | @con.query('CREATE TABLE t (col1 INTEGER, col2 STRING)') 68 | @con.query('INSERT INTO t VALUES($col1, $col2)', col2: 'a', col1: 1) 69 | 70 | pending_result = @con.async_query( 71 | 'SELECT col1, col2 FROM t WHERE col1 = $col1 and col2 = $col2', 72 | col2: 'a', 73 | col1: 1 74 | ) 75 | pending_result.execute_task 76 | sleep 0.1 77 | result = pending_result.execute_pending 78 | assert_equal([1, 'a'], result.each.first) 79 | end 80 | 81 | def test_async_query_with_invalid_params 82 | assert_raises(DuckDB::Error) { @con.async_query('foo', 'bar') } 83 | 84 | assert_raises(ArgumentError) { @con.async_query } 85 | 86 | assert_raises(TypeError) { @con.async_query(1) } 87 | 88 | assert_raises(DuckDB::Error) do 89 | invalid_sql = 'CREATE TABLE table1 (' 90 | @con.async_query(invalid_sql) 91 | end 92 | end 93 | 94 | def test_async_query_stream 95 | pending_result = @con.async_query_stream('CREATE TABLE table1 (id INTEGER)') 96 | assert_instance_of(DuckDB::PendingResult, pending_result) 97 | end 98 | 99 | def test_async_query_stream_with_valid_params 100 | @con.query('CREATE TABLE t (col1 INTEGER, col2 STRING)') 101 | @con.query('INSERT INTO t VALUES(?, ?)', 1, 'a') 102 | pending_result = @con.async_query_stream('SELECT col1, col2 FROM t WHERE col1 = ? and col2 = ?', 1, 'a') 103 | pending_result.execute_task 104 | sleep 0.1 105 | result = pending_result.execute_pending 106 | assert_equal([1, 'a'], result.each.first) 107 | end 108 | 109 | def test_async_query_stream_with_invalid_params 110 | assert_raises(DuckDB::Error) { @con.async_query_stream('foo', 'bar') } 111 | 112 | assert_raises(ArgumentError) { @con.async_query_stream } 113 | 114 | assert_raises(TypeError) { @con.async_query_stream(1) } 115 | 116 | assert_raises(DuckDB::Error) do 117 | invalid_sql = 'CREATE TABLE table1 (' 118 | @con.async_query_stream(invalid_sql) 119 | end 120 | end 121 | 122 | def test_async_query_stream_with_valid_hash_params 123 | @con.query('CREATE TABLE t (col1 INTEGER, col2 STRING)') 124 | @con.query('INSERT INTO t VALUES($col1, $col2)', col2: 'a', col1: 1) 125 | 126 | pending_result = @con.async_query_stream( 127 | 'SELECT col1, col2 FROM t WHERE col1 = $col1 and col2 = $col2', 128 | col2: 'a', 129 | col1: 1 130 | ) 131 | pending_result.execute_task 132 | sleep 0.1 133 | result = pending_result.execute_pending 134 | assert_equal([1, 'a'], result.each.first) 135 | end 136 | 137 | def test_execute 138 | @con.execute('CREATE TABLE t (col1 INTEGER, col2 STRING)') 139 | assert_instance_of(DuckDB::Result, @con.execute('INSERT INTO t VALUES(?, ?)', 1, 'a')) 140 | r = @con.execute('SELECT col1, col2 FROM t WHERE col1 = ? and col2 = ?', 1, 'a') 141 | 142 | r = r.to_a # fix for using duckdb_fetch_chunk in Result#chunk_each 143 | 144 | assert_equal(1, r.each.first[0]) 145 | assert_equal('a', r.each.first[1]) 146 | end 147 | 148 | def test_query_progress 149 | @con.query('SET threads=1') 150 | @con.query('CREATE TABLE tbl as SELECT range a, mod(range, 10) b FROM range(10000)') 151 | @con.query('CREATE TABLE tbl2 as SELECT range a, FROM range(10000)') 152 | @con.query('SET ENABLE_PROGRESS_BAR=true') 153 | @con.query('SET ENABLE_PROGRESS_BAR_PRINT=false') 154 | 155 | assert_equal(-1, @con.query_progress.percentage) 156 | pending_result = @con.async_query('SELECT count(*) FROM tbl where a = (SELECT min(a) FROM tbl2)') 157 | assert_equal(0, @con.query_progress.percentage) 158 | pending_result.execute_task while @con.query_progress.percentage.zero? 159 | query_progress = @con.query_progress 160 | assert_instance_of(DuckDB::QueryProgress, query_progress) 161 | 162 | percentage = query_progress.percentage 163 | assert_operator(percentage, :>, 0, "QueryProgress#percentage(#{percentage}) to be > 0") 164 | 165 | rows_processed = query_progress.rows_processed 166 | assert_operator(rows_processed, :>, 0, "QueryProgress#rows_processed(#{rows_processed}) to be > 0") 167 | 168 | total_rows_to_process = query_progress.total_rows_to_process 169 | assert_operator( 170 | total_rows_to_process, :>, 0, 171 | "QueryProgress.total_rows_to_process(#{total_rows_to_process}) to be > 0" 172 | ) 173 | 174 | assert_operator( 175 | total_rows_to_process, :>=, rows_processed, 176 | "QueryProgress: total_rows_to_process(#{total_rows_to_process}) to be >= rows_processed(#{rows_processed})" 177 | ) 178 | 179 | # test interrupt 180 | @con.interrupt 181 | while pending_result.state == :not_ready 182 | pending_result.execute_task 183 | assert(pending_result.state != :ready, 'pending_result.state should not be :ready') 184 | end 185 | end 186 | 187 | def test_disconnect 188 | @con.disconnect 189 | exception = assert_raises(DuckDB::Error) do 190 | @con.execute('CREATE TABLE t (col1 INTEGER, col2 STRING)') 191 | end 192 | assert_match(/Database connection closed/, exception.message) 193 | end 194 | 195 | def test_close 196 | @con.close 197 | exception = assert_raises(DuckDB::Error) do 198 | @con.execute('CREATE TABLE t (col1 INTEGER, col2 STRING)') 199 | end 200 | assert_match(/Database connection closed/, exception.message) 201 | end 202 | 203 | def test_connect 204 | @con.disconnect 205 | exception = assert_raises(DuckDB::Error) do 206 | @con.execute('CREATE TABLE t (col1 INTEGER, col2 STRING)') 207 | end 208 | assert_match(/Database connection closed/, exception.message) 209 | @con.connect(@db) 210 | assert_instance_of(DuckDB::Result, @con.execute('CREATE TABLE t (col1 INTEGER, col2 STRING)')) 211 | end 212 | 213 | def test_connect_with_block 214 | @con.disconnect 215 | @con.connect(@db) do |con| 216 | assert_instance_of(DuckDB::Result, con.execute('CREATE TABLE t (col1 INTEGER, col2 STRING)')) 217 | end 218 | exception = assert_raises(DuckDB::Error) do 219 | @con.execute('CREATE TABLE t (col1 INTEGER, col2 STRING)') 220 | end 221 | assert_match(/Database connection closed/, exception.message) 222 | end 223 | 224 | def test_open 225 | @con.disconnect 226 | exception = assert_raises(DuckDB::Error) do 227 | @con.execute('CREATE TABLE t (col1 INTEGER, col2 STRING)') 228 | end 229 | assert_match(/Database connection closed/, exception.message) 230 | @con.open(@db) 231 | assert_instance_of(DuckDB::Result, @con.execute('CREATE TABLE t (col1 INTEGER, col2 STRING)')) 232 | end 233 | 234 | def test_prepared_statement 235 | @con.execute('CREATE TABLE t (col1 INTEGER, col2 STRING)') 236 | assert_instance_of(DuckDB::PreparedStatement, @con.prepared_statement('SELECT * FROM t WHERE col1 = $1')) 237 | end 238 | 239 | def test_prepare 240 | @con.execute('CREATE TABLE t (col1 INTEGER, col2 STRING)') 241 | assert_instance_of(DuckDB::PreparedStatement, @con.prepare('SELECT * FROM t WHERE col1 = $1')) 242 | end 243 | 244 | def test_appender 245 | @con.execute('CREATE TABLE t (col1 INTEGER, col2 STRING)') 246 | assert_instance_of(DuckDB::Appender, @con.appender('t')) 247 | end 248 | 249 | def test_appender_with_schema 250 | @con.execute('CREATE SCHEMA a; CREATE TABLE a.b (col1 INTEGER, col2 STRING)') 251 | assert_instance_of(DuckDB::Appender, @con.appender('a.b')) 252 | end 253 | 254 | def test_appender_with_block 255 | @con.execute('CREATE TABLE t (col1 INTEGER, col2 STRING)') 256 | @con.appender('t') do |appender| 257 | appender.append_row(1, 'foo') 258 | end 259 | r = @con.query('SELECT col1, col2 FROM t') 260 | assert_equal([1, 'foo'], r.first) 261 | end 262 | end 263 | end 264 | -------------------------------------------------------------------------------- /test/duckdb_test/database_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'tempfile' 5 | 6 | module DuckDBTest 7 | class DatabaseTest < Minitest::Test 8 | def setup 9 | @path = create_path 10 | end 11 | 12 | def teardown 13 | File.unlink(@path) if File.exist?(@path) 14 | walf = "#{@path}.wal" 15 | File.unlink(walf) if File.exist?(walf) 16 | end 17 | 18 | def test_s__open 19 | assert_raises(NoMethodError) { DuckDB::Database._open } 20 | end 21 | 22 | def test_s_open 23 | assert_instance_of(DuckDB::Database, DuckDB::Database.open) 24 | end 25 | 26 | def test_s_open_argument 27 | db = DuckDB::Database.open(@path) 28 | assert_instance_of(DuckDB::Database, db) 29 | db.close 30 | 31 | assert_raises(TypeError) { DuckDB::Database.open('foo', 'bar') } 32 | assert_raises(TypeError) { DuckDB::Database.open(1) } 33 | 34 | assert_raises(DuckDB::Error) do 35 | not_exist_path = "#{create_path}/#{create_path}" 36 | DuckDB::Database.open(not_exist_path) 37 | end 38 | end 39 | 40 | def test_s_open_with_config 41 | config = DuckDB::Config.new 42 | config['default_order'] = 'DESC' 43 | db = DuckDB::Database.open(nil, config) 44 | conn = db.connect 45 | conn.execute('CREATE TABLE t (col1 INTEGER);') 46 | conn.execute('INSERT INTO t VALUES(3),(1),(4),(2);') 47 | r = conn.execute('SELECT * FROM t ORDER BY col1') 48 | assert_equal([4], r.first) 49 | 50 | config['default_order'] = 'ASC' 51 | db = DuckDB::Database.open(nil, config) 52 | conn = db.connect 53 | conn.execute('CREATE TABLE t (col1 INTEGER);') 54 | conn.execute('INSERT INTO t VALUES(3),(1),(4),(2);') 55 | r = conn.execute('SELECT * FROM t ORDER BY col1') 56 | assert_equal([1], r.first) 57 | end 58 | 59 | def test_s_open_block 60 | result = DuckDB::Database.open do |db| 61 | assert_instance_of(DuckDB::Database, db) 62 | con = db.connect 63 | assert_instance_of(DuckDB::Connection, con) 64 | con.query('CREATE TABLE t (id INTEGER)') 65 | end 66 | assert_instance_of(DuckDB::Result, result) 67 | end 68 | 69 | def test_connect 70 | assert_instance_of(DuckDB::Connection, DuckDB::Database.open.connect) 71 | end 72 | 73 | def test_connect_with_block 74 | result = DuckDB::Database.open do |db| 75 | db.connect do |con| 76 | assert_instance_of(DuckDB::Connection, con) 77 | con.query('CREATE TABLE t (id INTEGER)') 78 | end 79 | end 80 | assert_instance_of(DuckDB::Result, result) 81 | end 82 | 83 | def test_close 84 | db = DuckDB::Database.open 85 | con = db.connect 86 | db.close 87 | exception = assert_raises(DuckDB::Error) do 88 | con.query('SELECT * from DUMMY') 89 | end 90 | 91 | assert_match(/DUMMY does not exist/, exception.message) 92 | end 93 | 94 | private 95 | 96 | def create_path 97 | "#{Time.now.strftime('%Y%m%d%H%M%S')}-#{Process.pid}-#{rand(100..999)}" 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /test/duckdb_test/duckdb_version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DuckDBTest 4 | class DuckDBVersion 5 | include Comparable 6 | 7 | def self.duckdb_version 8 | return @duckdb_version if @duckdb_version 9 | 10 | DuckDB::Database.open('version.duckdb') do |db| 11 | db.connect do |con| 12 | r = con.query('SELECT VERSION();') 13 | @duckdb_version = DuckDBVersion.new(r.first.first.sub('v', '')) 14 | end 15 | end 16 | ensure 17 | FileUtils.rm_f('version.duckdb') 18 | end 19 | 20 | # Ruby 2.6.X does not support comparing Gem::Version object with string. 21 | # Gem::Version.new(x) >= '0.3.3' #=> Exception 22 | def initialize(str) 23 | @version = Gem::Version.new(str) 24 | end 25 | 26 | def <=>(other) 27 | @version <=> Gem::Version.new(other) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/duckdb_test/enum_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module DuckDBTest 6 | class EnumTest < Minitest::Test 7 | def self.create_table 8 | @db ||= DuckDB::Database.open # FIXME 9 | con = @db.connect 10 | con.query(create_enum_sql) 11 | con.query(create_table_sql) 12 | con.query('INSERT INTO enum_test (id, mood) VALUES (1, $1)', 'sad') 13 | con 14 | end 15 | 16 | def self.con 17 | @con ||= create_table 18 | end 19 | 20 | def self.create_enum_sql 21 | <<~SQL 22 | CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy', '𝘾𝝾օɭ 😎') 23 | SQL 24 | end 25 | 26 | def self.create_table_sql 27 | <<~SQL 28 | CREATE TABLE enum_test ( 29 | id INTEGER PRIMARY KEY, 30 | mood mood 31 | ) 32 | SQL 33 | end 34 | 35 | def setup 36 | con = self.class.con 37 | @result = con.query('SELECT * FROM enum_test WHERE id = 1') 38 | end 39 | 40 | def test_result__enum_dictionary_size 41 | assert_equal(4, @result.send(:_enum_dictionary_size, 1)) 42 | end 43 | 44 | def test_result__enum_dictionary_value 45 | assert_equal('sad', @result.send(:_enum_dictionary_value, 1, 0)) 46 | assert_equal('ok', @result.send(:_enum_dictionary_value, 1, 1)) 47 | assert_equal('𝘾𝝾օɭ 😎', @result.send(:_enum_dictionary_value, 1, 3)) 48 | end 49 | 50 | def test_result_enum_dictionary_values 51 | assert_equal(['sad', 'ok', 'happy', '𝘾𝝾օɭ 😎'], @result.enum_dictionary_values(1)) 52 | end 53 | 54 | def test_enum_insert_select 55 | assert_equal([1, 'sad'], @result.first) 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /test/duckdb_test/extracted_statements_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'time' 5 | 6 | module DuckDBTest 7 | class ExtractedStatementsTest < Minitest::Test 8 | def setup 9 | @db = DuckDB::Database.open 10 | @con = @db.connect 11 | end 12 | 13 | def test_s_new 14 | assert_instance_of DuckDB::ExtractedStatements, DuckDB::ExtractedStatements.new(@con, 'SELECT 1') 15 | 16 | assert_raises TypeError do 17 | DuckDB::ExtractedStatements.new(1, 'SELECT 2') 18 | end 19 | 20 | assert_raises TypeError do 21 | DuckDB::ExtractedStatements.new(@con, 2) 22 | end 23 | end 24 | 25 | def test_s_new_with_invalid_sql 26 | ex = assert_raises DuckDB::Error do 27 | DuckDB::ExtractedStatements.new(@con, 'SELECT 1; INVALID STATEMENT; SELECT 3') 28 | end 29 | assert_match(/\AParser Error: syntax error at or near "INVALID"/, ex.message) 30 | end 31 | 32 | def test_size 33 | stmts = DuckDB::ExtractedStatements.new(@con, 'SELECT 1; SELECT 2; SELECT 3') 34 | assert_equal 3, stmts.size 35 | end 36 | 37 | def test_prepared_statement 38 | stmts = DuckDB::ExtractedStatements.new(@con, 'SELECT 1; SELECT 2; SELECT 3') 39 | stmt = stmts.prepared_statement(@con, 0) 40 | assert_instance_of(DuckDB::PreparedStatement, stmt) 41 | r = stmt.execute 42 | assert_equal([[1]], r.to_a) 43 | 44 | stmt = stmts.prepared_statement(@con, 1) 45 | r = stmt.execute 46 | assert_equal([[2]], r.to_a) 47 | 48 | stmt = stmts.prepared_statement(@con, 2) 49 | r = stmt.execute 50 | assert_equal([[3]], r.to_a) 51 | end 52 | 53 | def test_prepared_statement_with_invalid_index 54 | stmts = DuckDB::ExtractedStatements.new(@con, 'SELECT 1; SELECT 2; SELECT 3') 55 | ex = assert_raises DuckDB::Error do 56 | stmts.prepared_statement(@con, 3) 57 | end 58 | assert_equal 'Failed to create DuckDB::PreparedStatement object.', ex.message 59 | 60 | ex = assert_raises DuckDB::Error do 61 | stmts.prepared_statement(@con, -1) 62 | end 63 | assert_equal 'Failed to create DuckDB::PreparedStatement object.', ex.message 64 | end 65 | 66 | def test_destroy 67 | stmts = DuckDB::ExtractedStatements.new(@con, 'SELECT 1; SELECT 2; SELECT 3') 68 | assert_nil(stmts.destroy) 69 | end 70 | 71 | def test_each_without_block 72 | stmts = DuckDB::ExtractedStatements.new(@con, 'SELECT 1; SELECT 2; SELECT 3') 73 | enum_stmts = stmts.each 74 | assert_instance_of(Enumerator, enum_stmts) 75 | assert_equal(3, enum_stmts.size) 76 | end 77 | 78 | def test_each_with_block 79 | stmts = DuckDB::ExtractedStatements.new(@con, 'SELECT 1; SELECT 2; SELECT 3') 80 | i = 1 81 | stmts.each do |stmt| 82 | r = stmt.execute 83 | assert_equal([[i]], r.to_a) 84 | i += 1 85 | end 86 | end 87 | 88 | def teardown 89 | @con.close 90 | @db.close 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /test/duckdb_test/instance_cache_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'fileutils' 5 | 6 | if defined?(DuckDB::InstanceCache) 7 | 8 | module DuckDBTest 9 | class InstanceCacheTest < Minitest::Test 10 | def test_s_new 11 | assert_instance_of DuckDB::InstanceCache, DuckDB::InstanceCache.new 12 | end 13 | 14 | def test_get_or_create 15 | cache = DuckDB::InstanceCache.new 16 | path = 'test_shared_db.db' 17 | 30.times do 18 | thread = Thread.new do 19 | db = cache.get_or_create(path) 20 | assert_instance_of DuckDB::Database, db 21 | db.close 22 | end 23 | db = cache.get_or_create(path) 24 | assert_instance_of DuckDB::Database, db 25 | db.close 26 | thread.join 27 | 28 | FileUtils.rm_f(path) 29 | end 30 | end 31 | 32 | def test_get_or_create_without_path 33 | cache = DuckDB::InstanceCache.new 34 | db = cache.get_or_create 35 | assert_instance_of DuckDB::Database, db 36 | db.close 37 | end 38 | 39 | def test_get_or_create_with_empty_path 40 | cache = DuckDB::InstanceCache.new 41 | db = cache.get_or_create('') 42 | assert_instance_of DuckDB::Database, db 43 | db.close 44 | end 45 | 46 | def test_get_or_create_with_memory 47 | cache = DuckDB::InstanceCache.new 48 | db = cache.get_or_create(':memory:') 49 | assert_instance_of DuckDB::Database, db 50 | db.close 51 | end 52 | 53 | def test_get_or_create_with_config 54 | cache = DuckDB::InstanceCache.new 55 | config = DuckDB::Config.new 56 | config['default_order'] = 'DESC' 57 | db = cache.get_or_create(nil, config) 58 | con = db.connect 59 | con.query('CREATE TABLE numbers (number INTEGER)') 60 | con.query('INSERT INTO numbers VALUES (2), (1), (4), (3)') 61 | 62 | result = con.query('SELECT number FROM numbers ORDER BY number') 63 | assert_equal(4, result.first.first) 64 | con.close 65 | db.close 66 | cache.destroy 67 | end 68 | 69 | def test_destroy 70 | cache = DuckDB::InstanceCache.new 71 | assert_nil cache.destroy 72 | end 73 | end 74 | end 75 | 76 | end 77 | -------------------------------------------------------------------------------- /test/duckdb_test/interval_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module DuckDBTest 6 | class IntervalTest < Minitest::Test 7 | def test_s_iso8601_parse 8 | assert_equal( 9 | DuckDB::Interval.new(interval_months: 14, interval_days: 3, interval_micros: 14_706_700_000), 10 | DuckDB::Interval.iso8601_parse('P1Y2M3DT4H5M6.7S') 11 | ) 12 | assert_equal( 13 | DuckDB::Interval.new(interval_months: 14, interval_days: 3, interval_micros: 14_706_000_007), 14 | DuckDB::Interval.iso8601_parse('P1Y2M3DT4H5M6.000007S') 15 | ) 16 | assert_equal( 17 | DuckDB::Interval.new(interval_months: -14, interval_days: -3, interval_micros: -14_706_700_000), 18 | DuckDB::Interval.iso8601_parse('-P1Y2M3DT4H5M6.7S') 19 | ) 20 | assert_equal( 21 | DuckDB::Interval.new(interval_months: -14, interval_days: -3, interval_micros: -14_706_000_007), 22 | DuckDB::Interval.iso8601_parse('-P1Y2M3DT4H5M6.000007S') 23 | ) 24 | assert_equal( 25 | DuckDB::Interval.new(interval_months: -14, interval_days: -3, interval_micros: -14_706_700_000), 26 | DuckDB::Interval.iso8601_parse('P-1Y-2M-3DT-4H-5M-6.7S') 27 | ) 28 | assert_equal( 29 | DuckDB::Interval.new(interval_months: -14, interval_days: -3, interval_micros: -14_706_000_007), 30 | DuckDB::Interval.iso8601_parse('P-1Y-2M-3DT-4H-5M-6.000007S') 31 | ) 32 | 33 | assert_equal( 34 | DuckDB::Interval.new(interval_months: 12), 35 | DuckDB::Interval.iso8601_parse('P1Y') 36 | ) 37 | assert_equal( 38 | DuckDB::Interval.new(interval_months: 1), 39 | DuckDB::Interval.iso8601_parse('P1M') 40 | ) 41 | assert_equal( 42 | DuckDB::Interval.new(interval_days: 1), 43 | DuckDB::Interval.iso8601_parse('P1D') 44 | ) 45 | assert_equal( 46 | DuckDB::Interval.new(interval_micros: 3_600_000_000), 47 | DuckDB::Interval.iso8601_parse('PT1H') 48 | ) 49 | assert_equal( 50 | DuckDB::Interval.new(interval_micros: 60_000_000), 51 | DuckDB::Interval.iso8601_parse('PT1M') 52 | ) 53 | assert_equal( 54 | DuckDB::Interval.new(interval_micros: 1_000_000), 55 | DuckDB::Interval.iso8601_parse('PT1S') 56 | ) 57 | assert_equal( 58 | DuckDB::Interval.new(interval_micros: 1), 59 | DuckDB::Interval.iso8601_parse('PT0.000001S') 60 | ) 61 | assert_raises(ArgumentError) { DuckDB::Interval.iso8601_parse('1Y2M3DT4H5M6.7') } 62 | end 63 | 64 | def test_s_mk_interval 65 | assert_equal( 66 | DuckDB::Interval.new(interval_months: 14, interval_days: 3, interval_micros: 14_706_700_000), 67 | DuckDB::Interval.mk_interval(year: 1, month: 2, day: 3, hour: 4, min: 5, sec: 6, usec: 700_000) 68 | ) 69 | assert_equal( 70 | DuckDB::Interval.new(interval_months: 14, interval_days: 3, interval_micros: 14_706_000_007), 71 | DuckDB::Interval.mk_interval(year: 1, month: 2, day: 3, hour: 4, min: 5, sec: 6, usec: 7) 72 | ) 73 | assert_equal( 74 | DuckDB::Interval.new(interval_months: 0, interval_days: 0, interval_micros: 0), 75 | DuckDB::Interval.mk_interval(year: 0, month: 0, day: 0, hour: 0, min: 0, sec: 0, usec: 0) 76 | ) 77 | assert_equal( 78 | DuckDB::Interval.new, 79 | DuckDB::Interval.mk_interval 80 | ) 81 | assert_equal( 82 | DuckDB::Interval.new(interval_months: -14, interval_days: -3, interval_micros: -14_706_700_000), 83 | DuckDB::Interval.mk_interval(year: -1, month: -2, day: -3, hour: -4, min: -5, sec: -6, usec: -700_000) 84 | ) 85 | assert_equal( 86 | DuckDB::Interval.new(interval_months: -14, interval_days: -3, interval_micros: -14_706_000_007), 87 | DuckDB::Interval.mk_interval(year: -1, month: -2, day: -3, hour: -4, min: -5, sec: -6, usec: -7) 88 | ) 89 | assert_equal( 90 | DuckDB::Interval.new(interval_months: 12), 91 | DuckDB::Interval.mk_interval(year: 1) 92 | ) 93 | assert_equal( 94 | DuckDB::Interval.new(interval_months: 1), 95 | DuckDB::Interval.mk_interval(month: 1) 96 | ) 97 | assert_equal( 98 | DuckDB::Interval.new(interval_days: 1), 99 | DuckDB::Interval.mk_interval(day: 1) 100 | ) 101 | assert_equal( 102 | DuckDB::Interval.new(interval_micros: 3_600_000_000), 103 | DuckDB::Interval.mk_interval(hour: 1) 104 | ) 105 | assert_equal( 106 | DuckDB::Interval.new(interval_micros: 60_000_000), 107 | DuckDB::Interval.mk_interval(min: 1) 108 | ) 109 | assert_equal( 110 | DuckDB::Interval.new(interval_micros: 1_000_000), 111 | DuckDB::Interval.mk_interval(sec: 1) 112 | ) 113 | assert_equal( 114 | DuckDB::Interval.new(interval_micros: 1), 115 | DuckDB::Interval.mk_interval(usec: 1) 116 | ) 117 | end 118 | 119 | def test_initialize 120 | interval = DuckDB::Interval.new 121 | assert_instance_of(DuckDB::Interval, interval) 122 | assert_equal(0, interval.interval_months) 123 | assert_equal(0, interval.interval_days) 124 | assert_equal(0, interval.interval_micros) 125 | end 126 | 127 | def test_equality 128 | interval1 = DuckDB::Interval.new 129 | interval2 = DuckDB::Interval.new 130 | assert_equal(interval1, interval2) 131 | end 132 | 133 | def test_eql? 134 | interval1 = DuckDB::Interval.new 135 | interval2 = DuckDB::Interval.new 136 | assert_equal(true, interval1.eql?(interval2)) 137 | end 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /test/duckdb_test/logical_type_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module DuckDBTest 6 | class LogicalTypeTest < Minitest::Test 7 | CREATE_TYPE_ENUM_SQL = <<~SQL 8 | CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy', '𝘾𝝾օɭ 😎'); 9 | SQL 10 | 11 | CREATE_TABLE_SQL = <<~SQL 12 | CREATE TABLE table1( 13 | boolean_col BOOLEAN, 14 | tinyint_col TINYINT, 15 | smallint_col SMALLINT, 16 | integer_col INTEGER, 17 | bigint_col BIGINT, 18 | utinyint_col UTINYINT, 19 | usmallint_col USMALLINT, 20 | uinteger_col UINTEGER, 21 | ubigint_col UBIGINT, 22 | real_col REAL, 23 | double_col DOUBLE, 24 | date_col DATE, 25 | time_col TIME, 26 | timestamp_col timestamp, 27 | interval_col INTERVAL, 28 | hugeint_col HUGEINT, 29 | varchar_col VARCHAR, 30 | decimal_col DECIMAL(9, 6), 31 | enum_col mood, 32 | int_list_col INT[], 33 | varchar_list_col VARCHAR[], 34 | int_array_col INT[3], 35 | varchar_list_array_col VARCHAR[2], 36 | struct_col STRUCT(word VARCHAR, length INTEGER), 37 | uuid_col UUID, 38 | map_col MAP(INTEGER, VARCHAR), 39 | union_col UNION(num INTEGER, str VARCHAR) 40 | ); 41 | SQL 42 | 43 | INSERT_SQL = <<~SQL 44 | INSERT INTO table1 VALUES 45 | ( 46 | true, 47 | 1, 48 | 32767, 49 | 2147483647, 50 | 9223372036854775807, 51 | 1, 52 | 32767, 53 | 2147483647, 54 | 9223372036854775807, 55 | 12345.375, 56 | 123.456789, 57 | '2019-11-03', 58 | '12:34:56', 59 | '2019-11-03 12:34:56', 60 | '1 day', 61 | 170141183460469231731687303715884105727, 62 | 'string', 63 | 123.456789, 64 | 'sad', 65 | [1, 2, 3], 66 | ['a', 'b', 'c'], 67 | [1, 2, 3], 68 | [['a', 'b'], ['c', 'd']], 69 | ROW('Ruby', 4), 70 | '#{SecureRandom.uuid}', 71 | MAP{1: 'foo'}, 72 | 1::INTEGER, 73 | ) 74 | SQL 75 | 76 | SELECT_SQL = 'SELECT * FROM table1' 77 | 78 | EXPECTED_TYPES = %i[ 79 | boolean 80 | tinyint 81 | smallint 82 | integer 83 | bigint 84 | utinyint 85 | usmallint 86 | uinteger 87 | ubigint 88 | float 89 | double 90 | date 91 | time 92 | timestamp 93 | interval 94 | hugeint 95 | varchar 96 | decimal 97 | enum 98 | list 99 | list 100 | array 101 | array 102 | struct 103 | uuid 104 | map 105 | union 106 | ].freeze 107 | 108 | def setup 109 | @db = DuckDB::Database.open 110 | @con = @db.connect 111 | create_data(@con) 112 | result = @con.query(SELECT_SQL) 113 | @columns = result.columns 114 | end 115 | 116 | def test_type 117 | logical_types = @columns.map(&:logical_type) 118 | assert_equal(EXPECTED_TYPES, logical_types.map(&:type)) 119 | end 120 | 121 | def test_alias 122 | enum_column = @columns.find { |column| column.type == :enum } 123 | enum_logical_type = enum_column.logical_type 124 | assert_equal("mood", enum_logical_type.alias=("mood")) 125 | assert_equal("mood", enum_logical_type.alias) 126 | end 127 | 128 | def test_decimal_internal_type 129 | decimal_column = @columns.find { |column| column.type == :decimal } 130 | assert_equal(:integer, decimal_column.logical_type.internal_type) 131 | end 132 | 133 | def test_decimal_width 134 | decimal_column = @columns.find { |column| column.type == :decimal } 135 | assert_equal(9, decimal_column.logical_type.width) 136 | end 137 | 138 | def test_decimal_scale 139 | decimal_column = @columns.find { |column| column.type == :decimal } 140 | assert_equal(6, decimal_column.logical_type.scale) 141 | end 142 | 143 | def test_list_child_type 144 | list_columns = @columns.select { |column| column.type == :list } 145 | child_types = list_columns.map do |list_column| 146 | list_column.logical_type.child_type 147 | end 148 | assert(child_types.all? { |child_type| child_type.is_a?(DuckDB::LogicalType) }) 149 | assert_equal([:integer, :varchar], child_types.map(&:type)) 150 | end 151 | 152 | def test_map_child_type 153 | map_column = @columns.detect { |column| column.type == :map } 154 | child_type = map_column.logical_type.child_type 155 | assert(child_type.is_a?(DuckDB::LogicalType)) 156 | assert_equal(:struct, child_type.type) 157 | end 158 | 159 | def test_array_child_type 160 | array_columns = @columns.select { |column| column.type == :array } 161 | child_types = array_columns.map do |array_column| 162 | array_column.logical_type.child_type 163 | end 164 | assert(child_types.all? { |child_type| child_type.is_a?(DuckDB::LogicalType) }) 165 | assert_equal([:integer, :varchar], child_types.map(&:type)) 166 | end 167 | 168 | def test_array_size 169 | array_columns = @columns.select { |column| column.type == :array } 170 | array_sizes = array_columns.map do |array_column| 171 | array_column.logical_type.size 172 | end 173 | assert_equal([3, 2], array_sizes) 174 | end 175 | 176 | def test_map_key_type 177 | map_column = @columns.find { |column| column.type == :map } 178 | key_type = map_column.logical_type.key_type 179 | assert(key_type.is_a?(DuckDB::LogicalType)) 180 | assert_equal(:integer, key_type.type) 181 | end 182 | 183 | def test_map_value_type 184 | map_column = @columns.find { |column| column.type == :map } 185 | value_type = map_column.logical_type.value_type 186 | assert(value_type.is_a?(DuckDB::LogicalType)) 187 | assert_equal(:varchar, value_type.type) 188 | end 189 | 190 | def test_union_member_count 191 | union_column = @columns.find { |column| column.type == :union } 192 | assert_equal(2, union_column.logical_type.member_count) 193 | end 194 | 195 | def test_union_each_member_name 196 | union_column = @columns.find { |column| column.type == :union } 197 | union_logical_type = union_column.logical_type 198 | member_names = union_logical_type.each_member_name.to_a 199 | assert_equal(["num", "str"], member_names) 200 | end 201 | 202 | def test_union_each_member_type 203 | union_column = @columns.find { |column| column.type == :union } 204 | union_logical_type = union_column.logical_type 205 | member_types = union_logical_type.each_member_type.to_a 206 | assert(member_types.all? { |member_type| member_type.is_a?(DuckDB::LogicalType) }) 207 | assert_equal([:integer, :varchar], member_types.map(&:type)) 208 | end 209 | 210 | def test_struct_child_count 211 | struct_column = @columns.find { |column| column.type == :struct } 212 | assert_equal(2, struct_column.logical_type.child_count) 213 | end 214 | 215 | def test_struct_each_child_name 216 | struct_column = @columns.find { |column| column.type == :struct } 217 | struct_logical_type = struct_column.logical_type 218 | child_names = struct_logical_type.each_child_name.to_a 219 | assert_equal(["word", "length"], child_names) 220 | end 221 | 222 | def test_struct_each_child_type 223 | struct_column = @columns.find { |column| column.type == :struct } 224 | struct_logical_type = struct_column.logical_type 225 | child_types = struct_logical_type.each_child_type.to_a 226 | assert(child_types.all? { |child_type| child_type.is_a?(DuckDB::LogicalType) }) 227 | assert_equal([:varchar, :integer], child_types.map(&:type)) 228 | end 229 | 230 | def test_enum_internal_type 231 | enum_column = @columns.find { |column| column.type == :enum } 232 | assert_equal(:utinyint, enum_column.logical_type.internal_type) 233 | end 234 | 235 | def test_enum_dictionary_size 236 | enum_column = @columns.find { |column| column.type == :enum } 237 | assert_equal(4, enum_column.logical_type.dictionary_size) 238 | end 239 | 240 | def test_enum_each_dictionary_value 241 | enum_column = @columns.find { |column| column.type == :enum } 242 | enum_logical_type = enum_column.logical_type 243 | dictionary_values = enum_logical_type.each_dictionary_value.to_a 244 | assert_equal(["sad", "ok", "happy", "𝘾𝝾օɭ 😎"], dictionary_values) 245 | end 246 | 247 | private 248 | 249 | def create_data(con) 250 | con.query(CREATE_TYPE_ENUM_SQL) 251 | con.query(CREATE_TABLE_SQL) 252 | con.query(INSERT_SQL) 253 | end 254 | end 255 | end 256 | -------------------------------------------------------------------------------- /test/duckdb_test/pending_result_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module DuckDBTest 6 | class PendingResultTest < Minitest::Test 7 | def setup 8 | @db = DuckDB::Database.open 9 | @con = @db.connect 10 | # @con.query('CREATE TABLE int_vals (int_val INTEGER)') 11 | # @con.query('INSERT INTO int_vals VALUES (1), (2), (3), (4), (5)') 12 | @con.query('SET threads=1') 13 | @con.query('CREATE TABLE tbl as SELECT range a, mod(range, 10) b FROM range(10000)') 14 | @con.query('CREATE TABLE tbl2 as SELECT range a, FROM range(10000)') 15 | @con.query('SET ENABLE_PROGRESS_BAR=true') 16 | @con.query('SET ENABLE_PROGRESS_BAR_PRINT=false') 17 | @stmt = @con.prepared_statement('SELECT count(*) FROM tbl where a = (SELECT min(a) FROM tbl2)') 18 | # pending_result = @con.async_query('SELECT count(*) FROM tbl where a = (SELECT min(a) FROM tbl2)') 19 | end 20 | 21 | def test_state 22 | pending_result = @stmt.pending_prepared 23 | assert_equal :not_ready, pending_result.state 24 | 25 | pending_result.execute_task while pending_result.state == :not_ready 26 | assert_equal(:ready, pending_result.state) 27 | 28 | pending_result.execute_pending 29 | assert_equal(:ready, pending_result.state) 30 | 31 | pending_result.execute_task 32 | assert_equal(:error, pending_result.state) 33 | end 34 | 35 | def test_execution_finished? 36 | pending_result = @stmt.pending_prepared 37 | assert_equal false, pending_result.execution_finished? 38 | 39 | pending_result.execute_task while pending_result.state == :not_ready 40 | assert_equal true, pending_result.execution_finished? 41 | 42 | pending_result.execute_task 43 | assert_equal true, pending_result.execution_finished? 44 | end 45 | 46 | def test_execute_pending 47 | pending_result = @stmt.pending_prepared 48 | pending_result.execute_task while pending_result.state == :not_ready 49 | assert_equal :ready, pending_result.state 50 | assert_equal [[1]], pending_result.execute_pending.to_a 51 | end 52 | 53 | def test_execute_check_state 54 | pending_result = @stmt.pending_prepared 55 | state = pending_result.execute_check_state 56 | assert_equal(:no_tasks, state) 57 | 58 | pending_result.execute_task while pending_result.state == :not_ready 59 | 60 | state = pending_result.execute_check_state 61 | assert_includes(%i[error ready], state) 62 | end 63 | 64 | def teardown 65 | @con.close 66 | @db.close 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /test/duckdb_test/prepared_statement_bind_decimal_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DuckDBTest 4 | class PreparedStatementDecimalTest < Minitest::Test 5 | def setup 6 | @db = DuckDB::Database.open 7 | @con = @db.connect 8 | @con.query('CREATE TABLE decimals (decimal_value HUGEINT)') 9 | end 10 | 11 | def prepare_test_value(value) 12 | @con.query("INSERT INTO decimals VALUES (#{value})") 13 | @prepared = @con.prepared_statement('SELECT * FROM decimals WHERE decimal_value = ?') 14 | end 15 | 16 | def teardown 17 | @con.close 18 | @db.close 19 | end 20 | 21 | # FIXME: @prepared.bind(1, BigDecimal('1.0')) should not raise DuckDB::Error. 22 | def test_decimal 23 | prepare_test_value(1.0) 24 | r = @prepared.bind(1, BigDecimal('1.0')).execute 25 | assert_equal(1.0, r.first.first) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/duckdb_test/result_array_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module DuckDBTest 4 | class ResultArrayTest < Minitest::Test 5 | def setup 6 | @db = DuckDB::Database.open 7 | @conn = @db.connect 8 | end 9 | 10 | def test_result_array 11 | @conn.execute('CREATE TABLE test (value INTEGER[3]);') 12 | @conn.execute('INSERT INTO test VALUES (array_value(1, 2, 3));') 13 | @conn.execute('INSERT INTO test VALUES (array_value(4, 5, 6));') 14 | result = @conn.execute('SELECT value FROM test;') 15 | ary = result.each.to_a 16 | assert_equal([[[1, 2, 3]], [[4, 5, 6]]], ary) 17 | end 18 | 19 | def test_result_array_with_null 20 | @conn.execute('CREATE TABLE test (value INTEGER[3]);') 21 | @conn.execute('INSERT INTO test VALUES (array_value(1, 2, 3));') 22 | @conn.execute('INSERT INTO test VALUES (array_value(4, 5, NULL));') 23 | result = @conn.execute('SELECT value FROM test;') 24 | ary = result.each.to_a 25 | assert_equal([[[1, 2, 3]], [[4, 5, nil]]], ary) 26 | end 27 | 28 | def test_result_array_varchar 29 | @conn.execute('CREATE TABLE test (value varchar[3]);') 30 | @conn.execute("INSERT INTO test VALUES (array_value('abc', 'de', 'f'));") 31 | @conn.execute("INSERT INTO test VALUES (array_value('𝘶ñîҫȫ𝘥ẹ 𝖘ţ𝗋ⅰɲ𝓰 😃', 'あいうえお', '123'));") 32 | result = @conn.execute('SELECT value FROM test;') 33 | ary = result.each.to_a 34 | assert_equal([[['abc', 'de', 'f']], [['𝘶ñîҫȫ𝘥ẹ 𝖘ţ𝗋ⅰɲ𝓰 😃', 'あいうえお', '123']]], ary) 35 | end 36 | 37 | def teardown 38 | @conn.execute('DROP TABLE test;') 39 | @conn.close 40 | @db.close 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/duckdb_test/result_bit_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module DuckDBTest 6 | class ResultBitTest < Minitest::Test 7 | def setup 8 | @db = DuckDB::Database.open 9 | @conn = @db.connect 10 | end 11 | 12 | def test_result_bit 13 | @conn.execute('CREATE TABLE test (value BIT);') 14 | @conn.execute("INSERT INTO test VALUES ('1'::BIT);") 15 | @conn.execute("INSERT INTO test VALUES ('0101101'::BIT);") 16 | @conn.execute("INSERT INTO test VALUES ('0'::BIT);") 17 | @conn.execute("INSERT INTO test VALUES ('00000000'::BIT);") 18 | result = @conn.execute('SELECT value FROM test;') 19 | ary = result.each.to_a 20 | assert_equal([['1'], ['0101101'], ['0'], ['00000000']], ary) 21 | end 22 | 23 | def test_result_bit_over_8_bits 24 | @conn.execute('CREATE TABLE test (value BIT);') 25 | over_8_bits = '1010101001' 26 | @conn.execute("INSERT INTO test VALUES ('#{over_8_bits}'::BIT);") 27 | result = @conn.execute('SELECT value FROM test;') 28 | ary = result.each.to_a 29 | assert_equal([[over_8_bits]], ary) 30 | end 31 | 32 | def test_result_bit_long 33 | @conn.execute('CREATE TABLE test (value BIT);') 34 | long_bits = '11111111111111111111111111111111111110101010101010101010101010101010101010101011100000000' 35 | @conn.execute("INSERT INTO test VALUES ('#{long_bits}'::BIT);") 36 | result = @conn.execute('SELECT value FROM test;') 37 | ary = result.each.to_a 38 | assert_equal([[long_bits]], ary) 39 | end 40 | 41 | def test_result_bit_nil 42 | @conn.execute('CREATE TABLE test (value BIT);') 43 | @conn.execute('INSERT INTO test VALUES (NULL);') 44 | result = @conn.execute('SELECT value FROM test;') 45 | ary = result.each.to_a 46 | assert_equal([[nil]], ary) 47 | end 48 | 49 | def teardown 50 | @conn.execute('DROP TABLE test;') 51 | @conn.close 52 | @db.close 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/duckdb_test/result_list_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module DuckDBTest 4 | class ResultListTest < Minitest::Test 5 | def setup 6 | @db = DuckDB::Database.open 7 | @conn = @db.connect 8 | end 9 | 10 | def test_result_list 11 | @conn.execute('CREATE TABLE test (value INTEGER[]);') 12 | @conn.execute('INSERT INTO test VALUES ([1, 2]);') 13 | @conn.execute('INSERT INTO test VALUES ([3, 4, 5]);') 14 | @conn.execute('INSERT INTO test VALUES ([6, 7, 8, 9]);') 15 | result = @conn.execute('SELECT value FROM test;') 16 | ary = result.each.to_a 17 | assert_equal([[[1, 2]], [[3, 4, 5]], [[6, 7, 8, 9]]], ary) 18 | end 19 | 20 | def test_result_list_with_null 21 | @conn.execute('CREATE TABLE test (value INTEGER[]);') 22 | @conn.execute('INSERT INTO test VALUES ([1, 2]);') 23 | @conn.execute('INSERT INTO test VALUES ([3, 4, NULL]);') 24 | @conn.execute('INSERT INTO test VALUES ([6, 7, 8, 9]);') 25 | result = @conn.execute('SELECT value FROM test;') 26 | ary = result.each.to_a 27 | assert_equal([[[1, 2]], [[3, 4, nil]], [[6, 7, 8, 9]]], ary) 28 | end 29 | 30 | def test_result_list_varchar 31 | @conn.execute('CREATE TABLE test (value VARCHAR[]);') 32 | @conn.execute("INSERT INTO test VALUES (['a', 'b']);") 33 | @conn.execute("INSERT INTO test VALUES (['c', 'd', 'e']);") 34 | @conn.execute("INSERT INTO test VALUES (['f', 'g', 'h', 'i']);") 35 | result = @conn.execute('SELECT value FROM test;') 36 | ary = result.each.to_a 37 | assert_equal([[%w[a b]], [%w[c d e]], [%w[f g h i]]], ary) 38 | end 39 | 40 | def test_result_list_of_list 41 | @conn.execute('CREATE TABLE test (value INTEGER[][]);') 42 | @conn.execute('INSERT INTO test VALUES ([[1, 2, 3], [4, 5]]);') 43 | @conn.execute('INSERT INTO test VALUES ([[6], [7, 8], [9, 10]]);') 44 | result = @conn.execute('SELECT value FROM test;') 45 | ary = result.each.to_a 46 | assert_equal([[[[1, 2, 3], [4, 5]]], [[[6], [7, 8], [9, 10]]]], ary) 47 | end 48 | 49 | def test_result_list_of_multiple_list_columns 50 | @conn.execute('CREATE TABLE test (val1 INTEGER[], val2 INTEGER[][]);') 51 | @conn.execute('INSERT INTO test values ([1, 2], [[3, 4]]);') 52 | result = @conn.execute('SELECT * FROM test;') 53 | ary = result.each.to_a 54 | assert_equal([1, 2], ary.first[0]) 55 | assert_equal([[3, 4]], ary.first[1]) 56 | end 57 | 58 | def teardown 59 | @conn.execute('DROP TABLE test;') 60 | @conn.close 61 | @db.close 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/duckdb_test/result_map_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module DuckDBTest 4 | class ResultMapTest < Minitest::Test 5 | def setup 6 | @db = DuckDB::Database.open 7 | @conn = @db.connect 8 | end 9 | 10 | def test_result_map 11 | @conn.execute('CREATE TABLE test (value MAP(INTEGER, INTEGER));') 12 | @conn.execute('INSERT INTO test VALUES (MAP{1: 2, 3: 4});') 13 | @conn.execute('INSERT INTO test VALUES (MAP{5: 6, 7: 8, 9: 10});') 14 | @conn.execute('INSERT INTO test VALUES (MAP{7: 8});') 15 | result = @conn.execute('SELECT value FROM test;') 16 | ary = result.each.to_a 17 | assert_equal([[{ 1 => 2, 3 => 4 }], [{ 5 => 6, 7 => 8, 9 => 10 }], [{ 7 => 8 }]], ary) 18 | end 19 | 20 | def test_result_map_with_nil 21 | @conn.execute('CREATE TABLE test (value MAP(INTEGER, INTEGER));') 22 | @conn.execute('INSERT INTO test VALUES (MAP{1: 2, 3: 4});') 23 | @conn.execute('INSERT INTO test VALUES (MAP{5: NULL, 7: 8, 9: 10});') 24 | @conn.execute('INSERT INTO test VALUES (MAP{7: 8});') 25 | result = @conn.execute('SELECT value FROM test;') 26 | ary = result.each.to_a 27 | assert_equal([[{ 1 => 2, 3 => 4 }], [{ 5 => nil, 7 => 8, 9 => 10 }], [{ 7 => 8 }]], ary) 28 | end 29 | 30 | def test_result_map_map 31 | @conn.execute('CREATE TABLE test (value MAP(INTEGER, MAP(INTEGER, INTEGER)));') 32 | @conn.execute('INSERT INTO test VALUES (MAP{1: MAP{2: 3, 4: 5}, 6: MAP{7: 8}});') 33 | result = @conn.execute('SELECT value FROM test;') 34 | ary = result.each.to_a 35 | assert_equal([[{ 1 => { 2 => 3, 4 => 5 }, 6 => { 7 => 8 } }]], ary) 36 | end 37 | 38 | def teardown 39 | @conn.execute('DROP TABLE test;') 40 | @conn.close 41 | @db.close 42 | end 43 | end 44 | end 45 | 46 | -------------------------------------------------------------------------------- /test/duckdb_test/result_struct_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module DuckDBTest 4 | class ResultStructTest < Minitest::Test 5 | def setup 6 | @db = DuckDB::Database.open 7 | @conn = @db.connect 8 | end 9 | 10 | def test_result_struct 11 | @conn.execute('CREATE TABLE test (s STRUCT(v VARCHAR, i INTEGER));') 12 | @conn.execute("INSERT INTO test VALUES (ROW('abc', 12));") 13 | @conn.execute("INSERT INTO test VALUES (ROW('de', 5));") 14 | result = @conn.execute('SELECT * FROM test;') 15 | ary = result.each.to_a 16 | assert_equal({ v: 'abc', i: 12 }, ary.first[0]) 17 | assert_equal({ v: 'de', i: 5 }, ary.last[0]) 18 | end 19 | 20 | def test_result_struct_with_nil 21 | @conn.execute('CREATE TABLE test (s STRUCT(v VARCHAR, i INTEGER));') 22 | @conn.execute("INSERT INTO test VALUES (ROW('abc', 12));") 23 | @conn.execute('INSERT INTO test VALUES (ROW(NULL, 5));') 24 | result = @conn.execute('SELECT * FROM test;') 25 | ary = result.each.to_a 26 | assert_equal({ v: 'abc', i: 12 }, ary.first[0]) 27 | assert_equal({ v: nil, i: 5 }, ary.last[0]) 28 | end 29 | 30 | def test_result_struct_with_key_having_space 31 | @conn.execute('CREATE TABLE test (s STRUCT("v 1" VARCHAR, i INTEGER));') 32 | @conn.execute("INSERT INTO test VALUES (ROW('abc', 12));") 33 | result = @conn.execute('SELECT * FROM test;') 34 | ary = result.each.to_a 35 | assert_equal({ 'v 1': 'abc', i: 12 }, ary.first[0]) 36 | end 37 | 38 | def test_result_struct_struct 39 | @conn.execute('CREATE TABLE test (s STRUCT(v STRUCT(a VARCHAR, b INTEGER), i INTEGER));') 40 | @conn.execute("INSERT INTO test VALUES (ROW(ROW('abc', 12), 34));") 41 | result = @conn.execute('SELECT * FROM test;') 42 | ary = result.each.to_a 43 | assert_equal({ v: { a: 'abc', b: 12 }, i: 34 }, ary.first[0]) 44 | end 45 | 46 | def teardown 47 | @conn.execute('DROP TABLE test;') 48 | @conn.close 49 | @db.close 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/duckdb_test/result_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module DuckDBTest 6 | class ResultTest < Minitest::Test 7 | def setup 8 | @@con ||= create_data 9 | @result = @@con.query('SELECT * from table1') 10 | 11 | # fix for using duckdb_fetch_chunk in Result#chunk_each 12 | @all_records = @result.to_a 13 | 14 | @ary = first_record 15 | end 16 | 17 | def test_s_new 18 | assert_raises(DuckDB::Error) { DuckDB::Result.new.each.to_a } 19 | end 20 | 21 | def test_result 22 | assert_instance_of(DuckDB::Result, @result) 23 | end 24 | 25 | def test_each 26 | assert_instance_of(Array, @ary) 27 | end 28 | 29 | def test_each_without_block_is_enumerator 30 | assert_instance_of(Enumerator, @result.each) 31 | end 32 | 33 | def test_each_without_block 34 | # fix for using duckdb_fetch_chunk in Result#chunk_each 35 | result = @@con.query('SELECT * from table1') 36 | 37 | expected_ary = [ 38 | expected_boolean, 39 | expected_smallint, 40 | expected_integer, 41 | expected_bigint, 42 | expected_hugeint, 43 | expected_float, 44 | expected_double, 45 | expected_string, 46 | Date.parse(expected_date), 47 | Time.parse(expected_timestamp), 48 | expected_blob, 49 | expected_boolean_false 50 | ] 51 | 52 | assert_equal([expected_ary, 0], result.each.with_index.to_a.first) 53 | end 54 | 55 | def test_result_boolean 56 | assert_equal(expected_boolean, @ary[0]) 57 | end 58 | 59 | def test_result_smallint 60 | assert_equal(expected_smallint, @ary[1]) 61 | end 62 | 63 | def test_result_integer 64 | assert_equal(expected_integer, @ary[2]) 65 | end 66 | 67 | def test_result_bigint 68 | assert_equal(expected_bigint, @ary[3]) 69 | end 70 | 71 | def test_result_hugeint 72 | assert_equal(expected_hugeint, @ary[4]) 73 | end 74 | 75 | def test_result_float 76 | assert_equal(expected_float, @ary[5]) 77 | end 78 | 79 | def test_result_double 80 | assert_equal(expected_double, @ary[6]) 81 | end 82 | 83 | def test_result_varchar 84 | assert_equal(expected_string, @ary[7]) 85 | end 86 | 87 | def test_result_date 88 | assert_equal(Date.parse(expected_date), @ary[8]) 89 | end 90 | 91 | def test_result_timestamp 92 | assert_equal(Time.parse(expected_timestamp), @ary[9]) 93 | end 94 | 95 | def test_result_null 96 | # fix for using duckdb_fetch_chunk in Result#chunk_each 97 | result = @@con.query('SELECT * from table1') 98 | 99 | assert_equal(Array.new(12), result.reverse_each.first) 100 | end 101 | 102 | def test_including_enumerable 103 | assert_includes(DuckDB::Result.ancestors, Enumerable) 104 | end 105 | 106 | def test_rows_changed 107 | DuckDB::Database.open do |db| 108 | db.connect do |con| 109 | r = con.query('CREATE TABLE t2 (id INT)') 110 | assert_equal(0, r.rows_changed) 111 | r = con.query('INSERT INTO t2 VALUES (1), (2), (3)') 112 | assert_equal(3, r.rows_changed) 113 | r = con.query('UPDATE t2 SET id = id + 1 WHERE id > 1') 114 | assert_equal(2, r.rows_changed) 115 | r = con.query('DELETE FROM t2 WHERE id = 0') 116 | assert_equal(0, r.rows_changed) 117 | r = con.query('DELETE FROM t2 WHERE id = 4') 118 | assert_equal(1, r.rows_changed) 119 | end 120 | end 121 | end 122 | 123 | def test_column_count 124 | assert_equal(12, @result.column_count) 125 | assert_equal(12, @result.column_size) 126 | r = @@con.query('SELECT boolean_col, smallint_col from table1') 127 | assert_equal(2, r.column_count) 128 | assert_equal(2, r.column_size) 129 | end 130 | 131 | def test_columns 132 | assert_instance_of(DuckDB::Column, @result.columns.first) 133 | end 134 | 135 | def test__column_type 136 | assert_equal(1, @result.send(:_column_type, 0)) 137 | assert_equal(3, @result.send(:_column_type, 1)) 138 | assert_equal(4, @result.send(:_column_type, 2)) 139 | assert_equal(5, @result.send(:_column_type, 3)) 140 | assert_equal(16, @result.send(:_column_type, 4)) 141 | assert_equal(10, @result.send(:_column_type, 5)) 142 | assert_equal(11, @result.send(:_column_type, 6)) 143 | assert_equal(17, @result.send(:_column_type, 7)) 144 | assert_equal(13, @result.send(:_column_type, 8)) 145 | assert_equal(12, @result.send(:_column_type, 9)) 146 | end 147 | 148 | def test_return_type 149 | result = @@con.query('SELECT * from table1') 150 | assert_equal(:query_result, result.return_type) 151 | 152 | result = @@con.query('CREATE TABLE t2 (id INT)') 153 | assert_equal(:nothing, result.return_type) 154 | 155 | result = @@con.query('INSERT INTO t2 VALUES (1)') 156 | assert_equal(:changed_rows, result.return_type) 157 | end 158 | 159 | def test_statement_type 160 | assert_equal(:select, @result.statement_type) 161 | end 162 | 163 | def xtest__to_hugeint 164 | assert_only_without_chunk_each do 165 | assert_equal(expected_hugeint, @result.send(:_to_hugeint, 0, 4)) 166 | end 167 | end 168 | 169 | private 170 | 171 | def create_data 172 | @@db ||= DuckDB::Database.open # FIXME 173 | con = @@db.connect 174 | con.query(create_table_sql) 175 | con.query(insert_sql) 176 | con 177 | end 178 | 179 | def create_table_sql 180 | <<-SQL 181 | CREATE TABLE table1( 182 | boolean_col BOOLEAN, 183 | smallint_col SMALLINT, 184 | integer_col INTEGER, 185 | bigint_col BIGINT, 186 | hugeint_col HUGEINT, 187 | real_col REAL, 188 | double_col DOUBLE, 189 | varchar_col VARCHAR, 190 | date_col DATE, 191 | timestamp_col timestamp, 192 | blob_col BLOB, 193 | boolean_col2 BOOLEAN, 194 | ) 195 | SQL 196 | end 197 | 198 | def insert_sql 199 | <<-SQL 200 | INSERT INTO table1 VALUES 201 | ( 202 | #{expected_boolean}, 203 | #{expected_smallint}, 204 | #{expected_integer}, 205 | #{expected_bigint}, 206 | #{expected_hugeint}, 207 | #{expected_float}, 208 | #{expected_double}, 209 | '#{expected_string}', 210 | '#{expected_date}', 211 | '#{expected_timestamp}', 212 | '#{expected_blob}', 213 | '#{expected_boolean_false}' 214 | ), 215 | (NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL) 216 | SQL 217 | end 218 | 219 | def expected_boolean 220 | true 221 | end 222 | 223 | def expected_boolean_false 224 | false 225 | end 226 | 227 | def expected_smallint 228 | 32_767 229 | end 230 | 231 | def expected_integer 232 | 2_147_483_647 233 | end 234 | 235 | def expected_bigint 236 | 9_223_372_036_854_775_807 237 | end 238 | 239 | def expected_hugeint 240 | 170_141_183_460_469_231_731_687_303_715_884_105_727 241 | end 242 | 243 | def expected_float 244 | 12_345.375 245 | end 246 | 247 | def expected_double 248 | 123.456789 249 | end 250 | 251 | def expected_string 252 | '𝘶ñîҫȫ𝘥ẹ 𝖘ţ𝗋ⅰɲ𝓰 😃' 253 | end 254 | 255 | def expected_date 256 | '2019-11-03' 257 | end 258 | 259 | def expected_timestamp 260 | '2019-11-03 12:34:56' 261 | end 262 | 263 | def expected_blob 264 | 'blob'.encode('ASCII-8BIT') 265 | end 266 | 267 | def first_record 268 | # fix for using duckdb_fetch_chunk in Result#chunk_each 269 | # @result.first 270 | @all_records.first 271 | end 272 | end 273 | end 274 | -------------------------------------------------------------------------------- /test/duckdb_test/result_time_tz_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module DuckDBTest 6 | class ResultTimeTzTest < Minitest::Test 7 | def setup 8 | @db = DuckDB::Database.open 9 | @conn = @db.connect 10 | @conn.execute('INSTALL icu;') 11 | @conn.execute('LOAD icu;') 12 | end 13 | 14 | def test_result_timestamp_tz 15 | @conn.execute('SET TimeZone="Asia/Tokyo";') 16 | @conn.execute('CREATE TABLE test (value TIMETZ);') 17 | @conn.execute("INSERT INTO test VALUES ('2019-01-02 12:34:56.123456789');") 18 | result = @conn.execute('SELECT value FROM test;') 19 | time = result.each.to_a.first.first 20 | assert_equal('12:34:56.123456+09:00', time.strftime('%H:%M:%S.%6N%:z')) 21 | end 22 | 23 | def teardown 24 | @conn.execute('DROP TABLE test;') 25 | @conn.close 26 | @db.close 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/duckdb_test/result_timestamp_ns_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module DuckDBTest 6 | class ResultTimestampNsTest < Minitest::Test 7 | def setup 8 | @db = DuckDB::Database.open 9 | @conn = @db.connect 10 | end 11 | 12 | def test_result_timestamp_ns 13 | @conn.execute('CREATE TABLE test (value TIMESTAMP_NS);') 14 | @conn.execute("INSERT INTO test VALUES ('2019-01-02 12:34:56.123456789');") 15 | result = @conn.execute('SELECT value FROM test;') 16 | ary = result.each.to_a 17 | assert_equal([[Time.local(2019, 1, 2, 12, 34, 56, 123_456)]], ary) 18 | end 19 | 20 | def teardown 21 | @conn.execute('DROP TABLE test;') 22 | @conn.close 23 | @db.close 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/duckdb_test/result_timestamp_tz_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module DuckDBTest 6 | class ResultTimeStampTzTest < Minitest::Test 7 | def setup 8 | @db = DuckDB::Database.open 9 | @conn = @db.connect 10 | @conn.execute('INSTALL icu;') 11 | @conn.execute('LOAD icu;') 12 | end 13 | 14 | def test_result_timestamp_tz_plus 15 | @conn.execute('SET TimeZone="Europe/London";') 16 | @conn.execute('CREATE TABLE test (value TIMESTAMPTZ);') 17 | @conn.execute("INSERT INTO test VALUES ('2019-01-02 12:34:56.123456789');") 18 | result = @conn.execute('SELECT value FROM test;') 19 | time = result.each.to_a.first.first 20 | assert_equal('2019-01-02 12:34:56.123456+00:00', time.strftime('%Y-%m-%d %H:%M:%S.%6N%:z')) 21 | end 22 | 23 | def test_result_timestamp_tz_minus 24 | @conn.execute('SET TimeZone="Europe/Berlin";') 25 | @conn.execute('CREATE TABLE test (value TIMESTAMPTZ);') 26 | @conn.execute("INSERT INTO test VALUES ('2019-01-02 12:34:56.123456789');") 27 | result = @conn.execute('SELECT value FROM test;') 28 | time = result.each.to_a.first.first 29 | assert_equal('2019-01-02 11:34:56.123456+00:00', time.strftime('%Y-%m-%d %H:%M:%S.%6N%:z')) 30 | end 31 | 32 | def teardown 33 | @conn.execute('DROP TABLE test;') 34 | @conn.close 35 | @db.close 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/duckdb_test/result_to_decimal_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module DuckDBTest 6 | class ResultToDecimalTest < Minitest::Test 7 | def setup 8 | @db = DuckDB::Database.open 9 | @con = @db.connect 10 | @con.query('CREATE TABLE decimals (decimal_value DECIMAL(30,8))') 11 | end 12 | 13 | def prepare_test_value(value) 14 | @con.query("INSERT INTO decimals VALUES (#{value})") 15 | end 16 | 17 | def teardown 18 | @con.close 19 | @db.close 20 | end 21 | 22 | def do_result_to_decimal_test(value) 23 | prepare_test_value(value) 24 | result = @con.query('SELECT decimal_value FROM decimals') 25 | 26 | # fix for using duckdb_fetch_chunk in Result#chunk_each 27 | result = result.to_a 28 | 29 | assert_equal(value, result.first.first) 30 | assert_instance_of(BigDecimal, result.first.first) 31 | end 32 | 33 | def test_result_to_decimal_positive1 34 | do_result_to_decimal_test(1.23456789) 35 | end 36 | 37 | def test_result_to_decimal_positive2 38 | do_result_to_decimal_test(123.456789) 39 | end 40 | 41 | def test_result_to_decimal_positive3 42 | do_result_to_decimal_test(123_456_789) 43 | end 44 | 45 | def test_result_to_decimal_zero 46 | do_result_to_decimal_test(0) 47 | end 48 | 49 | def test_result_to_decimal_one 50 | do_result_to_decimal_test(1) 51 | end 52 | 53 | def test_result_to_decimal_positive4 54 | do_result_to_decimal_test(0.00000001) 55 | end 56 | 57 | def test_result_to_decimal_positive5 58 | do_result_to_decimal_test(0.00000123) 59 | end 60 | 61 | def test_result_to_decimal_positive6 62 | do_result_to_decimal_test(0.1) 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/duckdb_test/result_union_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module DuckDBTest 6 | class ResultUnionTest < Minitest::Test 7 | def setup 8 | @db = DuckDB::Database.open 9 | @conn = @db.connect 10 | end 11 | 12 | def test_result_union 13 | @conn.execute('CREATE TABLE test (u UNION(a INTEGER, b VARCHAR, c TIMESTAMP, d BIGINT));') 14 | @conn.execute('INSERT INTO test VALUES (1::INTEGER);') 15 | @conn.execute("INSERT INTO test VALUES ('abc'::VARCHAR);") 16 | @conn.execute("INSERT INTO test VALUES ('2020-01-01 00:00:00'::TIMESTAMP);") 17 | @conn.execute('INSERT INTO test VALUES (2::BIGINT);') 18 | 19 | result = @conn.execute('SELECT * FROM test;') 20 | ary = result.each.to_a 21 | assert_equal([[1], ['abc'], [Time.local(2020, 1, 1)], [2]], ary) 22 | end 23 | 24 | def test_result_union_with_null 25 | @conn.execute('CREATE TABLE test (u UNION(a INTEGER, b VARCHAR, c TIMESTAMP, d BIGINT));') 26 | @conn.execute('INSERT INTO test VALUES (1::INTEGER);') 27 | @conn.execute('INSERT INTO test VALUES (NULL);') 28 | result = @conn.execute('SELECT * FROM test;') 29 | ary = result.each.to_a 30 | assert_equal([[1], [nil]], ary) 31 | end 32 | 33 | def teardown 34 | @conn.execute('DROP TABLE test;') 35 | @conn.close 36 | @db.close 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/ng/connection_query_ng.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'duckdb' 3 | 4 | temp_file = "#{File.expand_path('.', __dir__)}/data.duckdb" 5 | puts temp_file 6 | 7 | db = DuckDB::Database.open(temp_file) 8 | con = db.connect 9 | 10 | con.query('CREATE TABLE test (id INTEGER)') 11 | con.query('INSERT INTO test VALUES (?), (?)', 1, 2) 12 | 13 | con.close 14 | db.close 15 | 16 | file_last_saved1 = File.mtime(temp_file) 17 | 18 | i = 0 19 | while file_last_saved1 == File.mtime(temp_file) && i < 5000 20 | sleep 0.0001 21 | i += 1 22 | end 23 | 24 | file_last_saved2 = File.mtime(temp_file) 25 | 26 | p file_last_saved1 27 | p file_last_saved2 28 | File.delete(temp_file) 29 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift File.expand_path('../lib', __dir__) 4 | require 'duckdb' 5 | 6 | require_relative 'duckdb_test/duckdb_version' 7 | 8 | if defined?(GC.verify_compaction_references) == 'method' 9 | if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.2.0') 10 | GC.verify_compaction_references(expand_heap: true, toward: :empty) 11 | else 12 | GC.verify_compaction_references(double_heap: true, toward: :empty) unless /3.0/ =~ RUBY_VERSION 13 | end 14 | end 15 | 16 | module DuckDBTest 17 | def duckdb_library_version 18 | Gem::Version.new(DuckDB::LIBRARY_VERSION) 19 | end 20 | 21 | module_function :duckdb_library_version 22 | end 23 | 24 | require 'minitest/autorun' 25 | --------------------------------------------------------------------------------