├── .editorconfig ├── .gemtest ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── downstream.yml │ ├── rdoc.yml │ └── upstream.yml ├── .gitignore ├── .rdoc_options ├── .rubocop.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── FAQ.md ├── Gemfile ├── INSTALLATION.md ├── LICENSE ├── README.md ├── Rakefile ├── adr └── 2024-09-fork-safety.md ├── appveyor.yml ├── bin ├── build-gems ├── test-gem-build ├── test-gem-file-contents ├── test-gem-install └── test-gem-set ├── dependencies.yml ├── ext └── sqlite3 │ ├── aggregator.c │ ├── aggregator.h │ ├── backup.c │ ├── backup.h │ ├── database.c │ ├── database.h │ ├── exception.c │ ├── exception.h │ ├── extconf.rb │ ├── sqlite3.c │ ├── sqlite3_ruby.h │ ├── statement.c │ ├── statement.h │ └── timespec.h ├── lib ├── sqlite3.rb └── sqlite3 │ ├── constants.rb │ ├── database.rb │ ├── errors.rb │ ├── fork_safety.rb │ ├── pragmas.rb │ ├── resultset.rb │ ├── statement.rb │ ├── value.rb │ ├── version.rb │ └── version_info.rb ├── patches └── .gitkeep ├── rakelib ├── check-manifest.rake ├── format.rake ├── native.rake └── test.rake ├── sqlite3.gemspec └── test ├── helper.rb ├── test_backup.rb ├── test_collation.rb ├── test_database.rb ├── test_database_flags.rb ├── test_database_readonly.rb ├── test_database_readwrite.rb ├── test_database_uri.rb ├── test_discarding.rb ├── test_encoding.rb ├── test_integration.rb ├── test_integration_aggregate.rb ├── test_integration_open_close.rb ├── test_integration_pending.rb ├── test_integration_resultset.rb ├── test_integration_statement.rb ├── test_pragmas.rb ├── test_resource_cleanup.rb ├── test_result_set.rb ├── test_sqlite3.rb ├── test_statement.rb └── test_statement_execute.rb /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | 6 | [*.{c,h}] 7 | end_of_line = lf 8 | indent_size = 4 9 | indent_style = space 10 | insert_final_newline = true 11 | tab_width = 8 12 | trim_trailing_whitespace = true 13 | -------------------------------------------------------------------------------- /.gemtest: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparklemotion/sqlite3-ruby/782653e53cd207f185dd49f6f7dc16e850c2e818/.gemtest -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | version: 2 3 | updates: 4 | - package-ecosystem: "bundler" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: sqlite3-ruby test suite 2 | concurrency: 3 | group: "${{ github.workflow }}-${{ github.ref }}" 4 | cancel-in-progress: true 5 | on: 6 | workflow_dispatch: 7 | schedule: 8 | - cron: "0 8 * * 3" # At 08:00 on Wednesday # https://crontab.guru/#0_8_*_*_3 9 | push: 10 | branches: 11 | - main 12 | - "*-stable" 13 | tags: 14 | - v*.*.* 15 | pull_request: 16 | types: [opened, synchronize] 17 | branches: 18 | - '*' 19 | 20 | env: 21 | BUNDLE_WITHOUT: "development" 22 | 23 | jobs: 24 | ruby_versions: 25 | outputs: 26 | setup_ruby: "['3.1', '3.2', '3.3', '3.4']" 27 | image_tag: "['3.1', '3.2', '3.3', '3.4']" 28 | runs-on: ubuntu-latest 29 | steps: 30 | - run: echo "generating rubies ..." 31 | 32 | # 33 | # basic tests 34 | # 35 | rubocop: 36 | runs-on: ubuntu-latest 37 | env: 38 | BUNDLE_WITHOUT: "" # we need rubocop, obviously 39 | steps: 40 | - uses: actions/checkout@v4 41 | - uses: ruby/setup-ruby@v1 42 | with: 43 | ruby-version: "3.3" 44 | bundler-cache: true 45 | - run: bundle exec rake rubocop 46 | 47 | basic: 48 | needs: rubocop 49 | runs-on: ubuntu-latest 50 | steps: 51 | - uses: actions/checkout@v4 52 | - uses: ruby/setup-ruby-pkgs@v1 53 | with: 54 | ruby-version: "3.3" 55 | bundler-cache: true 56 | apt-get: libsqlite3-dev 57 | - run: bundle exec rake compile -- --enable-system-libraries 58 | - run: bundle exec rake test 59 | 60 | test: 61 | needs: [basic, ruby_versions] 62 | strategy: 63 | fail-fast: false 64 | matrix: 65 | os: [ubuntu, macos, windows] 66 | ruby: ${{ fromJSON(needs.ruby_versions.outputs.setup_ruby) }} 67 | syslib: [enable, disable] 68 | include: 69 | # additional compilation flags for homebrew 70 | - { os: macos, syslib: enable, compile_flags: "--with-opt-dir=$(brew --prefix sqlite3)" } 71 | # additional versions of ruby to test 72 | - { os: ubuntu, ruby: truffleruby, syslib: disable } 73 | - { os: windows, ruby: ucrt, syslib: enable } 74 | - { os: windows, ruby: mswin, syslib: enable } 75 | runs-on: ${{ matrix.os }}-latest 76 | steps: 77 | - if: matrix.os == 'windows' 78 | name: configure git crlf 79 | run: | 80 | git config --system core.autocrlf false 81 | git config --system core.eol lf 82 | - uses: actions/checkout@v4 83 | - uses: ruby/setup-ruby-pkgs@v1 84 | with: 85 | ruby-version: ${{ matrix.ruby }} 86 | bundler-cache: true 87 | apt-get: libsqlite3-dev 88 | mingw: sqlite3 89 | vcpkg: sqlite3 90 | - if: matrix.syslib == 'disable' 91 | uses: actions/cache@v4 92 | with: 93 | path: ports 94 | key: ports-${{ matrix.os }}-${{ hashFiles('ext/sqlite3/extconf.rb','dependencies.yml') }} 95 | - run: bundle exec rake compile -- --${{ matrix.syslib }}-system-libraries ${{ matrix.compile_flags }} 96 | - run: bundle exec rake test 97 | 98 | fedora: 99 | # reported at https://github.com/sparklemotion/sqlite3-ruby/issues/354 100 | # TODO remove once https://github.com/flavorjones/mini_portile/issues/118 is fixed 101 | needs: basic 102 | name: "fedora:40" 103 | runs-on: ubuntu-latest 104 | container: 105 | image: fedora:40 106 | steps: 107 | - run: | 108 | dnf group install -y "C Development Tools and Libraries" 109 | dnf install -y ruby ruby-devel 110 | - uses: actions/checkout@v4 111 | - run: bundle install 112 | - run: bundle exec rake compile -- --disable-system-libraries 113 | - run: bundle exec rake test 114 | 115 | bsd: 116 | needs: basic 117 | name: "FreeBSD" 118 | runs-on: ubuntu-latest 119 | steps: 120 | - uses: actions/checkout@v4 121 | - uses: vmactions/freebsd-vm@v1 122 | with: 123 | usesh: true 124 | copyback: false 125 | prepare: pkg install -y ruby devel/ruby-gems pkgconf 126 | envs: BUNDLE_WITHOUT 127 | run: | 128 | gem install bundler 129 | bundle install --local || bundle install 130 | bundle exec rake compile -- --disable-system-libraries 131 | bundle exec rake test 132 | 133 | sqlcipher: 134 | needs: [basic, ruby_versions] 135 | strategy: 136 | fail-fast: false 137 | matrix: 138 | os: [ubuntu, macos, windows] 139 | ruby: ${{ fromJSON(needs.ruby_versions.outputs.setup_ruby) }} 140 | include: 141 | - { os: windows, ruby: mingw } 142 | - { os: windows, ruby: mswin } 143 | runs-on: ${{ matrix.os }}-latest 144 | steps: 145 | - if: matrix.os == 'windows' 146 | name: configure git crlf 147 | run: | 148 | git config --system core.autocrlf false 149 | git config --system core.eol lf 150 | - uses: actions/checkout@v4 151 | - uses: ruby/setup-ruby-pkgs@v1 152 | with: 153 | ruby-version: ${{ matrix.ruby }} 154 | bundler-cache: true 155 | apt-get: libsqlcipher-dev 156 | brew: sqlcipher 157 | mingw: sqlcipher 158 | vcpkg: sqlcipher 159 | - run: bundle exec rake compile -- --with-sqlcipher 160 | - run: bundle exec rake test 161 | 162 | valgrind: 163 | needs: basic 164 | runs-on: ubuntu-latest 165 | steps: 166 | - uses: actions/checkout@v4 167 | - uses: ruby/setup-ruby-pkgs@v1 168 | with: 169 | ruby-version: "3.3" 170 | bundler-cache: true 171 | apt-get: valgrind 172 | - uses: actions/cache@v4 173 | with: 174 | path: ports 175 | key: ports-ubuntu-${{ hashFiles('ext/sqlite3/extconf.rb','dependencies.yml') }} 176 | - run: bundle exec rake compile 177 | - run: bundle exec rake test:valgrind 178 | 179 | # 180 | # gem tests (source and native) 181 | # 182 | native_setup: 183 | needs: basic 184 | name: "Setup for native gem tests" 185 | runs-on: ubuntu-latest 186 | outputs: 187 | rcd_image_version: ${{ steps.rcd_image_version.outputs.rcd_image_version }} 188 | steps: 189 | - uses: actions/checkout@v4 190 | - uses: actions/cache@v4 191 | with: 192 | path: ports/archives 193 | key: ports-archives-tarball-${{ hashFiles('ext/sqlite3/extconf.rb','dependencies.yml') }} 194 | - uses: ruby/setup-ruby@v1 195 | with: 196 | ruby-version: "3.3" 197 | bundler-cache: true 198 | - run: bundle exec ruby ./ext/sqlite3/extconf.rb --download-dependencies 199 | - id: rcd_image_version 200 | run: bundle exec ruby -e 'require "rake_compiler_dock"; puts "rcd_image_version=#{RakeCompilerDock::IMAGE_VERSION}"' >> $GITHUB_OUTPUT 201 | 202 | build_source_gem: 203 | needs: native_setup 204 | name: "build source" 205 | runs-on: ubuntu-latest 206 | steps: 207 | - uses: actions/checkout@v4 208 | - uses: actions/cache@v4 209 | with: 210 | path: ports/archives 211 | key: ports-archives-tarball-${{ hashFiles('ext/sqlite3/extconf.rb','dependencies.yml') }} 212 | - uses: ruby/setup-ruby@v1 213 | with: 214 | ruby-version: "3.3" 215 | bundler-cache: true 216 | - run: ./bin/test-gem-build gems ruby 217 | - uses: actions/upload-artifact@v4 218 | with: 219 | name: source-gem 220 | path: gems 221 | retention-days: 1 222 | 223 | install_source_linux: 224 | needs: [build_source_gem, ruby_versions] 225 | name: "test source" 226 | strategy: 227 | fail-fast: false 228 | matrix: 229 | os: [ubuntu, macos, windows] 230 | ruby: ${{ fromJSON(needs.ruby_versions.outputs.setup_ruby) }} 231 | syslib: [enable, disable] 232 | include: 233 | # additional compilation flags for homebrew 234 | - { os: macos, syslib: enable, compile_flags: "--with-opt-dir=$(brew --prefix sqlite3)" } 235 | runs-on: ${{ matrix.os }}-latest 236 | steps: 237 | - uses: actions/checkout@v4 238 | - uses: ruby/setup-ruby-pkgs@v1 239 | with: 240 | ruby-version: ${{ matrix.ruby }} 241 | apt-get: libsqlite3-dev pkg-config 242 | mingw: sqlite3 243 | - uses: actions/download-artifact@v4 244 | with: 245 | name: source-gem 246 | path: gems 247 | - run: ./bin/test-gem-install gems -- --${{ matrix.syslib }}-system-libraries ${{ matrix.compile_flags }} 248 | shell: sh 249 | 250 | build_native_gem: 251 | needs: native_setup 252 | name: "build native" 253 | strategy: 254 | fail-fast: false 255 | matrix: 256 | platform: 257 | - aarch64-linux-gnu 258 | - aarch64-linux-musl 259 | - arm-linux-gnu 260 | - arm-linux-musl 261 | - arm64-darwin 262 | - x64-mingw-ucrt 263 | - x86-linux-gnu 264 | - x86-linux-musl 265 | - x86_64-darwin 266 | - x86_64-linux-gnu 267 | - x86_64-linux-musl 268 | runs-on: ubuntu-latest 269 | steps: 270 | - uses: actions/checkout@v4 271 | - uses: actions/cache@v4 272 | with: 273 | path: ports/archives 274 | key: ports-archives-tarball-${{ hashFiles('ext/sqlite3/extconf.rb','dependencies.yml') }} 275 | - run: | 276 | docker run --rm -v $PWD:/work -w /work \ 277 | ghcr.io/rake-compiler/rake-compiler-dock-image:${{ needs.native_setup.outputs.rcd_image_version }}-mri-${{ matrix.platform }} \ 278 | ./bin/test-gem-build gems ${{ matrix.platform }} 279 | - uses: actions/upload-artifact@v4 280 | with: 281 | name: "cruby-${{ matrix.platform }}-gem" 282 | path: gems 283 | retention-days: 1 284 | 285 | test_architecture_matrix: 286 | name: "${{ matrix.platform }} ${{ matrix.ruby }}" 287 | needs: [build_native_gem, ruby_versions] 288 | strategy: 289 | fail-fast: false 290 | matrix: 291 | platform: 292 | - aarch64-linux-gnu 293 | - aarch64-linux-musl 294 | - arm-linux-gnu 295 | - arm-linux-musl 296 | - x86-linux-gnu 297 | - x86-linux-musl 298 | - x86_64-linux-gnu 299 | - x86_64-linux-musl 300 | ruby: ${{ fromJSON(needs.ruby_versions.outputs.image_tag) }} 301 | include: 302 | # declare docker image for each platform 303 | - { platform: aarch64-linux-musl, docker_tag: "-alpine", bootstrap: "apk add build-base &&" } 304 | - { platform: arm-linux-musl, docker_tag: "-alpine", bootstrap: "apk add build-base &&" } 305 | - { platform: x86-linux-musl, docker_tag: "-alpine", bootstrap: "apk add build-base &&" } 306 | - { platform: x86_64-linux-musl, docker_tag: "-alpine", bootstrap: "apk add build-base &&" } 307 | # declare docker platform for each platform 308 | - { platform: aarch64-linux-gnu, docker_platform: "--platform=linux/arm64" } 309 | - { platform: aarch64-linux-musl, docker_platform: "--platform=linux/arm64" } 310 | - { platform: arm-linux-gnu, docker_platform: "--platform=linux/arm/v7" } 311 | - { platform: arm-linux-musl, docker_platform: "--platform=linux/arm/v7" } 312 | - { platform: x86-linux-gnu, docker_platform: "--platform=linux/386" } 313 | - { platform: x86-linux-musl, docker_platform: "--platform=linux/386" } 314 | runs-on: ubuntu-latest 315 | steps: 316 | - uses: actions/checkout@v4 317 | - uses: actions/download-artifact@v4 318 | with: 319 | name: cruby-${{ matrix.platform }}-gem 320 | path: gems 321 | - run: | 322 | docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 323 | docker run --rm -v $PWD:/work -w /work \ 324 | ${{ matrix.docker_platform}} ruby:${{ matrix.ruby }}${{ matrix.docker_tag }} \ 325 | sh -c " 326 | ${{ matrix.bootstrap }} 327 | ./bin/test-gem-install ./gems 328 | " 329 | 330 | test_the_rest: 331 | name: "${{ matrix.platform }} ${{ matrix.ruby }}" 332 | needs: [build_native_gem, ruby_versions] 333 | strategy: 334 | fail-fast: false 335 | matrix: 336 | os: [windows-latest, macos-13, macos-14] 337 | ruby: ${{ fromJSON(needs.ruby_versions.outputs.setup_ruby) }} 338 | include: 339 | - os: macos-13 340 | platform: x86_64-darwin 341 | - os: macos-14 342 | platform: arm64-darwin 343 | - os: windows-latest 344 | platform: x64-mingw-ucrt 345 | runs-on: ${{ matrix.os }} 346 | steps: 347 | - uses: actions/checkout@v4 348 | - uses: ruby/setup-ruby@v1 349 | with: 350 | ruby-version: "${{ matrix.ruby }}" 351 | - uses: actions/download-artifact@v4 352 | with: 353 | name: cruby-${{ matrix.platform }}-gem 354 | path: gems 355 | - run: ./bin/test-gem-install gems 356 | shell: sh 357 | 358 | cruby-x86_64-linux-musl-install: 359 | needs: build_native_gem 360 | strategy: 361 | fail-fast: false 362 | matrix: 363 | include: 364 | - { ruby: "3.1", flavor: "alpine3.18" } 365 | - { ruby: "3.1", flavor: "alpine3.19" } 366 | - { ruby: "3.2", flavor: "alpine3.18" } 367 | - { ruby: "3.2", flavor: "alpine3.19" } 368 | - { ruby: "3.3", flavor: "alpine3.18" } 369 | - { ruby: "3.3", flavor: "alpine3.19" } 370 | - { ruby: "3.4", flavor: "alpine" } 371 | runs-on: ubuntu-latest 372 | container: 373 | image: ruby:${{matrix.ruby}}-${{matrix.flavor}} 374 | steps: 375 | - uses: actions/checkout@v4 376 | - uses: actions/download-artifact@v4 377 | with: 378 | name: cruby-x86_64-linux-musl-gem 379 | path: gems 380 | - run: apk add build-base 381 | - run: ./bin/test-gem-install ./gems 382 | -------------------------------------------------------------------------------- /.github/workflows/downstream.yml: -------------------------------------------------------------------------------- 1 | name: downstream 2 | concurrency: 3 | group: "${{github.workflow}}-${{github.ref}}" 4 | cancel-in-progress: true 5 | on: 6 | workflow_dispatch: 7 | schedule: 8 | - cron: "0 8 * * 3" # At 08:00 on Wednesday # https://crontab.guru/#0_8_*_*_3 9 | push: 10 | branches: 11 | - main 12 | - "*-stable" 13 | tags: 14 | - v*.*.* 15 | pull_request: 16 | types: [opened, synchronize] 17 | branches: 18 | - '*' 19 | 20 | jobs: 21 | activerecord: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: ruby/setup-ruby-pkgs@v1 26 | with: 27 | ruby-version: "3.4" 28 | bundler: latest 29 | bundler-cache: true 30 | apt-get: sqlite3 # active record test suite uses the sqlite3 cli 31 | - uses: actions/cache@v4 32 | with: 33 | path: ports 34 | key: ports-ubuntu-${{ hashFiles('ext/sqlite3/extconf.rb','dependencies.yml') }} 35 | - run: bundle exec rake compile 36 | - name: checkout rails and configure 37 | run: | 38 | git clone --depth 1 --branch main https://github.com/rails/rails 39 | cd rails 40 | bundle install --prefer-local 41 | bundle remove sqlite3 42 | bundle add sqlite3 --path=".." 43 | - name: run tests 44 | run: | 45 | cd rails/activerecord 46 | bundle show --paths sqlite3 47 | bundle exec rake test:sqlite3 48 | -------------------------------------------------------------------------------- /.github/workflows/rdoc.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: rdocs 3 | 4 | on: 5 | workflow_dispatch: 6 | push: 7 | tags: 8 | - v*.*.* 9 | 10 | permissions: 11 | contents: read 12 | pages: write 13 | id-token: write 14 | 15 | concurrency: 16 | group: "pages" 17 | cancel-in-progress: false 18 | 19 | jobs: 20 | deploy: 21 | environment: 22 | name: github-pages 23 | url: ${{ steps.deployment.outputs.page_url }} 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: actions/configure-pages@v5 28 | - uses: ruby/setup-ruby@v1 29 | with: 30 | ruby-version: "3.3" 31 | bundler-cache: true 32 | - run: bundle exec rdoc 33 | - uses: actions/upload-pages-artifact@v3 34 | with: 35 | path: 'doc' 36 | - uses: actions/deploy-pages@v4 37 | id: deployment 38 | -------------------------------------------------------------------------------- /.github/workflows/upstream.yml: -------------------------------------------------------------------------------- 1 | name: upstream 2 | concurrency: 3 | group: "${{github.workflow}}-${{github.ref}}" 4 | cancel-in-progress: true 5 | on: 6 | workflow_dispatch: 7 | schedule: 8 | - cron: "0 8 * * 3" # At 08:00 on Wednesday # https://crontab.guru/#0_8_*_*_3 9 | pull_request: 10 | types: [opened, synchronize] 11 | branches: 12 | - '*' 13 | paths: 14 | - .github/workflows/upstream.yml # this file 15 | 16 | jobs: 17 | sqlite-head: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - run: | 22 | git clone --depth=1 https://github.com/sqlite/sqlite 23 | git -C sqlite log -n1 24 | - uses: ruby/setup-ruby-pkgs@v1 25 | with: 26 | ruby-version: "3.3" 27 | bundler-cache: true 28 | - run: bundle exec rake compile -- --with-sqlite-source-dir=${GITHUB_WORKSPACE}/sqlite 29 | - run: bundle exec rake test 30 | 31 | ruby-head: 32 | name: ${{matrix.ruby}}-${{matrix.lib}} 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | include: 37 | - { os: ubuntu-latest, ruby: truffleruby-head, lib: packaged } 38 | - { os: ubuntu-latest, ruby: head, lib: packaged } 39 | - { os: ubuntu-latest, ruby: head, lib: system } 40 | 41 | runs-on: ${{matrix.os}} 42 | steps: 43 | - uses: actions/checkout@v4 44 | - uses: ruby/setup-ruby-pkgs@v1 45 | with: 46 | ruby-version: ${{matrix.ruby}} 47 | bundler-cache: true 48 | apt-get: libsqlite3-dev 49 | - if: matrix.lib == 'packaged' 50 | uses: actions/cache@v4 51 | with: 52 | path: ports 53 | key: ports-${{matrix.os}}-${{hashFiles('ext/sqlite3/extconf.rb','dependencies.yml')}} 54 | 55 | - run: bundle exec rake compile -- --disable-system-libraries 56 | if: matrix.lib == 'packaged' 57 | 58 | - run: bundle exec rake compile -- --enable-system-libraries 59 | if: matrix.lib == 'system' 60 | 61 | - run: bundle exec rake test 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.so 2 | *.bundle 3 | *.swp 4 | *.log 5 | *.o 6 | 7 | test/test.db 8 | 9 | Gemfile.lock 10 | 11 | doc/ 12 | gems/ 13 | issues/ 14 | pkg/ 15 | ports/ 16 | tmp/ 17 | vendor/ 18 | -------------------------------------------------------------------------------- /.rdoc_options: -------------------------------------------------------------------------------- 1 | --- 2 | encoding: UTF-8 3 | static_path: [] 4 | rdoc_include: [] 5 | page_dir: 6 | charset: UTF-8 7 | exclude: 8 | - "~\\z" 9 | - "\\.orig\\z" 10 | - "\\.rej\\z" 11 | - "\\.bak\\z" 12 | - "\\.gemspec\\z" 13 | - "issues" 14 | - "bin" 15 | - "rakelib" 16 | - "ext/sqlite3/extconf.rb" 17 | - "vendor" 18 | - "ports" 19 | - "tmp" 20 | - "pkg" 21 | hyperlink_all: false 22 | line_numbers: false 23 | locale: 24 | locale_dir: locale 25 | locale_name: 26 | main_page: "README.md" 27 | markup: rdoc 28 | output_decoration: true 29 | show_hash: false 30 | skip_tests: true 31 | tab_width: 8 32 | template_stylesheets: [] 33 | title: 34 | visibility: :protected 35 | webcvs: 36 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # from https://evilmartians.com/chronicles/rubocoping-with-legacy-bring-your-ruby-code-up-to-standard 2 | require: 3 | - standard 4 | - standard-custom 5 | - standard-performance 6 | - rubocop-performance 7 | - rubocop-minitest 8 | 9 | inherit_gem: 10 | standard: config/base.yml 11 | standard-custom: config/base.yml 12 | standard-performance: config/base.yml 13 | 14 | AllCops: 15 | SuggestExtensions: false 16 | TargetRubyVersion: 3.1 17 | 18 | Naming/InclusiveLanguage: 19 | Enabled: true 20 | 21 | Minitest/AssertInDelta: # new in 0.10 22 | Enabled: true 23 | Minitest/AssertKindOf: # new in 0.10 24 | Enabled: true 25 | Minitest/AssertOperator: # new in 0.32 26 | Enabled: true 27 | Minitest/AssertOutput: # new in 0.10 28 | Enabled: true 29 | Minitest/AssertPathExists: # new in 0.10 30 | Enabled: true 31 | Minitest/AssertPredicate: # new in 0.18 32 | Enabled: true 33 | Minitest/AssertRaisesCompoundBody: # new in 0.21 34 | Enabled: true 35 | Minitest/AssertRaisesWithRegexpArgument: # new in 0.22 36 | Enabled: true 37 | Minitest/AssertSame: # new in 0.26 38 | Enabled: true 39 | Minitest/AssertSilent: # new in 0.10 40 | Enabled: true 41 | Minitest/AssertWithExpectedArgument: # new in 0.11 42 | Enabled: true 43 | Minitest/AssertionInLifecycleHook: # new in 0.10 44 | Enabled: true 45 | Minitest/DuplicateTestRun: # new in 0.19 46 | Enabled: true 47 | Minitest/EmptyLineBeforeAssertionMethods: # new in 0.23 48 | Enabled: false 49 | Minitest/LifecycleHooksOrder: # new in 0.28 50 | Enabled: true 51 | Minitest/LiteralAsActualArgument: # new in 0.10 52 | Enabled: true 53 | Minitest/MultipleAssertions: # new in 0.10 54 | Enabled: true 55 | Minitest/NonExecutableTestMethod: # new in 0.34 56 | Enabled: true 57 | Minitest/NonPublicTestMethod: # new in 0.27 58 | Enabled: true 59 | Minitest/RedundantMessageArgument: # new in 0.34 60 | Enabled: true 61 | Minitest/RefuteInDelta: # new in 0.10 62 | Enabled: true 63 | Minitest/RefuteKindOf: # new in 0.10 64 | Enabled: true 65 | Minitest/RefuteOperator: # new in 0.32 66 | Enabled: true 67 | Minitest/RefutePathExists: # new in 0.10 68 | Enabled: true 69 | Minitest/RefutePredicate: # new in 0.18 70 | Enabled: true 71 | Minitest/RefuteSame: # new in 0.26 72 | Enabled: true 73 | Minitest/ReturnInTestMethod: # new in 0.31 74 | Enabled: true 75 | Minitest/SkipEnsure: # new in 0.20 76 | Enabled: true 77 | Minitest/SkipWithoutReason: # new in 0.24 78 | Enabled: true 79 | Minitest/TestFileName: # new in 0.26 80 | Enabled: true 81 | Minitest/TestMethodName: # new in 0.10 82 | Enabled: true 83 | Minitest/UnreachableAssertion: # new in 0.14 84 | Enabled: true 85 | Minitest/UnspecifiedException: # new in 0.10 86 | Enabled: true 87 | Minitest/UselessAssertion: # new in 0.26 88 | Enabled: true 89 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to sqlite3-ruby 2 | 3 | **This document is a work-in-progress.** 4 | 5 | This doc is a short introduction on how to modify and maintain the sqlite3-ruby gem. 6 | 7 | 8 | ## Architecture notes 9 | 10 | ### Decision record 11 | 12 | As of 2024-09, we're starting to keep some architecture decisions in the subdirectory `/adr`, so 13 | please look there for additional information. 14 | 15 | ### Garbage collection 16 | 17 | All statements keep pointers back to their respective database connections. 18 | The `@connection` instance variable on the `Statement` handle keeps the database 19 | connection alive. 20 | 21 | We use `sqlite3_close_v2` in `Database#close` since v2.1.0 which defers _actually_ closing the 22 | connection and freeing the underlying memory until all open statments are closed; though the 23 | `Database` object will immediately behave as though it's been fully closed. If a Database is not 24 | explicitly closed, it will be closed when it is GCed. 25 | 26 | `Statement#close` finalizes the underlying statement. If a Statement is not explicitly closed, it 27 | will be closed/finalized when it is GCed. 28 | 29 | 30 | ## Building gems 31 | 32 | As a prerequisite please make sure you have `docker` correctly installed, so that you're able to cross-compile the native gems. 33 | 34 | Run `bin/build-gems` which will package gems for all supported platforms, and run some basic sanity tests on those packages using `bin/test-gem-set` and `bin/test-gem-file-contents`. 35 | 36 | 37 | ## Updating the version of libsqlite3 38 | 39 | Update `/dependencies.yml` to reflect: 40 | 41 | - the version of libsqlite3 42 | - the URL from which to download 43 | - the checksum of the file, which will need to be verified manually (see comments in that file) 44 | 45 | 46 | ## Making a release 47 | 48 | A quick checklist to cutting a release of the sqlite3 gem: 49 | 50 | - [ ] make sure CI is green! 51 | - bump the version 52 | - [ ] update `CHANGELOG.md` and `lib/sqlite3/version.rb` 53 | - [ ] create a git tag using a format that matches the pattern `v\d+\.\d+\.\d+`, e.g. `v1.3.13` 54 | - build the native gems 55 | - [ ] run `bin/build-gems` and make sure it completes and all the tests pass 56 | - push 57 | - [ ] `git push && git push --tags` 58 | - [ ] `for g in gems/*.gem ; do gem push $g ; done` 59 | - announce 60 | - [ ] create a release at https://github.com/sparklemotion/sqlite3-ruby/releases and include sha2 checksums 61 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | 2 | ## How do I do a database query? 3 | ### I just want an array of the rows... 4 | 5 | Use the `Database#execute` method. If you don't give it a block, it will 6 | return an array of all the rows: 7 | 8 | ```ruby 9 | require 'sqlite3' 10 | 11 | db = SQLite3::Database.new( "test.db" ) 12 | rows = db.execute( "select * from test" ) 13 | ``` 14 | 15 | ### I'd like to use a block to iterate through the rows... 16 | 17 | Use the `Database#execute` method. If you give it a block, each row of the 18 | result will be yielded to the block: 19 | 20 | 21 | ```ruby 22 | require 'sqlite3' 23 | 24 | db = SQLite3::Database.new( "test.db" ) 25 | db.execute( "select * from test" ) do |row| 26 | ... 27 | end 28 | ``` 29 | 30 | ### I need to get the column names as well as the rows... 31 | 32 | Use the `Database#execute2` method. This works just like `Database#execute`; 33 | if you don't give it a block, it returns an array of rows; otherwise, it 34 | will yield each row to the block. _However_, the first row returned is 35 | always an array of the column names from the query: 36 | 37 | 38 | ```ruby 39 | require 'sqlite3' 40 | 41 | db = SQLite3::Database.new( "test.db" ) 42 | columns, *rows = db.execute2( "select * from test" ) 43 | 44 | # or use a block: 45 | 46 | columns = nil 47 | db.execute2( "select * from test" ) do |row| 48 | if columns.nil? 49 | columns = row 50 | else 51 | # process row 52 | end 53 | end 54 | ``` 55 | 56 | ### I just want the first row of the result set... 57 | 58 | Easy. Just call `Database#get_first_row`: 59 | 60 | 61 | ```ruby 62 | row = db.get_first_row( "select * from table" ) 63 | ``` 64 | 65 | 66 | This also supports bind variables, just like `Database#execute` 67 | and friends. 68 | 69 | ### I just want the first value of the first row of the result set... 70 | 71 | Also easy. Just call `Database#get_first_value`: 72 | 73 | 74 | ```ruby 75 | count = db.get_first_value( "select count(*) from table" ) 76 | ``` 77 | 78 | 79 | This also supports bind variables, just like `Database#execute` 80 | and friends. 81 | 82 | ## How do I prepare a statement for repeated execution? 83 | 84 | If the same statement is going to be executed repeatedly, you can speed 85 | things up a bit by _preparing_ the statement. You do this via the 86 | `Database#prepare` method. It returns a `Statement` object, and you can 87 | then invoke `#execute` on that to get the `ResultSet`: 88 | 89 | 90 | ```ruby 91 | stmt = db.prepare( "select * from person" ) 92 | 93 | 1000.times do 94 | stmt.execute do |result| 95 | ... 96 | end 97 | end 98 | 99 | stmt.close 100 | 101 | # or, use a block 102 | 103 | db.prepare( "select * from person" ) do |stmt| 104 | 1000.times do 105 | stmt.execute do |result| 106 | ... 107 | end 108 | end 109 | end 110 | ``` 111 | 112 | 113 | This is made more useful by the ability to bind variables to placeholders 114 | via the `Statement#bind_param` and `Statement#bind_params` methods. (See the 115 | next FAQ for details.) 116 | 117 | ## How do I use placeholders in an SQL statement? 118 | 119 | Placeholders in an SQL statement take any of the following formats: 120 | 121 | 122 | * `?` 123 | * `?_nnn_` 124 | * `:_word_` 125 | 126 | 127 | Where _n_ is an integer, and _word_ is an alpha-numeric identifier (or 128 | number). When the placeholder is associated with a number, that number 129 | identifies the index of the bind variable to replace it with. When it 130 | is an identifier, it identifies the name of the corresponding bind 131 | variable. (In the instance of the first format--a single question 132 | mark--the placeholder is assigned a number one greater than the last 133 | index used, or 1 if it is the first.) 134 | 135 | 136 | For example, here is a query using these placeholder formats: 137 | 138 | 139 | ```sql 140 | select * 141 | from table 142 | where ( c = ?2 or c = ? ) 143 | and d = :name 144 | and e = :1 145 | ``` 146 | 147 | 148 | This defines 5 different placeholders: 1, 2, 3, and "name". 149 | 150 | 151 | You replace these placeholders by _binding_ them to values. This can be 152 | accomplished in a variety of ways. 153 | 154 | 155 | The `Database#execute`, and `Database#execute2` methods all accept additional 156 | arguments following the SQL statement. These arguments are assumed to be 157 | bind parameters, and they are bound (positionally) to their corresponding 158 | placeholders: 159 | 160 | 161 | ```ruby 162 | db.execute( "select * from table where a = ? and b = ?", 163 | "hello", 164 | "world" ) 165 | ``` 166 | 167 | 168 | The above would replace the first question mark with 'hello' and the 169 | second with 'world'. If the placeholders have an explicit index given, they 170 | will be replaced with the bind parameter at that index (1-based). 171 | 172 | 173 | If a Hash is given as a bind parameter, then its key/value pairs are bound 174 | to the placeholders. This is how you bind by name: 175 | 176 | 177 | ```ruby 178 | db.execute( "select * from table where a = :name and b = :value", 179 | "name" => "bob", 180 | "value" => "priceless" ) 181 | ``` 182 | 183 | 184 | You can also bind explicitly using the `Statement` object itself. Just pass 185 | additional parameters to the `Statement#execute` statement: 186 | 187 | 188 | ```ruby 189 | db.prepare( "select * from table where a = :name and b = ?" ) do |stmt| 190 | stmt.execute "value", "name" => "bob" 191 | end 192 | ``` 193 | 194 | 195 | Or do a `Database#prepare` to get the `Statement`, and then use either 196 | `Statement#bind_param` or `Statement#bind_params`: 197 | 198 | 199 | ```ruby 200 | stmt = db.prepare( "select * from table where a = :name and b = ?" ) 201 | 202 | stmt.bind_param( "name", "bob" ) 203 | stmt.bind_param( 1, "value" ) 204 | 205 | # or 206 | 207 | stmt.bind_params( "value", "name" => "bob" ) 208 | ``` 209 | 210 | ## How do I discover metadata about a query result? 211 | 212 | IMPORTANT: `Database#execute` returns an Array of Array of Strings 213 | which will have no metadata about the query or the result, such 214 | as column names. 215 | 216 | 217 | There are 2 main sources of query metadata: 218 | 219 | * `Statement` 220 | * `ResultSet` 221 | 222 | 223 | You can get a `Statement` via `Database#prepare`, and you can get 224 | a `ResultSet` via `Statement#execute` or `Database#query`. 225 | 226 | 227 | ```ruby 228 | sql = 'select * from table' 229 | 230 | # No metadata 231 | rows = db.execute(sql) 232 | rows.class # => Array, no metadata 233 | rows.first.class # => Array, no metadata 234 | rows.first.first.class #=> String, no metadata 235 | 236 | # Statement has metadata 237 | stmt = db.prepare(sql) 238 | stmt.columns # => [ ... ] 239 | stmt.types # => [ ... ] 240 | 241 | # ResultSet has metadata 242 | results = stmt.execute 243 | results.columns # => [ ... ] 244 | results.types # => [ ... ] 245 | 246 | # ResultSet has metadata 247 | results = db.query(sql) 248 | results.columns # => [ ... ] 249 | results.types # => [ ... ] 250 | ``` 251 | 252 | ## I'd like the rows to be indexible by column name. 253 | 254 | By default, each row from a query is returned as an `Array` of values. This 255 | means that you can only obtain values by their index. Sometimes, however, 256 | you would like to obtain values by their column name. 257 | 258 | 259 | The first way to do this is to set the Database property `results_as_hash` 260 | to true. If you do this, then all rows will be returned as Hash objects, 261 | with the column names as the keys. (In this case, the `fields` property 262 | is unavailable on the row, although the "types" property remains.) 263 | 264 | 265 | ```ruby 266 | db.results_as_hash = true 267 | db.execute( "select * from table" ) do |row| 268 | p row['column1'] 269 | p row['column2'] 270 | end 271 | ``` 272 | 273 | 274 | A more granular way to do this is via `ResultSet#next_hash` or 275 | `ResultSet#each_hash`. 276 | 277 | 278 | ```ruby 279 | results = db.query( "select * from table" ) 280 | row = results.next_hash 281 | p row['column1'] 282 | ``` 283 | 284 | 285 | Another way is to use Ara Howard's 286 | [`ArrayFields`](http://rubyforge.org/projects/arrayfields) 287 | module. Just `require "arrayfields"`, and all of your rows will be indexable 288 | by column name, even though they are still arrays! 289 | 290 | 291 | ```ruby 292 | require 'arrayfields' 293 | 294 | ... 295 | db.execute( "select * from table" ) do |row| 296 | p row[0] == row['column1'] 297 | p row[1] == row['column2'] 298 | end 299 | ``` 300 | 301 | ## How do I insert binary data into the database? 302 | 303 | Use blobs. Blobs are new features of SQLite3. You have to use bind 304 | variables to make it work: 305 | 306 | 307 | ```ruby 308 | db.execute( "insert into foo ( ?, ? )", 309 | SQLite3::Blob.new( "\0\1\2\3\4\5" ), 310 | SQLite3::Blob.new( "a\0b\0c\0d ) ) 311 | ``` 312 | 313 | 314 | The blob values must be indicated explicitly by binding each parameter to 315 | a value of type `SQLite3::Blob`. 316 | 317 | ## How do I do a DDL (insert, update, delete) statement? 318 | 319 | You can actually do inserts, updates, and deletes in exactly the same way 320 | as selects, but in general the `Database#execute` method will be most 321 | convenient: 322 | 323 | 324 | ```ruby 325 | db.execute( "insert into table values ( ?, ? )", *bind_vars ) 326 | ``` 327 | 328 | ## How do I execute multiple statements in a single string? 329 | 330 | The standard query methods (`Database#execute`, `Database#execute2`, 331 | `Database#query`, and `Statement#execute`) will only execute the first 332 | statement in the string that is given to them. Thus, if you have a 333 | string with multiple SQL statements, each separated by a string, 334 | you can't use those methods to execute them all at once. 335 | 336 | 337 | Instead, use `Database#execute_batch`: 338 | 339 | 340 | ```ruby 341 | sql = <= 2.29) 13 | - `aarch64-linux-musl` 14 | - `arm-linux-gnu` (requires: glibc >= 2.29) 15 | - `arm-linux-musl` 16 | - `arm64-darwin` 17 | - `x64-mingw-ucrt` 18 | - `x86-linux-gnu` (requires: glibc >= 2.17) 19 | - `x86-linux-musl` 20 | - `x86_64-darwin` 21 | - `x86_64-linux-gnu` (requires: glibc >= 2.17) 22 | - `x86_64-linux-musl` 23 | 24 | ⚠ Musl linux users should update to Bundler >= 2.5.6 to avoid https://github.com/rubygems/rubygems/issues/7432 25 | 26 | If you are using one of these Ruby versions on one of these platforms, the native gem is the recommended way to install sqlite3-ruby. 27 | 28 | For example, on a linux system running Ruby 3.1: 29 | 30 | ``` text 31 | $ ruby -v 32 | ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-linux] 33 | 34 | $ time gem install sqlite3 35 | Fetching sqlite3-1.5.0-x86_64-linux.gem 36 | Successfully installed sqlite3-1.5.0-x86_64-linux 37 | 1 gem installed 38 | 39 | real 0m4.274s 40 | user 0m0.734s 41 | sys 0m0.165s 42 | ``` 43 | 44 | #### Avoiding the precompiled native gem 45 | 46 | The maintainers strongly urge you to use a native gem if at all possible. It will be a better experience for you and allow us to focus our efforts on improving functionality rather than diagnosing installation issues. 47 | 48 | If you're on a platform that supports a native gem but you want to avoid using it in your project, do one of the following: 49 | 50 | - If you're not using Bundler, then run `gem install sqlite3 --platform=ruby` 51 | - If you are using Bundler 52 | - version 2.3.18 or later, you can specify [`gem "sqlite3", force_ruby_platform: true`](https://bundler.io/v2.3/man/gemfile.5.html#FORCE_RUBY_PLATFORM) 53 | - version 2.1 or later, then you'll need to run `bundle config set force_ruby_platform true` 54 | - version 2.0 or earlier, then you'll need to run `bundle config force_ruby_platform true` 55 | 56 | 57 | ### Compiling the source gem 58 | 59 | If you are on a platform or version of Ruby that is not covered by the Native Gems, then the vanilla "ruby platform" (non-native) gem will be installed by the `gem install` or `bundle` commands. 60 | 61 | 62 | #### Packaged libsqlite3 63 | 64 | By default, as of v1.5.0 of this library, the latest available version of libsqlite3 is packaged with the gem and will be compiled and used automatically. This takes a bit longer than the native gem, but will provide a modern, well-supported version of libsqlite3. 65 | 66 | ⚠ A prerequisite to build the gem with the packaged sqlite3 is that you must have `pkgconf` installed. 67 | 68 | For example, on a linux system running Ruby 2.5: 69 | 70 | ``` text 71 | $ ruby -v 72 | ruby 2.5.9p229 (2021-04-05 revision 67939) [x86_64-linux] 73 | 74 | $ time gem install sqlite3 75 | Building native extensions. This could take a while... 76 | Successfully installed sqlite3-1.5.0 77 | 1 gem installed 78 | 79 | real 0m20.620s 80 | user 0m23.361s 81 | sys 0m5.839s 82 | ``` 83 | 84 | ##### Controlling compilation flags for sqlite 85 | 86 | Upstream sqlite allows for the setting of some parameters at compile time. If you're an expert and would like to set these, you may do so at gem install time in two different ways ... 87 | 88 | **If you're installing the gem using `gem install`** then you can pass in these compile-time flags like this: 89 | 90 | ``` sh 91 | gem install sqlite3 --platform=ruby -- \ 92 | --with-sqlite-cflags="-DSQLITE_DEFAULT_CACHE_SIZE=9999 -DSQLITE_DEFAULT_PAGE_SIZE=4444" 93 | ``` 94 | 95 | or the equivalent: 96 | 97 | ``` sh 98 | CFLAGS="-DSQLITE_DEFAULT_CACHE_SIZE=9999 -DSQLITE_DEFAULT_PAGE_SIZE=4444" \ 99 | gem install sqlite3 --platform=ruby 100 | ``` 101 | 102 | **If you're installing the gem using `bundler`** then you should first pin the gem to the "ruby" platform gem, so that you are compiling from source: 103 | 104 | ``` ruby 105 | # Gemfile 106 | gem "sqlite3", force_ruby_platform: true # requires bundler >= 2.3.18 107 | ``` 108 | 109 | and then set up a bundler config parameter for `build.sqlite3`: 110 | 111 | ``` sh 112 | bundle config set build.sqlite3 \ 113 | "--with-sqlite-cflags='-DSQLITE_DEFAULT_CACHE_SIZE=9999 -DSQLITE_DEFAULT_PAGE_SIZE=4444'" 114 | ``` 115 | 116 | NOTE the use of single quotes within the double-quoted string to ensure the space between compiler flags is interpreted correctly. The contents of your `.bundle/config` file should look like: 117 | 118 | ``` yaml 119 | --- 120 | BUNDLE_BUILD__SQLITE3: "--with-sqlite-cflags='-DSQLITE_DEFAULT_CACHE_SIZE=9999 -DSQLITE_DEFAULT_PAGE_SIZE=4444'" 121 | ``` 122 | 123 | 124 | #### System libsqlite3 125 | 126 | If you would prefer to build the sqlite3-ruby gem against your system libsqlite3, which requires that you install libsqlite3 and its development files yourself, you may do so by using the `--enable-system-libraries` flag at gem install time. 127 | 128 | PLEASE NOTE: 129 | 130 | - you must avoid installing a precompiled native gem (see [previous section](#avoiding-the-precompiled-native-gem)) 131 | - only versions of libsqlite3 `>= 3.5.0` are supported, 132 | - and some library features may depend on how your libsqlite3 was compiled. 133 | 134 | For example, on a linux system running Ruby 2.5: 135 | 136 | ``` text 137 | $ time gem install sqlite3 -- --enable-system-libraries 138 | Building native extensions with: '--enable-system-libraries' 139 | This could take a while... 140 | Successfully installed sqlite3-1.5.0 141 | 1 gem installed 142 | 143 | real 0m4.234s 144 | user 0m3.809s 145 | sys 0m0.912s 146 | ``` 147 | 148 | If you're using bundler, you can opt into system libraries like this: 149 | 150 | ``` sh 151 | bundle config build.sqlite3 --enable-system-libraries 152 | ``` 153 | 154 | If you have sqlite3 installed in a non-standard location, you may need to specify the location of the include and lib files by using `--with-sqlite-include` and `--with-sqlite-lib` options (or a `--with-sqlite-dir` option, see [MakeMakefile#dir_config](https://ruby-doc.org/stdlib-3.1.1/libdoc/mkmf/rdoc/MakeMakefile.html#method-i-dir_config)). If you have pkg-config installed and configured properly, this may not be necessary. 155 | 156 | ``` sh 157 | gem install sqlite3 -- \ 158 | --enable-system-libraries \ 159 | --with-sqlite3-include=/opt/local/include \ 160 | --with-sqlite3-lib=/opt/local/lib 161 | ``` 162 | 163 | 164 | #### System libsqlcipher 165 | 166 | If you'd like to link against a system-installed libsqlcipher, you may do so by using the `--with-sqlcipher` flag: 167 | 168 | ``` text 169 | $ time gem install sqlite3 -- --with-sqlcipher 170 | Building native extensions with: '--with-sqlcipher' 171 | This could take a while... 172 | Successfully installed sqlite3-1.5.0 173 | 1 gem installed 174 | 175 | real 0m4.772s 176 | user 0m3.906s 177 | sys 0m0.896s 178 | ``` 179 | 180 | If you have sqlcipher installed in a non-standard location, you may need to specify the location of the include and lib files by using `--with-sqlite-include` and `--with-sqlite-lib` options (or a `--with-sqlite-dir` option, see [MakeMakefile#dir_config](https://ruby-doc.org/stdlib-3.1.1/libdoc/mkmf/rdoc/MakeMakefile.html#method-i-dir_config)). If you have pkg-config installed and configured properly, this may not be necessary. 181 | 182 | 183 | ## Using SQLite3 extensions 184 | 185 | ### How do I load a sqlite extension? 186 | 187 | Some add-ons are available to sqlite as "extensions". The instructions that upstream sqlite provides at https://www.sqlite.org/loadext.html are the canonical source of advice, but here's a brief example showing how you can do this with the `sqlite3` ruby gem. 188 | 189 | In this example, I'll be loading the ["spellfix" extension](https://www.sqlite.org/spellfix1.html): 190 | 191 | ``` text 192 | # download spellfix.c from somewherehttp://www.sqlite.org/src/finfo?name=ext/misc/spellfix.c 193 | $ wget https://raw.githubusercontent.com/sqlite/sqlite/master/ext/misc/spellfix.c 194 | spellfix.c 100%[=================================================>] 100.89K --.-KB/s in 0.09s 195 | 196 | # follow instructions at https://www.sqlite.org/loadext.html 197 | # (you will need sqlite3 development packages for this) 198 | $ gcc -g -fPIC -shared spellfix.c -o spellfix.o 199 | 200 | $ ls -lt 201 | total 192 202 | -rwxrwxr-x 1 flavorjones flavorjones 87984 2023-05-24 10:44 spellfix.o 203 | -rw-rw-r-- 1 flavorjones flavorjones 103310 2023-05-24 10:43 spellfix.c 204 | ``` 205 | 206 | Then, in your application, use that `spellfix.o` file like this: 207 | 208 | ``` ruby 209 | require "sqlite3" 210 | 211 | db = SQLite3::Database.new(':memory:') 212 | db.enable_load_extension(true) 213 | db.load_extension("/path/to/sqlite/spellfix.o") 214 | db.execute("CREATE VIRTUAL TABLE demo USING spellfix1;") 215 | ``` 216 | 217 | ### How do I use my own sqlite3 shared library? 218 | 219 | Some folks have strong opinions about what features they want compiled into sqlite3; or may be using a package like SQLite Encryption Extension ("SEE"). This section will explain how to get your Ruby application to load that specific shared library. 220 | 221 | If you've installed your alternative as an autotools-style installation, the directory structure will look like this: 222 | 223 | ``` 224 | /opt/sqlite3 225 | ├── bin 226 | │   └── sqlite3 227 | ├── include 228 | │   ├── sqlite3.h 229 | │   └── sqlite3ext.h 230 | ├── lib 231 | │   ├── libsqlite3.a 232 | │   ├── libsqlite3.la 233 | │   ├── libsqlite3.so -> libsqlite3.so.0.8.6 234 | │   ├── libsqlite3.so.0 -> libsqlite3.so.0.8.6 235 | │   ├── libsqlite3.so.0.8.6 236 | │   └── pkgconfig 237 | │   └── sqlite3.pc 238 | └── share 239 | └── man 240 | └── man1 241 | └── sqlite3.1 242 | ``` 243 | 244 | You can build this gem against that library like this: 245 | 246 | ``` 247 | gem install sqlite3 --platform=ruby -- \ 248 | --enable-system-libraries \ 249 | --with-opt-dir=/opt/sqlite 250 | ``` 251 | 252 | Explanation: 253 | 254 | - use `--platform=ruby` to avoid the precompiled native gems (see the README) 255 | - the `--` separates arguments passed to "gem install" from arguments passed to the C extension builder 256 | - use `--enable-system-libraries` to avoid the vendored sqlite3 source 257 | - use `--with-opt-dir=/path/to/installation` to point the build process at the desired header files and shared object files 258 | 259 | Alternatively, if you've simply downloaded an "amalgamation" and so your compiled library and header files are in arbitrary locations, try this more detailed command: 260 | 261 | ``` 262 | gem install sqlite3 --platform=ruby -- \ 263 | --enable-system-libraries \ 264 | --with-opt-include=/path/to/include \ 265 | --with-opt-lib=/path/to/lib 266 | ``` 267 | 268 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2004-2024, Jamis Buck, Luis Lavena, Aaron Patterson, Mike Dalessio, et al. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted 4 | provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions 7 | and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of 10 | conditions and the following disclaimer in the documentation and/or other materials provided with 11 | the distribution. 12 | 13 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to 14 | endorse or promote products derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 17 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 18 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 19 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 22 | IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 23 | THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ruby Interface for SQLite3 2 | 3 | ## Overview 4 | 5 | This library allows Ruby programs to use the SQLite3 database engine (http://www.sqlite.org). 6 | 7 | Note that this module is only compatible with SQLite 3.6.16 or newer. 8 | 9 | * Source code: https://github.com/sparklemotion/sqlite3-ruby 10 | * Mailing list: http://groups.google.com/group/sqlite3-ruby 11 | * Download: http://rubygems.org/gems/sqlite3 12 | * Documentation: https://sparklemotion.github.io/sqlite3-ruby/ 13 | 14 | [![Test suite](https://github.com/sparklemotion/sqlite3-ruby/actions/workflows/ci.yml/badge.svg)](https://github.com/sparklemotion/sqlite3-ruby/actions/workflows/ci.yml) 15 | 16 | 17 | ## Quick start 18 | 19 | For help understanding the SQLite3 Ruby API, please read the [FAQ](./FAQ.md) and the [full API documentation](https://sparklemotion.github.io/sqlite3-ruby/). 20 | 21 | A few key classes whose APIs are often-used are: 22 | 23 | - SQLite3::Database ([rdoc](https://sparklemotion.github.io/sqlite3-ruby/SQLite3/Database.html)) 24 | - SQLite3::Statement ([rdoc](https://sparklemotion.github.io/sqlite3-ruby/SQLite3/Statement.html)) 25 | - SQLite3::ResultSet ([rdoc](https://sparklemotion.github.io/sqlite3-ruby/SQLite3/ResultSet.html)) 26 | 27 | If you have any questions that you feel should be addressed in the FAQ, please send them to [the mailing list](http://groups.google.com/group/sqlite3-ruby) or open a [discussion thread](https://github.com/sparklemotion/sqlite3-ruby/discussions/categories/q-a). 28 | 29 | 30 | ``` ruby 31 | require "sqlite3" 32 | 33 | # Open a database 34 | db = SQLite3::Database.new "test.db" 35 | 36 | # Create a table 37 | rows = db.execute <<-SQL 38 | create table numbers ( 39 | name varchar(30), 40 | val int 41 | ); 42 | SQL 43 | 44 | # Execute a few inserts 45 | { 46 | "one" => 1, 47 | "two" => 2, 48 | }.each do |pair| 49 | db.execute "insert into numbers values ( ?, ? )", pair 50 | end 51 | 52 | # Find a few rows 53 | db.execute( "select * from numbers" ) do |row| 54 | p row 55 | end 56 | # => ["one", 1] 57 | # ["two", 2] 58 | 59 | # Create another table with multiple columns 60 | db.execute <<-SQL 61 | create table students ( 62 | name varchar(50), 63 | email varchar(50), 64 | grade varchar(5), 65 | blog varchar(50) 66 | ); 67 | SQL 68 | 69 | # Execute inserts with parameter markers 70 | db.execute("INSERT INTO students (name, email, grade, blog) 71 | VALUES (?, ?, ?, ?)", ["Jane", "me@janedoe.com", "A", "http://blog.janedoe.com"]) 72 | 73 | db.execute( "select * from students" ) do |row| 74 | p row 75 | end 76 | # => ["Jane", "me@janedoe.com", "A", "http://blog.janedoe.com"] 77 | ``` 78 | 79 | ## Thread Safety 80 | 81 | When `SQLite3.threadsafe?` returns `true`, then SQLite3 has been compiled to 82 | support running in a multithreaded environment. However, this doesn't mean 83 | that all classes in the SQLite3 gem can be considered "thread safe". 84 | 85 | When `SQLite3.threadsafe?` returns `true`, it is safe to share only 86 | `SQLite3::Database` instances among threads without providing your own locking 87 | mechanism. For example, the following code is fine because only the database 88 | instance is shared among threads: 89 | 90 | ```ruby 91 | require 'sqlite3' 92 | 93 | db = SQLite3::Database.new ":memory:" 94 | 95 | latch = Queue.new 96 | 97 | ts = 10.times.map { 98 | Thread.new { 99 | latch.pop 100 | db.execute "SELECT '#{Thread.current.inspect}'" 101 | } 102 | } 103 | 10.times { latch << nil } 104 | 105 | p ts.map(&:value) 106 | ``` 107 | 108 | Other instances can be shared among threads, but they require that you provide 109 | your own locking for thread safety. For example, `SQLite3::Statement` objects 110 | (prepared statements) are mutable, so applications must take care to add 111 | appropriate locks to avoid data race conditions when sharing these objects 112 | among threads. 113 | 114 | Lets rewrite the above example but use a prepared statement and safely share 115 | the prepared statement among threads: 116 | 117 | ```ruby 118 | db = SQLite3::Database.new ":memory:" 119 | 120 | # Prepare a statement 121 | stmt = db.prepare "SELECT :inspect" 122 | stmt_lock = Mutex.new 123 | 124 | latch = Queue.new 125 | 126 | ts = 10.times.map { 127 | Thread.new { 128 | latch.pop 129 | 130 | # Add a lock when using the prepared statement. 131 | # Binding values, and walking over results will mutate the statement, so 132 | # in order to prevent other threads from "seeing" this thread's data, we 133 | # must lock when using the statement object 134 | stmt_lock.synchronize do 135 | stmt.execute(Thread.current.inspect).to_a 136 | end 137 | } 138 | } 139 | 140 | 10.times { latch << nil } 141 | 142 | p ts.map(&:value) 143 | 144 | stmt.close 145 | ``` 146 | 147 | It is generally recommended that if applications want to share a database among 148 | threads, they _only_ share the database instance object. Other objects are 149 | fine to share, but may require manual locking for thread safety. 150 | 151 | 152 | ## Fork Safety 153 | 154 | [Sqlite is not fork 155 | safe](https://www.sqlite.org/howtocorrupt.html#_carrying_an_open_database_connection_across_a_fork_) 156 | and instructs users to not carry an open writable database connection across a `fork()`. Using an inherited 157 | connection in the child may corrupt your database, leak memory, or cause other undefined behavior. 158 | 159 | To help protect users of this gem from accidental corruption due to this lack of fork safety, the gem will immediately close any open writable databases in the child after a fork. Discarding writable 160 | connections in the child will incur a small one-time memory leak per connection, but that's 161 | preferable to potentially corrupting your database. 162 | 163 | Whenever possible, close writable connections in the parent before forking. If absolutely necessary (and you know what you're doing), you may suppress the fork safety warnings by calling `SQLite3::ForkSafety.suppress_warnings!`. 164 | 165 | See [./adr/2024-09-fork-safety.md](./adr/2024-09-fork-safety.md) for more information and context. 166 | 167 | 168 | ## Support 169 | 170 | ### Installation or database extensions 171 | 172 | If you're having trouble with installation, please first read [`INSTALLATION.md`](./INSTALLATION.md). 173 | 174 | ### General help requests 175 | 176 | You can ask for help or support: 177 | 178 | * by emailing the [sqlite3-ruby mailing list](http://groups.google.com/group/sqlite3-ruby) 179 | * by opening a [discussion thread](https://github.com/sparklemotion/sqlite3-ruby/discussions/categories/q-a) on Github 180 | 181 | ### Bug reports 182 | 183 | You can file the bug at the [github issues page](https://github.com/sparklemotion/sqlite3-ruby/issues). 184 | 185 | 186 | ## Contributing 187 | 188 | See [`CONTRIBUTING.md`](./CONTRIBUTING.md). 189 | 190 | 191 | ## License 192 | 193 | This library is licensed under `BSD-3-Clause`, see [`LICENSE`](./LICENSE). 194 | 195 | ### Dependencies 196 | 197 | The source code of `sqlite` is distributed in the "ruby platform" gem. This code is public domain, 198 | see https://www.sqlite.org/copyright.html for details. 199 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # 2 | # NOTE: Keep this file clean. 3 | # Add your customizations inside tasks directory. 4 | # Thank You. 5 | # 6 | require "bundler" 7 | SQLITE3_SPEC = Bundler.load_gemspec("sqlite3.gemspec") 8 | 9 | task default: [:rubocop, :compile, :test] 10 | -------------------------------------------------------------------------------- /adr/2024-09-fork-safety.md: -------------------------------------------------------------------------------- 1 | 2 | # 2024-09 Automatically close database connections when carried across fork() 3 | 4 | ## Status 5 | 6 | Accepted, but we can revisit more complex solutions if we learn something that indicates that effort is worth it. 7 | 8 | 9 | ## Context 10 | 11 | In August 2024, Andy Croll opened an issue[^issue] describing sqlite file corruption related to solid queue. After investigation, we were able to reproduce corruption under certain circumstances when forking a process with open sqlite databases.[^repro] 12 | 13 | SQLite is known to not be fork-safe[^howto], so this was not entirely surprising though it was the first time your author had personally seen corruption in the wild. The corruption became much more likely after the sqlite3-ruby gem improved its memory management with respect to open statements[^gemleak] in v2.0.0. 14 | 15 | Advice from upstream contributors[^advice] is, essentially: don't fork if you have open database connections. Or, if you have forked, don't call `sqlite3_close` on those connections and thereby leak some amount of memory in the child process. Neither of these options are ideal, see below. 16 | 17 | 18 | ## Decisions 19 | 20 | 1. Open writable database connections carried across a `fork()` will automatically be closed in the child process to mitigate the risk of corrupting the database file. 21 | 2. These connections will be incompletely closed ("discarded") which will result in a one-time memory leak in the child process. 22 | 23 | First, the gem will register an "after fork" handler via `Process._fork` that will close any open writable database connections in the child process. This is a best-effort attempt to avoid corruption, but it is not guaranteed to prevent corruption in all cases. Any connections closed by this handler will also emit a warning to let users know what's happening. 24 | 25 | Second, the sqlite3-ruby gem will store the ID of the process that opened each database connection. If, when a writable database is closed (either explicitly with `Database#close` or implicitly via GC or after-fork callback) the current process ID is different from the original process, then we "discard" the connection. 26 | 27 | "Discard" here means: 28 | 29 | - `sqlite3_close_v2` is not called on the database, because it is unsafe to do so per sqlite instructions[^howto]. 30 | - Open file descriptors associated with the database are closed. 31 | - Any memory that can be freed safely is recovered. 32 | - But some memory will be lost permanently (a one-time "memory leak"). 33 | - The `Database` object acts "closed", including returning `true` from `#closed?`. 34 | - Related `Statement` objects are rendered unusable and will raise an exception if used. 35 | 36 | Note that readonly databases are being treated as "fork safe" and are not affected by these changes. 37 | 38 | 39 | ## Consequences 40 | 41 | The positive consequence is that we remove a potential cause of database corruption for applications that fork with active sqlite database connections. 42 | 43 | The negative consequence is that, for each discarded connection, some memory will be permanently lost (leaked) in the child process. We consider this to be an acceptable tradeoff given the risk of data loss. 44 | 45 | 46 | ## Alternatives considered. 47 | 48 | ### 1. Require applications to close database connections before forking. 49 | 50 | This is the advice[^advice] given by the upstream maintainers of sqlite, and so was the first thing we tried to implement in Rails in [rails/rails#52931](https://github.com/rails/rails/pull/52931)[^before_fork]. That first simple implementation was not thread safe, however, and in order to make it thread-safe it would be necessary to pause all sqlite database activity, close the open connections, and then fork. At least one Rails core team member was not happy that this would interfere with database connections in the parent, and the complexity of a thread-safe solution seemed high, so this work was paused. 51 | 52 | ### 2. Memory arena 53 | 54 | Sqlite offers a configuration option to specify custom memory functions for malloc et al. It seems possible that the sqlite3-ruby gem could implement a custom arena that would be used by sqlite so that in a new process, after forking, all the memory underlying the sqlite Ruby objects could be discarded in a single operation. 55 | 56 | I think this approach is promising, but complex and risky. Sqlite is a complex library and uses shared memory in addition to the traditional heap. Would throwing away the heap memory (the arena) result in a segfault or other undefined behaviors or corruption? Determining the answer to that question feels expensive in and of itself, and any solution along these lines would not be supported by the sqlite authors. We can explore this space if the memory leak from discarded connections turns out to be a large source of pain. 57 | 58 | 59 | ## References 60 | 61 | - [Database connections carried across fork() will not be fully closed by flavorjones · Pull Request #558 · sparklemotion/sqlite3-ruby](https://github.com/sparklemotion/sqlite3-ruby/pull/558) 62 | 63 | 64 | ## Footnotes 65 | 66 | [^issue]: [SQLite queue database corruption · Issue #324 · rails/solid_queue](https://github.com/rails/solid_queue/issues/324) 67 | [^repro]: [flavorjones/2024-09-13-sqlite-corruption: Temporary repo, reproduction of sqlite database corruption.](https://github.com/flavorjones/2024-09-13-sqlite-corruption) 68 | [^howto]: [How To Corrupt An SQLite Database File: §2.6 Carrying an open database connection across a fork()](https://www.sqlite.org/howtocorrupt.html#_carrying_an_open_database_connection_across_a_fork_) 69 | [^gemleak]: [Always call sqlite3_finalize in deallocate func by haileys · Pull Request #392 · sparklemotion/sqlite3-ruby](https://github.com/sparklemotion/sqlite3-ruby/pull/392) 70 | [^advice]: [SQLite Forum: Correct way of carrying connections over forked processes](https://sqlite.org/forum/forumpost/1fa07728204567a0a136f442cb1c59e3117da96898b7fa3290b0063ae7f6f012) 71 | [^before_fork]: [SQLite3Adapter: Ensure fork-safety by flavorjones · Pull Request #52931 · rails/rails](https://github.com/rails/rails/pull/52931#issuecomment-2351365601) 72 | [^config]: [SQlite3 Configuration Options](https://www.sqlite.org/c3ref/c_config_covering_index_scan.html) 73 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | --- 2 | image: Visual Studio 2019 3 | 4 | branches: 5 | only: 6 | - master 7 | 8 | skip_branch_with_pr: true 9 | 10 | clone_depth: 1 11 | 12 | cache: 13 | - vendor/bundle 14 | - ports/archives 15 | 16 | install: 17 | - SET PATH=C:\ruby%ruby_version%\bin;%PATH% 18 | - ruby --version 19 | - gem --version 20 | - gem install bundler --conservative 21 | - bundler --version 22 | - bundle config --local path vendor/bundle 23 | - bundle install 24 | 25 | build: off 26 | 27 | test_script: 28 | - bundle exec rake -rdevkit compile test 29 | 30 | environment: 31 | matrix: 32 | - ruby_version: "33" 33 | - ruby_version: "32" 34 | - ruby_version: "31" 35 | - ruby_version: "30" 36 | -------------------------------------------------------------------------------- /bin/build-gems: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # 3 | # script to build gems for all relevant platforms 4 | # 5 | set -o errexit 6 | set -o nounset 7 | set -x 8 | 9 | rm -rf tmp pkg gems 10 | mkdir -p gems 11 | 12 | # prelude: vendor dependencies 13 | bundle update 14 | bundle package 15 | 16 | # safety check: let's check that things work 17 | bundle exec rake clean clobber 18 | bundle exec rake compile test 19 | 20 | # package the gems, including precompiled native 21 | bundle exec rake clean clobber 22 | bundle exec rake gem:all 23 | cp -v pkg/sqlite3*.gem gems 24 | 25 | # test those gem files! 26 | bin/test-gem-set gems/*.gem 27 | 28 | # checksums should be included in the release notes 29 | pushd gems 30 | ls *.gem | sort | xargs sha256sum 31 | popd 32 | -------------------------------------------------------------------------------- /bin/test-gem-build: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # 3 | # run as part of CI 4 | # 5 | if [[ $# -lt 2 ]] ; then 6 | echo "usage: $(basename $0) " 7 | exit 1 8 | fi 9 | 10 | set -e -u 11 | 12 | OUTPUT_DIR=$1 13 | BUILD_NATIVE_GEM=$2 14 | 15 | test -e /etc/os-release && cat /etc/os-release 16 | 17 | set -x 18 | 19 | bundle config set without development 20 | bundle install --local || bundle install 21 | bundle exec rake set-version-to-timestamp 22 | 23 | if [[ "${BUILD_NATIVE_GEM}" == "ruby" ]] ; then 24 | bundle exec ruby ext/sqlite3/extconf.rb --download-dependencies 25 | bundle exec rake gem 26 | else 27 | bundle exec rake gem:${BUILD_NATIVE_GEM}:buildit 28 | fi 29 | 30 | ./bin/test-gem-file-contents pkg/*.gem 31 | 32 | mkdir -p ${OUTPUT_DIR} 33 | cp -v pkg/*.gem ${OUTPUT_DIR} 34 | ls -l ${OUTPUT_DIR}/* 35 | -------------------------------------------------------------------------------- /bin/test-gem-file-contents: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | # 3 | # this script is intended to run as part of the CI test suite. 4 | # 5 | # it inspects the contents of a gem file -- both the files and the gemspec -- to ensure we're 6 | # packaging what we expect, and that we're not packaging anything we don't expect. 7 | # 8 | # this file isn't in the `test/` subdirectory because it's intended to be run standalone against a 9 | # built gem file (and not against the source code or behavior of the gem itself). 10 | # 11 | require "bundler/inline" 12 | 13 | gemfile do 14 | source "https://rubygems.org" 15 | gem "minitest" 16 | gem "minitest-reporters" 17 | end 18 | 19 | require "yaml" 20 | 21 | def usage_and_exit(message = nil) 22 | puts "ERROR: #{message}" if message 23 | puts "USAGE: #{File.basename(__FILE__)} [options]" 24 | exit(1) 25 | end 26 | 27 | usage_and_exit if ARGV.include?("-h") 28 | usage_and_exit unless (gemfile = ARGV[0]) 29 | usage_and_exit("#{gemfile} does not exist") unless File.file?(gemfile) 30 | usage_and_exit("#{gemfile} is not a gem") unless /\.gem$/.match?(gemfile) 31 | gemfile = File.expand_path(gemfile) 32 | 33 | gemfile_contents = Dir.mktmpdir do |dir| 34 | Dir.chdir(dir) do 35 | unless system("tar -xf #{gemfile} data.tar.gz") 36 | raise "could not unpack gem #{gemfile}" 37 | end 38 | 39 | `tar -ztf data.tar.gz`.split("\n") 40 | end 41 | end 42 | 43 | gemspec = Dir.mktmpdir do |dir| 44 | Dir.chdir(dir) do 45 | unless system("tar -xf #{gemfile} metadata.gz") 46 | raise "could not unpack gem #{gemfile}" 47 | end 48 | 49 | YAML.unsafe_load(`gunzip -c metadata.gz`) 50 | end 51 | end 52 | 53 | if ARGV.include?("-v") 54 | puts "---------- gemfile contents ----------" 55 | puts gemfile_contents 56 | puts 57 | puts "---------- gemspec ----------" 58 | puts gemspec.to_ruby 59 | puts 60 | end 61 | 62 | require "minitest/autorun" 63 | require "minitest/reporters" 64 | Minitest::Reporters.use!([Minitest::Reporters::SpecReporter.new]) 65 | 66 | puts "Testing '#{gemfile}' (#{gemspec.platform})" 67 | describe File.basename(gemfile) do 68 | let(:supported_ruby_versions) { ["3.1", "3.2", "3.3", "3.4"] } 69 | 70 | describe "setup" do 71 | it "gemfile contains some files" do 72 | actual = gemfile_contents.length 73 | assert_operator(actual, :>, 10, "expected gemfile to contain more than #{actual} files") 74 | end 75 | 76 | it "gemspec is a Gem::Specification" do 77 | assert_equal(Gem::Specification, gemspec.class) 78 | end 79 | end 80 | 81 | describe "all platforms" do 82 | ["lib"].each do |dir| 83 | it "contains every ruby file in #{dir}/" do 84 | expected = `git ls-files #{dir}`.split("\n").grep(/\.rb$/).sort 85 | skip "looks like this isn't a git repository" if expected.empty? 86 | actual = gemfile_contents.select { |f| f.start_with?("#{dir}/") }.grep(/\.rb$/).sort 87 | assert_equal(expected, actual) 88 | end 89 | end 90 | 91 | ["test"].each do |dir| 92 | it "does not contain files from #{dir}/" do 93 | actual = gemfile_contents.select { |f| f.start_with?("#{dir}/") }.grep(/\.rb$/) 94 | assert_empty(actual) 95 | end 96 | end 97 | 98 | it "does not contain the Gemfile" do 99 | refute_includes(gemfile_contents, "Gemfile") 100 | end 101 | end 102 | 103 | if gemspec.platform == Gem::Platform::RUBY 104 | describe "ruby platform" do 105 | it "depends on mini_portile2" do 106 | assert(gemspec.dependencies.find { |d| d.name == "mini_portile2" }) 107 | end 108 | 109 | it "contains extension C and header files" do 110 | assert_equal(6, gemfile_contents.count { |f| File.fnmatch?("ext/**/*.c", f) }) 111 | assert_equal(7, gemfile_contents.count { |f| File.fnmatch?("ext/**/*.h", f) }) 112 | end 113 | 114 | it "includes C files in extra_rdoc_files" do 115 | assert_equal(6, gemspec.extra_rdoc_files.count { |f| File.fnmatch?("ext/**/*.c", f) }) 116 | end 117 | 118 | it "contains the port files" do 119 | dependencies = YAML.load_file(File.join(__dir__, "..", "dependencies.yml"), symbolize_names: true) 120 | sqlite_tarball = File.basename(dependencies[:sqlite3][:files].first[:url]) 121 | actual_ports = gemfile_contents.grep(%r{^ports/}) 122 | 123 | assert_equal(["ports/archives/#{sqlite_tarball}"], actual_ports) 124 | end 125 | 126 | it "contains the patch files" do 127 | assert_equal(Dir.glob("patches/*.patch").length, gemfile_contents.count { |f| File.fnmatch?("patches/*", f) }) 128 | end 129 | 130 | it "sets metadata for msys2" do 131 | refute_nil(gemspec.metadata["msys2_mingw_dependencies"]) 132 | end 133 | 134 | it "sets required_ruby_version appropriately" do 135 | supported_ruby_versions.each do |v| 136 | assert( 137 | gemspec.required_ruby_version.satisfied_by?(Gem::Version.new(v)), 138 | "required_ruby_version='#{gemspec.required_ruby_version}' should support ruby #{v}" 139 | ) 140 | end 141 | end 142 | end 143 | end 144 | 145 | if gemspec.platform.is_a?(Gem::Platform) && gemspec.platform.cpu 146 | describe "native platform" do 147 | it "does not depend on mini_portile2" do 148 | refute(gemspec.dependencies.find { |d| d.name == "mini_portile2" }) 149 | end 150 | 151 | it "contains extension C and header files" do 152 | assert_equal(6, gemfile_contents.count { |f| File.fnmatch?("ext/**/*.c", f) }) 153 | assert_equal(7, gemfile_contents.count { |f| File.fnmatch?("ext/**/*.h", f) }) 154 | end 155 | 156 | it "includes C files in extra_rdoc_files" do 157 | assert_equal(6, gemspec.extra_rdoc_files.count { |f| File.fnmatch?("ext/**/*.c", f) }) 158 | end 159 | 160 | it "does not contain the port files" do 161 | assert_empty(gemfile_contents.grep(%r{^ports/})) 162 | end 163 | 164 | it "does not contain the patch files" do 165 | assert_empty(gemfile_contents.grep(%r{^patches/})) 166 | end 167 | 168 | it "contains expected shared library files " do 169 | supported_ruby_versions.each do |version| 170 | actual = gemfile_contents.find do |p| 171 | File.fnmatch?("lib/sqlite3/#{version}/sqlite3_native.{so,bundle}", p, File::FNM_EXTGLOB) 172 | end 173 | assert(actual, "expected to find shared library file for ruby #{version}") 174 | end 175 | 176 | actual = gemfile_contents.find do |p| 177 | File.fnmatch?("lib/sqlite3/sqlite3_native.{so,bundle}", p, File::FNM_EXTGLOB) 178 | end 179 | refute(actual, "did not expect to find shared library file in lib/sqlite3") 180 | 181 | actual = gemfile_contents.find_all do |p| 182 | File.fnmatch?("lib/sqlite3/**/*.{so,bundle}", p, File::FNM_EXTGLOB) 183 | end 184 | assert_equal( 185 | supported_ruby_versions.length, 186 | actual.length, 187 | "did not expect extra shared library files" 188 | ) 189 | end 190 | 191 | it "sets required_ruby_version appropriately" do 192 | supported_ruby_versions.each do |v| 193 | assert( 194 | gemspec.required_ruby_version.satisfied_by?(Gem::Version.new(v)), 195 | "required_ruby_version='#{gemspec.required_ruby_version}' should support ruby #{v}" 196 | ) 197 | end 198 | end 199 | 200 | it "does not set metadata for msys2" do 201 | assert_nil(gemspec.metadata["msys2_mingw_dependencies"]) 202 | end 203 | end 204 | end 205 | end 206 | -------------------------------------------------------------------------------- /bin/test-gem-install: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env sh 2 | # 3 | # run as part of CI 4 | # 5 | if [ $# -lt 1 ] ; then 6 | echo "usage: $(basename $0) [install_flags]" 7 | exit 1 8 | fi 9 | 10 | GEMS_DIR=$1 11 | shift 12 | INSTALL_FLAGS=$* 13 | 14 | test -e /etc/os-release && cat /etc/os-release 15 | 16 | set -e -x -u 17 | 18 | cd $GEMS_DIR 19 | 20 | gemfile=$(ls *.gem | head -n1) 21 | ls -l ${gemfile} 22 | gem install --no-document ${gemfile} -- ${INSTALL_FLAGS} 23 | gem list -d sqlite3 24 | 25 | cd .. 26 | 27 | bundle config set without development 28 | bundle install --local || bundle install 29 | 30 | rm -rf lib ext # ensure we don't use the local files 31 | rake test 32 | -------------------------------------------------------------------------------- /bin/test-gem-set: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # 3 | # script to test a set of gem files 4 | # 5 | set -o errexit 6 | set -o nounset 7 | set -o pipefail 8 | 9 | gems=$* 10 | gem_platform_local=`ruby -e "puts Gem::Platform.local.to_s"` 11 | 12 | function remove_all { 13 | gem uninstall --all --force $1 14 | } 15 | 16 | function test_installation { 17 | gem=$1 18 | 19 | # test installation 20 | remove_all sqlite3 21 | gem install $gem 22 | ruby -r sqlite3 -e 'pp SQLite3::SQLITE_VERSION, SQLite3::SQLITE_LOADED_VERSION' 23 | 24 | if [[ $gem =~ sqlite3-[^-]*\.gem ]] ; then 25 | # test installation against system libs without mini_portile, linux distro repackagers 26 | # (note that we don't care about running the gem in this case, just building it) 27 | # see https://github.com/sparklemotion/sqlite3-ruby/pull/381 for background 28 | remove_all sqlite3 29 | remove_all mini_portile2 30 | gem install --local --force $gem -- --enable-system-libraries 31 | if gem list -i mini_portile2 ; then 32 | echo "FAIL: should not have installed mini_portile2" 33 | exit 1 34 | fi 35 | 36 | # test installation against system libs 37 | remove_all sqlite3 38 | gem install $gem -- --enable-system-libraries 39 | ruby -r sqlite3 -e 'pp SQLite3::SQLITE_VERSION, SQLite3::SQLITE_LOADED_VERSION' 40 | fi 41 | } 42 | 43 | for gem in $gems ; do 44 | ./bin/test-gem-file-contents $gem 45 | done 46 | 47 | for gem in $gems ; do 48 | if [[ $gem =~ sqlite3-[^-]+(-${gem_platform_local})?\.gem$ ]] ; then 49 | test_installation $gem 50 | fi 51 | done 52 | -------------------------------------------------------------------------------- /dependencies.yml: -------------------------------------------------------------------------------- 1 | sqlite3: 2 | # checksum verified by first checking the published sha3(256) checksum against https://sqlite.org/download.html: 3 | # c12e84ba9772391d41644a0a9be37bad25791fc2a9b9395962e5f83f805e877f 4 | # 5 | # $ sha3sum -a 256 ports/archives/sqlite-autoconf-3500100.tar.gz 6 | # c12e84ba9772391d41644a0a9be37bad25791fc2a9b9395962e5f83f805e877f ports/archives/sqlite-autoconf-3500100.tar.gz 7 | # 8 | # $ sha256sum ports/archives/sqlite-autoconf-3500100.tar.gz 9 | # 00a65114d697cfaa8fe0630281d76fd1b77afcd95cd5e40ec6a02cbbadbfea71 ports/archives/sqlite-autoconf-3500100.tar.gz 10 | version: "3.50.1" 11 | files: 12 | - url: "https://sqlite.org/2025/sqlite-autoconf-3500100.tar.gz" 13 | sha256: "00a65114d697cfaa8fe0630281d76fd1b77afcd95cd5e40ec6a02cbbadbfea71" 14 | -------------------------------------------------------------------------------- /ext/sqlite3/aggregator.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | /* wraps a factory "handler" class. The "-aggregators" instance variable of 5 | * the SQLite3::Database holds an array of all AggrogatorWrappers. 6 | * 7 | * An AggregatorWrapper holds the following instance variables: 8 | * -handler_klass: the handler that creates the instances. 9 | * -instances: array of all the cAggregatorInstance objects currently 10 | * in-flight for this aggregator. */ 11 | static VALUE cAggregatorWrapper; 12 | 13 | /* wraps a instance of the "handler" class. Loses its reference at the end of 14 | * the xFinal callback. 15 | * 16 | * An AggregatorInstance holds the following instance variables: 17 | * -handler_instance: the instance to call `step` and `finalize` on. 18 | * -exc_status: status returned by rb_protect. 19 | * != 0 if an exception occurred. If an exception occurred 20 | * `step` and `finalize` won't be called any more. */ 21 | static VALUE cAggregatorInstance; 22 | 23 | typedef struct rb_sqlite3_protected_funcall_args { 24 | VALUE self; 25 | ID method; 26 | int argc; 27 | VALUE *params; 28 | } protected_funcall_args_t; 29 | 30 | /* why isn't there something like this in the ruby API? */ 31 | static VALUE 32 | rb_sqlite3_protected_funcall_body(VALUE protected_funcall_args_ptr) 33 | { 34 | protected_funcall_args_t *args = 35 | (protected_funcall_args_t *)protected_funcall_args_ptr; 36 | 37 | return rb_funcall2(args->self, args->method, args->argc, args->params); 38 | } 39 | 40 | static VALUE 41 | rb_sqlite3_protected_funcall(VALUE self, ID method, int argc, VALUE *params, 42 | int *exc_status) 43 | { 44 | protected_funcall_args_t args = { 45 | .self = self, .method = method, .argc = argc, .params = params 46 | }; 47 | return rb_protect(rb_sqlite3_protected_funcall_body, (VALUE)(&args), exc_status); 48 | } 49 | 50 | /* called in rb_sqlite3_aggregator_step and rb_sqlite3_aggregator_final. It 51 | * checks if the execution context already has an associated instance. If it 52 | * has one, it returns it. If there is no instance yet, it creates one and 53 | * associates it with the context. */ 54 | static VALUE 55 | rb_sqlite3_aggregate_instance(sqlite3_context *ctx) 56 | { 57 | VALUE aw = (VALUE) sqlite3_user_data(ctx); 58 | VALUE handler_klass = rb_iv_get(aw, "-handler_klass"); 59 | VALUE inst; 60 | VALUE *inst_ptr = sqlite3_aggregate_context(ctx, (int)sizeof(VALUE)); 61 | 62 | if (!inst_ptr) { 63 | rb_fatal("SQLite is out-of-merory"); 64 | } 65 | 66 | inst = *inst_ptr; 67 | 68 | if (inst == Qfalse) { /* Qfalse == 0 */ 69 | VALUE instances = rb_iv_get(aw, "-instances"); 70 | int exc_status; 71 | 72 | inst = rb_class_new_instance(0, NULL, cAggregatorInstance); 73 | rb_iv_set(inst, "-handler_instance", rb_sqlite3_protected_funcall( 74 | handler_klass, rb_intern("new"), 0, NULL, &exc_status)); 75 | rb_iv_set(inst, "-exc_status", INT2NUM(exc_status)); 76 | 77 | rb_ary_push(instances, inst); 78 | 79 | *inst_ptr = inst; 80 | } 81 | 82 | if (inst == Qnil) { 83 | rb_fatal("SQLite called us back on an already destroyed aggregate instance"); 84 | } 85 | 86 | return inst; 87 | } 88 | 89 | /* called by rb_sqlite3_aggregator_final. Unlinks and frees the 90 | * aggregator_instance_t, so the handler_instance won't be marked any more 91 | * and Ruby's GC may free it. */ 92 | static void 93 | rb_sqlite3_aggregate_instance_destroy(sqlite3_context *ctx) 94 | { 95 | VALUE aw = (VALUE) sqlite3_user_data(ctx); 96 | VALUE instances = rb_iv_get(aw, "-instances"); 97 | VALUE *inst_ptr = sqlite3_aggregate_context(ctx, 0); 98 | VALUE inst; 99 | 100 | if (!inst_ptr || (inst = *inst_ptr)) { 101 | return; 102 | } 103 | 104 | if (inst == Qnil) { 105 | rb_fatal("attempt to destroy aggregate instance twice"); 106 | } 107 | 108 | rb_iv_set(inst, "-handler_instance", Qnil); // may catch use-after-free 109 | if (rb_ary_delete(instances, inst) == Qnil) { 110 | rb_fatal("must be in instances at that point"); 111 | } 112 | 113 | *inst_ptr = Qnil; 114 | } 115 | 116 | static void 117 | rb_sqlite3_aggregator_step(sqlite3_context *ctx, int argc, sqlite3_value **argv) 118 | { 119 | VALUE inst = rb_sqlite3_aggregate_instance(ctx); 120 | VALUE handler_instance = rb_iv_get(inst, "-handler_instance"); 121 | VALUE *params = NULL; 122 | VALUE one_param; 123 | int exc_status = NUM2INT(rb_iv_get(inst, "-exc_status")); 124 | int i; 125 | 126 | if (exc_status) { 127 | return; 128 | } 129 | 130 | if (argc == 1) { 131 | one_param = sqlite3val2rb(argv[0]); 132 | params = &one_param; 133 | } 134 | if (argc > 1) { 135 | params = xcalloc((size_t)argc, sizeof(VALUE)); 136 | for (i = 0; i < argc; i++) { 137 | params[i] = sqlite3val2rb(argv[i]); 138 | } 139 | } 140 | rb_sqlite3_protected_funcall( 141 | handler_instance, rb_intern("step"), argc, params, &exc_status); 142 | if (argc > 1) { 143 | xfree(params); 144 | } 145 | 146 | rb_iv_set(inst, "-exc_status", INT2NUM(exc_status)); 147 | } 148 | 149 | /* we assume that this function is only called once per execution context */ 150 | static void 151 | rb_sqlite3_aggregator_final(sqlite3_context *ctx) 152 | { 153 | VALUE inst = rb_sqlite3_aggregate_instance(ctx); 154 | VALUE handler_instance = rb_iv_get(inst, "-handler_instance"); 155 | int exc_status = NUM2INT(rb_iv_get(inst, "-exc_status")); 156 | 157 | if (!exc_status) { 158 | VALUE result = rb_sqlite3_protected_funcall( 159 | handler_instance, rb_intern("finalize"), 0, NULL, &exc_status); 160 | if (!exc_status) { 161 | set_sqlite3_func_result(ctx, result); 162 | } 163 | } 164 | 165 | if (exc_status) { 166 | /* the user should never see this, as Statement.step() will pick up the 167 | * outstanding exception and raise it instead of generating a new one 168 | * for SQLITE_ERROR with message "Ruby Exception occurred" */ 169 | sqlite3_result_error(ctx, "Ruby Exception occurred", -1); 170 | } 171 | 172 | rb_sqlite3_aggregate_instance_destroy(ctx); 173 | } 174 | 175 | /* call-seq: define_aggregator2(aggregator) 176 | * 177 | * Define an aggregrate function according to a factory object (the "handler") 178 | * that knows how to obtain to all the information. The handler must provide 179 | * the following class methods: 180 | * 181 | * +arity+:: corresponds to the +arity+ parameter of #create_aggregate. This 182 | * message is optional, and if the handler does not respond to it, 183 | * the function will have an arity of -1. 184 | * +name+:: this is the name of the function. The handler _must_ implement 185 | * this message. 186 | * +new+:: this must be implemented by the handler. It should return a new 187 | * instance of the object that will handle a specific invocation of 188 | * the function. 189 | * 190 | * The handler instance (the object returned by the +new+ message, described 191 | * above), must respond to the following messages: 192 | * 193 | * +step+:: this is the method that will be called for each step of the 194 | * aggregate function's evaluation. It should take parameters according 195 | * to the *arity* definition. 196 | * +finalize+:: this is the method that will be called to finalize the 197 | * aggregate function's evaluation. It should not take arguments. 198 | * 199 | * Note the difference between this function and #create_aggregate_handler 200 | * is that no FunctionProxy ("ctx") object is involved. This manifests in two 201 | * ways: The return value of the aggregate function is the return value of 202 | * +finalize+ and neither +step+ nor +finalize+ take an additional "ctx" 203 | * parameter. 204 | */ 205 | VALUE 206 | rb_sqlite3_define_aggregator2(VALUE self, VALUE aggregator, VALUE ruby_name) 207 | { 208 | /* define_aggregator is added as a method to SQLite3::Database in database.c */ 209 | sqlite3RubyPtr ctx = sqlite3_database_unwrap(self); 210 | int arity, status; 211 | VALUE aw; 212 | VALUE aggregators; 213 | 214 | if (!ctx->db) { 215 | rb_raise(rb_path2class("SQLite3::Exception"), "cannot use a closed database"); 216 | } 217 | 218 | if (rb_respond_to(aggregator, rb_intern("arity"))) { 219 | VALUE ruby_arity = rb_funcall(aggregator, rb_intern("arity"), 0); 220 | arity = NUM2INT(ruby_arity); 221 | } else { 222 | arity = -1; 223 | } 224 | 225 | if (arity < -1 || arity > 127) { 226 | #ifdef PRIsVALUE 227 | rb_raise(rb_eArgError, "%"PRIsVALUE" arity=%d out of range -1..127", 228 | self, arity); 229 | #else 230 | rb_raise(rb_eArgError, "Aggregator arity=%d out of range -1..127", arity); 231 | #endif 232 | } 233 | 234 | if (!rb_ivar_defined(self, rb_intern("-aggregators"))) { 235 | rb_iv_set(self, "-aggregators", rb_ary_new()); 236 | } 237 | aggregators = rb_iv_get(self, "-aggregators"); 238 | 239 | aw = rb_class_new_instance(0, NULL, cAggregatorWrapper); 240 | rb_iv_set(aw, "-handler_klass", aggregator); 241 | rb_iv_set(aw, "-instances", rb_ary_new()); 242 | 243 | status = sqlite3_create_function( 244 | ctx->db, 245 | StringValueCStr(ruby_name), 246 | arity, 247 | SQLITE_UTF8, 248 | (void *)aw, 249 | NULL, 250 | rb_sqlite3_aggregator_step, 251 | rb_sqlite3_aggregator_final 252 | ); 253 | 254 | CHECK(ctx->db, status); 255 | 256 | rb_ary_push(aggregators, aw); 257 | 258 | return self; 259 | } 260 | 261 | void 262 | rb_sqlite3_aggregator_init(void) 263 | { 264 | /* rb_class_new generatos class with undefined allocator in ruby 1.9 */ 265 | cAggregatorWrapper = rb_funcall(rb_cClass, rb_intern("new"), 0); 266 | rb_gc_register_mark_object(cAggregatorWrapper); 267 | 268 | cAggregatorInstance = rb_funcall(rb_cClass, rb_intern("new"), 0); 269 | rb_gc_register_mark_object(cAggregatorInstance); 270 | } 271 | -------------------------------------------------------------------------------- /ext/sqlite3/aggregator.h: -------------------------------------------------------------------------------- 1 | #ifndef SQLITE3_AGGREGATOR_RUBY 2 | #define SQLITE3_AGGREGATOR_RUBY 3 | 4 | #include 5 | 6 | VALUE rb_sqlite3_define_aggregator2(VALUE self, VALUE aggregator, VALUE ruby_name); 7 | 8 | void rb_sqlite3_aggregator_init(void); 9 | 10 | #endif 11 | -------------------------------------------------------------------------------- /ext/sqlite3/backup.c: -------------------------------------------------------------------------------- 1 | #ifdef HAVE_SQLITE3_BACKUP_INIT 2 | 3 | #include 4 | 5 | #define REQUIRE_OPEN_BACKUP(_ctxt) \ 6 | if(!_ctxt->p) \ 7 | rb_raise(rb_path2class("SQLite3::Exception"), "cannot use a closed backup"); 8 | 9 | VALUE cSqlite3Backup; 10 | 11 | static size_t 12 | backup_memsize(const void *data) 13 | { 14 | sqlite3BackupRubyPtr ctx = (sqlite3BackupRubyPtr)data; 15 | // NB: can't account for ctx->p because the type is incomplete. 16 | return sizeof(*ctx); 17 | } 18 | 19 | static const rb_data_type_t backup_type = { 20 | "SQLite3::Backup", 21 | { 22 | NULL, 23 | RUBY_TYPED_DEFAULT_FREE, 24 | backup_memsize, 25 | }, 26 | 0, 27 | 0, 28 | RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED, 29 | }; 30 | 31 | static VALUE 32 | allocate(VALUE klass) 33 | { 34 | sqlite3BackupRubyPtr ctx; 35 | return TypedData_Make_Struct(klass, sqlite3BackupRuby, &backup_type, ctx); 36 | } 37 | 38 | /* call-seq: SQLite3::Backup.new(dstdb, dstname, srcdb, srcname) 39 | * 40 | * Initialize backup the backup. 41 | * 42 | * dstdb: 43 | * the destination SQLite3::Database object. 44 | * dstname: 45 | * the destination's database name. 46 | * srcdb: 47 | * the source SQLite3::Database object. 48 | * srcname: 49 | * the source's database name. 50 | * 51 | * The database name is "main", "temp", or the name specified in an 52 | * ATTACH statement. 53 | * 54 | * This feature requires SQLite 3.6.11 or later. 55 | * 56 | * require 'sqlite3' 57 | * sdb = SQLite3::Database.new('src.sqlite3') 58 | * 59 | * ddb = SQLite3::Database.new(':memory:') 60 | * b = SQLite3::Backup.new(ddb, 'main', sdb, 'main') 61 | * p [b.remaining, b.pagecount] # invalid value; for example [0, 0] 62 | * begin 63 | * p b.step(1) #=> OK or DONE 64 | * p [b.remaining, b.pagecount] 65 | * end while b.remaining > 0 66 | * b.finish 67 | * 68 | * ddb = SQLite3::Database.new(':memory:') 69 | * b = SQLite3::Backup.new(ddb, 'main', sdb, 'main') 70 | * b.step(-1) #=> DONE 71 | * b.finish 72 | * 73 | */ 74 | static VALUE 75 | initialize(VALUE self, VALUE dstdb, VALUE dstname, VALUE srcdb, VALUE srcname) 76 | { 77 | sqlite3BackupRubyPtr ctx; 78 | sqlite3RubyPtr ddb_ctx, sdb_ctx; 79 | sqlite3_backup *pBackup; 80 | 81 | TypedData_Get_Struct(self, sqlite3BackupRuby, &backup_type, ctx); 82 | ddb_ctx = sqlite3_database_unwrap(dstdb); 83 | sdb_ctx = sqlite3_database_unwrap(srcdb); 84 | 85 | if (!sdb_ctx->db) { 86 | rb_raise(rb_eArgError, "cannot backup from a closed database"); 87 | } 88 | if (!ddb_ctx->db) { 89 | rb_raise(rb_eArgError, "cannot backup to a closed database"); 90 | } 91 | 92 | pBackup = sqlite3_backup_init(ddb_ctx->db, StringValuePtr(dstname), 93 | sdb_ctx->db, StringValuePtr(srcname)); 94 | if (pBackup) { 95 | ctx->p = pBackup; 96 | } else { 97 | CHECK(ddb_ctx->db, sqlite3_errcode(ddb_ctx->db)); 98 | } 99 | 100 | return self; 101 | } 102 | 103 | /* call-seq: SQLite3::Backup#step(nPage) 104 | * 105 | * Copy database pages up to +nPage+. 106 | * If negative, copy all remaining source pages. 107 | * 108 | * If all pages are copied, it returns SQLite3::Constants::ErrorCode::DONE. 109 | * When coping is not done, it returns SQLite3::Constants::ErrorCode::OK. 110 | * When some errors occur, it returns the error code. 111 | */ 112 | static VALUE 113 | step(VALUE self, VALUE nPage) 114 | { 115 | sqlite3BackupRubyPtr ctx; 116 | int status; 117 | 118 | TypedData_Get_Struct(self, sqlite3BackupRuby, &backup_type, ctx); 119 | REQUIRE_OPEN_BACKUP(ctx); 120 | status = sqlite3_backup_step(ctx->p, NUM2INT(nPage)); 121 | return INT2NUM(status); 122 | } 123 | 124 | /* call-seq: SQLite3::Backup#finish 125 | * 126 | * Destroy the backup object. 127 | */ 128 | static VALUE 129 | finish(VALUE self) 130 | { 131 | sqlite3BackupRubyPtr ctx; 132 | 133 | TypedData_Get_Struct(self, sqlite3BackupRuby, &backup_type, ctx); 134 | REQUIRE_OPEN_BACKUP(ctx); 135 | (void)sqlite3_backup_finish(ctx->p); 136 | ctx->p = NULL; 137 | return Qnil; 138 | } 139 | 140 | /* call-seq: SQLite3::Backup#remaining 141 | * 142 | * Returns the number of pages still to be backed up. 143 | * 144 | * Note that the value is only updated after step() is called, 145 | * so before calling step() returned value is invalid. 146 | */ 147 | static VALUE 148 | remaining(VALUE self) 149 | { 150 | sqlite3BackupRubyPtr ctx; 151 | 152 | TypedData_Get_Struct(self, sqlite3BackupRuby, &backup_type, ctx); 153 | REQUIRE_OPEN_BACKUP(ctx); 154 | return INT2NUM(sqlite3_backup_remaining(ctx->p)); 155 | } 156 | 157 | /* call-seq: SQLite3::Backup#pagecount 158 | * 159 | * Returns the total number of pages in the source database file. 160 | * 161 | * Note that the value is only updated after step() is called, 162 | * so before calling step() returned value is invalid. 163 | */ 164 | static VALUE 165 | pagecount(VALUE self) 166 | { 167 | sqlite3BackupRubyPtr ctx; 168 | 169 | TypedData_Get_Struct(self, sqlite3BackupRuby, &backup_type, ctx); 170 | REQUIRE_OPEN_BACKUP(ctx); 171 | return INT2NUM(sqlite3_backup_pagecount(ctx->p)); 172 | } 173 | 174 | void 175 | init_sqlite3_backup(void) 176 | { 177 | #if 0 178 | VALUE mSqlite3 = rb_define_module("SQLite3"); 179 | #endif 180 | cSqlite3Backup = rb_define_class_under(mSqlite3, "Backup", rb_cObject); 181 | 182 | rb_define_alloc_func(cSqlite3Backup, allocate); 183 | rb_define_method(cSqlite3Backup, "initialize", initialize, 4); 184 | rb_define_method(cSqlite3Backup, "step", step, 1); 185 | rb_define_method(cSqlite3Backup, "finish", finish, 0); 186 | rb_define_method(cSqlite3Backup, "remaining", remaining, 0); 187 | rb_define_method(cSqlite3Backup, "pagecount", pagecount, 0); 188 | } 189 | 190 | #endif 191 | -------------------------------------------------------------------------------- /ext/sqlite3/backup.h: -------------------------------------------------------------------------------- 1 | #if !defined(SQLITE3_BACKUP_RUBY) && defined(HAVE_SQLITE3_BACKUP_INIT) 2 | #define SQLITE3_BACKUP_RUBY 3 | 4 | #include 5 | 6 | struct _sqlite3BackupRuby { 7 | sqlite3_backup *p; 8 | }; 9 | 10 | typedef struct _sqlite3BackupRuby sqlite3BackupRuby; 11 | typedef sqlite3BackupRuby *sqlite3BackupRubyPtr; 12 | 13 | void init_sqlite3_backup(); 14 | 15 | #endif 16 | -------------------------------------------------------------------------------- /ext/sqlite3/database.h: -------------------------------------------------------------------------------- 1 | #ifndef SQLITE3_DATABASE_RUBY 2 | #define SQLITE3_DATABASE_RUBY 3 | 4 | #include 5 | 6 | /* bits in the `flags` field */ 7 | #define SQLITE3_RB_DATABASE_READONLY 0x01 8 | #define SQLITE3_RB_DATABASE_DISCARDED 0x02 9 | 10 | struct _sqlite3Ruby { 11 | sqlite3 *db; 12 | VALUE busy_handler; 13 | int stmt_timeout; 14 | struct timespec stmt_deadline; 15 | rb_pid_t owner; 16 | int flags; 17 | }; 18 | 19 | typedef struct _sqlite3Ruby sqlite3Ruby; 20 | typedef sqlite3Ruby *sqlite3RubyPtr; 21 | 22 | void init_sqlite3_database(); 23 | void set_sqlite3_func_result(sqlite3_context *ctx, VALUE result); 24 | 25 | sqlite3RubyPtr sqlite3_database_unwrap(VALUE database); 26 | VALUE sqlite3val2rb(sqlite3_value *val); 27 | 28 | #endif 29 | -------------------------------------------------------------------------------- /ext/sqlite3/exception.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | static VALUE 4 | status2klass(int status) 5 | { 6 | /* Consider only lower 8 bits, to work correctly when 7 | extended result codes are enabled. */ 8 | switch (status & 0xff) { 9 | case SQLITE_OK: 10 | return Qnil; 11 | case SQLITE_ERROR: 12 | return rb_path2class("SQLite3::SQLException"); 13 | case SQLITE_INTERNAL: 14 | return rb_path2class("SQLite3::InternalException"); 15 | case SQLITE_PERM: 16 | return rb_path2class("SQLite3::PermissionException"); 17 | case SQLITE_ABORT: 18 | return rb_path2class("SQLite3::AbortException"); 19 | case SQLITE_BUSY: 20 | return rb_path2class("SQLite3::BusyException"); 21 | case SQLITE_LOCKED: 22 | return rb_path2class("SQLite3::LockedException"); 23 | case SQLITE_NOMEM: 24 | return rb_path2class("SQLite3::MemoryException"); 25 | case SQLITE_READONLY: 26 | return rb_path2class("SQLite3::ReadOnlyException"); 27 | case SQLITE_INTERRUPT: 28 | return rb_path2class("SQLite3::InterruptException"); 29 | case SQLITE_IOERR: 30 | return rb_path2class("SQLite3::IOException"); 31 | case SQLITE_CORRUPT: 32 | return rb_path2class("SQLite3::CorruptException"); 33 | case SQLITE_NOTFOUND: 34 | return rb_path2class("SQLite3::NotFoundException"); 35 | case SQLITE_FULL: 36 | return rb_path2class("SQLite3::FullException"); 37 | case SQLITE_CANTOPEN: 38 | return rb_path2class("SQLite3::CantOpenException"); 39 | case SQLITE_PROTOCOL: 40 | return rb_path2class("SQLite3::ProtocolException"); 41 | case SQLITE_EMPTY: 42 | return rb_path2class("SQLite3::EmptyException"); 43 | case SQLITE_SCHEMA: 44 | return rb_path2class("SQLite3::SchemaChangedException"); 45 | case SQLITE_TOOBIG: 46 | return rb_path2class("SQLite3::TooBigException"); 47 | case SQLITE_CONSTRAINT: 48 | return rb_path2class("SQLite3::ConstraintException"); 49 | case SQLITE_MISMATCH: 50 | return rb_path2class("SQLite3::MismatchException"); 51 | case SQLITE_MISUSE: 52 | return rb_path2class("SQLite3::MisuseException"); 53 | case SQLITE_NOLFS: 54 | return rb_path2class("SQLite3::UnsupportedException"); 55 | case SQLITE_AUTH: 56 | return rb_path2class("SQLite3::AuthorizationException"); 57 | case SQLITE_FORMAT: 58 | return rb_path2class("SQLite3::FormatException"); 59 | case SQLITE_RANGE: 60 | return rb_path2class("SQLite3::RangeException"); 61 | case SQLITE_NOTADB: 62 | return rb_path2class("SQLite3::NotADatabaseException"); 63 | default: 64 | return rb_path2class("SQLite3::Exception"); 65 | } 66 | } 67 | 68 | void 69 | rb_sqlite3_raise(sqlite3 *db, int status) 70 | { 71 | VALUE klass = status2klass(status); 72 | if (NIL_P(klass)) { 73 | return; 74 | } 75 | 76 | VALUE exception = rb_exc_new2(klass, sqlite3_errmsg(db)); 77 | rb_iv_set(exception, "@code", INT2FIX(status)); 78 | 79 | rb_exc_raise(exception); 80 | } 81 | 82 | /* 83 | * accepts a sqlite3 error message as the final argument, which will be `sqlite3_free`d 84 | */ 85 | void 86 | rb_sqlite3_raise_msg(sqlite3 *db, int status, const char *msg) 87 | { 88 | VALUE klass = status2klass(status); 89 | if (NIL_P(klass)) { 90 | return; 91 | } 92 | 93 | VALUE exception = rb_exc_new2(klass, msg); 94 | rb_iv_set(exception, "@code", INT2FIX(status)); 95 | sqlite3_free((void *)msg); 96 | 97 | rb_exc_raise(exception); 98 | } 99 | 100 | void 101 | rb_sqlite3_raise_with_sql(sqlite3 *db, int status, const char *sql) 102 | { 103 | VALUE klass = status2klass(status); 104 | if (NIL_P(klass)) { 105 | return; 106 | } 107 | 108 | const char *error_msg = sqlite3_errmsg(db); 109 | int error_offset = -1; 110 | #ifdef HAVE_SQLITE3_ERROR_OFFSET 111 | error_offset = sqlite3_error_offset(db); 112 | #endif 113 | 114 | VALUE exception = rb_exc_new2(klass, error_msg); 115 | rb_iv_set(exception, "@code", INT2FIX(status)); 116 | if (sql) { 117 | rb_iv_set(exception, "@sql", rb_str_new2(sql)); 118 | rb_iv_set(exception, "@sql_offset", INT2FIX(error_offset)); 119 | } 120 | 121 | rb_exc_raise(exception); 122 | } 123 | -------------------------------------------------------------------------------- /ext/sqlite3/exception.h: -------------------------------------------------------------------------------- 1 | #ifndef SQLITE3_EXCEPTION_RUBY 2 | #define SQLITE3_EXCEPTION_RUBY 3 | 4 | #define CHECK(_db, _status) rb_sqlite3_raise(_db, _status); 5 | #define CHECK_MSG(_db, _status, _msg) rb_sqlite3_raise_msg(_db, _status, _msg); 6 | #define CHECK_PREPARE(_db, _status, _sql) rb_sqlite3_raise_with_sql(_db, _status, _sql) 7 | 8 | void rb_sqlite3_raise(sqlite3 *db, int status); 9 | void rb_sqlite3_raise_msg(sqlite3 *db, int status, const char *msg); 10 | void rb_sqlite3_raise_with_sql(sqlite3 *db, int status, const char *sql); 11 | 12 | #endif 13 | -------------------------------------------------------------------------------- /ext/sqlite3/extconf.rb: -------------------------------------------------------------------------------- 1 | require "mkmf" 2 | require "yaml" 3 | 4 | module Sqlite3 5 | module ExtConf 6 | ENV_ALLOWLIST = ["CC", "CFLAGS", "LDFLAGS", "LIBS", "CPPFLAGS", "LT_SYS_LIBRARY_PATH", "CPP"] 7 | 8 | class << self 9 | def configure 10 | configure_cross_compiler 11 | 12 | if system_libraries? 13 | message "Building sqlite3-ruby using system #{libname}.\n" 14 | configure_system_libraries 15 | else 16 | message "Building sqlite3-ruby using packaged sqlite3.\n" 17 | configure_packaged_libraries 18 | end 19 | 20 | configure_extension 21 | 22 | create_makefile("sqlite3/sqlite3_native") 23 | end 24 | 25 | def configure_cross_compiler 26 | RbConfig::CONFIG["CC"] = RbConfig::MAKEFILE_CONFIG["CC"] = ENV["CC"] if ENV["CC"] 27 | ENV["CC"] = RbConfig::CONFIG["CC"] 28 | end 29 | 30 | def system_libraries? 31 | sqlcipher? || enable_config("system-libraries") 32 | end 33 | 34 | def libname 35 | sqlcipher? ? "sqlcipher" : "sqlite3" 36 | end 37 | 38 | def sqlcipher? 39 | with_config("sqlcipher") || 40 | with_config("sqlcipher-dir") || 41 | with_config("sqlcipher-include") || 42 | with_config("sqlcipher-lib") 43 | end 44 | 45 | def configure_system_libraries 46 | pkg_config(libname) 47 | append_cppflags("-DUSING_SQLCIPHER") if sqlcipher? 48 | end 49 | 50 | def configure_packaged_libraries 51 | minimal_recipe.tap do |recipe| 52 | recipe.configure_options += [ 53 | "--disable-shared", 54 | "--enable-static", 55 | "--enable-fts5" 56 | ] 57 | ENV.to_h.tap do |env| 58 | user_cflags = with_config("sqlite-cflags") 59 | more_cflags = [ 60 | "-fPIC", # needed for linking the static library into a shared library 61 | "-O2", # see https://github.com/sparklemotion/sqlite3-ruby/issues/335 for some benchmarks 62 | "-fvisibility=hidden", # see https://github.com/rake-compiler/rake-compiler-dock/issues/87 63 | "-DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1", 64 | "-DSQLITE_USE_URI=1", 65 | "-DSQLITE_ENABLE_DBPAGE_VTAB=1", 66 | "-DSQLITE_ENABLE_DBSTAT_VTAB=1" 67 | ] 68 | env["CFLAGS"] = [user_cflags, env["CFLAGS"], more_cflags].flatten.join(" ") 69 | recipe.configure_options += env.select { |k, v| ENV_ALLOWLIST.include?(k) } 70 | .map { |key, value| "#{key}=#{value.strip}" } 71 | end 72 | 73 | unless File.exist?(File.join(recipe.target, recipe.host, recipe.name, recipe.version)) 74 | recipe.cook 75 | end 76 | recipe.activate 77 | 78 | # on macos, pkg-config will not return --cflags without this 79 | ENV["PKG_CONFIG_ALLOW_SYSTEM_CFLAGS"] = "t" 80 | 81 | # only needed for Ruby 3.1.3, see https://bugs.ruby-lang.org/issues/19233 82 | RbConfig::CONFIG["PKG_CONFIG"] = config_string("PKG_CONFIG") || "pkg-config" 83 | 84 | lib_path = File.join(recipe.path, "lib") 85 | pcfile = File.join(lib_path, "pkgconfig", "sqlite3.pc") 86 | abort_pkg_config("pkg_config") unless pkg_config(pcfile) 87 | 88 | # see https://bugs.ruby-lang.org/issues/18490 89 | ldflags = xpopen(["pkg-config", "--libs", "--static", pcfile], err: [:child, :out], &:read) 90 | abort_pkg_config("xpopen") unless $?.success? 91 | ldflags = ldflags.split 92 | 93 | # see https://github.com/flavorjones/mini_portile/issues/118 94 | "-L#{lib_path}".tap do |lib_path_flag| 95 | ldflags.prepend(lib_path_flag) unless ldflags.include?(lib_path_flag) 96 | end 97 | 98 | ldflags.each { |ldflag| append_ldflags(ldflag) } 99 | 100 | append_cppflags("-DUSING_PACKAGED_LIBRARIES") 101 | append_cppflags("-DUSING_PRECOMPILED_LIBRARIES") if cross_build? 102 | end 103 | end 104 | 105 | def configure_extension 106 | append_cflags("-fvisibility=hidden") # see https://github.com/rake-compiler/rake-compiler-dock/issues/87 107 | 108 | if find_header("sqlite3.h") 109 | # noop 110 | elsif sqlcipher? && find_header("sqlcipher/sqlite3.h") 111 | append_cppflags("-DUSING_SQLCIPHER_INC_SUBDIR") 112 | else 113 | abort_could_not_find("sqlite3.h") 114 | end 115 | 116 | abort_could_not_find(libname) unless find_library(libname, "sqlite3_libversion_number", "sqlite3.h") 117 | 118 | # Truffle Ruby doesn't support this yet: 119 | # https://github.com/oracle/truffleruby/issues/3408 120 | have_func("rb_enc_interned_str_cstr") 121 | 122 | # Functions defined in 1.9 but not 1.8 123 | have_func("rb_proc_arity") 124 | 125 | # Functions defined in 2.1 but not 2.0 126 | have_func("rb_integer_pack") 127 | 128 | # These functions may not be defined 129 | have_func("sqlite3_initialize") 130 | have_func("sqlite3_backup_init") 131 | have_func("sqlite3_column_database_name") 132 | have_func("sqlite3_enable_load_extension") 133 | have_func("sqlite3_load_extension") 134 | 135 | unless have_func("sqlite3_open_v2") # https://www.sqlite.org/releaselog/3_5_0.html 136 | abort("\nPlease use a version of SQLite3 >= 3.5.0\n\n") 137 | end 138 | 139 | have_func("sqlite3_prepare_v2") 140 | have_func("sqlite3_db_name", "sqlite3.h") # v3.39.0 141 | have_func("sqlite3_error_offset", "sqlite3.h") # v3.38.0 142 | 143 | have_type("sqlite3_int64", "sqlite3.h") 144 | have_type("sqlite3_uint64", "sqlite3.h") 145 | end 146 | 147 | def minimal_recipe 148 | require "mini_portile2" 149 | 150 | MiniPortile.new(libname, sqlite3_config[:version]).tap do |recipe| 151 | if sqlite_source_dir 152 | recipe.source_directory = sqlite_source_dir 153 | else 154 | recipe.files = sqlite3_config[:files] 155 | recipe.target = File.join(package_root_dir, "ports") 156 | recipe.patch_files = Dir[File.join(package_root_dir, "patches", "*.patch")].sort 157 | end 158 | end 159 | end 160 | 161 | def package_root_dir 162 | File.expand_path(File.join(File.dirname(__FILE__), "..", "..")) 163 | end 164 | 165 | def sqlite3_config 166 | mini_portile_config[:sqlite3] 167 | end 168 | 169 | def mini_portile_config 170 | YAML.load_file(File.join(package_root_dir, "dependencies.yml"), symbolize_names: true) 171 | end 172 | 173 | def abort_could_not_find(missing) 174 | abort("\nCould not find #{missing}.\nPlease visit https://github.com/sparklemotion/sqlite3-ruby for installation instructions.\n\n") 175 | end 176 | 177 | def abort_pkg_config(id) 178 | abort("\nCould not configure the build properly (#{id}). Please install the `pkg-config` utility.\n\n") 179 | end 180 | 181 | def cross_build? 182 | enable_config("cross-build") 183 | end 184 | 185 | def sqlite_source_dir 186 | arg_config("--with-sqlite-source-dir") 187 | end 188 | 189 | def download 190 | minimal_recipe.download 191 | end 192 | 193 | def darwin? 194 | RbConfig::CONFIG["target_os"].include?("darwin") 195 | end 196 | 197 | def print_help 198 | print(<<~TEXT) 199 | USAGE: ruby #{$PROGRAM_NAME} [options] 200 | 201 | Flags that are always valid: 202 | 203 | --disable-system-libraries 204 | Use the packaged libraries, and ignore the system libraries. 205 | (This is the default behavior.) 206 | 207 | --enable-system-libraries 208 | Use system libraries instead of building and using the packaged libraries. 209 | 210 | --with-sqlcipher 211 | Use libsqlcipher instead of libsqlite3. 212 | (Implies `--enable-system-libraries`.) 213 | 214 | --with-sqlite-source-dir=DIRECTORY 215 | (dev only) Build sqlite from the source code in DIRECTORY 216 | 217 | --help 218 | Display this message. 219 | 220 | 221 | Flags only used when using system libraries: 222 | 223 | General (applying to all system libraries): 224 | 225 | --with-opt-dir=DIRECTORY 226 | Look for headers and libraries in DIRECTORY. 227 | 228 | --with-opt-lib=DIRECTORY 229 | Look for libraries in DIRECTORY. 230 | 231 | --with-opt-include=DIRECTORY 232 | Look for headers in DIRECTORY. 233 | 234 | Related to sqlcipher: 235 | 236 | --with-sqlcipher-dir=DIRECTORY 237 | Look for sqlcipher headers and library in DIRECTORY. 238 | (Implies `--with-sqlcipher` and `--enable-system-libraries`.) 239 | 240 | --with-sqlcipher-lib=DIRECTORY 241 | Look for sqlcipher library in DIRECTORY. 242 | (Implies `--with-sqlcipher` and `--enable-system-libraries`.) 243 | 244 | --with-sqlcipher-include=DIRECTORY 245 | Look for sqlcipher headers in DIRECTORY. 246 | (Implies `--with-sqlcipher` and `--enable-system-libraries`.) 247 | 248 | 249 | Flags only used when building and using the packaged libraries: 250 | 251 | --with-sqlite-cflags=CFLAGS 252 | Explicitly pass compiler flags to the sqlite library build. These flags will 253 | appear on the commandline before any flags set in the CFLAGS environment 254 | variable. This is useful for setting compilation options in your project's 255 | bundler config. See INSTALLATION.md for more information. 256 | 257 | --enable-cross-build 258 | Enable cross-build mode. (You probably do not want to set this manually.) 259 | 260 | 261 | Environment variables used for compiling the gem's C extension: 262 | 263 | CC 264 | Use this path to invoke the compiler instead of `RbConfig::CONFIG['CC']` 265 | 266 | 267 | Environment variables passed through to the compilation of sqlite: 268 | 269 | CC 270 | CPPFLAGS 271 | CFLAGS 272 | LDFLAGS 273 | LIBS 274 | LT_SYS_LIBRARY_PATH 275 | CPP 276 | 277 | TEXT 278 | end 279 | end 280 | end 281 | end 282 | 283 | if arg_config("--help") 284 | Sqlite3::ExtConf.print_help 285 | exit!(0) 286 | end 287 | 288 | if arg_config("--download-dependencies") 289 | Sqlite3::ExtConf.download 290 | exit!(0) 291 | end 292 | 293 | Sqlite3::ExtConf.configure 294 | -------------------------------------------------------------------------------- /ext/sqlite3/sqlite3.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | VALUE mSqlite3; 4 | VALUE cSqlite3Blob; 5 | 6 | int 7 | bignum_to_int64(VALUE value, sqlite3_int64 *result) 8 | { 9 | #ifdef HAVE_RB_INTEGER_PACK 10 | const int nails = 0; 11 | int t = rb_integer_pack(value, result, 1, sizeof(*result), nails, 12 | INTEGER_PACK_NATIVE_BYTE_ORDER | 13 | INTEGER_PACK_2COMP); 14 | switch (t) { 15 | case -2: 16 | case +2: 17 | return 0; 18 | case +1: 19 | if (!nails) { 20 | if (*result < 0) { return 0; } 21 | } 22 | break; 23 | case -1: 24 | if (!nails) { 25 | if (*result >= 0) { return 0; } 26 | } else { 27 | *result += INT64_MIN; 28 | } 29 | break; 30 | } 31 | return 1; 32 | #else 33 | # ifndef RBIGNUM_LEN 34 | # define RBIGNUM_LEN(x) RBIGNUM(x)->len 35 | # endif 36 | const long len = RBIGNUM_LEN(value); 37 | if (len == 0) { 38 | *result = 0; 39 | return 1; 40 | } 41 | if (len > 63 / (SIZEOF_BDIGITS * CHAR_BIT) + 1) { return 0; } 42 | if (len == 63 / (SIZEOF_BDIGITS * CHAR_BIT) + 1) { 43 | const BDIGIT *digits = RBIGNUM_DIGITS(value); 44 | BDIGIT blast = digits[len - 1]; 45 | BDIGIT bmax = (BDIGIT)1UL << (63 % (CHAR_BIT * SIZEOF_BDIGITS)); 46 | if (blast > bmax) { return 0; } 47 | if (blast == bmax) { 48 | if (RBIGNUM_POSITIVE_P(value)) { 49 | return 0; 50 | } else { 51 | long i = len - 1; 52 | while (i) { 53 | if (digits[--i]) { return 0; } 54 | } 55 | } 56 | } 57 | } 58 | *result = (sqlite3_int64)NUM2LL(value); 59 | return 1; 60 | #endif 61 | } 62 | 63 | static VALUE 64 | libversion(VALUE UNUSED(klass)) 65 | { 66 | return INT2NUM(sqlite3_libversion_number()); 67 | } 68 | 69 | static VALUE 70 | using_sqlcipher(VALUE UNUSED(klass)) 71 | { 72 | #ifdef USING_SQLCIPHER 73 | return Qtrue; 74 | #else 75 | return Qfalse; 76 | #endif 77 | } 78 | 79 | /* Returns the compile time setting of the SQLITE_THREADSAFE flag. 80 | * See: https://www.sqlite.org/c3ref/threadsafe.html 81 | */ 82 | static VALUE 83 | threadsafe_p(VALUE UNUSED(klass)) 84 | { 85 | return INT2NUM(sqlite3_threadsafe()); 86 | } 87 | 88 | /* 89 | * call-seq: 90 | * status(parameter) → Hash 91 | * status(parameter, reset_flag = false) → Hash 92 | * 93 | * Queries the SQLite3 library for run-time status information. Passing a truthy +reset_flag+ will 94 | * reset the highwater mark to the current value. 95 | * 96 | * [Parameters] 97 | * - +parameter+ (Integer, SQLite3::Constants::Status): The status parameter to query. 98 | * - +reset_flag+ (Boolean): Whether to reset the highwater mark. (default is +false+) 99 | * 100 | * [Returns] 101 | * A Hash containing +:current+ and +:highwater+ keys for integer values. 102 | */ 103 | static VALUE 104 | rb_sqlite3_status(int argc, VALUE *argv, VALUE klass) 105 | { 106 | VALUE opArg, resetFlagArg; 107 | 108 | rb_scan_args(argc, argv, "11", &opArg, &resetFlagArg); 109 | 110 | int op = NUM2INT(opArg); 111 | bool resetFlag = RTEST(resetFlagArg); 112 | 113 | int pCurrent = 0; 114 | int pHighwater = 0; 115 | sqlite3_status(op, &pCurrent, &pHighwater, resetFlag); 116 | 117 | VALUE hash = rb_hash_new(); 118 | rb_hash_aset(hash, ID2SYM(rb_intern("current")), INT2FIX(pCurrent)); 119 | rb_hash_aset(hash, ID2SYM(rb_intern("highwater")), INT2FIX(pHighwater)); 120 | 121 | return hash; 122 | } 123 | 124 | void 125 | init_sqlite3_constants(void) 126 | { 127 | VALUE mSqlite3Constants; 128 | VALUE mSqlite3Open; 129 | 130 | mSqlite3Constants = rb_define_module_under(mSqlite3, "Constants"); 131 | 132 | /* sqlite3_open_v2 flags for Database::new */ 133 | mSqlite3Open = rb_define_module_under(mSqlite3Constants, "Open"); 134 | 135 | /* symbols = IO.readlines('sqlite3.h').map { |n| /\A#define\s+(SQLITE_OPEN_\w+)\s/ =~ n && $1 }.compact 136 | * pad = symbols.map(&:length).max - 9 137 | * symbols.each { |s| printf %Q{ rb_define_const(mSqlite3Open, %-#{pad}s INT2FIX(#{s}));\n}, '"' + s[12..-1] + '",' } 138 | */ 139 | rb_define_const(mSqlite3Open, "READONLY", INT2FIX(SQLITE_OPEN_READONLY)); 140 | rb_define_const(mSqlite3Open, "READWRITE", INT2FIX(SQLITE_OPEN_READWRITE)); 141 | rb_define_const(mSqlite3Open, "CREATE", INT2FIX(SQLITE_OPEN_CREATE)); 142 | rb_define_const(mSqlite3Open, "DELETEONCLOSE", INT2FIX(SQLITE_OPEN_DELETEONCLOSE)); 143 | rb_define_const(mSqlite3Open, "EXCLUSIVE", INT2FIX(SQLITE_OPEN_EXCLUSIVE)); 144 | rb_define_const(mSqlite3Open, "MAIN_DB", INT2FIX(SQLITE_OPEN_MAIN_DB)); 145 | rb_define_const(mSqlite3Open, "TEMP_DB", INT2FIX(SQLITE_OPEN_TEMP_DB)); 146 | rb_define_const(mSqlite3Open, "TRANSIENT_DB", INT2FIX(SQLITE_OPEN_TRANSIENT_DB)); 147 | rb_define_const(mSqlite3Open, "MAIN_JOURNAL", INT2FIX(SQLITE_OPEN_MAIN_JOURNAL)); 148 | rb_define_const(mSqlite3Open, "TEMP_JOURNAL", INT2FIX(SQLITE_OPEN_TEMP_JOURNAL)); 149 | rb_define_const(mSqlite3Open, "SUBJOURNAL", INT2FIX(SQLITE_OPEN_SUBJOURNAL)); 150 | rb_define_const(mSqlite3Open, "MASTER_JOURNAL", 151 | INT2FIX(SQLITE_OPEN_MASTER_JOURNAL)); /* pre-3.33.0 */ 152 | rb_define_const(mSqlite3Open, "SUPER_JOURNAL", INT2FIX(SQLITE_OPEN_MASTER_JOURNAL)); 153 | rb_define_const(mSqlite3Open, "NOMUTEX", INT2FIX(SQLITE_OPEN_NOMUTEX)); 154 | rb_define_const(mSqlite3Open, "FULLMUTEX", INT2FIX(SQLITE_OPEN_FULLMUTEX)); 155 | #ifdef SQLITE_OPEN_AUTOPROXY 156 | /* SQLITE_VERSION_NUMBER>=3007002 */ 157 | rb_define_const(mSqlite3Open, "AUTOPROXY", INT2FIX(SQLITE_OPEN_AUTOPROXY)); 158 | rb_define_const(mSqlite3Open, "SHAREDCACHE", INT2FIX(SQLITE_OPEN_SHAREDCACHE)); 159 | rb_define_const(mSqlite3Open, "PRIVATECACHE", INT2FIX(SQLITE_OPEN_PRIVATECACHE)); 160 | rb_define_const(mSqlite3Open, "WAL", INT2FIX(SQLITE_OPEN_WAL)); 161 | #endif 162 | #ifdef SQLITE_OPEN_URI 163 | /* SQLITE_VERSION_NUMBER>=3007007 */ 164 | rb_define_const(mSqlite3Open, "URI", INT2FIX(SQLITE_OPEN_URI)); 165 | #endif 166 | #ifdef SQLITE_OPEN_MEMORY 167 | /* SQLITE_VERSION_NUMBER>=3007013 */ 168 | rb_define_const(mSqlite3Open, "MEMORY", INT2FIX(SQLITE_OPEN_MEMORY)); 169 | #endif 170 | } 171 | 172 | RUBY_FUNC_EXPORTED 173 | void 174 | Init_sqlite3_native(void) 175 | { 176 | /* 177 | * SQLite3 is a wrapper around the popular database 178 | * sqlite[http://sqlite.org]. 179 | * 180 | * For an example of usage, see SQLite3::Database. 181 | */ 182 | mSqlite3 = rb_define_module("SQLite3"); 183 | 184 | /* A class for differentiating between strings and blobs, when binding them 185 | * into statements. 186 | */ 187 | cSqlite3Blob = rb_define_class_under(mSqlite3, "Blob", rb_cString); 188 | 189 | /* Initialize the sqlite3 library */ 190 | #ifdef HAVE_SQLITE3_INITIALIZE 191 | sqlite3_initialize(); 192 | #endif 193 | 194 | init_sqlite3_constants(); 195 | init_sqlite3_database(); 196 | init_sqlite3_statement(); 197 | #ifdef HAVE_SQLITE3_BACKUP_INIT 198 | init_sqlite3_backup(); 199 | #endif 200 | rb_define_singleton_method(mSqlite3, "sqlcipher?", using_sqlcipher, 0); 201 | rb_define_singleton_method(mSqlite3, "libversion", libversion, 0); 202 | rb_define_singleton_method(mSqlite3, "threadsafe", threadsafe_p, 0); 203 | rb_define_singleton_method(mSqlite3, "status", rb_sqlite3_status, -1); 204 | 205 | /* (String) The version of the sqlite3 library compiled with (e.g., "3.46.1") */ 206 | rb_define_const(mSqlite3, "SQLITE_VERSION", rb_str_new2(SQLITE_VERSION)); 207 | 208 | /* (Integer) The version of the sqlite3 library compiled with (e.g., 346001) */ 209 | rb_define_const(mSqlite3, "SQLITE_VERSION_NUMBER", INT2FIX(SQLITE_VERSION_NUMBER)); 210 | 211 | /* (String) The version of the sqlite3 library loaded at runtime (e.g., "3.46.1") */ 212 | rb_define_const(mSqlite3, "SQLITE_LOADED_VERSION", rb_str_new2(sqlite3_libversion())); 213 | 214 | #ifdef USING_PACKAGED_LIBRARIES 215 | rb_define_const(mSqlite3, "SQLITE_PACKAGED_LIBRARIES", Qtrue); 216 | #else 217 | rb_define_const(mSqlite3, "SQLITE_PACKAGED_LIBRARIES", Qfalse); 218 | #endif 219 | 220 | #ifdef USING_PRECOMPILED_LIBRARIES 221 | rb_define_const(mSqlite3, "SQLITE_PRECOMPILED_LIBRARIES", Qtrue); 222 | #else 223 | rb_define_const(mSqlite3, "SQLITE_PRECOMPILED_LIBRARIES", Qfalse); 224 | #endif 225 | } 226 | -------------------------------------------------------------------------------- /ext/sqlite3/sqlite3_ruby.h: -------------------------------------------------------------------------------- 1 | #ifndef SQLITE3_RUBY 2 | #define SQLITE3_RUBY 3 | 4 | #include 5 | 6 | #ifdef UNUSED 7 | #elif defined(__GNUC__) 8 | # define UNUSED(x) UNUSED_ ## x __attribute__((unused)) 9 | #elif defined(__LCLINT__) 10 | # define UNUSED(x) /*@unused@*/ x 11 | #else 12 | # define UNUSED(x) x 13 | #endif 14 | 15 | #include 16 | 17 | #define USASCII_P(_obj) (rb_enc_get_index(_obj) == rb_usascii_encindex()) 18 | #define UTF8_P(_obj) (rb_enc_get_index(_obj) == rb_utf8_encindex()) 19 | #define UTF16_LE_P(_obj) (rb_enc_get_index(_obj) == rb_enc_find_index("UTF-16LE")) 20 | #define UTF16_BE_P(_obj) (rb_enc_get_index(_obj) == rb_enc_find_index("UTF-16BE")) 21 | #define SQLITE3_UTF8_STR_NEW2(_obj) (rb_utf8_str_new_cstr(_obj)) 22 | 23 | #ifdef USING_SQLCIPHER_INC_SUBDIR 24 | # include 25 | #else 26 | # include 27 | #endif 28 | 29 | #ifndef HAVE_TYPE_SQLITE3_INT64 30 | typedef sqlite_int64 sqlite3_int64; 31 | #endif 32 | 33 | #ifndef HAVE_TYPE_SQLITE3_UINT64 34 | typedef sqlite_uint64 sqlite3_uint64; 35 | #endif 36 | 37 | extern VALUE mSqlite3; 38 | extern VALUE cSqlite3Blob; 39 | 40 | #include 41 | #include 42 | #include 43 | #include 44 | #include 45 | 46 | int bignum_to_int64(VALUE big, sqlite3_int64 *result); 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /ext/sqlite3/statement.h: -------------------------------------------------------------------------------- 1 | #ifndef SQLITE3_STATEMENT_RUBY 2 | #define SQLITE3_STATEMENT_RUBY 3 | 4 | #include 5 | 6 | struct _sqlite3StmtRuby { 7 | sqlite3_stmt *st; 8 | sqlite3Ruby *db; 9 | int done_p; 10 | }; 11 | 12 | typedef struct _sqlite3StmtRuby sqlite3StmtRuby; 13 | typedef sqlite3StmtRuby *sqlite3StmtRubyPtr; 14 | 15 | void init_sqlite3_statement(); 16 | 17 | #endif 18 | -------------------------------------------------------------------------------- /ext/sqlite3/timespec.h: -------------------------------------------------------------------------------- 1 | #define timespecclear(tsp) (tsp)->tv_sec = (tsp)->tv_nsec = 0 2 | #define timespecisset(tsp) ((tsp)->tv_sec || (tsp)->tv_nsec) 3 | #define timespecisvalid(tsp) \ 4 | ((tsp)->tv_nsec >= 0 && (tsp)->tv_nsec < 1000000000L) 5 | #define timespeccmp(tsp, usp, cmp) \ 6 | (((tsp)->tv_sec == (usp)->tv_sec) ? \ 7 | ((tsp)->tv_nsec cmp (usp)->tv_nsec) : \ 8 | ((tsp)->tv_sec cmp (usp)->tv_sec)) 9 | #define timespecsub(tsp, usp, vsp) \ 10 | do { \ 11 | (vsp)->tv_sec = (tsp)->tv_sec - (usp)->tv_sec; \ 12 | (vsp)->tv_nsec = (tsp)->tv_nsec - (usp)->tv_nsec; \ 13 | if ((vsp)->tv_nsec < 0) { \ 14 | (vsp)->tv_sec--; \ 15 | (vsp)->tv_nsec += 1000000000L; \ 16 | } \ 17 | } while (0) 18 | #define timespecafter(tsp, usp) \ 19 | (((tsp)->tv_sec > (usp)->tv_sec) || \ 20 | ((tsp)->tv_sec == (usp)->tv_sec && (tsp)->tv_nsec > (usp)->tv_nsec)) 21 | -------------------------------------------------------------------------------- /lib/sqlite3.rb: -------------------------------------------------------------------------------- 1 | # support multiple ruby version (fat binaries under windows) 2 | begin 3 | RUBY_VERSION =~ /(\d+\.\d+)/ 4 | require "sqlite3/#{$1}/sqlite3_native" 5 | rescue LoadError 6 | require "sqlite3/sqlite3_native" 7 | end 8 | 9 | require "sqlite3/database" 10 | require "sqlite3/version" 11 | 12 | module SQLite3 13 | # Was sqlite3 compiled with thread safety on? 14 | def self.threadsafe? 15 | threadsafe > 0 16 | end 17 | end 18 | 19 | require "sqlite3/version_info" 20 | -------------------------------------------------------------------------------- /lib/sqlite3/constants.rb: -------------------------------------------------------------------------------- 1 | module SQLite3 2 | module Constants 3 | # 4 | # CAPI3REF: Text Encodings 5 | # 6 | # These constant define integer codes that represent the various 7 | # text encodings supported by SQLite. 8 | # 9 | module TextRep 10 | # IMP: R-37514-35566 11 | UTF8 = 1 12 | # IMP: R-03371-37637 13 | UTF16LE = 2 14 | # IMP: R-51971-34154 15 | UTF16BE = 3 16 | # Use native byte order 17 | UTF16 = 4 18 | # Deprecated 19 | ANY = 5 20 | # sqlite3_create_collation only 21 | DETERMINISTIC = 0x800 22 | end 23 | 24 | # 25 | # CAPI3REF: Fundamental Datatypes 26 | # 27 | # ^(Every value in SQLite has one of five fundamental datatypes: 28 | # 29 | #
    30 | #
  • 64-bit signed integer 31 | #
  • 64-bit IEEE floating point number 32 | #
  • string 33 | #
  • BLOB 34 | #
  • NULL 35 | #
)^ 36 | # 37 | # These constants are codes for each of those types. 38 | # 39 | module ColumnType 40 | INTEGER = 1 41 | FLOAT = 2 42 | TEXT = 3 43 | BLOB = 4 44 | NULL = 5 45 | end 46 | 47 | # 48 | # CAPI3REF: Result Codes 49 | # 50 | # Many SQLite functions return an integer result code from the set shown 51 | # here in order to indicate success or failure. 52 | # 53 | # New error codes may be added in future versions of SQLite. 54 | # 55 | module ErrorCode 56 | # Successful result 57 | OK = 0 58 | # SQL error or missing database 59 | ERROR = 1 60 | # An internal logic error in SQLite 61 | INTERNAL = 2 62 | # Access permission denied 63 | PERM = 3 64 | # Callback routine requested an abort 65 | ABORT = 4 66 | # The database file is locked 67 | BUSY = 5 68 | # A table in the database is locked 69 | LOCKED = 6 70 | # A malloc() failed 71 | NOMEM = 7 72 | # Attempt to write a readonly database 73 | READONLY = 8 74 | # Operation terminated by sqlite_interrupt() 75 | INTERRUPT = 9 76 | # Some kind of disk I/O error occurred 77 | IOERR = 10 78 | # The database disk image is malformed 79 | CORRUPT = 11 80 | # (Internal Only) Table or record not found 81 | NOTFOUND = 12 82 | # Insertion failed because database is full 83 | FULL = 13 84 | # Unable to open the database file 85 | CANTOPEN = 14 86 | # Database lock protocol error 87 | PROTOCOL = 15 88 | # (Internal Only) Database table is empty 89 | EMPTY = 16 90 | # The database schema changed 91 | SCHEMA = 17 92 | # Too much data for one row of a table 93 | TOOBIG = 18 94 | # Abort due to constraint violation 95 | CONSTRAINT = 19 96 | # Data type mismatch 97 | MISMATCH = 20 98 | # Library used incorrectly 99 | MISUSE = 21 100 | # Uses OS features not supported on host 101 | NOLFS = 22 102 | # Authorization denied 103 | AUTH = 23 104 | # Not used 105 | FORMAT = 24 106 | # 2nd parameter to sqlite3_bind out of range 107 | RANGE = 25 108 | # File opened that is not a database file 109 | NOTADB = 26 110 | # Notifications from sqlite3_log() 111 | NOTICE = 27 112 | # Warnings from sqlite3_log() 113 | WARNING = 28 114 | # sqlite_step() has another row ready 115 | ROW = 100 116 | # sqlite_step() has finished executing 117 | DONE = 101 118 | end 119 | 120 | # 121 | # CAPI3REF: Status Parameters 122 | # 123 | # These integer constants designate various run-time status parameters 124 | # that can be returned by SQLite3.status 125 | # 126 | module Status 127 | # This parameter is the current amount of memory checked out using sqlite3_malloc(), either 128 | # directly or indirectly. The figure includes calls made to sqlite3_malloc() by the 129 | # application and internal memory usage by the SQLite library. Auxiliary page-cache memory 130 | # controlled by SQLITE_CONFIG_PAGECACHE is not included in this parameter. The amount returned 131 | # is the sum of the allocation sizes as reported by the xSize method in sqlite3_mem_methods. 132 | MEMORY_USED = 0 133 | 134 | # This parameter returns the number of pages used out of the pagecache memory allocator that 135 | # was configured using SQLITE_CONFIG_PAGECACHE. The value returned is in pages, not in bytes. 136 | PAGECACHE_USED = 1 137 | 138 | # This parameter returns the number of bytes of page cache allocation which could not be 139 | # satisfied by the SQLITE_CONFIG_PAGECACHE buffer and where forced to overflow to 140 | # sqlite3_malloc(). The returned value includes allocations that overflowed because they where 141 | # too large (they were larger than the "sz" parameter to SQLITE_CONFIG_PAGECACHE) and 142 | # allocations that overflowed because no space was left in the page cache. 143 | PAGECACHE_OVERFLOW = 2 144 | 145 | # NOT USED 146 | SCRATCH_USED = 3 147 | 148 | # NOT USED 149 | SCRATCH_OVERFLOW = 4 150 | 151 | # This parameter records the largest memory allocation request handed to sqlite3_malloc() or 152 | # sqlite3_realloc() (or their internal equivalents). Only the value returned in the 153 | # *pHighwater parameter to sqlite3_status() is of interest. The value written into the 154 | # *pCurrent parameter is undefined. 155 | MALLOC_SIZE = 5 156 | 157 | # The *pHighwater parameter records the deepest parser stack. The *pCurrent value is 158 | # undefined. The *pHighwater value is only meaningful if SQLite is compiled with 159 | # YYTRACKMAXSTACKDEPTH. 160 | PARSER_STACK = 6 161 | 162 | # This parameter records the largest memory allocation request handed to the pagecache memory 163 | # allocator. Only the value returned in the *pHighwater parameter to sqlite3_status() is of 164 | # interest. The value written into the *pCurrent parameter is undefined. 165 | PAGECACHE_SIZE = 7 166 | 167 | # NOT USED 168 | SCRATCH_SIZE = 8 169 | 170 | # This parameter records the number of separate memory allocations currently checked out. 171 | MALLOC_COUNT = 9 172 | end 173 | 174 | module Optimize 175 | # Debugging mode. Do not actually perform any optimizations but instead return one line of 176 | # text for each optimization that would have been done. Off by default. 177 | DEBUG = 0x00001 178 | 179 | # Run ANALYZE on tables that might benefit. On by default. 180 | ANALYZE_TABLES = 0x00002 181 | 182 | # When running ANALYZE, set a temporary PRAGMA analysis_limit to prevent excess run-time. On 183 | # by default. 184 | LIMIT_ANALYZE = 0x00010 185 | 186 | # Check the size of all tables, not just tables that have not been recently used, to see if 187 | # any have grown and shrunk significantly and hence might benefit from being re-analyzed. Off 188 | # by default. 189 | CHECK_ALL_TABLES = 0x10000 190 | 191 | # Useful for adding a bit to the default behavior, for example 192 | # 193 | # db.optimize(Optimize::DEFAULT | Optimize::CHECK_ALL_TABLES) 194 | # 195 | DEFAULT = ANALYZE_TABLES | LIMIT_ANALYZE 196 | end 197 | end 198 | end 199 | -------------------------------------------------------------------------------- /lib/sqlite3/errors.rb: -------------------------------------------------------------------------------- 1 | require "sqlite3/constants" 2 | 3 | module SQLite3 4 | class Exception < ::StandardError 5 | # A convenience for accessing the error code for this exception. 6 | attr_reader :code 7 | 8 | # If the error is associated with a SQL query, this is the query 9 | attr_reader :sql 10 | 11 | # If the error is associated with a particular offset in a SQL query, this is the non-negative 12 | # offset. If the offset is not available, this will be -1. 13 | attr_reader :sql_offset 14 | 15 | def message 16 | [super, sql_error].compact.join(":\n") 17 | end 18 | 19 | private def sql_error 20 | return nil unless @sql 21 | return @sql.chomp unless @sql_offset >= 0 22 | 23 | offset = @sql_offset 24 | sql.lines.flat_map do |line| 25 | if offset >= 0 && line.length > offset 26 | blanks = " " * offset 27 | offset = -1 28 | [line.chomp, blanks + "^"] 29 | else 30 | offset -= line.length if offset 31 | line.chomp 32 | end 33 | end.join("\n") 34 | end 35 | end 36 | 37 | class SQLException < Exception; end 38 | 39 | class InternalException < Exception; end 40 | 41 | class PermissionException < Exception; end 42 | 43 | class AbortException < Exception; end 44 | 45 | class BusyException < Exception; end 46 | 47 | class LockedException < Exception; end 48 | 49 | class MemoryException < Exception; end 50 | 51 | class ReadOnlyException < Exception; end 52 | 53 | class InterruptException < Exception; end 54 | 55 | class IOException < Exception; end 56 | 57 | class CorruptException < Exception; end 58 | 59 | class NotFoundException < Exception; end 60 | 61 | class FullException < Exception; end 62 | 63 | class CantOpenException < Exception; end 64 | 65 | class ProtocolException < Exception; end 66 | 67 | class EmptyException < Exception; end 68 | 69 | class SchemaChangedException < Exception; end 70 | 71 | class TooBigException < Exception; end 72 | 73 | class ConstraintException < Exception; end 74 | 75 | class MismatchException < Exception; end 76 | 77 | class MisuseException < Exception; end 78 | 79 | class UnsupportedException < Exception; end 80 | 81 | class AuthorizationException < Exception; end 82 | 83 | class FormatException < Exception; end 84 | 85 | class RangeException < Exception; end 86 | 87 | class NotADatabaseException < Exception; end 88 | end 89 | -------------------------------------------------------------------------------- /lib/sqlite3/fork_safety.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "weakref" 4 | 5 | module SQLite3 6 | # based on Rails's active_support/fork_tracker.rb 7 | module ForkSafety 8 | module CoreExt # :nodoc: 9 | def _fork 10 | pid = super 11 | if pid == 0 12 | ForkSafety.discard 13 | end 14 | pid 15 | end 16 | end 17 | 18 | @databases = [] 19 | @mutex = Mutex.new 20 | @suppress = false 21 | 22 | class << self 23 | def hook! # :nodoc: 24 | ::Process.singleton_class.prepend(CoreExt) 25 | end 26 | 27 | def track(database) # :nodoc: 28 | @mutex.synchronize do 29 | @databases << WeakRef.new(database) 30 | end 31 | end 32 | 33 | def discard # :nodoc: 34 | warned = @suppress 35 | @databases.each do |db| 36 | next unless db.weakref_alive? 37 | 38 | begin 39 | unless db.closed? || db.readonly? 40 | unless warned 41 | # If you are here, you may want to read 42 | # https://github.com/sparklemotion/sqlite3-ruby/pull/558 43 | warn("Writable sqlite database connection(s) were inherited from a forked process. " \ 44 | "This is unsafe and the connections are being closed to prevent possible data " \ 45 | "corruption. Please close writable sqlite database connections before forking.", 46 | uplevel: 0) 47 | warned = true 48 | end 49 | db.close 50 | end 51 | rescue WeakRef::RefError 52 | # GC may run while this method is executing, and that's OK 53 | end 54 | end 55 | @databases.clear 56 | end 57 | 58 | # Call to suppress the fork-related warnings. 59 | def suppress_warnings! 60 | @suppress = true 61 | end 62 | end 63 | end 64 | end 65 | 66 | SQLite3::ForkSafety.hook! 67 | -------------------------------------------------------------------------------- /lib/sqlite3/resultset.rb: -------------------------------------------------------------------------------- 1 | require "sqlite3/constants" 2 | require "sqlite3/errors" 3 | 4 | module SQLite3 5 | # The ResultSet object encapsulates the enumerability of a query's output. 6 | # It is a simple cursor over the data that the query returns. It will 7 | # very rarely (if ever) be instantiated directly. Instead, clients should 8 | # obtain a ResultSet instance via Statement#execute. 9 | class ResultSet 10 | include Enumerable 11 | 12 | # Create a new ResultSet attached to the given database, using the 13 | # given sql text. 14 | def initialize db, stmt 15 | @db = db 16 | @stmt = stmt 17 | end 18 | 19 | # Reset the cursor, so that a result set which has reached end-of-file 20 | # can be rewound and reiterated. 21 | def reset(*bind_params) 22 | @stmt.reset! 23 | @stmt.bind_params(*bind_params) 24 | end 25 | 26 | # Query whether the cursor has reached the end of the result set or not. 27 | def eof? 28 | @stmt.done? 29 | end 30 | 31 | # Obtain the next row from the cursor. If there are no more rows to be 32 | # had, this will return +nil+. 33 | # 34 | # The returned value will be an array, unless Database#results_as_hash has 35 | # been set to +true+, in which case the returned value will be a hash. 36 | # 37 | # For arrays, the column names are accessible via the +fields+ property, 38 | # and the column types are accessible via the +types+ property. 39 | # 40 | # For hashes, the column names are the keys of the hash, and the column 41 | # types are accessible via the +types+ property. 42 | def next 43 | @stmt.step 44 | end 45 | 46 | # Required by the Enumerable mixin. Provides an internal iterator over the 47 | # rows of the result set. 48 | def each 49 | while (node = self.next) 50 | yield node 51 | end 52 | end 53 | 54 | # Provides an internal iterator over the rows of the result set where 55 | # each row is yielded as a hash. 56 | def each_hash 57 | while (node = next_hash) 58 | yield node 59 | end 60 | end 61 | 62 | # Closes the statement that spawned this result set. 63 | # Use with caution! Closing a result set will automatically 64 | # close any other result sets that were spawned from the same statement. 65 | def close 66 | @stmt.close 67 | end 68 | 69 | # Queries whether the underlying statement has been closed or not. 70 | def closed? 71 | @stmt.closed? 72 | end 73 | 74 | # Returns the types of the columns returned by this result set. 75 | def types 76 | @stmt.types 77 | end 78 | 79 | # Returns the names of the columns returned by this result set. 80 | def columns 81 | @stmt.columns 82 | end 83 | 84 | # Return the next row as a hash 85 | def next_hash 86 | row = @stmt.step 87 | return nil if @stmt.done? 88 | 89 | @stmt.columns.zip(row).to_h 90 | end 91 | end 92 | 93 | class HashResultSet < ResultSet # :nodoc: 94 | alias_method :next, :next_hash 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/sqlite3/statement.rb: -------------------------------------------------------------------------------- 1 | require "sqlite3/errors" 2 | require "sqlite3/resultset" 3 | 4 | class String 5 | def to_blob 6 | SQLite3::Blob.new(self) 7 | end 8 | end 9 | 10 | module SQLite3 11 | # A statement represents a prepared-but-unexecuted SQL query. It will rarely 12 | # (if ever) be instantiated directly by a client, and is most often obtained 13 | # via the Database#prepare method. 14 | class Statement 15 | include Enumerable 16 | 17 | # This is any text that followed the first valid SQL statement in the text 18 | # with which the statement was initialized. If there was no trailing text, 19 | # this will be the empty string. 20 | attr_reader :remainder 21 | 22 | # call-seq: SQLite3::Statement.new(db, sql) 23 | # 24 | # Create a new statement attached to the given Database instance, and which 25 | # encapsulates the given SQL text. If the text contains more than one 26 | # statement (i.e., separated by semicolons), then the #remainder property 27 | # will be set to the trailing text. 28 | def initialize(db, sql) 29 | raise ArgumentError, "prepare called on a closed database" if db.closed? 30 | 31 | sql = sql.encode(Encoding::UTF_8) if sql && sql.encoding != Encoding::UTF_8 32 | 33 | @connection = db 34 | @columns = nil 35 | @types = nil 36 | @remainder = prepare db, sql 37 | end 38 | 39 | # Binds the given variables to the corresponding placeholders in the SQL 40 | # text. 41 | # 42 | # See Database#execute for a description of the valid placeholder 43 | # syntaxes. 44 | # 45 | # Example: 46 | # 47 | # stmt = db.prepare( "select * from table where a=? and b=?" ) 48 | # stmt.bind_params( 15, "hello" ) 49 | # 50 | # See also #execute, #bind_param, Statement#bind_param, and 51 | # Statement#bind_params. 52 | def bind_params(*bind_vars) 53 | index = 1 54 | bind_vars.flatten.each do |var| 55 | if Hash === var 56 | var.each { |key, val| bind_param key, val } 57 | else 58 | bind_param index, var 59 | index += 1 60 | end 61 | end 62 | end 63 | 64 | # Execute the statement. This creates a new ResultSet object for the 65 | # statement's virtual machine. If a block was given, the new ResultSet will 66 | # be yielded to it; otherwise, the ResultSet will be returned. 67 | # 68 | # Any parameters will be bound to the statement using #bind_params. 69 | # 70 | # Example: 71 | # 72 | # stmt = db.prepare( "select * from table" ) 73 | # stmt.execute do |result| 74 | # ... 75 | # end 76 | # 77 | # See also #bind_params, #execute!. 78 | def execute(*bind_vars) 79 | reset! if active? || done? 80 | 81 | bind_params(*bind_vars) unless bind_vars.empty? 82 | results = @connection.build_result_set self 83 | 84 | step if column_count == 0 85 | 86 | yield results if block_given? 87 | results 88 | end 89 | 90 | # Execute the statement. If no block was given, this returns an array of 91 | # rows returned by executing the statement. Otherwise, each row will be 92 | # yielded to the block. 93 | # 94 | # Any parameters will be bound to the statement using #bind_params. 95 | # 96 | # Example: 97 | # 98 | # stmt = db.prepare( "select * from table" ) 99 | # stmt.execute! do |row| 100 | # ... 101 | # end 102 | # 103 | # See also #bind_params, #execute. 104 | def execute!(*bind_vars, &block) 105 | execute(*bind_vars) 106 | block ? each(&block) : to_a 107 | end 108 | 109 | # Returns true if the statement is currently active, meaning it has an 110 | # open result set. 111 | def active? 112 | !done? 113 | end 114 | 115 | # Return an array of the column names for this statement. Note that this 116 | # may execute the statement in order to obtain the metadata; this makes it 117 | # a (potentially) expensive operation. 118 | def columns 119 | get_metadata unless @columns 120 | @columns 121 | end 122 | 123 | def each 124 | loop do 125 | val = step 126 | break self if done? 127 | yield val 128 | end 129 | end 130 | 131 | # Return an array of the data types for each column in this statement. Note 132 | # that this may execute the statement in order to obtain the metadata; this 133 | # makes it a (potentially) expensive operation. 134 | def types 135 | must_be_open! 136 | get_metadata unless @types 137 | @types 138 | end 139 | 140 | # Performs a sanity check to ensure that the statement is not 141 | # closed. If it is, an exception is raised. 142 | def must_be_open! # :nodoc: 143 | if closed? 144 | raise SQLite3::Exception, "cannot use a closed statement" 145 | end 146 | end 147 | 148 | # Returns a Hash containing information about the statement. 149 | # The contents of the hash are implementation specific and may change in 150 | # the future without notice. The hash includes information about internal 151 | # statistics about the statement such as: 152 | # - +fullscan_steps+: the number of times that SQLite has stepped forward 153 | # in a table as part of a full table scan 154 | # - +sorts+: the number of sort operations that have occurred 155 | # - +autoindexes+: the number of rows inserted into transient indices 156 | # that were created automatically in order to help joins run faster 157 | # - +vm_steps+: the number of virtual machine operations executed by the 158 | # prepared statement 159 | # - +reprepares+: the number of times that the prepare statement has been 160 | # automatically regenerated due to schema changes or changes to bound 161 | # parameters that might affect the query plan 162 | # - +runs+: the number of times that the prepared statement has been run 163 | # - +filter_misses+: the number of times that the Bloom filter returned 164 | # a find, and thus the join step had to be processed as normal 165 | # - +filter_hits+: the number of times that a join step was bypassed 166 | # because a Bloom filter returned not-found 167 | def stat key = nil 168 | if key 169 | stat_for(key) 170 | else 171 | stats_as_hash 172 | end 173 | end 174 | 175 | private 176 | 177 | # A convenience method for obtaining the metadata about the query. Note 178 | # that this will actually execute the SQL, which means it can be a 179 | # (potentially) expensive operation. 180 | def get_metadata 181 | @columns = Array.new(column_count) do |column| 182 | column_name column 183 | end 184 | @types = Array.new(column_count) do |column| 185 | val = column_decltype(column) 186 | val&.downcase 187 | end 188 | end 189 | end 190 | end 191 | -------------------------------------------------------------------------------- /lib/sqlite3/value.rb: -------------------------------------------------------------------------------- 1 | require "sqlite3/constants" 2 | 3 | module SQLite3 4 | class Value 5 | attr_reader :handle 6 | 7 | def initialize(db, handle) 8 | @driver = db.driver 9 | @handle = handle 10 | end 11 | 12 | def null? 13 | type == :null 14 | end 15 | 16 | def to_blob 17 | @driver.value_blob(@handle) 18 | end 19 | 20 | def length(utf16 = false) 21 | if utf16 22 | @driver.value_bytes16(@handle) 23 | else 24 | @driver.value_bytes(@handle) 25 | end 26 | end 27 | 28 | def to_f 29 | @driver.value_double(@handle) 30 | end 31 | 32 | def to_i 33 | @driver.value_int(@handle) 34 | end 35 | 36 | def to_int64 37 | @driver.value_int64(@handle) 38 | end 39 | 40 | def to_s(utf16 = false) 41 | @driver.value_text(@handle, utf16) 42 | end 43 | 44 | def type 45 | case @driver.value_type(@handle) 46 | when Constants::ColumnType::INTEGER then :int 47 | when Constants::ColumnType::FLOAT then :float 48 | when Constants::ColumnType::TEXT then :text 49 | when Constants::ColumnType::BLOB then :blob 50 | when Constants::ColumnType::NULL then :null 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/sqlite3/version.rb: -------------------------------------------------------------------------------- 1 | module SQLite3 2 | # (String) the version of the sqlite3 gem, e.g. "2.1.1" 3 | VERSION = "2.7.0" 4 | end 5 | -------------------------------------------------------------------------------- /lib/sqlite3/version_info.rb: -------------------------------------------------------------------------------- 1 | module SQLite3 2 | # a hash of descriptive metadata about the current version of the sqlite3 gem 3 | VERSION_INFO = { 4 | ruby: RUBY_DESCRIPTION, 5 | gem: { 6 | version: SQLite3::VERSION 7 | }, 8 | sqlite: { 9 | compiled: SQLite3::SQLITE_VERSION, 10 | loaded: SQLite3::SQLITE_LOADED_VERSION, 11 | packaged: SQLite3::SQLITE_PACKAGED_LIBRARIES, 12 | precompiled: SQLite3::SQLITE_PRECOMPILED_LIBRARIES, 13 | sqlcipher: SQLite3.sqlcipher?, 14 | threadsafe: SQLite3.threadsafe? 15 | } 16 | } 17 | end 18 | -------------------------------------------------------------------------------- /patches/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparklemotion/sqlite3-ruby/782653e53cd207f185dd49f6f7dc16e850c2e818/patches/.gitkeep -------------------------------------------------------------------------------- /rakelib/check-manifest.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # replacement for Hoe's task of the same name 4 | 5 | desc "Perform a sanity check on the gemspec file list" 6 | task :check_manifest do 7 | ignore_directories = %w{ 8 | .DS_Store 9 | .bundle 10 | .git 11 | .github 12 | .ruby-lsp 13 | adr 14 | bin 15 | doc 16 | gems 17 | issues 18 | patches 19 | pkg 20 | ports 21 | rakelib 22 | test 23 | tmp 24 | vendor 25 | [0-9]* 26 | } 27 | ignore_files = %w[ 28 | .editorconfig 29 | .gitignore 30 | .rdoc_options 31 | .rubocop.yml 32 | Gemfile* 33 | Rakefile 34 | [a-z]*.{log,out} 35 | [0-9]* 36 | appveyor.yml 37 | lib/sqlite3/**/sqlite3*.{jar,so} 38 | lib/sqlite3/sqlite3*.{jar,so} 39 | *.gemspec 40 | ] 41 | 42 | intended_directories = Dir.children(".") 43 | .select { |filename| File.directory?(filename) } 44 | .reject { |filename| ignore_directories.any? { |ig| File.fnmatch?(ig, filename) } } 45 | 46 | intended_files = Dir.children(".") 47 | .select { |filename| File.file?(filename) } 48 | .reject { |filename| ignore_files.any? { |ig| File.fnmatch?(ig, filename, File::FNM_EXTGLOB) } } 49 | 50 | intended_files += Dir.glob(intended_directories.map { |d| File.join(d, "/**/*") }) 51 | .select { |filename| File.file?(filename) } 52 | .reject { |filename| ignore_files.any? { |ig| File.fnmatch?(ig, filename, File::FNM_EXTGLOB) } } 53 | .sort 54 | 55 | spec_files = SQLITE3_SPEC.files.sort 56 | 57 | missing_files = intended_files - spec_files 58 | extra_files = spec_files - intended_files 59 | 60 | unless missing_files.empty? 61 | puts "missing:" 62 | missing_files.sort.each { |f| puts "- #{f}" } 63 | end 64 | unless extra_files.empty? 65 | puts "unexpected:" 66 | extra_files.sort.each { |f| puts "+ #{f}" } 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /rakelib/format.rake: -------------------------------------------------------------------------------- 1 | require "rake/clean" 2 | 3 | begin 4 | require "rubocop/rake_task" 5 | 6 | module AstyleHelper 7 | class << self 8 | def run(files) 9 | assert 10 | command = ["astyle", args, files].flatten.shelljoin 11 | system(command) 12 | end 13 | 14 | def assert 15 | require "mkmf" 16 | find_executable0("astyle") || raise("Could not find command 'astyle'") 17 | end 18 | 19 | def args 20 | [ 21 | # indentation 22 | "--indent=spaces=4", 23 | "--indent-switches", 24 | 25 | # brackets 26 | "--style=1tbs", 27 | "--keep-one-line-blocks", 28 | 29 | # where do we want spaces 30 | "--unpad-paren", 31 | "--pad-header", 32 | "--pad-oper", 33 | "--pad-comma", 34 | 35 | # "void *pointer" and not "void* pointer" 36 | "--align-pointer=name", 37 | 38 | # function definitions and declarations 39 | "--break-return-type", 40 | "--attach-return-type-decl", 41 | 42 | # gotta set a limit somewhere 43 | "--max-code-length=100", 44 | 45 | # be quiet about files that haven't changed 46 | "--formatted", 47 | "--verbose" 48 | ] 49 | end 50 | 51 | def c_files 52 | SQLITE3_SPEC.files.grep(%r{ext/sqlite3/.*\.[ch]\Z}) 53 | end 54 | end 55 | end 56 | 57 | namespace "format" do 58 | desc "Format C code" 59 | task "c" do 60 | puts "Running astyle on C files ..." 61 | AstyleHelper.run(AstyleHelper.c_files) 62 | end 63 | 64 | CLEAN.add(AstyleHelper.c_files.map { |f| "#{f}.orig" }) 65 | 66 | desc "Format Ruby code" 67 | task "ruby" => "rubocop:autocorrect" 68 | end 69 | 70 | RuboCop::RakeTask.new 71 | 72 | task "format" => ["format:c", "format:ruby"] 73 | rescue LoadError => e 74 | puts "NOTE: Rubocop is not available in this environment: #{e.message}" 75 | end 76 | -------------------------------------------------------------------------------- /rakelib/native.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rubygems/package_task" 5 | require "rake/extensiontask" 6 | require "rake_compiler_dock" 7 | require "yaml" 8 | 9 | cross_platforms = [ 10 | "aarch64-linux-gnu", 11 | "aarch64-linux-musl", 12 | "arm-linux-gnu", 13 | "arm-linux-musl", 14 | "x86-linux-gnu", 15 | "x86-linux-musl", 16 | "x86_64-linux-gnu", 17 | "x86_64-linux-musl", 18 | "arm64-darwin", 19 | "x86_64-darwin", 20 | "x64-mingw-ucrt" 21 | ] 22 | RakeCompilerDock.set_ruby_cc_version("~> 3.1") 23 | 24 | Gem::PackageTask.new(SQLITE3_SPEC).define # packaged_tarball version of the gem for platform=ruby 25 | task "package" => cross_platforms.map { |p| "gem:#{p}" } # "package" task for all the native platforms 26 | 27 | def gem_build_path 28 | File.join("pkg", SQLITE3_SPEC.full_name) 29 | end 30 | 31 | def add_file_to_gem(relative_source_path) 32 | if relative_source_path.nil? || !File.exist?(relative_source_path) 33 | raise "Cannot find file '#{relative_source_path}'" 34 | end 35 | 36 | dest_path = File.join(gem_build_path, relative_source_path) 37 | dest_dir = File.dirname(dest_path) 38 | 39 | mkdir_p(dest_dir) unless Dir.exist?(dest_dir) 40 | rm_f(dest_path) if File.exist?(dest_path) 41 | safe_ln(relative_source_path, dest_path) 42 | 43 | SQLITE3_SPEC.files << relative_source_path 44 | end 45 | 46 | task gem_build_path do 47 | dependencies = YAML.load_file(File.join(__dir__, "..", "dependencies.yml"), symbolize_names: true) 48 | sqlite_tarball = File.basename(dependencies[:sqlite3][:files].first[:url]) 49 | archive = Dir.glob(File.join("ports", "archives", sqlite_tarball)).first 50 | add_file_to_gem(archive) 51 | 52 | patches = `#{["git", "ls-files", "patches"].shelljoin}`.split("\n").grep(/\.patch\z/) 53 | patches.each { |patch| add_file_to_gem patch } 54 | end 55 | 56 | Rake::ExtensionTask.new("sqlite3_native", SQLITE3_SPEC) do |ext| 57 | ext.ext_dir = "ext/sqlite3" 58 | ext.lib_dir = "lib/sqlite3" 59 | ext.cross_compile = true 60 | ext.cross_platform = cross_platforms 61 | ext.cross_config_options << "--enable-cross-build" # so extconf.rb knows we're cross-compiling 62 | ext.cross_compiling do |spec| 63 | # remove things not needed for precompiled gems 64 | spec.dependencies.reject! { |dep| dep.name == "mini_portile2" } 65 | spec.metadata.delete("msys2_mingw_dependencies") 66 | end 67 | end 68 | 69 | namespace "gem" do 70 | cross_platforms.each do |platform| 71 | desc "build native gem for #{platform}" 72 | task platform do 73 | RakeCompilerDock.sh(<<~EOF, platform: platform, verbose: true) 74 | gem install bundler --no-document && 75 | bundle && 76 | bundle exec rake gem:#{platform}:buildit 77 | EOF 78 | end 79 | 80 | namespace platform do 81 | # this runs in the rake-compiler-dock docker container 82 | task "buildit" do 83 | # use Task#invoke because the pkg/*gem task is defined at runtime 84 | Rake::Task["native:#{platform}"].invoke 85 | Rake::Task["pkg/#{SQLITE3_SPEC.full_name}-#{Gem::Platform.new(platform)}.gem"].invoke 86 | end 87 | end 88 | end 89 | 90 | desc "build native gem for all platforms" 91 | task "all" => [cross_platforms, "gem"].flatten 92 | end 93 | 94 | desc "Temporarily set VERSION to a unique timestamp" 95 | task "set-version-to-timestamp" do 96 | # this task is used by bin/test-gem-build 97 | # to test building, packaging, and installing a precompiled gem 98 | version_constant_re = /^\s*VERSION\s*=\s*["'](.*)["']$/ 99 | 100 | version_file_path = File.join(__dir__, "../lib/sqlite3/version.rb") 101 | version_file_contents = File.read(version_file_path) 102 | 103 | current_version_string = version_constant_re.match(version_file_contents)[1] 104 | current_version = Gem::Version.new(current_version_string) 105 | 106 | fake_version = Gem::Version.new(format("%s.test.%s", current_version.bump, Time.now.strftime("%Y.%m%d.%H%M"))) 107 | 108 | unless version_file_contents.gsub!(version_constant_re, " VERSION = \"#{fake_version}\"") 109 | raise("Could not hack the VERSION constant") 110 | end 111 | 112 | File.write(version_file_path, version_file_contents) 113 | 114 | puts "NOTE: wrote version as \"#{fake_version}\"" 115 | end 116 | 117 | CLEAN.add("{ext,lib}/**/*.{o,so}", "pkg") 118 | CLOBBER.add("ports/*").exclude(%r{ports/archives$}) 119 | 120 | # when packaging the gem, if the tarball isn't cached, we need to fetch it. the easiest thing to do 121 | # is to run the compile phase to invoke the extconf and have mini_portile download the file for us. 122 | # this is wasteful and in the future I would prefer to separate mini_portile from the extconf to 123 | # allow us to download without compiling. 124 | Rake::Task["package"].prerequisites.prepend("compile") 125 | -------------------------------------------------------------------------------- /rakelib/test.rake: -------------------------------------------------------------------------------- 1 | require "minitest/test_task" 2 | test_config = lambda do |t| 3 | t.libs << "test" 4 | t.libs << "lib" 5 | 6 | glob = "test/**/test_*.rb" 7 | if t.respond_to?(:test_files=) 8 | t.test_files = FileList[glob] # Rake::TestTask (RubyMemcheck) 9 | else 10 | t.test_globs = [glob] # Minitest::TestTask 11 | end 12 | end 13 | 14 | Minitest::TestTask.create(:test, &test_config) 15 | 16 | begin 17 | require "ruby_memcheck" 18 | rescue LoadError => e 19 | warn("NOTE: ruby_memcheck is not available in this environment: #{e}") 20 | end 21 | 22 | class GdbTestTask < Minitest::TestTask 23 | def ruby(*args, **options, &block) 24 | command = "gdb --args #{RUBY} #{args.join(" ")}" 25 | sh(command, **options, &block) 26 | end 27 | end 28 | 29 | namespace :test do 30 | if defined?(RubyMemcheck) 31 | RubyMemcheck::TestTask.new(:valgrind, &test_config) 32 | end 33 | 34 | GdbTestTask.create(:gdb) 35 | end 36 | -------------------------------------------------------------------------------- /sqlite3.gemspec: -------------------------------------------------------------------------------- 1 | begin 2 | require_relative "lib/sqlite3/version" 3 | rescue LoadError 4 | puts "WARNING: could not load Sqlite3::VERSION" 5 | end 6 | 7 | Gem::Specification.new do |s| 8 | s.name = "sqlite3" 9 | s.version = defined?(SQLite3::VERSION) ? SQLite3::VERSION : "0.0.0" 10 | 11 | s.summary = "Ruby library to interface with the SQLite3 database engine (http://www.sqlite.org)." 12 | s.description = <<~TEXT 13 | Ruby library to interface with the SQLite3 database engine (http://www.sqlite.org). Precompiled 14 | binaries are available for common platforms for recent versions of Ruby. 15 | TEXT 16 | 17 | s.authors = ["Jamis Buck", "Luis Lavena", "Aaron Patterson", "Mike Dalessio"] 18 | 19 | s.licenses = ["BSD-3-Clause"] 20 | 21 | s.required_ruby_version = Gem::Requirement.new(">= 3.1") 22 | 23 | s.homepage = "https://github.com/sparklemotion/sqlite3-ruby" 24 | s.metadata = { 25 | "homepage_uri" => "https://github.com/sparklemotion/sqlite3-ruby", 26 | "bug_tracker_uri" => "https://github.com/sparklemotion/sqlite3-ruby/issues", 27 | "documentation_uri" => "https://sparklemotion.github.io/sqlite3-ruby/", 28 | "changelog_uri" => "https://github.com/sparklemotion/sqlite3-ruby/blob/master/CHANGELOG.md", 29 | "source_code_uri" => "https://github.com/sparklemotion/sqlite3-ruby", 30 | 31 | # https://github.com/oneclick/rubyinstaller2/wiki/For-gem-developers#msys2-library-dependency 32 | "msys2_mingw_dependencies" => "sqlite3", 33 | 34 | # https://guides.rubygems.org/mfa-requirement-opt-in/ 35 | "rubygems_mfa_required" => "true" 36 | } 37 | 38 | s.files = [ 39 | ".gemtest", 40 | "CHANGELOG.md", 41 | "CONTRIBUTING.md", 42 | "FAQ.md", 43 | "INSTALLATION.md", 44 | "LICENSE", 45 | "README.md", 46 | "dependencies.yml", 47 | "ext/sqlite3/aggregator.c", 48 | "ext/sqlite3/aggregator.h", 49 | "ext/sqlite3/backup.c", 50 | "ext/sqlite3/backup.h", 51 | "ext/sqlite3/database.c", 52 | "ext/sqlite3/database.h", 53 | "ext/sqlite3/exception.c", 54 | "ext/sqlite3/exception.h", 55 | "ext/sqlite3/extconf.rb", 56 | "ext/sqlite3/sqlite3.c", 57 | "ext/sqlite3/sqlite3_ruby.h", 58 | "ext/sqlite3/statement.c", 59 | "ext/sqlite3/statement.h", 60 | "ext/sqlite3/timespec.h", 61 | "lib/sqlite3.rb", 62 | "lib/sqlite3/constants.rb", 63 | "lib/sqlite3/database.rb", 64 | "lib/sqlite3/errors.rb", 65 | "lib/sqlite3/fork_safety.rb", 66 | "lib/sqlite3/pragmas.rb", 67 | "lib/sqlite3/resultset.rb", 68 | "lib/sqlite3/statement.rb", 69 | "lib/sqlite3/value.rb", 70 | "lib/sqlite3/version.rb", 71 | "lib/sqlite3/version_info.rb" 72 | ] 73 | 74 | s.extra_rdoc_files = [ 75 | "CHANGELOG.md", 76 | "README.md", 77 | "ext/sqlite3/aggregator.c", 78 | "ext/sqlite3/backup.c", 79 | "ext/sqlite3/database.c", 80 | "ext/sqlite3/exception.c", 81 | "ext/sqlite3/sqlite3.c", 82 | "ext/sqlite3/statement.c" 83 | ] 84 | s.rdoc_options = ["--main", "README.md"] 85 | 86 | s.add_dependency("mini_portile2", "~> 2.8.0") 87 | 88 | s.extensions << "ext/sqlite3/extconf.rb" 89 | end 90 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require "sqlite3" 2 | require "minitest/autorun" 3 | require "yaml" 4 | 5 | puts SQLite3::VERSION_INFO.to_yaml 6 | 7 | module SQLite3 8 | class TestCase < Minitest::Test 9 | alias_method :assert_not_equal, :refute_equal 10 | alias_method :assert_not_nil, :refute_nil 11 | alias_method :assert_raise, :assert_raises 12 | 13 | def assert_nothing_raised 14 | yield 15 | end 16 | 17 | def i_am_running_in_valgrind 18 | # https://stackoverflow.com/questions/365458/how-can-i-detect-if-a-program-is-running-from-within-valgrind/62364698#62364698 19 | ENV["LD_PRELOAD"] =~ /valgrind|vgpreload/ 20 | end 21 | 22 | def windows? 23 | ::RUBY_PLATFORM =~ /mingw|mswin/ 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/test_backup.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | module SQLite3 4 | if defined?(SQLite3::Backup) 5 | class TestBackup < SQLite3::TestCase 6 | def setup 7 | @sdb = SQLite3::Database.new(":memory:") 8 | @ddb = SQLite3::Database.new(":memory:") 9 | @sdb.execute("CREATE TABLE foo (idx, val);") 10 | @data = ("A".."Z").map { |x| x * 40 } 11 | @data.each_with_index do |v, i| 12 | @sdb.execute("INSERT INTO foo (idx, val) VALUES (?, ?);", [i, v]) 13 | end 14 | end 15 | 16 | def test_backup_step 17 | b = SQLite3::Backup.new(@ddb, "main", @sdb, "main") 18 | while b.step(1) == SQLite3::Constants::ErrorCode::OK 19 | assert_not_equal(0, b.remaining) 20 | end 21 | assert_equal(0, b.remaining) 22 | b.finish 23 | assert_equal(@data.length, @ddb.execute("SELECT * FROM foo;").length) 24 | end 25 | 26 | def test_backup_all 27 | b = SQLite3::Backup.new(@ddb, "main", @sdb, "main") 28 | assert_equal(SQLite3::Constants::ErrorCode::DONE, b.step(-1)) 29 | assert_equal(0, b.remaining) 30 | b.finish 31 | assert_equal(@data.length, @ddb.execute("SELECT * FROM foo;").length) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/test_collation.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | module SQLite3 4 | class TestCollation < SQLite3::TestCase 5 | class Comparator 6 | attr_reader :calls 7 | def initialize 8 | @calls = [] 9 | end 10 | 11 | def compare left, right 12 | @calls << [left, right] 13 | left <=> right 14 | end 15 | end 16 | 17 | def setup 18 | @db = SQLite3::Database.new(":memory:") 19 | @create = "create table ex(id int, data string)" 20 | @db.execute(@create) 21 | [[1, "hello"], [2, "world"]].each do |vals| 22 | @db.execute("insert into ex (id, data) VALUES (?, ?)", vals) 23 | end 24 | end 25 | 26 | def test_custom_collation 27 | comparator = Comparator.new 28 | 29 | @db.collation "foo", comparator 30 | 31 | assert_equal comparator, @db.collations["foo"] 32 | @db.execute("select data from ex order by 1 collate foo") 33 | assert_equal 1, comparator.calls.length 34 | end 35 | 36 | def test_remove_collation 37 | comparator = Comparator.new 38 | 39 | @db.collation "foo", comparator 40 | @db.collation "foo", nil 41 | 42 | assert_nil @db.collations["foo"] 43 | assert_raises(SQLite3::SQLException) do 44 | @db.execute("select data from ex order by 1 collate foo") 45 | end 46 | end 47 | 48 | def test_encoding 49 | comparator = Comparator.new 50 | @db.collation "foo", comparator 51 | @db.execute("select data from ex order by 1 collate foo") 52 | 53 | a, b = *comparator.calls.first 54 | 55 | assert_equal Encoding.find("UTF-8"), a.encoding 56 | assert_equal Encoding.find("UTF-8"), b.encoding 57 | end 58 | 59 | def test_encoding_default_internal 60 | warn_before = $-w 61 | $-w = false 62 | before_enc = Encoding.default_internal 63 | 64 | Encoding.default_internal = "EUC-JP" 65 | comparator = Comparator.new 66 | @db.collation "foo", comparator 67 | @db.execute("select data from ex order by 1 collate foo") 68 | 69 | a, b = *comparator.calls.first 70 | 71 | assert_equal Encoding.find("EUC-JP"), a.encoding 72 | assert_equal Encoding.find("EUC-JP"), b.encoding 73 | ensure 74 | Encoding.default_internal = before_enc 75 | $-w = warn_before 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /test/test_database_flags.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | module SQLite3 4 | class TestDatabaseFlags < SQLite3::TestCase 5 | def setup 6 | File.unlink "test-flags.db" if File.exist?("test-flags.db") 7 | @db = SQLite3::Database.new("test-flags.db") 8 | @db.execute("CREATE TABLE foos (id integer)") 9 | @db.close 10 | end 11 | 12 | def teardown 13 | @db.close unless @db.closed? 14 | File.unlink "test-flags.db" if File.exist?("test-flags.db") 15 | end 16 | 17 | def test_open_database_flags_constants 18 | defined_to_date = [:READONLY, :READWRITE, :CREATE, :DELETEONCLOSE, 19 | :EXCLUSIVE, :MAIN_DB, :TEMP_DB, :TRANSIENT_DB, 20 | :MAIN_JOURNAL, :TEMP_JOURNAL, :SUBJOURNAL, 21 | :MASTER_JOURNAL, :SUPER_JOURNAL, :NOMUTEX, :FULLMUTEX] 22 | if SQLite3::SQLITE_VERSION_NUMBER > 3007002 23 | defined_to_date += [:AUTOPROXY, :SHAREDCACHE, :PRIVATECACHE, :WAL] 24 | end 25 | if SQLite3::SQLITE_VERSION_NUMBER > 3007007 26 | defined_to_date += [:URI] 27 | end 28 | if SQLite3::SQLITE_VERSION_NUMBER > 3007013 29 | defined_to_date += [:MEMORY] 30 | end 31 | assert_equal defined_to_date.sort, SQLite3::Constants::Open.constants.sort 32 | end 33 | 34 | def test_open_database_flags_conflicts_with_readonly 35 | assert_raise(RuntimeError) do 36 | @db = SQLite3::Database.new("test-flags.db", flags: 2, readonly: true) 37 | end 38 | end 39 | 40 | def test_open_database_flags_conflicts_with_readwrite 41 | assert_raise(RuntimeError) do 42 | @db = SQLite3::Database.new("test-flags.db", flags: 2, readwrite: true) 43 | end 44 | end 45 | 46 | def test_open_database_readonly_flags 47 | @db = SQLite3::Database.new("test-flags.db", flags: SQLite3::Constants::Open::READONLY) 48 | assert_predicate @db, :readonly? 49 | end 50 | 51 | def test_open_database_readwrite_flags 52 | @db = SQLite3::Database.new("test-flags.db", flags: SQLite3::Constants::Open::READWRITE) 53 | refute_predicate @db, :readonly? 54 | end 55 | 56 | def test_open_database_readonly_flags_cant_open 57 | File.unlink "test-flags.db" 58 | assert_raise(SQLite3::CantOpenException) do 59 | @db = SQLite3::Database.new("test-flags.db", flags: SQLite3::Constants::Open::READONLY) 60 | end 61 | end 62 | 63 | def test_open_database_readwrite_flags_cant_open 64 | File.unlink "test-flags.db" 65 | assert_raise(SQLite3::CantOpenException) do 66 | @db = SQLite3::Database.new("test-flags.db", flags: SQLite3::Constants::Open::READWRITE) 67 | end 68 | end 69 | 70 | def test_open_database_misuse_flags 71 | assert_raise(SQLite3::MisuseException) do 72 | flags = SQLite3::Constants::Open::READONLY | SQLite3::Constants::Open::READWRITE # <== incompatible flags 73 | @db = SQLite3::Database.new("test-flags.db", flags: flags) 74 | end 75 | end 76 | 77 | def test_open_database_create_flags 78 | File.unlink "test-flags.db" 79 | flags = SQLite3::Constants::Open::READWRITE | SQLite3::Constants::Open::CREATE 80 | @db = SQLite3::Database.new("test-flags.db", flags: flags) do |db| 81 | db.execute("CREATE TABLE foos (id integer)") 82 | db.execute("INSERT INTO foos (id) VALUES (12)") 83 | end 84 | assert_path_exists "test-flags.db" 85 | end 86 | 87 | def test_open_database_exotic_flags 88 | flags = SQLite3::Constants::Open::READWRITE | SQLite3::Constants::Open::CREATE 89 | exotic_flags = SQLite3::Constants::Open::NOMUTEX | SQLite3::Constants::Open::TEMP_DB 90 | @db = SQLite3::Database.new("test-flags.db", flags: flags | exotic_flags) 91 | @db.execute("INSERT INTO foos (id) VALUES (12)") 92 | assert_equal 1, @db.changes 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /test/test_database_readonly.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | module SQLite3 4 | class TestDatabaseReadonly < SQLite3::TestCase 5 | def setup 6 | File.unlink "test-readonly.db" if File.exist?("test-readonly.db") 7 | @db = SQLite3::Database.new("test-readonly.db") 8 | @db.execute("CREATE TABLE foos (id integer)") 9 | @db.close 10 | end 11 | 12 | def teardown 13 | @db.close unless @db.closed? 14 | File.unlink "test-readonly.db" if File.exist?("test-readonly.db") 15 | end 16 | 17 | def test_open_readonly_database 18 | @db = SQLite3::Database.new("test-readonly.db", readonly: true) 19 | assert_predicate @db, :readonly? 20 | end 21 | 22 | def test_open_readonly_not_exists_database 23 | File.unlink "test-readonly.db" 24 | assert_raise(SQLite3::CantOpenException) do 25 | @db = SQLite3::Database.new("test-readonly.db", readonly: true) 26 | end 27 | end 28 | 29 | def test_insert_readonly_database 30 | @db = SQLite3::Database.new("test-readonly.db", readonly: true) 31 | assert_raise(SQLite3::ReadOnlyException) do 32 | @db.execute("INSERT INTO foos (id) VALUES (12)") 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/test_database_readwrite.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | module SQLite3 4 | class TestDatabaseReadwrite < SQLite3::TestCase 5 | def setup 6 | File.unlink "test-readwrite.db" if File.exist?("test-readwrite.db") 7 | @db = SQLite3::Database.new("test-readwrite.db") 8 | @db.execute("CREATE TABLE foos (id integer)") 9 | @db.close 10 | end 11 | 12 | def teardown 13 | @db.close unless @db.closed? 14 | File.unlink "test-readwrite.db" if File.exist?("test-readwrite.db") 15 | end 16 | 17 | def test_open_readwrite_database 18 | @db = SQLite3::Database.new("test-readwrite.db", readwrite: true) 19 | refute_predicate @db, :readonly? 20 | end 21 | 22 | def test_open_readwrite_readonly_database 23 | assert_raise(RuntimeError) do 24 | @db = SQLite3::Database.new("test-readwrite.db", readwrite: true, readonly: true) 25 | end 26 | end 27 | 28 | def test_open_readwrite_not_exists_database 29 | File.unlink "test-readwrite.db" 30 | assert_raise(SQLite3::CantOpenException) do 31 | @db = SQLite3::Database.new("test-readwrite.db", readonly: true) 32 | end 33 | end 34 | 35 | def test_insert_readwrite_database 36 | @db = SQLite3::Database.new("test-readwrite.db", readwrite: true) 37 | @db.execute("INSERT INTO foos (id) VALUES (12)") 38 | assert_equal 1, @db.changes 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/test_database_uri.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | require "tempfile" 3 | require "pathname" 4 | 5 | module SQLite3 6 | class TestDatabaseURI < SQLite3::TestCase 7 | def test_open_absolute_file_uri 8 | skip("windows uri paths are hard") if windows? 9 | skip("system libraries may not allow URIs") unless SQLite3::SQLITE_PACKAGED_LIBRARIES 10 | 11 | Tempfile.open "test.db" do |file| 12 | db = SQLite3::Database.new("file:#{file.path}") 13 | assert db 14 | db.close 15 | end 16 | end 17 | 18 | def test_open_relative_file_uri 19 | skip("windows uri paths are hard") if windows? 20 | skip("system libraries may not allow URIs") unless SQLite3::SQLITE_PACKAGED_LIBRARIES 21 | 22 | Dir.mktmpdir do |dir| 23 | Dir.chdir dir do 24 | db = SQLite3::Database.new("file:test.db") 25 | assert db 26 | assert_path_exists "test.db" 27 | db.close 28 | end 29 | end 30 | end 31 | 32 | def test_open_file_uri_readonly 33 | skip("windows uri paths are hard") if windows? 34 | skip("system libraries may not allow URIs") unless SQLite3::SQLITE_PACKAGED_LIBRARIES 35 | 36 | Tempfile.open "test.db" do |file| 37 | db = SQLite3::Database.new("file:#{file.path}?mode=ro") 38 | 39 | assert_raise(SQLite3::ReadOnlyException) do 40 | db.execute("CREATE TABLE foos (id integer)") 41 | end 42 | 43 | db.close 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/test_discarding.rb: -------------------------------------------------------------------------------- 1 | require_relative "helper" 2 | 3 | module SQLite3 4 | class TestDiscardDatabase < SQLite3::TestCase 5 | DBPATH = "test.db" 6 | 7 | def setup 8 | FileUtils.rm_f(DBPATH) 9 | super 10 | end 11 | 12 | def teardown 13 | super 14 | FileUtils.rm_f(DBPATH) 15 | end 16 | 17 | def in_a_forked_process 18 | @read, @write = IO.pipe 19 | old_stderr, $stderr = $stderr, StringIO.new 20 | 21 | Process.fork do 22 | @read.close 23 | begin 24 | yield @write 25 | rescue => e 26 | old_stderr.write("child exception: #{e.message}") 27 | end 28 | @write.write($stderr.string) 29 | @write.close 30 | exit! 31 | end 32 | 33 | $stderr = old_stderr 34 | @write.close 35 | *@results = *@read.readlines 36 | @read.close 37 | end 38 | 39 | def test_fork_discards_an_open_readwrite_connection 40 | skip("interpreter doesn't support fork") unless Process.respond_to?(:fork) 41 | skip("valgrind doesn't handle forking") if i_am_running_in_valgrind 42 | 43 | GC.start 44 | begin 45 | db = SQLite3::Database.new(DBPATH) 46 | 47 | in_a_forked_process do |write| 48 | write.write(db.closed? ? "ok\n" : "fail\n") 49 | end 50 | 51 | assertion, *stderr = *@results 52 | 53 | assert_equal("ok", assertion.chomp, "closed? did not return true") 54 | assert_equal(1, stderr.count, "unexpected output on stderr: #{stderr.inspect}") 55 | assert_match( 56 | /warning: Writable sqlite database connection\(s\) were inherited from a forked process/, 57 | stderr.first, 58 | "expected warning was not emitted" 59 | ) 60 | ensure 61 | db&.close 62 | end 63 | end 64 | 65 | def test_fork_does_not_discard_closed_connections 66 | skip("interpreter doesn't support fork") unless Process.respond_to?(:fork) 67 | skip("valgrind doesn't handle forking") if i_am_running_in_valgrind 68 | 69 | GC.start 70 | begin 71 | db = SQLite3::Database.new(DBPATH) 72 | db.close 73 | 74 | in_a_forked_process do |write| 75 | write.write(db.closed? ? "ok\n" : "fail\n") 76 | write.write($stderr.string) # should be empty write, no warnings emitted 77 | write.write("done\n") 78 | end 79 | 80 | assertion, *rest = *@results 81 | 82 | assert_equal("ok", assertion.chomp, "closed? did not return true") 83 | assert_equal(1, rest.count, "unexpected output on stderr: #{rest.inspect}") 84 | assert_equal("done", rest.first.chomp, "unexpected output on stderr: #{rest.inspect}") 85 | ensure 86 | db&.close 87 | end 88 | end 89 | 90 | def test_fork_does_not_discard_readonly_connections 91 | skip("interpreter doesn't support fork") unless Process.respond_to?(:fork) 92 | skip("valgrind doesn't handle forking") if i_am_running_in_valgrind 93 | 94 | GC.start 95 | begin 96 | SQLite3::Database.open(DBPATH) do |db| 97 | db.execute("create table foo (bar int)") 98 | db.execute("insert into foo values (1)") 99 | end 100 | 101 | db = SQLite3::Database.new(DBPATH, readonly: true) 102 | 103 | in_a_forked_process do |write| 104 | write.write(db.closed? ? "fail\n" : "ok\n") # should be open and readable 105 | write.write((db.execute("select * from foo") == [[1]]) ? "ok\n" : "fail\n") 106 | write.write($stderr.string) # should be an empty write, no warnings emitted 107 | write.write("done\n") 108 | end 109 | 110 | assertion1, assertion2, *rest = *@results 111 | 112 | assert_equal("ok", assertion1.chomp, "closed? did not return false") 113 | assert_equal("ok", assertion2.chomp, "could not read from database") 114 | assert_equal(1, rest.count, "unexpected output on stderr: #{rest.inspect}") 115 | assert_equal("done", rest.first.chomp, "unexpected output on stderr: #{rest.inspect}") 116 | ensure 117 | db&.close 118 | end 119 | end 120 | 121 | def test_close_does_not_discard_readonly_connections 122 | skip("interpreter doesn't support fork") unless Process.respond_to?(:fork) 123 | skip("valgrind doesn't handle forking") if i_am_running_in_valgrind 124 | 125 | GC.start 126 | begin 127 | SQLite3::Database.open(DBPATH) do |db| 128 | db.execute("create table foo (bar int)") 129 | db.execute("insert into foo values (1)") 130 | end 131 | 132 | db = SQLite3::Database.new(DBPATH, readonly: true) 133 | 134 | in_a_forked_process do |write| 135 | write.write(db.closed? ? "fail\n" : "ok\n") # should be open and readable 136 | db.close 137 | write.write($stderr.string) # should be an empty write, no warnings emitted 138 | write.write("done\n") 139 | end 140 | 141 | assertion, *rest = *@results 142 | 143 | assert_equal("ok", assertion.chomp, "closed? did not return false") 144 | assert_equal(1, rest.count, "unexpected output on stderr: #{rest.inspect}") 145 | assert_equal("done", rest.first.chomp, "unexpected output on stderr: #{rest.inspect}") 146 | ensure 147 | db&.close 148 | end 149 | end 150 | 151 | def test_a_discarded_connection_with_statements 152 | skip("discard leaks memory") if i_am_running_in_valgrind 153 | 154 | begin 155 | db = SQLite3::Database.new(DBPATH) 156 | db.execute("create table foo (bar int)") 157 | db.execute("insert into foo values (1)") 158 | stmt = db.prepare("select * from foo") 159 | 160 | db.send(:discard) 161 | 162 | e = assert_raises(SQLite3::Exception) { stmt.execute } 163 | assert_match(/cannot use a statement associated with a discarded database/, e.message) 164 | 165 | assert_nothing_raised { stmt.close } 166 | assert_predicate(stmt, :closed?) 167 | ensure 168 | db&.close 169 | end 170 | end 171 | end 172 | end 173 | -------------------------------------------------------------------------------- /test/test_encoding.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | module SQLite3 4 | class TestEncoding < SQLite3::TestCase 5 | def setup 6 | @db = SQLite3::Database.new(":memory:") 7 | @create = "create table ex(id int, data string)" 8 | @insert = "insert into ex(id, data) values (?, ?)" 9 | @db.execute(@create) 10 | end 11 | 12 | def teardown 13 | @db.close 14 | end 15 | 16 | def test_change_encoding 17 | db = SQLite3::Database.new(":memory:") 18 | assert_equal Encoding.find("UTF-8"), db.encoding 19 | 20 | db.execute "PRAGMA encoding='UTF-16le'" 21 | assert_equal Encoding.find("UTF-16le"), db.encoding 22 | end 23 | 24 | def test_encoding_when_results_are_hash 25 | db = SQLite3::Database.new(":memory:", results_as_hash: true) 26 | assert_equal Encoding.find("UTF-8"), db.encoding 27 | 28 | db = SQLite3::Database.new(":memory:") 29 | assert_equal Encoding.find("UTF-8"), db.encoding 30 | end 31 | 32 | def test_select_encoding_on_utf_16 33 | str = "foo" 34 | utf16 = ([1].pack("I") == [1].pack("N")) ? "UTF-16BE" : "UTF-16LE" 35 | db = SQLite3::Database.new(":memory:".encode(utf16)) 36 | db.execute @create 37 | db.execute "insert into ex (id, data) values (1, \"#{str}\")" 38 | 39 | stmt = db.prepare "select * from ex where data = ?" 40 | ["US-ASCII", utf16, "EUC-JP", "UTF-8"].each do |enc| 41 | stmt.bind_param 1, str.encode(enc) 42 | assert_equal 1, stmt.to_a.length 43 | stmt.reset! 44 | end 45 | stmt.close 46 | end 47 | 48 | def test_insert_encoding 49 | str = "foo" 50 | utf16 = ([1].pack("I") == [1].pack("N")) ? "UTF-16BE" : "UTF-16LE" 51 | db = SQLite3::Database.new(":memory:".encode(utf16)) 52 | db.execute @create 53 | stmt = db.prepare @insert 54 | 55 | ["US-ASCII", utf16, "EUC-JP", "UTF-8"].each_with_index do |enc, i| 56 | stmt.bind_param 1, i 57 | stmt.bind_param 2, str.encode(enc) 58 | stmt.to_a 59 | stmt.reset! 60 | end 61 | stmt.close 62 | 63 | db.execute("select data from ex").flatten.each do |s| 64 | assert_equal str, s 65 | end 66 | end 67 | 68 | def test_default_internal_is_honored 69 | warn_before = $-w 70 | $-w = false 71 | 72 | before_enc = Encoding.default_internal 73 | 74 | str = "壁に耳あり、障子に目あり" 75 | stmt = @db.prepare("insert into ex(data) values (?)") 76 | stmt.bind_param 1, str 77 | stmt.step 78 | stmt.close 79 | 80 | Encoding.default_internal = "EUC-JP" 81 | string = @db.execute("select data from ex").first.first 82 | 83 | assert_equal Encoding.default_internal, string.encoding 84 | assert_equal str.encode("EUC-JP"), string 85 | assert_equal str, string.encode(str.encoding) 86 | ensure 87 | Encoding.default_internal = before_enc 88 | $-w = warn_before 89 | end 90 | 91 | def test_blob_is_binary 92 | str = "猫舌" 93 | @db.execute("create table foo(data text)") 94 | stmt = @db.prepare("insert into foo(data) values (?)") 95 | stmt.bind_param(1, SQLite3::Blob.new(str)) 96 | stmt.step 97 | stmt.close 98 | 99 | string = @db.execute("select data from foo").first.first 100 | assert_equal Encoding.find("ASCII-8BIT"), string.encoding 101 | assert_equal str, string.dup.force_encoding("UTF-8") 102 | end 103 | 104 | def test_blob_is_ascii8bit 105 | str = "猫舌" 106 | @db.execute("create table foo(data text)") 107 | stmt = @db.prepare("insert into foo(data) values (?)") 108 | stmt.bind_param(1, str.dup.force_encoding("ASCII-8BIT")) 109 | stmt.step 110 | stmt.close 111 | 112 | string = @db.execute("select data from foo").first.first 113 | assert_equal Encoding.find("ASCII-8BIT"), string.encoding 114 | assert_equal str, string.dup.force_encoding("UTF-8") 115 | end 116 | 117 | def test_blob_with_eucjp 118 | str = "猫舌".encode("EUC-JP") 119 | @db.execute("create table foo(data text)") 120 | stmt = @db.prepare("insert into foo(data) values (?)") 121 | stmt.bind_param(1, SQLite3::Blob.new(str)) 122 | stmt.step 123 | stmt.close 124 | 125 | string = @db.execute("select data from foo").first.first 126 | assert_equal Encoding.find("ASCII-8BIT"), string.encoding 127 | assert_equal str, string.dup.force_encoding("EUC-JP") 128 | end 129 | 130 | def test_db_with_eucjp 131 | db = SQLite3::Database.new(":memory:".encode("EUC-JP")) 132 | assert_equal(Encoding.find("UTF-8"), db.encoding) 133 | end 134 | 135 | def test_db_with_utf16 136 | utf16 = ([1].pack("I") == [1].pack("N")) ? "UTF-16BE" : "UTF-16LE" 137 | 138 | db = SQLite3::Database.new(":memory:".encode(utf16)) 139 | assert_equal(Encoding.find(utf16), db.encoding) 140 | end 141 | 142 | def test_statement_eucjp 143 | str = "猫舌" 144 | @db.execute("insert into ex(data) values ('#{str}')".encode("EUC-JP")) 145 | row = @db.execute("select data from ex") 146 | assert_equal @db.encoding, row.first.first.encoding 147 | assert_equal str, row.first.first 148 | end 149 | 150 | def test_statement_utf8 151 | str = "猫舌" 152 | @db.execute("insert into ex(data) values ('#{str}')") 153 | row = @db.execute("select data from ex") 154 | assert_equal @db.encoding, row.first.first.encoding 155 | assert_equal str, row.first.first 156 | end 157 | 158 | def test_encoding 159 | assert_equal Encoding.find("UTF-8"), @db.encoding 160 | end 161 | 162 | def test_utf_8 163 | str = "猫舌" 164 | @db.execute(@insert, [10, str]) 165 | row = @db.execute("select data from ex") 166 | assert_equal @db.encoding, row.first.first.encoding 167 | assert_equal str, row.first.first 168 | end 169 | 170 | def test_euc_jp 171 | str = "猫舌".encode("EUC-JP") 172 | @db.execute(@insert, [10, str]) 173 | row = @db.execute("select data from ex") 174 | assert_equal @db.encoding, row.first.first.encoding 175 | assert_equal str.encode("UTF-8"), row.first.first 176 | end 177 | end 178 | end 179 | -------------------------------------------------------------------------------- /test/test_integration_aggregate.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | class IntegrationAggregateTestCase < SQLite3::TestCase 4 | def setup 5 | @db = SQLite3::Database.new(":memory:") 6 | @db.transaction do 7 | @db.execute "create table foo ( a integer primary key, b text, c integer )" 8 | @db.execute "insert into foo ( b, c ) values ( 'foo', 10 )" 9 | @db.execute "insert into foo ( b, c ) values ( 'bar', 11 )" 10 | @db.execute "insert into foo ( b, c ) values ( 'bar', 12 )" 11 | end 12 | end 13 | 14 | def teardown 15 | @db.close 16 | end 17 | 18 | def test_create_aggregate_without_block 19 | step = proc do |ctx, a| 20 | ctx[:sum] ||= 0 21 | ctx[:sum] += a.to_i 22 | end 23 | 24 | final = proc { |ctx| ctx.result = ctx[:sum] } 25 | 26 | @db.create_aggregate("accumulate", 1, step, final) 27 | 28 | value = @db.get_first_value("select accumulate(a) from foo") 29 | assert_equal 6, value 30 | 31 | # calling #get_first_value twice don't add up to the latest result 32 | value = @db.get_first_value("select accumulate(a) from foo") 33 | assert_equal 6, value 34 | end 35 | 36 | def test_create_aggregate_with_block 37 | @db.create_aggregate("accumulate", 1) do 38 | step do |ctx, a| 39 | ctx[:sum] ||= 0 40 | ctx[:sum] += a.to_i 41 | end 42 | 43 | finalize { |ctx| ctx.result = ctx[:sum] } 44 | end 45 | 46 | value = @db.get_first_value("select accumulate(a) from foo") 47 | assert_equal 6, value 48 | end 49 | 50 | def test_create_aggregate_with_group_by 51 | @db.create_aggregate("accumulate", 1) do 52 | step do |ctx, a| 53 | ctx[:sum] ||= 0 54 | ctx[:sum] += a.to_i 55 | end 56 | 57 | finalize { |ctx| ctx.result = ctx[:sum] } 58 | end 59 | 60 | values = @db.execute("select b, accumulate(c) from foo group by b order by b") 61 | assert_equal "bar", values[0][0] 62 | assert_equal 23, values[0][1] 63 | assert_equal "foo", values[1][0] 64 | assert_equal 10, values[1][1] 65 | end 66 | 67 | def test_create_aggregate_with_the_same_function_twice_in_a_query 68 | @db.create_aggregate("accumulate", 1) do 69 | step do |ctx, a| 70 | ctx[:sum] ||= 0 71 | ctx[:sum] += a.to_i 72 | end 73 | 74 | finalize { |ctx| ctx.result = ctx[:sum] } 75 | end 76 | 77 | values = @db.get_first_row("select accumulate(a), accumulate(c) from foo") 78 | assert_equal 6, values[0] 79 | assert_equal 33, values[1] 80 | end 81 | 82 | def test_create_aggregate_with_two_different_functions 83 | @db.create_aggregate("accumulate", 1) do 84 | step do |ctx, a| 85 | ctx[:sum] ||= 0 86 | ctx[:sum] += a.to_i 87 | end 88 | 89 | finalize { |ctx| ctx.result = ctx[:sum] } 90 | end 91 | 92 | @db.create_aggregate("multiply", 1) do 93 | step do |ctx, a| 94 | ctx[:sum] ||= 1 95 | ctx[:sum] *= a.to_i 96 | end 97 | 98 | finalize { |ctx| ctx.result = ctx[:sum] } 99 | end 100 | 101 | GC.start 102 | 103 | values = @db.get_first_row("select accumulate(a), multiply(c) from foo") 104 | assert_equal 6, values[0] 105 | assert_equal 1320, values[1] 106 | 107 | value = @db.get_first_value("select accumulate(c) from foo") 108 | assert_equal 33, value 109 | 110 | value = @db.get_first_value("select multiply(a) from foo") 111 | assert_equal 6, value 112 | end 113 | 114 | def test_create_aggregate_overwrite_function 115 | @db.create_aggregate("accumulate", 1) do 116 | step do |ctx, a| 117 | ctx[:sum] ||= 0 118 | ctx[:sum] += a.to_i 119 | end 120 | 121 | finalize { |ctx| ctx.result = ctx[:sum] } 122 | end 123 | 124 | value = @db.get_first_value("select accumulate(c) from foo") 125 | assert_equal 33, value 126 | 127 | GC.start 128 | 129 | @db.create_aggregate("accumulate", 1) do 130 | step do |ctx, a| 131 | ctx[:sum] ||= 1 132 | ctx[:sum] *= a.to_i 133 | end 134 | 135 | finalize { |ctx| ctx.result = ctx[:sum] } 136 | end 137 | 138 | value = @db.get_first_value("select accumulate(c) from foo") 139 | assert_equal 1320, value 140 | end 141 | 142 | def test_create_aggregate_overwrite_function_with_different_arity 143 | @db.create_aggregate("accumulate", -1) do 144 | step do |ctx, *args| 145 | ctx[:sum] ||= 0 146 | args.each { |a| ctx[:sum] += a.to_i } 147 | end 148 | 149 | finalize { |ctx| ctx.result = ctx[:sum] } 150 | end 151 | 152 | @db.create_aggregate("accumulate", 2) do 153 | step do |ctx, a, b| 154 | ctx[:sum] ||= 1 155 | ctx[:sum] *= (a.to_i + b.to_i) 156 | end 157 | 158 | finalize { |ctx| ctx.result = ctx[:sum] } 159 | end 160 | 161 | GC.start 162 | 163 | values = @db.get_first_row("select accumulate(c), accumulate(a,c) from foo") 164 | assert_equal 33, values[0] 165 | assert_equal 2145, values[1] 166 | end 167 | 168 | def test_create_aggregate_with_invalid_arity 169 | assert_raise ArgumentError do 170 | @db.create_aggregate("accumulate", 1000) do 171 | step { |ctx, *args| } 172 | finalize { |ctx| } 173 | end 174 | end 175 | end 176 | 177 | class CustomException < RuntimeError 178 | end 179 | 180 | def test_create_aggregate_with_exception_in_step 181 | @db.create_aggregate("raiseexception", 1) do 182 | step do |ctx, a| 183 | raise CustomException.new("bogus aggregate handler") 184 | end 185 | 186 | finalize { |ctx| ctx.result = 42 } 187 | end 188 | 189 | assert_raise CustomException do 190 | @db.get_first_value("select raiseexception(a) from foo") 191 | end 192 | end 193 | 194 | def test_create_aggregate_with_exception_in_finalize 195 | @db.create_aggregate("raiseexception", 1) do 196 | step do |ctx, a| 197 | raise CustomException.new("bogus aggregate handler") 198 | end 199 | 200 | finalize do |ctx| 201 | raise CustomException.new("bogus aggregate handler") 202 | end 203 | end 204 | 205 | assert_raise CustomException do 206 | @db.get_first_value("select raiseexception(a) from foo") 207 | end 208 | end 209 | 210 | def test_create_aggregate_with_no_data 211 | @db.create_aggregate("accumulate", 1) do 212 | step do |ctx, a| 213 | ctx[:sum] ||= 0 214 | ctx[:sum] += a.to_i 215 | end 216 | 217 | finalize { |ctx| ctx.result = ctx[:sum] || 0 } 218 | end 219 | 220 | value = @db.get_first_value( 221 | "select accumulate(a) from foo where a = 100" 222 | ) 223 | assert_equal 0, value 224 | end 225 | 226 | class AggregateHandler 227 | class << self 228 | def arity 229 | 1 230 | end 231 | 232 | def text_rep 233 | SQLite3::Constants::TextRep::ANY 234 | end 235 | 236 | def name 237 | "multiply" 238 | end 239 | end 240 | def step(ctx, a) 241 | ctx[:buffer] ||= 1 242 | ctx[:buffer] *= a.to_i 243 | end 244 | 245 | def finalize(ctx) 246 | ctx.result = ctx[:buffer] 247 | end 248 | end 249 | 250 | def test_aggregate_initialized_twice 251 | initialized = 0 252 | handler = Class.new(AggregateHandler) do 253 | define_method(:initialize) do 254 | initialized += 1 255 | super() 256 | end 257 | end 258 | 259 | @db.create_aggregate_handler handler 260 | @db.get_first_value("select multiply(a) from foo") 261 | @db.get_first_value("select multiply(a) from foo") 262 | assert_equal 2, initialized 263 | end 264 | 265 | def test_create_aggregate_handler_call_with_wrong_arity 266 | @db.create_aggregate_handler AggregateHandler 267 | 268 | assert_raise(SQLite3::SQLException) do 269 | @db.get_first_value("select multiply(a,c) from foo") 270 | end 271 | end 272 | 273 | class RaiseExceptionStepAggregateHandler 274 | class << self 275 | def arity 276 | 1 277 | end 278 | 279 | def text_rep 280 | SQLite3::Constants::TextRep::ANY 281 | end 282 | 283 | def name 284 | "raiseexception" 285 | end 286 | end 287 | def step(ctx, a) 288 | raise CustomException.new("bogus aggregate handler") 289 | end 290 | 291 | def finalize(ctx) 292 | ctx.result = nil 293 | end 294 | end 295 | 296 | def test_create_aggregate_handler_with_exception_step 297 | @db.create_aggregate_handler RaiseExceptionStepAggregateHandler 298 | assert_raise CustomException do 299 | @db.get_first_value("select raiseexception(a) from foo") 300 | end 301 | end 302 | 303 | class RaiseExceptionNewAggregateHandler 304 | class << self 305 | def name 306 | "raiseexception" 307 | end 308 | end 309 | def initialize 310 | raise CustomException.new("bogus aggregate handler") 311 | end 312 | 313 | def step(ctx, a) 314 | end 315 | 316 | def finalize(ctx) 317 | ctx.result = nil 318 | end 319 | end 320 | 321 | def test_create_aggregate_handler_with_exception_new 322 | @db.create_aggregate_handler RaiseExceptionNewAggregateHandler 323 | assert_raise CustomException do 324 | @db.get_first_value("select raiseexception(a) from foo") 325 | end 326 | end 327 | 328 | def test_create_aggregate_handler 329 | @db.create_aggregate_handler AggregateHandler 330 | value = @db.get_first_value("select multiply(a) from foo") 331 | assert_equal 6, value 332 | end 333 | 334 | class AccumulateAggregator 335 | def step(*args) 336 | @sum ||= 0 337 | args.each { |a| @sum += a.to_i } 338 | end 339 | 340 | def finalize 341 | @sum 342 | end 343 | end 344 | 345 | class AccumulateAggregator2 346 | def step(a, b) 347 | @sum ||= 1 348 | @sum *= (a.to_i + b.to_i) 349 | end 350 | 351 | def finalize 352 | @sum 353 | end 354 | end 355 | 356 | def test_define_aggregator_with_two_different_arities 357 | @db.define_aggregator("accumulate", AccumulateAggregator.new) 358 | @db.define_aggregator("accumulate", AccumulateAggregator2.new) 359 | 360 | GC.start 361 | 362 | values = @db.get_first_row("select accumulate(c), accumulate(a,c) from foo") 363 | assert_equal 33, values[0] 364 | assert_equal 2145, values[1] 365 | end 366 | end 367 | -------------------------------------------------------------------------------- /test/test_integration_open_close.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | class IntegrationOpenCloseTestCase < SQLite3::TestCase 4 | def test_create_close 5 | db = SQLite3::Database.new("test-create.db") 6 | assert_path_exists "test-create.db" 7 | assert_nothing_raised { db.close } 8 | ensure 9 | begin 10 | File.delete("test-create.db") 11 | rescue 12 | nil 13 | end 14 | end 15 | 16 | def test_open_close 17 | File.open("test-open.db", "w") { |f| } 18 | assert_path_exists "test-open.db" 19 | db = SQLite3::Database.new("test-open.db") 20 | assert_nothing_raised { db.close } 21 | ensure 22 | begin 23 | File.delete("test-open.db") 24 | rescue 25 | nil 26 | end 27 | end 28 | 29 | def test_bad_open 30 | assert_raise(SQLite3::CantOpenException) do 31 | SQLite3::Database.new(".") 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/test_integration_pending.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | class IntegrationPendingTestCase < SQLite3::TestCase 4 | class ThreadSynchronizer 5 | def initialize 6 | @main_to_thread = Queue.new 7 | @thread_to_main = Queue.new 8 | end 9 | 10 | def send_to_thread state 11 | @main_to_thread.push state 12 | end 13 | 14 | def send_to_main state 15 | @thread_to_main.push state 16 | end 17 | 18 | def wait_for_thread expected_state, non_block = false 19 | state = @thread_to_main.pop(non_block) 20 | raise "Invalid state #{state}. #{expected_state} is expected" if state != expected_state 21 | end 22 | 23 | def wait_for_main expected_state, non_block = false 24 | state = @main_to_thread.pop(non_block) 25 | raise "Invalid state #{state}. #{expected_state} is expected" if state != expected_state 26 | end 27 | 28 | def close_thread 29 | @thread_to_main.close 30 | end 31 | 32 | def close_main 33 | @main_to_thread.close 34 | end 35 | 36 | def close 37 | close_thread 38 | close_main 39 | end 40 | end 41 | 42 | def setup 43 | @db = SQLite3::Database.new("test.db") 44 | @db.transaction do 45 | @db.execute "create table foo ( a integer primary key, b text )" 46 | @db.execute "insert into foo ( b ) values ( 'foo' )" 47 | @db.execute "insert into foo ( b ) values ( 'bar' )" 48 | @db.execute "insert into foo ( b ) values ( 'baz' )" 49 | end 50 | end 51 | 52 | def teardown 53 | @db.close 54 | File.delete("test.db") 55 | end 56 | 57 | def test_busy_handler_impatient 58 | synchronizer = ThreadSynchronizer.new 59 | handler_call_count = 0 60 | 61 | t = Thread.new(synchronizer) do |sync| 62 | db2 = SQLite3::Database.open("test.db") 63 | db2.transaction(:exclusive) do 64 | sync.send_to_main :ready_0 65 | sync.wait_for_main :end_1 66 | end 67 | ensure 68 | db2&.close 69 | sync.close_thread 70 | end 71 | synchronizer.wait_for_thread :ready_0 72 | 73 | @db.busy_handler do 74 | handler_call_count += 1 75 | false 76 | end 77 | 78 | assert_raise(SQLite3::BusyException) do 79 | @db.execute "insert into foo (b) values ( 'from 2' )" 80 | end 81 | assert_equal 1, handler_call_count 82 | 83 | synchronizer.send_to_thread :end_1 84 | synchronizer.close_main 85 | t.join 86 | end 87 | 88 | def test_busy_timeout 89 | @db.busy_timeout 1000 90 | synchronizer = ThreadSynchronizer.new 91 | 92 | t = Thread.new(synchronizer) do |sync| 93 | db2 = SQLite3::Database.open("test.db") 94 | db2.transaction(:exclusive) do 95 | sync.send_to_main :ready_0 96 | sync.wait_for_main :end_1 97 | end 98 | ensure 99 | db2&.close 100 | sync.close_thread 101 | end 102 | synchronizer.wait_for_thread :ready_0 103 | 104 | start_time = Time.now 105 | assert_raise(SQLite3::BusyException) do 106 | @db.execute "insert into foo (b) values ( 'from 2' )" 107 | end 108 | end_time = Time.now 109 | assert_operator(end_time - start_time, :>=, 1.0) 110 | 111 | synchronizer.send_to_thread :end_1 112 | synchronizer.close_main 113 | t.join 114 | end 115 | 116 | def test_busy_handler_timeout_releases_gvl 117 | @db.busy_handler_timeout = 100 118 | 119 | t1sync = ThreadSynchronizer.new 120 | t2sync = ThreadSynchronizer.new 121 | 122 | busy = Mutex.new 123 | busy.lock 124 | 125 | count = 0 126 | active_thread = Thread.new(t1sync) do |sync| 127 | sync.send_to_main :ready 128 | sync.wait_for_main :start 129 | 130 | loop do 131 | sleep 0.005 132 | count += 1 133 | begin 134 | sync.wait_for_main :end, true 135 | break 136 | rescue ThreadError 137 | end 138 | end 139 | sync.send_to_main :done 140 | end 141 | 142 | blocking_thread = Thread.new(t2sync) do |sync| 143 | db2 = SQLite3::Database.open("test.db") 144 | db2.transaction(:exclusive) do 145 | sync.send_to_main :ready 146 | busy.lock 147 | end 148 | sync.send_to_main :done 149 | ensure 150 | db2&.close 151 | end 152 | 153 | t1sync.wait_for_thread :ready 154 | t2sync.wait_for_thread :ready 155 | 156 | t1sync.send_to_thread :start 157 | assert_raises(SQLite3::BusyException) do 158 | @db.execute "insert into foo (b) values ( 'from 2' )" 159 | end 160 | t1sync.send_to_thread :end 161 | 162 | busy.unlock 163 | t2sync.wait_for_thread :done 164 | 165 | expected = if RUBY_PLATFORM.include?("linux") 166 | # 20 is the theoretical max if timeout is 100ms and active thread sleeps 5ms 167 | 15 168 | else 169 | # in CI, macos and windows systems seem to really not thread very well, so let's set a lower bar. 170 | 2 171 | end 172 | assert_operator(count, :>=, expected) 173 | ensure 174 | active_thread&.join 175 | blocking_thread&.join 176 | 177 | t1sync&.close 178 | t2sync&.close 179 | end 180 | 181 | def test_busy_handler_outwait 182 | synchronizer = ThreadSynchronizer.new 183 | handler_call_count = 0 184 | 185 | t = Thread.new(synchronizer) do |sync| 186 | db2 = SQLite3::Database.open("test.db") 187 | db2.transaction(:exclusive) do 188 | sync.send_to_main :ready_0 189 | sync.wait_for_main :busy_handler_called_1 190 | end 191 | sync.send_to_main :end_of_transaction_2 192 | ensure 193 | db2&.close 194 | sync.close_thread 195 | end 196 | synchronizer.wait_for_thread :ready_0 197 | 198 | @db.busy_handler do |count| 199 | handler_call_count += 1 200 | synchronizer.send_to_thread :busy_handler_called_1 201 | synchronizer.wait_for_thread :end_of_transaction_2 202 | true 203 | end 204 | 205 | assert_nothing_raised do 206 | @db.execute "insert into foo (b) values ( 'from 2' )" 207 | end 208 | assert_equal 1, handler_call_count 209 | 210 | synchronizer.close_main 211 | t.join 212 | end 213 | end 214 | -------------------------------------------------------------------------------- /test/test_integration_resultset.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | class IntegrationResultSetTestCase < SQLite3::TestCase 4 | def setup 5 | @db = SQLite3::Database.new(":memory:") 6 | @db.transaction do 7 | @db.execute "create table foo ( a integer primary key, b text )" 8 | @db.execute "insert into foo ( b ) values ( 'foo' )" 9 | @db.execute "insert into foo ( b ) values ( 'bar' )" 10 | @db.execute "insert into foo ( b ) values ( 'baz' )" 11 | end 12 | @stmt = @db.prepare("select * from foo where a in ( ?, ? )") 13 | @result = @stmt.execute 14 | end 15 | 16 | def teardown 17 | @stmt.close 18 | @db.close 19 | end 20 | 21 | def test_column_names_should_be_frozen 22 | assert @stmt.columns.all?(&:frozen?) 23 | end 24 | 25 | def test_reset_unused 26 | assert_nothing_raised { @result.reset } 27 | assert_empty @result.to_a 28 | end 29 | 30 | def test_reset_used 31 | @result.to_a 32 | assert_nothing_raised { @result.reset } 33 | assert_empty @result.to_a 34 | end 35 | 36 | def test_reset_with_bind 37 | @result.to_a 38 | assert_nothing_raised { @result.reset(1, 2) } 39 | assert_equal 2, @result.to_a.length 40 | end 41 | 42 | def test_eof_inner 43 | @result.reset(1) 44 | refute_predicate @result, :eof? 45 | end 46 | 47 | def test_eof_edge 48 | @result.reset(1) 49 | @result.next # to first row 50 | @result.next # to end of result set 51 | assert_predicate @result, :eof? 52 | end 53 | 54 | def test_next_eof 55 | @result.reset(1) 56 | assert_not_nil @result.next 57 | assert_nil @result.next 58 | end 59 | 60 | def test_next_no_type_translation_no_hash 61 | @result.reset(1) 62 | assert_equal [1, "foo"], @result.next 63 | end 64 | 65 | def test_next_type_translation 66 | @result.reset(1) 67 | assert_equal [1, "foo"], @result.next 68 | end 69 | 70 | def test_next_type_translation_with_untyped_column 71 | @db.query("select count(*) from foo") do |result| 72 | assert_equal [3], result.next 73 | end 74 | end 75 | 76 | def test_type_translation_with_null_column 77 | time = "1974-07-25 14:39:00" 78 | 79 | @db.execute "create table bar ( a integer, b time, c string )" 80 | @db.execute "insert into bar (a, b, c) values (NULL, '#{time}', 'hello')" 81 | @db.execute "insert into bar (a, b, c) values (1, NULL, 'hello')" 82 | @db.execute "insert into bar (a, b, c) values (2, '#{time}', NULL)" 83 | @db.query("select * from bar") do |result| 84 | assert_equal [nil, time, "hello"], result.next 85 | assert_equal [1, nil, "hello"], result.next 86 | assert_equal [2, time, nil], result.next 87 | end 88 | end 89 | 90 | def test_real_translation 91 | @db.execute("create table foo_real(a real)") 92 | @db.execute("insert into foo_real values (42)") 93 | @db.query("select a, sum(a), typeof(a), typeof(sum(a)) from foo_real") do |result| 94 | result = result.next 95 | assert_kind_of Float, result[0] 96 | assert_kind_of Float, result[1] 97 | assert_kind_of String, result[2] 98 | assert_kind_of String, result[3] 99 | end 100 | end 101 | 102 | def test_next_results_as_hash 103 | @db.results_as_hash = true 104 | @result = @stmt.execute 105 | @result.reset(1) 106 | hash = @result.next 107 | assert_equal({"a" => 1, "b" => "foo"}, 108 | hash) 109 | assert_equal 1, hash[@result.columns[0]] 110 | assert_equal "foo", hash[@result.columns[1]] 111 | end 112 | 113 | def test_each 114 | called = 0 115 | @result.reset(1, 2) 116 | @result.each { |row| called += 1 } 117 | assert_equal 2, called 118 | end 119 | 120 | def test_enumerable 121 | @result.reset(1, 2) 122 | assert_equal 2, @result.to_a.length 123 | end 124 | 125 | def test_types 126 | assert_equal ["integer", "text"], @result.types 127 | end 128 | 129 | def test_columns 130 | assert_equal ["a", "b"], @result.columns 131 | end 132 | 133 | def test_close 134 | stmt = @db.prepare("select * from foo") 135 | result = stmt.execute 136 | refute_predicate result, :closed? 137 | result.close 138 | assert_predicate result, :closed? 139 | assert_predicate stmt, :closed? 140 | assert_raise(SQLite3::Exception) { result.reset } 141 | assert_raise(SQLite3::Exception) { result.next } 142 | assert_raise(SQLite3::Exception) { result.each } 143 | assert_raise(SQLite3::Exception) { result.close } 144 | assert_raise(SQLite3::Exception) { result.types } 145 | assert_raise(SQLite3::Exception) { result.columns } 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /test/test_integration_statement.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | class IntegrationStatementTestCase < SQLite3::TestCase 4 | def setup 5 | @db = SQLite3::Database.new(":memory:") 6 | @db.transaction do 7 | @db.execute "create table foo ( a integer primary key, b text )" 8 | @db.execute "insert into foo ( b ) values ( 'foo' )" 9 | @db.execute "insert into foo ( b ) values ( 'bar' )" 10 | @db.execute "insert into foo ( b ) values ( 'baz' )" 11 | end 12 | @stmt = @db.prepare("select * from foo where a in ( ?, :named )") 13 | end 14 | 15 | def teardown 16 | @stmt.close 17 | @db.close 18 | end 19 | 20 | def test_remainder_empty 21 | assert_equal "", @stmt.remainder 22 | end 23 | 24 | def test_remainder_nonempty 25 | called = false 26 | @db.prepare("select * from foo;\n blah") do |stmt| 27 | called = true 28 | assert_equal "\n blah", stmt.remainder 29 | end 30 | assert called 31 | end 32 | 33 | def test_bind_params_empty 34 | assert_nothing_raised { @stmt.bind_params } 35 | assert_empty @stmt.execute! 36 | end 37 | 38 | def test_bind_params_array 39 | @stmt.bind_params 1, 2 40 | assert_equal 2, @stmt.execute!.length 41 | end 42 | 43 | def test_bind_params_hash 44 | @stmt.bind_params ":named" => 2 45 | assert_equal 1, @stmt.execute!.length 46 | end 47 | 48 | def test_bind_params_hash_without_colon 49 | @stmt.bind_params "named" => 2 50 | assert_equal 1, @stmt.execute!.length 51 | end 52 | 53 | def test_bind_params_hash_as_symbol 54 | @stmt.bind_params named: 2 55 | assert_equal 1, @stmt.execute!.length 56 | end 57 | 58 | def test_bind_params_mixed 59 | @stmt.bind_params(1, ":named" => 2) 60 | assert_equal 2, @stmt.execute!.length 61 | end 62 | 63 | def test_bind_param_by_index 64 | @stmt.bind_params(1, 2) 65 | assert_equal 2, @stmt.execute!.length 66 | end 67 | 68 | def test_bind_param_by_name_bad 69 | assert_raise(SQLite3::Exception) { @stmt.bind_param("@named", 2) } 70 | end 71 | 72 | def test_bind_param_by_name_good 73 | @stmt.bind_param(":named", 2) 74 | assert_equal 1, @stmt.execute!.length 75 | end 76 | 77 | def test_bind_param_with_various_types 78 | @db.transaction do 79 | @db.execute "create table all_types ( a integer primary key, b float, c string, d integer )" 80 | @db.execute "insert into all_types ( b, c, d ) values ( 1.5, 'hello', 68719476735 )" 81 | end 82 | 83 | assert_equal 1, @db.execute("select * from all_types where b = ?", 1.5).length 84 | assert_equal 1, @db.execute("select * from all_types where c = ?", "hello").length 85 | assert_equal 1, @db.execute("select * from all_types where d = ?", 68719476735).length 86 | end 87 | 88 | def test_execute_no_bind_no_block 89 | assert_instance_of SQLite3::ResultSet, @stmt.execute 90 | end 91 | 92 | def test_execute_with_bind_no_block 93 | assert_instance_of SQLite3::ResultSet, @stmt.execute(1, 2) 94 | end 95 | 96 | def test_execute_no_bind_with_block 97 | called = false 98 | @stmt.execute { |row| called = true } 99 | assert called 100 | end 101 | 102 | def test_execute_with_bind_with_block 103 | called = 0 104 | @stmt.execute(1, 2) { |row| called += 1 } 105 | assert_equal 1, called 106 | end 107 | 108 | def test_reexecute 109 | r = @stmt.execute(1, 2) 110 | assert_equal 2, r.to_a.length 111 | assert_nothing_raised { r = @stmt.execute(1, 2) } 112 | assert_equal 2, r.to_a.length 113 | end 114 | 115 | def test_execute_bang_no_bind_no_block 116 | assert_empty @stmt.execute! 117 | end 118 | 119 | def test_execute_bang_with_bind_no_block 120 | assert_equal 2, @stmt.execute!(1, 2).length 121 | end 122 | 123 | def test_execute_bang_no_bind_with_block 124 | called = 0 125 | @stmt.execute! { |row| called += 1 } 126 | assert_equal 0, called 127 | end 128 | 129 | def test_execute_bang_with_bind_with_block 130 | called = 0 131 | @stmt.execute!(1, 2) { |row| called += 1 } 132 | assert_equal 2, called 133 | end 134 | 135 | def test_columns 136 | c1 = @stmt.columns 137 | c2 = @stmt.columns 138 | assert_same c1, c2 139 | assert_equal 2, c1.length 140 | end 141 | 142 | def test_columns_computed 143 | called = false 144 | @db.prepare("select count(*) from foo") do |stmt| 145 | called = true 146 | assert_equal ["count(*)"], stmt.columns 147 | end 148 | assert called 149 | end 150 | 151 | def test_types 152 | t1 = @stmt.types 153 | t2 = @stmt.types 154 | assert_same t1, t2 155 | assert_equal 2, t1.length 156 | end 157 | 158 | def test_types_computed 159 | called = false 160 | @db.prepare("select count(*) from foo") do |stmt| 161 | called = true 162 | assert_equal [nil], stmt.types 163 | end 164 | assert called 165 | end 166 | 167 | def test_close 168 | stmt = @db.prepare("select * from foo") 169 | refute_predicate stmt, :closed? 170 | stmt.close 171 | assert_predicate stmt, :closed? 172 | assert_raise(SQLite3::Exception) { stmt.execute } 173 | assert_raise(SQLite3::Exception) { stmt.execute! } 174 | assert_raise(SQLite3::Exception) { stmt.close } 175 | assert_raise(SQLite3::Exception) { stmt.bind_params 5 } 176 | assert_raise(SQLite3::Exception) { stmt.bind_param 1, 5 } 177 | assert_raise(SQLite3::Exception) { stmt.columns } 178 | assert_raise(SQLite3::Exception) { stmt.types } 179 | end 180 | 181 | def test_committing_tx_with_statement_active 182 | called = false 183 | @db.prepare("select count(*) from foo") do |stmt| 184 | called = true 185 | count = stmt.execute!.first.first.to_i 186 | @db.transaction do 187 | @db.execute "insert into foo ( b ) values ( 'hello' )" 188 | end 189 | new_count = stmt.execute!.first.first.to_i 190 | assert_equal new_count, count + 1 191 | end 192 | assert called 193 | end 194 | 195 | def test_long_running_statements_get_interrupted_when_statement_timeout_set 196 | @db.statement_timeout = 10 197 | assert_raises(SQLite3::InterruptException) do 198 | @db.execute <<~SQL 199 | WITH RECURSIVE r(i) AS ( 200 | VALUES(0) 201 | UNION ALL 202 | SELECT i FROM r 203 | LIMIT 100000 204 | ) 205 | SELECT i FROM r ORDER BY i LIMIT 1; 206 | SQL 207 | end 208 | @db.statement_timeout = 0 209 | end 210 | end 211 | -------------------------------------------------------------------------------- /test/test_pragmas.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | module SQLite3 4 | class TestPragmas < SQLite3::TestCase 5 | BIGENDIAN = ([1].pack("I") == [1].pack("N")) 6 | 7 | class DatabaseTracker < SQLite3::Database 8 | attr_reader :test_statements 9 | 10 | def initialize(...) 11 | @test_statements = [] 12 | super 13 | end 14 | 15 | def execute(sql, bind_vars = [], &block) 16 | @test_statements << sql 17 | super 18 | end 19 | end 20 | 21 | def setup 22 | super 23 | @db = DatabaseTracker.new(":memory:") 24 | end 25 | 26 | def teardown 27 | @db.close 28 | end 29 | 30 | def test_temp_store_mode 31 | @db.temp_store = "memory" 32 | assert_equal 2, @db.temp_store 33 | @db.temp_store = 1 34 | assert_equal 1, @db.temp_store 35 | end 36 | 37 | def test_encoding 38 | @db.encoding = "utf-16le" 39 | assert_equal Encoding.find("utf-16le"), @db.encoding 40 | end 41 | 42 | def test_pragma_errors 43 | assert_raises(SQLite3::Exception) do 44 | @db.set_enum_pragma("foo", "bar", []) 45 | end 46 | 47 | assert_raises(SQLite3::Exception) do 48 | @db.set_boolean_pragma("read_uncommitted", "foo") 49 | end 50 | 51 | assert_raises(SQLite3::Exception) do 52 | @db.set_boolean_pragma("read_uncommitted", 42) 53 | end 54 | end 55 | 56 | def test_invalid_pragma 57 | assert_raises(SQLite3::Exception) do 58 | @db.journal_mode = 0 59 | end 60 | end 61 | 62 | def test_get_boolean_pragma 63 | refute(@db.get_boolean_pragma("read_uncommitted")) 64 | end 65 | 66 | def test_set_boolean_pragma 67 | @db.set_boolean_pragma("read_uncommitted", 1) 68 | 69 | assert(@db.get_boolean_pragma("read_uncommitted")) 70 | ensure 71 | @db.set_boolean_pragma("read_uncommitted", 0) 72 | end 73 | 74 | def test_optimize_with_no_args 75 | @db.optimize 76 | 77 | assert_equal(["PRAGMA optimize"], @db.test_statements) 78 | end 79 | 80 | def test_optimize_with_args 81 | @db.optimize(Constants::Optimize::DEFAULT) 82 | @db.optimize(Constants::Optimize::ANALYZE_TABLES | Constants::Optimize::LIMIT_ANALYZE) 83 | @db.optimize(Constants::Optimize::ANALYZE_TABLES | Constants::Optimize::DEBUG) 84 | @db.optimize(Constants::Optimize::DEFAULT | Constants::Optimize::CHECK_ALL_TABLES) 85 | 86 | assert_equal( 87 | [ 88 | "PRAGMA optimize=18", 89 | "PRAGMA optimize=18", 90 | "PRAGMA optimize=3", 91 | "PRAGMA optimize=65554" 92 | ], 93 | @db.test_statements 94 | ) 95 | end 96 | 97 | def test_encoding_uppercase 98 | assert_equal(Encoding::UTF_8, @db.encoding) 99 | 100 | @db.encoding = "UTF-16" 101 | native = BIGENDIAN ? Encoding::UTF_16BE : Encoding::UTF_16LE 102 | assert_equal(native, @db.encoding) 103 | 104 | @db.encoding = "UTF-16LE" 105 | assert_equal(Encoding::UTF_16LE, @db.encoding) 106 | 107 | @db.encoding = "UTF-16BE" 108 | assert_equal(Encoding::UTF_16BE, @db.encoding) 109 | 110 | @db.encoding = "UTF-8" 111 | assert_equal(Encoding::UTF_8, @db.encoding) 112 | end 113 | 114 | def test_encoding_lowercase 115 | assert_equal(Encoding::UTF_8, @db.encoding) 116 | 117 | @db.encoding = "utf-16" 118 | native = BIGENDIAN ? Encoding::UTF_16BE : Encoding::UTF_16LE 119 | assert_equal(native, @db.encoding) 120 | 121 | @db.encoding = "utf-16le" 122 | assert_equal(Encoding::UTF_16LE, @db.encoding) 123 | 124 | @db.encoding = "utf-16be" 125 | assert_equal(Encoding::UTF_16BE, @db.encoding) 126 | 127 | @db.encoding = "utf-8" 128 | assert_equal(Encoding::UTF_8, @db.encoding) 129 | end 130 | 131 | def test_encoding_objects 132 | assert_equal(Encoding::UTF_8, @db.encoding) 133 | 134 | @db.encoding = Encoding::UTF_16 135 | native = BIGENDIAN ? Encoding::UTF_16BE : Encoding::UTF_16LE 136 | assert_equal(native, @db.encoding) 137 | 138 | @db.encoding = Encoding::UTF_16LE 139 | assert_equal(Encoding::UTF_16LE, @db.encoding) 140 | 141 | @db.encoding = Encoding::UTF_16BE 142 | assert_equal(Encoding::UTF_16BE, @db.encoding) 143 | 144 | @db.encoding = Encoding::UTF_8 145 | assert_equal(Encoding::UTF_8, @db.encoding) 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /test/test_resource_cleanup.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | module SQLite3 4 | # these tests will cause ruby_memcheck to report a leak if we're not cleaning up resources 5 | class TestResourceCleanup < SQLite3::TestCase 6 | def test_cleanup_unclosed_database_object 7 | 100.times do 8 | SQLite3::Database.new(":memory:") 9 | end 10 | end 11 | 12 | def test_cleanup_unclosed_statement_object 13 | 100.times do 14 | db = SQLite3::Database.new(":memory:") 15 | db.execute("create table foo(text BLOB)") 16 | db.prepare("select * from foo") 17 | end 18 | end 19 | 20 | # # this leaks the result set 21 | # def test_cleanup_unclosed_resultset_object 22 | # db = SQLite3::Database.new(':memory:') 23 | # db.execute('create table foo(text BLOB)') 24 | # stmt = db.prepare('select * from foo') 25 | # stmt.execute 26 | # end 27 | 28 | # # this leaks the incompletely-closed connection 29 | # def test_cleanup_discarded_connections 30 | # FileUtils.rm_f "test.db" 31 | # db = SQLite3::Database.new("test.db") 32 | # db.execute("create table posts (title text)") 33 | # db.execute("insert into posts (title) values ('hello')") 34 | # db.close 35 | # 100.times do 36 | # db = SQLite3::Database.new("test.db") 37 | # db.execute("select * from posts limit 1") 38 | # stmt = db.prepare("select * from posts") 39 | # stmt.execute 40 | # stmt.close 41 | # db.discard 42 | # end 43 | # ensure 44 | # FileUtils.rm_f "test.db" 45 | # end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/test_result_set.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | module SQLite3 4 | class TestResultSet < SQLite3::TestCase 5 | def setup 6 | @db = SQLite3::Database.new ":memory:" 7 | super 8 | end 9 | 10 | def teardown 11 | super 12 | @db.close 13 | end 14 | 15 | def test_each_hash 16 | @db.execute "create table foo ( a integer primary key, b text )" 17 | list = ("a".."z").to_a 18 | list.each do |t| 19 | @db.execute "insert into foo (b) values (\"#{t}\")" 20 | end 21 | 22 | rs = @db.prepare("select * from foo").execute 23 | rs.each_hash do |hash| 24 | assert_equal list[hash["a"] - 1], hash["b"] 25 | end 26 | rs.close 27 | end 28 | 29 | def test_next_hash 30 | @db.execute "create table foo ( a integer primary key, b text )" 31 | list = ("a".."z").to_a 32 | list.each do |t| 33 | @db.execute "insert into foo (b) values (\"#{t}\")" 34 | end 35 | 36 | rs = @db.prepare("select * from foo").execute 37 | rows = [] 38 | while (row = rs.next_hash) 39 | rows << row 40 | end 41 | rows.each do |hash| 42 | assert_equal list[hash["a"] - 1], hash["b"] 43 | end 44 | rs.close 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/test_sqlite3.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | module SQLite3 4 | class TestSQLite3 < SQLite3::TestCase 5 | def test_libversion 6 | assert_not_nil SQLite3.libversion 7 | end 8 | 9 | def test_threadsafe 10 | assert_not_nil SQLite3.threadsafe 11 | end 12 | 13 | def test_threadsafe? 14 | if SQLite3.threadsafe > 0 15 | assert_predicate SQLite3, :threadsafe? 16 | else 17 | refute_predicate SQLite3, :threadsafe? 18 | end 19 | end 20 | 21 | def test_compiled_version_and_loaded_version 22 | assert_equal(SQLite3::SQLITE_VERSION, SQLite3::SQLITE_LOADED_VERSION) 23 | end 24 | 25 | def test_status 26 | status = SQLite3.status(SQLite3::Constants::Status::MEMORY_USED) 27 | assert_operator(status.fetch(:current), :>=, 0) 28 | assert_operator(status.fetch(:highwater), :>=, status.fetch(:current)) 29 | end 30 | 31 | def test_status_reset_highwater_mark 32 | status = SQLite3.status(SQLite3::Constants::Status::MEMORY_USED, false) 33 | assert_operator(status.fetch(:current), :>=, 0) 34 | assert_operator(status.fetch(:highwater), :>=, status.fetch(:current)) 35 | 36 | status = SQLite3.status(SQLite3::Constants::Status::MEMORY_USED, true) 37 | assert_operator(status.fetch(:current), :>=, 0) 38 | assert_operator(status.fetch(:highwater), :>=, status.fetch(:current)) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/test_statement_execute.rb: -------------------------------------------------------------------------------- 1 | require "helper" 2 | 3 | module SQLite3 4 | class TestStatementExecute < SQLite3::TestCase 5 | def setup 6 | @db = SQLite3::Database.new(":memory:") 7 | @db.execute_batch( 8 | "CREATE TABLE items (id integer PRIMARY KEY, number integer)" 9 | ) 10 | end 11 | 12 | def teardown 13 | @db.close 14 | end 15 | 16 | def test_execute_insert 17 | ps = @db.prepare("INSERT INTO items (number) VALUES (:n)") 18 | ps.execute("n" => 10) 19 | assert_equal 1, @db.get_first_value("SELECT count(*) FROM items") 20 | ps.close 21 | end 22 | 23 | def test_execute_update 24 | @db.execute("INSERT INTO items (number) VALUES (?)", [10]) 25 | 26 | ps = @db.prepare("UPDATE items SET number = :new WHERE number = :old") 27 | ps.execute("old" => 10, "new" => 20) 28 | assert_equal 20, @db.get_first_value("SELECT number FROM items") 29 | ps.close 30 | end 31 | 32 | def test_execute_delete 33 | @db.execute("INSERT INTO items (number) VALUES (?)", [20]) 34 | ps = @db.prepare("DELETE FROM items WHERE number = :n") 35 | ps.execute("n" => 20) 36 | assert_equal 0, @db.get_first_value("SELECT count(*) FROM items") 37 | ps.close 38 | end 39 | end 40 | end 41 | --------------------------------------------------------------------------------