diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..6bb8db45f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +*.bundle +Gemfile.lock +spec/configuration.yml +spec/my.cnf +tmp +vendor diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..90d122bb8 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,80 @@ +name: Build +on: [push, pull_request] +jobs: + build: + name: >- + ${{ matrix.os }} ruby ${{ matrix.ruby }} ${{ matrix.db }} + runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.allow-failure || false }} + strategy: + matrix: + include: + # Ruby 3.x on Ubuntu 24.04 LTS + - {os: ubuntu-24.04, ruby: 'head', db: mysql84} + - {os: ubuntu-24.04, ruby: '3.4', db: mysql84} + + # Ruby 3.x on Ubuntu 22.04 LTS + - {os: ubuntu-22.04, ruby: '3.4', db: mysql80} + - {os: ubuntu-22.04, ruby: '3.3', db: mysql80} + - {os: ubuntu-22.04, ruby: '3.2', db: mysql80} + - {os: ubuntu-22.04, ruby: '3.1', db: mysql80} + - {os: ubuntu-22.04, ruby: '3.0', db: mysql80} + + # Ruby 2.x on Ubuntu 20.04 LTS + - {os: ubuntu-20.04, ruby: '2.7', db: mysql80} + - {os: ubuntu-20.04, ruby: '2.6', db: mysql80} + - {os: ubuntu-20.04, ruby: '2.5', db: mysql80} + - {os: ubuntu-20.04, ruby: '2.4', db: mysql80} + - {os: ubuntu-20.04, ruby: '2.3', db: mysql80} + - {os: ubuntu-20.04, ruby: '2.2', db: mysql80} + - {os: ubuntu-20.04, ruby: '2.1', db: mysql80} + - {os: ubuntu-20.04, ruby: '2.0', db: mysql80} + + # MySQL 5.7 packages stopped after Ubuntu 18.04 Bionic + # - {os: ubuntu-18.04, ruby: '2.7', db: mysql57} + + # MariaDB LTS versions + # db: on Linux, ci/setup.sh installs the specified packages + # db: on MacOS, installs a Homebrew package use "name@X.Y" to specify a version + + - {os: ubuntu-24.04, ruby: '3.4', db: mariadb11.4} + - {os: ubuntu-22.04, ruby: '3.0', db: mariadb10.11} + - {os: ubuntu-22.04, ruby: '2.7', db: mariadb10.11} + - {os: ubuntu-22.04, ruby: '3.0', db: mariadb10.6} + - {os: ubuntu-20.04, ruby: '2.7', db: mariadb10.6} + + # TODO - Windows CI + # - {os: windows-2022, ruby: '3.2', db: mysql80} + # - {os: windows-2022, ruby: '2.7', db: mysql80} + + # Allow failure due to this issue: + # https://github.com/brianmario/mysql2/issues/1194 + - {os: macos-latest, ruby: '3.4', db: mariadb@11.4, ssl: openssl@3, allow-failure: true} + - {os: macos-latest, ruby: '3.4', db: mysql@8.4, ssl: openssl@3, allow-failure: true} + - {os: macos-latest, ruby: '2.6', db: mysql@8.0, ssl: openssl@1.1, allow-failure: true} + + # On the fail-fast: true, it cancels all in-progress jobs + # if any matrix job fails, which we don't want. + fail-fast: false + env: + BUNDLE_WITHOUT: development + # reduce MacOS CI time, don't need to clean a runtime that isn't saved + HOMEBREW_NO_INSTALL_CLEANUP: 1 + HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1 + steps: + - uses: actions/checkout@v3 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + - if: runner.os == 'Linux' || runner.os == 'macOS' + run: sudo echo "127.0.0.1 mysql2gem.example.com" | sudo tee -a /etc/hosts + - if: runner.os == 'Windows' + run: echo "127.0.0.1 mysql2gem.example.com" | tee -a C:/Windows/System32/drivers/etc/hosts + - run: echo 'DB=${{ matrix.db }}' >> $GITHUB_ENV + - run: bash ci/setup.sh + # Set the verbose option in the Makefile to print compiling command lines. + - run: echo "MAKEFLAGS=V=1" >> $GITHUB_ENV + - if: matrix.ssl + run: echo "rake_spec_opts=--with-openssl-dir=$(brew --prefix ${{ matrix.ssl }})" >> $GITHUB_ENV + - run: bundle exec rake spec -- $rake_spec_opts diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml new file mode 100644 index 000000000..d53c78d2f --- /dev/null +++ b/.github/workflows/container.yml @@ -0,0 +1,37 @@ +# Test Linux distributions which do not exist on GitHub Actions +# by the containers. +name: Container +on: [push, pull_request] +jobs: + build: + name: >- + ${{ matrix.distro }} ${{ matrix.image }} ${{ matrix.name_extra || '' }} + runs-on: ubuntu-20.04 # focal + continue-on-error: ${{ matrix.allow-failure || false }} + strategy: + matrix: + include: + # CentOS 7 system Ruby is the fixed version 2.0.0. + - {distro: centos, image: 'centos:7', name_extra: 'ruby 2.0.0'} + # Fedora latest stable version + - {distro: fedora, image: 'fedora:latest'} + # Fedora development version + - {distro: fedora, image: 'fedora:rawhide', ssl_cert_dir: '/tmp/mysql2', ssl_cert_host: 'localhost'} + # On the fail-fast: true, it cancels all in-progress jobs + # if any matrix job fails unlike Travis fast_finish. + fail-fast: false + steps: + - uses: actions/checkout@v3 + - run: docker build -t mysql2 -f ci/Dockerfile_${{ matrix.distro }} --build-arg IMAGE=${{ matrix.image }} . + # Add the "--cap-add=... --security-opt seccomp=..." options + # as a temporary workaround to avoid the following issue + # in the Fedora >= 34 containers. + # https://bugzilla.redhat.com/show_bug.cgi?id=1900021 + - run: | + docker run \ + --add-host=${{ matrix.ssl_cert_host || 'mysql2gem.example.com' }}:127.0.0.1 \ + -t \ + -e TEST_RUBY_MYSQL2_SSL_CERT_DIR="${{ matrix.ssl_cert_dir || '' }}" \ + -e TEST_RUBY_MYSQL2_SSL_CERT_HOST="${{ matrix.ssl_cert_host || '' }}" \ + --cap-add=SYS_PTRACE --security-opt seccomp=unconfined \ + mysql2 diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml new file mode 100644 index 000000000..ad8ef10ef --- /dev/null +++ b/.github/workflows/rubocop.yml @@ -0,0 +1,18 @@ +name: RuboCop + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + env: + BUNDLE_WITHOUT: development + steps: + - uses: actions/checkout@v5 + - name: Set up Ruby 3.4 + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.4 + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + - name: Run RuboCop + run: bundle exec rubocop diff --git a/.gitignore b/.gitignore index 4aada83d2..adecb40ed 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,7 @@ tmp vendor lib/mysql2/mysql2.rb spec/configuration.yml +spec/my.cnf +Gemfile.lock +.ruby-version +.rvmrc diff --git a/.rbenv-version b/.rbenv-version deleted file mode 100644 index 77fee73a8..000000000 --- a/.rbenv-version +++ /dev/null @@ -1 +0,0 @@ -1.9.3 diff --git a/.rspec b/.rspec index 61354bc67..49eab5626 100644 --- a/.rspec +++ b/.rspec @@ -1,3 +1,4 @@ ---format documentation --colour ---fail-fast +--format documentation +--order rand +--warnings diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 000000000..856e99da9 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,36 @@ +inherit_from: .rubocop_todo.yml + +AllCops: + TargetRubyVersion: 2.0 + SuggestExtensions: false + NewCops: disable + + DisplayCopNames: true + Exclude: + - 'pkg/**/*' + - 'tmp/**/*' + - 'vendor/**/*' + +Layout/CaseIndentation: + EnforcedStyle: end + +Layout/FirstHashElementIndentation: + EnforcedStyle: consistent + +Layout/EndAlignment: + EnforcedStyleAlignWith: variable + +Layout/HashAlignment: + EnforcedHashRocketStyle: table + +Style/TrailingCommaInArguments: + EnforcedStyleForMultiline: consistent_comma + +Style/TrailingCommaInArrayLiteral: + EnforcedStyleForMultiline: consistent_comma + +Style/TrailingCommaInHashLiteral: + EnforcedStyleForMultiline: consistent_comma + +Style/TrivialAccessors: + AllowPredicates: true diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 000000000..9a0c4b62b --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,155 @@ +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2025-09-23 00:00:00 UTC using RuboCop version 1.80.2. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 2 +# Configuration parameters: AllowedMethods. +# AllowedMethods: enums +Lint/ConstantDefinitionInBlock: + Exclude: + - 'spec/mysql2/client_spec.rb' + - 'tasks/rspec.rake' + +# Offense count: 1 +# Configuration parameters: AllowedParentClasses. +Lint/MissingSuper: + Exclude: + - 'lib/mysql2/em.rb' + +# Offense count: 2 +# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. +Metrics/AbcSize: + Max: 89 + +# Offense count: 34 +# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. +# AllowedMethods: refine +Metrics/BlockLength: + Max: 477 + +# Offense count: 1 +# Configuration parameters: CountBlocks, CountModifierForms. +Metrics/BlockNesting: + Max: 5 + +# Offense count: 1 +# Configuration parameters: CountComments, CountAsOne. +Metrics/ClassLength: + Max: 141 + +# Offense count: 3 +# Configuration parameters: AllowedMethods, AllowedPatterns. +Metrics/CyclomaticComplexity: + Max: 32 + +# Offense count: 6 +# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. +Metrics/MethodLength: + Max: 52 + +# Offense count: 2 +# Configuration parameters: AllowedMethods, AllowedPatterns. +Metrics/PerceivedComplexity: + Max: 30 + +# Offense count: 2 +# Configuration parameters: ForbiddenDelimiters. +# ForbiddenDelimiters: (?i-mx:(^|\s)(EO[A-Z]{1}|END)(\s|$)) +Naming/HeredocDelimiterNaming: + Exclude: + - 'tasks/compile.rake' + +# Offense count: 9 +# Configuration parameters: AllowedConstants. +Style/Documentation: + Exclude: + - 'spec/**/*' + - 'test/**/*' + - 'benchmark/active_record.rb' + - 'benchmark/allocations.rb' + - 'lib/mysql2.rb' + - 'lib/mysql2/client.rb' + - 'lib/mysql2/em.rb' + - 'lib/mysql2/error.rb' + - 'lib/mysql2/result.rb' + - 'lib/mysql2/statement.rb' + +# Offense count: 6 +# This cop supports safe autocorrection (--autocorrect). +Style/ExpandPathArguments: + Exclude: + - 'ext/mysql2/extconf.rb' + - 'mysql2.gemspec' + - 'spec/mysql2/client_spec.rb' + - 'support/mysql_enc_to_ruby.rb' + - 'tasks/compile.rake' + +# Offense count: 17 +# Configuration parameters: AllowedVariables. +Style/GlobalVars: + Exclude: + - 'ext/mysql2/extconf.rb' + +# Offense count: 7 +# This cop supports safe autocorrection (--autocorrect). +Style/IfUnlessModifier: + Exclude: + - 'lib/mysql2.rb' + - 'lib/mysql2/client.rb' + - 'spec/mysql2/client_spec.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowMethodComparison, ComparisonsThreshold. +Style/MultipleComparison: + Exclude: + - 'lib/mysql2/client.rb' + +# Offense count: 24 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: Strict, AllowedNumbers, AllowedPatterns. +Style/NumericLiterals: + MinDigits: 20 + +# Offense count: 14 +# This cop supports unsafe autocorrection (--autocorrect-all). +# Configuration parameters: Mode. +Style/StringConcatenation: + Exclude: + - 'benchmark/active_record.rb' + - 'benchmark/active_record_threaded.rb' + - 'benchmark/allocations.rb' + - 'benchmark/escape.rb' + - 'benchmark/query_with_mysql_casting.rb' + - 'benchmark/query_without_mysql_casting.rb' + - 'benchmark/sequel.rb' + - 'benchmark/setup_db.rb' + - 'ext/mysql2/extconf.rb' + - 'lib/mysql2/client.rb' + - 'tasks/compile.rake' + +# Offense count: 805 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. +# SupportedStyles: single_quotes, double_quotes +Style/StringLiterals: + Enabled: false + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: WordRegex. +# SupportedStyles: percent, brackets +Style/WordArray: + EnforcedStyle: percent + MinSize: 4 + +# Offense count: 43 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings. +# URISchemes: http, https +Layout/LineLength: + Max: 232 diff --git a/.rvmrc b/.rvmrc deleted file mode 100644 index ed6705c46..000000000 --- a/.rvmrc +++ /dev/null @@ -1 +0,0 @@ -rvm use 1.9.3@mysql2 --create diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 1fbe6e356..000000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: ruby -rvm: - - 1.8.7 - - 1.9.2 - - 1.9.3 - - 2.0.0 - - ree - - rbx-18mode - - rbx-19mode -bundler_args: --without benchmarks -script: - - bundle exec rake - - bundle exec rspec diff --git a/CHANGELOG.md b/CHANGELOG.md index d84fecd29..9a5bd0f25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,276 +1 @@ -# Changelog - -## 0.3.12 (not yet released) -## 0.3.12b5 (December 14, 2012) -* builds on Ruby 2.0-head and Rubinius 2.0-dev -* encoding names now stored in a Gperf lookup rather than an array -* long-standing bug fix: options set on a single query must not be applied to subsequent queries -* add method warning_count -* add method abandon_results! -* add setter for reconnect option -* remove options method (added in 0.3.12b1) -* support microsecond Time resolution -* several INT / UINT fixes - -## 0.3.12b4 (August 22, 2012) -* add write_timeout as well - -## 0.3.12b3 (August 22, 2012) -* several INT / LONG fixes -* fix linking to MySQL 5.5 - -## 0.3.12b2 (August 10, 2012) -* more_results is now more_results? - -## 0.3.12b1 (August 8, 2012) -* several threading and async bug fixes -* better handling of read and write timeouts -* add :local_infile connection option -* add MULTI_STATEMENTS connection flag and methods store_result, next_result, more_results -* add select_db and options methods -* add :stream query option -* add support for utf8mb4 encoding -* deprecation warnings for the :user, :pass, :hostname, :dbname, :db, :sock connection options - -## 0.3.11 (December 6th, 2011) -* change mysql error detection strategy from using mysql_field_count to the more explicit mysql_errno -* bugfix to avoid race condition with active connections that error out -* revert back to using xmalloc/xfree for allocations -* avoid potentially unsafe Ruby C API usage w/o GVL -* reacquire GVL before retrying on EINTR on connect - -## 0.3.10 (November 9th, 2011) - -## 0.3.9 (November 9th, 2011) - -## 0.3.8 (November 9th, 2011) -* remove fiber support from mysql2, the code has moved to the - em-synchrony gem. -* use rb_wait_for_single_fd() if available -* fixed a bug with inheriting query options -* remove ext/ from the default loadpath -* fix build issues on OSX with Xcode 4.2 (gcc-llvm compiler) - -## 0.3.7 (August 16th, 2011) -* ensure symbolized column names support encodings in 1.9 - -## 0.3.6 (June 17th, 2011) -* fix bug in Time/DateTime range detection -* (win32) fix bug where the Mysql2::Client object wasn't cleaned up properly if interrupted during a query -* add Mysql2::Result#count (aliased as size) to get the row count for the dataset - this can be especially helpful if you want to get the number of rows without having to inflate - the entire dataset into ruby (since this happens lazily) - -## 0.3.5 (June 15th, 2011) -* bug fix for Time/DateTime usage depending on 32/64bit Ruby - -## 0.3.4 (June 15th, 2011) -* fix a long standing bug where a signal would interrupt rb_thread_select and put the connection in a permanently broken state -* turn on casting in the ActiveRecord again, users can disable it if they need to for performance reasons - -## 0.3.3 (June 14th, 2011) -* disable async support, and access to the underlying file descriptor under Windows. It's never worked reliably and ruby-core has a lot of work to do in order to make it possible. -* added support for turning eager-casting off. This is especially useful in ORMs that will lazily cast values upon access. -* added a warning if a 0.2.x release is being used with ActiveRecord 3.1 since both the 0.2.x releases and AR 3.1 have mysql2 adapters, we want you to use the one in AR 3.1 -* added Mysql2::Client.escape (class-level method) -* disabled eager-casting in the bundled ActiveRecord adapter (for Rails 3.0 or less) - -## 0.3.2 (April 26th, 2011) -* Fix typo in initialization for older ActiveRecord versions - -## 0.3.1 (April 26th, 2011) -* Fix typo in initialization for older ActiveRecord versions - -## 0.3.0 (April 26th, 2011) -* switch to MySQL Connector/C for win32 builds -* win32 bugfixes -* BREAKING CHANGE: the ActiveRecord adapter has been pulled into Rails 3.1 and is no longer part of the gem -* added Mysql2::Client.escape (class-level) for raw one-off non-encoding-aware escaping - -## 0.2.18 (December 6th, 2011) -* change mysql error detection strategy from using mysql_field_count to the more explicit mysql_errno -* bugfix to avoid race condition with active connections that error out -* revert back to using xmalloc/xfree for allocations -* avoid potentially unsafe Ruby C API usage w/o GVL -* reacquire GVL before retrying on EINTR on connect - -## 0.2.17 (November 9th, 2011) - -## 0.2.16 (November 9th, 2011) - -## 0.2.15 (November 9th, 2011) - -## 0.2.14 (November 9th, 2011) -* use rb_wait_for_single_fd() if available -* fixed a bug with inheriting query options -* remove ext/ from the default loadpath -* fix build issues on OSX with Xcode 4.2 (gcc-llvm compiler) - -## 0.2.13 (August 16th, 2011) -* fix stupid bug around symbol encoding support (thanks coderrr!) - -## 0.2.12 (August 16th, 2011) -* ensure symbolized column names support encodings in 1.9 -* plugging sql vulnerability in mysql2 adapter - -## 0.2.11 (June 17th, 2011) -* fix bug in Time/DateTime range detection -* (win32) fix bug where the Mysql2::Client object wasn't cleaned up properly if interrupted during a query -* add Mysql2::Result#count (aliased as size) to get the row count for the dataset - this can be especially helpful if you want to get the number of rows without having to inflate - the entire dataset into ruby (since this happens lazily) - -## 0.2.10 (June 15th, 2011) -* bug fix for Time/DateTime usage depending on 32/64bit Ruby - -## 0.2.9 (June 15th, 2011) -* fix a long standing bug where a signal would interrupt rb_thread_select and put the connection in a permanently broken state -* turn on casting in the ActiveRecord again, users can disable it if they need to for performance reasons - -## 0.2.8 (June 14th, 2011) -* disable async support, and access to the underlying file descriptor under Windows. It's never worked reliably and ruby-core has a lot of work to do in order to make it possible. -* added support for turning eager-casting off. This is especially useful in ORMs that will lazily cast values upon access. -* added a warning if a 0.2.x release is being used with ActiveRecord 3.1 since both the 0.2.x releases and AR 3.1 have mysql2 adapters, we want you to use the one in AR 3.1 -* added Mysql2::Client.escape (class-level method) -* disabled eager-casting in the bundled ActiveRecord adapter (for Rails 3.0 or less) - -## 0.2.7 (March 28th, 2011) -* various fixes for em_mysql2 and fiber usage -* use our own Mysql2IndexDefinition class for better compatibility across ActiveRecord versions -* ensure the query is a string earlier in the Mysql2::Client#query codepath for 1.9 -* only set binary ruby encoding on fields that have a binary flag *and* encoding set -* a few various optimizations -* add support for :read_timeout to be set on a connection -* Fix to install with MariDB on Windows -* add fibered em connection without activerecord -* fix some 1.9.3 compilation warnings -* add LD_RUN_PATH when using hard coded mysql paths - this should help users with MySQL installed in non-standard locations -* for windows support, duplicate the socket from libmysql and create a temporary CRT fd -* fix for handling years before 1970 on Windows -* fixes to the Fiber adapter -* set wait_timeout maximum on Windows to 2147483 -* update supported range for Time objects -* upon being required, make sure the libmysql we're using is the one we were built against -* add Mysql2::Client#thread_id -* add Mysql2::Client#ping -* switch connection check in AR adapter to use Mysql2::Client#ping for efficiency -* prefer linking against thread-safe version of libmysqlclient -* define RSTRING_NOT_MODIFIED for an awesome rbx speed boost -* expose Mysql2::Client#encoding in 1.9, make sure we set the error message and sqlstate encodings accordingly -* do not segfault when raising for invalid charset (found in 1.9.3dev) - -## 0.2.6 (October 19th, 2010) -* version bump since the 0.2.5 win32 binary gems were broken - -## 0.2.5 (October 19th, 2010) -* fixes for easier Win32 binary gem deployment for targeting 1.8 and 1.9 in the same gem -* refactor of connection checks and management to avoid race conditions with the GC/threading to prevent the unexpected loss of connections -* update the default flags during connection -* add support for setting wait_timeout on AR adapter -* upgrade to rspec2 -* bugfix for an edge case where the GC would clean up a Mysql2::Client object before the underlying MYSQL pointer had been initialized -* fix to CFLAGS to allow compilation on SPARC with sunstudio compiler - Anko painting - -## 0.2.4 (September 17th, 2010) -* a few patches for win32 support from Luis Lavena - thanks man! -* bugfix from Eric Wong to avoid a potential stack overflow during Mysql2::Client#escape -* added the ability to turn internal row caching on/off via the :cache_rows => true/false option -* a couple of small patches for rbx compatibility -* set IndexDefinition#length in AR adapter - Kouhei Yanagita -* fix a long-standing data corruption bug - thank you thank you thank you to @joedamato (http://github.com/ice799) -* bugfix from calling mysql_close on a closed/freed connection surfaced by the above fix - -## 0.2.3 (August 20th, 2010) -* connection flags can now be passed to the constructor via the :flags key -* switch AR adapter connection over to use FOUND_ROWS option -* patch to ensure we use DateTime objects in place of Time for timestamps that are out of the supported range on 32bit platforms < 1.9.2 - -## 0.2.2 (August 19th, 2010) -* Change how AR adapter would send initial commands upon connecting -** we can make multiple session variable assignments in a single query -* fix signal handling when waiting on queries -* retry connect if interrupted by signals - -## 0.2.1 (August 16th, 2010) -* bring mysql2 ActiveRecord adapter back into gem - -## 0.2.0 (August 16th, 2010) -* switch back to letting libmysql manage all allocation/thread-state/freeing for the connection -* cache various numeric type conversions in hot-spots of the code for a little speed boost -* ActiveRecord adapter moved into Rails 3 core -** Don't worry 2.3.x users! We'll either release the adapter as a separate gem, or try to get it into 2.3.9 -* Fix for the "closed MySQL connection" error (GH #31) -* Fix for the "can't modify frozen object" error in 1.9.2 (GH #37) -* Introduce cascading query and result options (more info in README) -* Sequel adapter pulled into core (will be in the next release - 3.15.0 at the time of writing) -* add a safety check when attempting to send a query before a result has been fetched - -## 0.1.9 (July 17th, 2010) -* Support async ActiveRecord access with fibers and EventMachine (mperham) -* string encoding support for 1.9, respecting Encoding.default_internal -* added support for rake-compiler (tenderlove) -* bugfixes for ActiveRecord driver -** one minor bugfix for TimeZone support -** fix the select_rows method to return what it should according to the docs (r-stu31) -* Mysql2::Client#fields method added - returns the array of field names from a resultset, as strings -* Sequel adapter -** bugfix regarding sybolized field names (Eric Wong) -** fix query logging in Sequel adapter -* Lots of nice code cleanup (tenderlove) -** Mysql2::Error definition moved to pure-Ruby -** Mysql2::client#initialize definition moved to pure-Ruby -** Mysql2::Result partially moved to pure-Ruby - -## 0.1.8 (June 2nd, 2010) -* fixes for AR adapter for timezone juggling -* fixes to be able to run benchmarks and specs under 1.9.2 - -## 0.1.7 (May 22nd, 2010) -* fix a bug when using the disconnect! method on a closed connection in the AR driver - -## 0.1.6 (May 14th, 2010) -* more fixes to the AR adapter related to casting -* add missing index creation override method to AR adapter -* added sql_state and error_number methods to the Mysql2::Error exception class - -## 0.1.5 (May 12th, 2010) -* quite a few patches from Eric Wong related to thread-safety, non-blocking I/O and general cleanup -** wrap mysql_real_connect with rb_thread_blocking_region -** release GVL for possibly blocking mysql_* library calls -** [cleanup] quiet down warnings -** [cleanup] make all C symbols static -** add Mysql2::Client#close method -** correctly free the wrapped result in case of EOF -** Fix memory leak from the result wrapper struct itself -** make Mysql2::Client destructor safely non-blocking -* bug fixes for ActiveRecord adapter -** added casting for default values since they all come back from Mysql as strings (!?!) -** missing constant was added -** fixed a typo in the show_variable method -* switched over sscanf for date/time parsing in C -* made some specs a little finer-grained -* initial Sequel adapter added -* updated query benchmarks to reflect the difference between casting in C and in Ruby - -## 0.1.4 (April 23rd, 2010) -* optimization: implemented a local cache for rows that are lazily created in ruby during iteration. The MySQL C result is freed as soon as all the results have been cached -* optimization: implemented a local cache for field names so every row reuses the same objects as field names/keys -* refactor the Mysql2 connection adapter for ActiveRecord to not extend the Mysql adapter - now being a free-standing connection adapter - -## 0.1.3 (April 15th, 2010) -* added an EventMachine Deferrable API -* added an ActiveRecord connection adapter -** should be compatible with 2.3.5 and 3.0 (including Arel) - -## 0.1.2 (April 9th, 2010) -* fix a bug (copy/paste fail) around checking for empty TIME values and returning nil (thanks @marius) - -## 0.1.1 (April 6th, 2010) -* added affected_rows method (mysql_affected_rows) -* added last_id method (last_insert_id) -* enable reconnect option by default -* added initial async query support -* updated extconf (thanks to the mysqlplus project) for easier gem building - -## 0.1.0 (April 6th, 2010) -* initial release +Changes are maintained under [Releases](https://github.com/brianmario/mysql2/releases) diff --git a/Gemfile b/Gemfile index c0dfbf294..b8335fd20 100644 --- a/Gemfile +++ b/Gemfile @@ -1,12 +1,42 @@ -source :rubygems +source 'https://rubygems.org/' gemspec -# benchmarks -group :benchmarks do - gem 'activerecord' - gem 'mysql' +if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.2") + gem 'rake', '~> 13.0' +else + gem 'rake', '< 13' +end +gem 'rake-compiler', '~> 1.2.0' + +# For local debugging, irb is Gemified since Ruby 2.6 +gem 'irb', require: false + +group :test do + gem 'eventmachine' unless RUBY_PLATFORM =~ /mswin|mingw/ + gem 'rspec', '~> 3.2' + + gem 'rubocop' +end + +group :benchmarks, optional: true do + gem 'activerecord', '>= 3.0' + gem 'benchmark-ips' gem 'do_mysql' - gem 'sequel' gem 'faker' + # The installation of the mysql latest version 2.9.1 fails on Ruby >= 2.4. + gem 'mysql' if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.4') + gem 'sequel' end + +group :development do + gem 'pry' + gem 'rake-compiler-dock', '~> 0.7.0' +end + +# On MRI Ruby >= 3.0, rubysl-rake causes the conflict on GitHub Actions. +# platforms :rbx do +# gem 'rubysl-bigdecimal' +# gem 'rubysl-drb' +# gem 'rubysl-rake' +# end diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index 1fa6de09f..000000000 --- a/Gemfile.lock +++ /dev/null @@ -1,61 +0,0 @@ -PATH - remote: . - specs: - mysql2 (0.3.12b6) - -GEM - remote: http://rubygems.org/ - specs: - activemodel (3.2.1) - activesupport (= 3.2.1) - builder (~> 3.0.0) - activerecord (3.2.1) - activemodel (= 3.2.1) - activesupport (= 3.2.1) - arel (~> 3.0.0) - tzinfo (~> 0.3.29) - activesupport (3.2.1) - i18n (~> 0.6) - multi_json (~> 1.0) - addressable (2.2.6) - arel (3.0.0) - builder (3.0.0) - data_objects (0.10.8) - addressable (~> 2.1) - diff-lcs (1.1.3) - do_mysql (0.10.8) - data_objects (= 0.10.8) - eventmachine (0.12.10) - faker (1.0.1) - i18n (~> 0.4) - i18n (0.6.0) - multi_json (1.0.4) - mysql (2.8.1) - rake (0.9.4) - rake-compiler (0.8.1) - rake - rspec (2.8.0) - rspec-core (~> 2.8.0) - rspec-expectations (~> 2.8.0) - rspec-mocks (~> 2.8.0) - rspec-core (2.8.0) - rspec-expectations (2.8.0) - diff-lcs (~> 1.1.2) - rspec-mocks (2.8.0) - sequel (3.32.0) - tzinfo (0.3.31) - -PLATFORMS - ruby - -DEPENDENCIES - activerecord - do_mysql - eventmachine - faker - mysql - mysql2! - rake (~> 0.9.3) - rake-compiler (~> 0.8.1) - rspec (~> 2.8.0) - sequel diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..3a995e6b4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Brian Lopez + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MIT-LICENSE b/MIT-LICENSE deleted file mode 100644 index f80c5eedc..000000000 --- a/MIT-LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2010-2011 Brian Lopez - http://github.com/brianmario - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index ae93b3008..c35b0b49b 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,157 @@ -# Mysql2 - A modern, simple and very fast Mysql library for Ruby - binding to libmysql +# Mysql2 - A modern, simple and very fast MySQL library for Ruby - binding to libmysql -[![Build Status](https://travis-ci.org/brianmario/mysql2.png)](https://travis-ci.org/brianmario/mysql2) +GitHub Actions +[![GitHub Actions Status: Build](https://github.com/brianmario/mysql2/actions/workflows/build.yml/badge.svg)](https://github.com/brianmario/mysql2/actions/workflows/build.yml) +[![GitHub Actions Status: Container](https://github.com/brianmario/mysql2/actions/workflows/container.yml/badge.svg)](https://github.com/brianmario/mysql2/actions/workflows/container.yml) +Appveyor CI +[![Appveyor CI Status](https://ci.appveyor.com/api/projects/status/github/sodabrew/mysql2)](https://ci.appveyor.com/project/sodabrew/mysql2) The Mysql2 gem is meant to serve the extremely common use-case of connecting, querying and iterating on results. -Some database libraries out there serve as direct 1:1 mappings of the already complex C API's available. +Some database libraries out there serve as direct 1:1 mappings of the already complex C APIs available. This one is not. -It also forces the use of UTF-8 [or binary] for the connection [and all strings in 1.9, unless Encoding.default_internal is set then it'll convert from UTF-8 to that encoding] and uses encoding-aware MySQL API calls where it can. +It also forces the use of UTF-8 [or binary] for the connection and uses encoding-aware MySQL API calls where it can. -The API consists of two classes: +The API consists of three classes: -Mysql2::Client - your connection to the database +`Mysql2::Client` - your connection to the database. -Mysql2::Result - returned from issuing a #query on the connection. It includes Enumerable. +`Mysql2::Result` - returned from issuing a #query on the connection. It includes Enumerable. + +`Mysql2::Statement` - returned from issuing a #prepare on the connection. Execute the statement to get a Result. ## Installing +### General Instructions + ``` sh gem install mysql2 ``` -This gem links against MySQL's `libmysqlclient` C shared library. You may need to install a package such as `libmysqlclient-dev`, `mysql-devel`, or other appropriate package for your system. +This gem links against MySQL's `libmysqlclient` library or `Connector/C` +library, and compatible alternatives such as MariaDB. +You may need to install a package such as `libmariadb-dev`, `libmysqlclient-dev`, +`mysql-devel`, or other appropriate package for your system. See below for +system-specific instructions. + +By default, the mysql2 gem will try to find a copy of MySQL in this order: + +* Option `--with-mysql-dir`, if provided (see below). +* Option `--with-mysql-config`, if provided (see below). +* Several typical paths for `mysql_config` (default for the majority of users). +* The directory `/usr/local`. + +### Configuration options + +Use these options by `gem install mysql2 -- [--optionA] [--optionB=argument]`. + +* `--with-mysql-dir[=/path/to/mysqldir]` - +Specify the directory where MySQL is installed. The mysql2 gem will not use +`mysql_config`, but will instead look at `mysqldir/lib` and `mysqldir/include` +for the library and header files. +This option is mutually exclusive with `--with-mysql-config`. + +* `--with-mysql-config[=/path/to/mysql_config]` - +Specify a path to the `mysql_config` binary provided by your copy of MySQL. The +mysql2 gem will ask this `mysql_config` binary about the compiler and linker +arguments needed. +This option is mutually exclusive with `--with-mysql-dir`. + +* `--with-mysql-rpath=/path/to/mysql/lib` / `--without-mysql-rpath` - +Override the runtime path used to find the MySQL libraries. +This may be needed if you deploy to a system where these libraries +are located somewhere different than on your build system. +This overrides any rpath calculated by default or by the options above. + +* `--with-openssl-dir[=/path/to/openssl]` - Specify the directory where OpenSSL +is installed. In most cases, the Ruby runtime and MySQL client libraries will +link against a system-installed OpenSSL library and this option is not needed. +Use this option when non-default library paths are needed. + +* `--with-sanitize[=address,cfi,integer,memory,thread,undefined]` - +Enable sanitizers for Clang / GCC. If no argument is given, try to enable +all sanitizers or fail if none are available. If a command-separated list of +specific sanitizers is given, configure will fail unless they all are available. +Note that the some sanitizers may incur a performance penalty, and the Address +Sanitizer may require a runtime library. +To see line numbers in backtraces, declare these environment variables +(adjust the llvm-symbolizer path as needed for your system): + +``` sh + export ASAN_SYMBOLIZER_PATH=/usr/bin/llvm-symbolizer-3.4 + export ASAN_OPTIONS=symbolize=1 +``` + +### Linux and other Unixes + +You may need to install a package such as `libmariadb-dev`, `libmysqlclient-dev`, +`mysql-devel`, or `default-libmysqlclient-dev`; refer to your distribution's package guide to +find the particular package. The most common issue we see is a user who has +the library file `libmysqlclient.so` but is missing the header file `mysql.h` +-- double check that you have the _-dev_ packages installed. + +### macOS + + +You may use Homebrew, MacPorts, or a native MySQL installer package. The most +common paths will be automatically searched. If you want to select a specific +MySQL directory, use the `--with-mysql-dir` or `--with-mysql-config` options above. + +If you have not done so already, you will need to install the XCode select tools by running +`xcode-select --install`. + +Later versions of MacOS no longer distribute a linkable OpenSSL library. It is +common to use Homebrew or MacPorts to install OpenSSL. Make sure that both the +Ruby runtime and MySQL client libraries are compiled with the same OpenSSL +family, 3.x, since only one can be loaded at runtime. + +``` sh +$ brew install openssl@3 zstd +$ gem install mysql2 -- --with-openssl-dir=$(brew --prefix openssl@3) + +or + +$ sudo port install openssl3 +``` + +Since most Ruby projects use Bundler, you can set build options in the Bundler +config rather than manually installing a global mysql2 gem. This example shows +how to set build arguments with [Bundler config](https://bundler.io/man/bundle-config.1.html): + +``` sh +$ bundle config --local build.mysql2 -- --with-openssl-dir=$(brew --prefix openssl@3) +``` + +Another helpful trick is to use the same OpenSSL library that your Ruby was +built with, if it was built with an alternate OpenSSL path. This example finds +the argument `--with-openssl-dir=/some/path` from the Ruby build and adds that +to the [Bundler config](https://bundler.io/man/bundle-config.1.html): + +``` sh +$ bundle config --local build.mysql2 -- $(ruby -r rbconfig -e 'puts RbConfig::CONFIG["configure_args"]' | xargs -n1 | grep with-openssl-dir) +``` + +Note the additional double dashes (`--`) these separate command-line arguments +that `gem` or `bundler` interpret from the additional arguments that are passed +to the mysql2 build process. -If you have installed MySQL to a non-standard location, add `gem install mysql2 --with-mysql-config=/some/random/path/bin/mysql_config` +### Windows + +Make sure that you have Ruby and the DevKit compilers installed. We recommend +the [Ruby Installer](http://rubyinstaller.org) distribution. + +By default, the mysql2 gem will download and use MySQL Connector/C from +mysql.com. If you prefer to use a local installation of Connector/C, add the +flag `--with-mysql-dir=c:/mysql-connector-c-x-y-z` (_this path may use forward slashes_). + +By default, the `libmysql.dll` library will be copied into the mysql2 gem +directory. To prevent this, add the flag `--no-vendor-libmysql`. The mysql2 gem +will search for `libmysql.dll` in the following paths, in order: + +* Environment variable `RUBY_MYSQL2_LIBMYSQL_DLL=C:\path\to\libmysql.dll` + (_note the Windows-style backslashes_). +* In the mysql2 gem's own directory `vendor/libmysql.dll` +* In the system's default library search paths. ## Usage @@ -31,7 +160,7 @@ Connect to a database: ``` ruby # this takes a hash of options, almost all of which map directly # to the familiar database.yml in rails -# See http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/MysqlAdapter.html +# See http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Mysql2Adapter.html client = Mysql2::Client.new(:host => "localhost", :username => "root") ``` @@ -57,7 +186,10 @@ results.each do |row| # conveniently, row is a hash # the keys are the fields, as you'd expect # the values are pre-built ruby primitives mapped from their corresponding field types in MySQL - # Here's an otter: http://farm1.static.flickr.com/130/398077070_b8795d0ef3_b.jpg + puts row["id"] # row["id"].is_a? Integer + if row["dne"] # non-existent hash entry is nil + puts row["dne"] + end end ``` @@ -72,23 +204,61 @@ end How about with symbolized keys? ``` ruby -# NOTE: the :symbolize_keys and future options will likely move to the #query method soon -client.query("SELECT * FROM users WHERE group='githubbers'").each(:symbolize_keys => true) do |row| +client.query("SELECT * FROM users WHERE group='githubbers'", :symbolize_keys => true).each do |row| # do something with row, it's ready to rock end ``` -You can get the headers and the columns in the order that they were returned +You can get the headers, columns, and the field types in the order that they were returned by the query like this: ``` ruby headers = results.fields # <= that's an array of field names, in order +types = results.field_types # <= that's an array of field types, in order results.each(:as => :array) do |row| -# Each row is an array, ordered the same as the query results -# An otter's den is called a "holt" or "couch" + # Each row is an array, ordered the same as the query results + # An otter's den is called a "holt" or "couch" end ``` +Prepared statements are supported, as well. In a prepared statement, use a `?` +in place of each value and then execute the statement to retrieve a result set. +Pass your arguments to the execute method in the same number and order as the +question marks in the statement. Query options can be passed as keyword arguments +to the execute method. + +Be sure to read about the known limitations of prepared statements at +[https://dev.mysql.com/doc/refman/5.6/en/c-api-prepared-statement-problems.html](https://dev.mysql.com/doc/refman/5.6/en/c-api-prepared-statement-problems.html) + +``` ruby +statement = @client.prepare("SELECT * FROM users WHERE login_count = ?") +result1 = statement.execute(1) +result2 = statement.execute(2) + +statement = @client.prepare("SELECT * FROM users WHERE last_login >= ? AND location LIKE ?") +result = statement.execute(1, "CA") + +statement = @client.prepare("SELECT * FROM users WHERE last_login >= ? AND location LIKE ?") +result = statement.execute(1, "CA", :as => :array) +``` + +Session Tracking information can be accessed with + +``` ruby +c = Mysql2::Client.new( + host: "127.0.0.1", + username: "root", + flags: "SESSION_TRACK", + init_command: "SET @@SESSION.session_track_schema=ON" +) +c.query("INSERT INTO test VALUES (1)") +session_track_type = Mysql2::Client::SESSION_TRACK_SCHEMA +session_track_data = c.session_track(session_track_type) +``` + +The types of session track types can be found at +[https://dev.mysql.com/doc/refman/5.7/en/session-state-tracking.html](https://dev.mysql.com/doc/refman/5.7/en/session-state-tracking.html) + ## Connection options You may set the following connection options in Mysql2::Client.new(...): @@ -102,30 +272,184 @@ Mysql2::Client.new( :database, :socket = '/path/to/mysql.sock', :flags = REMEMBER_OPTIONS | LONG_PASSWORD | LONG_FLAG | TRANSACTIONS | PROTOCOL_41 | SECURE_CONNECTION | MULTI_STATEMENTS, - :encoding = 'utf8', + :encoding = 'utf8mb4', :read_timeout = seconds, :write_timeout = seconds, :connect_timeout = seconds, + :connect_attrs = {:program_name => $PROGRAM_NAME, ...}, :reconnect = true/false, :local_infile = true/false, + :secure_auth = true/false, + :get_server_public_key = true/false, + :default_file = '/path/to/my.cfg', + :default_group = 'my.cfg section', + :default_auth = 'authentication_windows_client' + :init_command => sql + ) +``` + +### Connecting to MySQL on localhost and elsewhere + +The underlying MySQL client library uses the `:host` parameter to determine the +type of connection to make, with special interpretation you should be aware of: + +* An empty value or `"localhost"` will attempt a local connection: + * On Unix, connect to the default local socket path. (To set a custom socket + path, use the `:socket` parameter). + * On Windows, connect using a shared-memory connection, if enabled, or TCP. +* A value of `"."` on Windows specifies a named-pipe connection. +* An IPv4 or IPv6 address will result in a TCP connection. +* Any other value will be looked up as a hostname for a TCP connection. + +### SSL/TLS options + +Setting any of the following options will enable an SSL/TLS connection, but +only if your MySQL client library and server have been compiled with SSL +support. MySQL client library defaults will be used for any parameters that are +left out or set to nil. Relative paths are allowed, and may be required by +managed hosting providers such as Heroku. + +``` ruby +Mysql2::Client.new( + # ...options as above..., + :sslkey => '/path/to/client-key.pem', + :sslcert => '/path/to/client-cert.pem', + :sslca => '/path/to/ca-cert.pem', + :sslcapath => '/path/to/cacerts', + :sslcipher => 'DHE-RSA-AES256-SHA', + :sslverify => true, # Removed in MySQL 8.0 + :ssl_mode => :disabled / :preferred / :required / :verify_ca / :verify_identity, ) ``` -You can also retrieve multiple result sets. For this to work you need to connect with -flags `Mysql2::Client::MULTI_STATEMENTS`. Using multiple result sets is normally used -when calling stored procedures that return more than one result set +For MySQL versions 5.7.11 and higher, use `:ssl_mode` to prefer or require an +SSL connection and certificate validation. For earlier versions of MySQL, use +the `:sslverify` boolean. For details on each of the `:ssl_mode` options, see +[https://dev.mysql.com/doc/refman/8.0/en/connection-options.html](https://dev.mysql.com/doc/refman/8.0/en/connection-options.html#option_general_ssl-mode). + +The `:ssl_mode` option will also set the appropriate MariaDB connection flags: + +| `:ssl_mode` | MariaDB option value | +| --- | --- | +| `:disabled` | MYSQL_OPT_SSL_ENFORCE = 0 | +| `:required` | MYSQL_OPT_SSL_ENFORCE = 1 | +| `:verify_identity` | MYSQL_OPT_SSL_VERIFY_SERVER_CERT = 1 | + +MariaDB does not support the `:preferred` or `:verify_ca` options. For more +information about SSL/TLS in MariaDB, see +[https://mariadb.com/kb/en/securing-connections-for-client-and-server/](https://mariadb.com/kb/en/securing-connections-for-client-and-server/) +and [https://mariadb.com/kb/en/mysql_optionsv/#tls-options](https://mariadb.com/kb/en/mysql_optionsv/#tls-options) + +### Secure auth + +Starting with MySQL 5.6.5, secure_auth is enabled by default on servers (it was disabled by default prior to this). +When secure_auth is enabled, the server will refuse a connection if the account password is stored in old pre-MySQL 4.1 format. +The MySQL 5.6.5 client library may also refuse to attempt a connection if provided an older format password. +To bypass this restriction in the client, pass the option `:secure_auth => false` to Mysql2::Client.new(). + +### Flags option parsing + +The `:flags` parameter accepts an integer, a string, or an array. The integer +form allows the client to assemble flags from constants defined under +`Mysql2::Client` such as `Mysql2::Client::FOUND_ROWS`. Use a bitwise `|` (OR) +to specify several flags. + +The string form will be split on whitespace and parsed as with the array form: +Plain flags are added to the default flags, while flags prefixed with `-` +(minus) are removed from the default flags. + +### Using Active Record's database.yml + +Active Record typically reads its configuration from a file named `database.yml` or an environment variable `DATABASE_URL`. +Use the value `mysql2` as the adapter name. For example: + +``` yaml +development: + adapter: mysql2 + encoding: utf8mb4 + database: my_db_name + username: root + password: my_password + host: 127.0.0.1 + port: 3306 + flags: + - -COMPRESS + - FOUND_ROWS + - MULTI_STATEMENTS + secure_auth: false +``` + +In this example, the compression flag is negated with `-COMPRESS`. + +### Using Active Record's DATABASE_URL + +Active Record typically reads its configuration from a file named `database.yml` or an environment variable `DATABASE_URL`. +Use the value `mysql2` as the protocol name. For example: + +``` sh +DATABASE_URL=mysql2://sql_user:sql_pass@sql_host_name:port/sql_db_name?option1=value1&option2=value2 +``` + +### Reading a MySQL config file + +You may read configuration options from a MySQL configuration file by passing +the `:default_file` and `:default_group` parameters. For example: + +``` ruby +Mysql2::Client.new(:default_file => '/user/.my.cnf', :default_group => 'client') +``` + +### Initial command on connect and reconnect + +If you specify the `:init_command` option, the SQL string you provide will be executed after the connection is established. +If `:reconnect` is set to `true`, init_command will also be executed after a successful reconnect. +It is useful if you want to provide session options which survive reconnection. + +``` ruby +Mysql2::Client.new(:init_command => "SET @@SESSION.sql_mode = 'STRICT_ALL_TABLES'") +``` + +### Multiple result sets + +You can also retrieve multiple result sets. For this to work you need to +connect with flags `Mysql2::Client::MULTI_STATEMENTS`. Multiple result sets can +be used with stored procedures that return more than one result set, and for +bundling several SQL statements into a single call to `client.query`. ``` ruby -client = Mysql2::Client.new(:host => "localhost", :username => "root", :flags => Mysql2::Client::MULTI_STATEMENTS ) -result = client.query( 'CALL sp_customer_list( 25, 10 )') +client = Mysql2::Client.new(:host => "localhost", :username => "root", :flags => Mysql2::Client::MULTI_STATEMENTS) +result = client.query('CALL sp_customer_list( 25, 10 )') # result now contains the first result set -while ( client.next_result) - result = client.store_result - # result now contains the next result set +while client.next_result + result = client.store_result + # result now contains the next result set end ``` -See https://gist.github.com/1367987 for using MULTI_STATEMENTS with Active Record. +Repeated calls to `client.next_result` will return true, false, or raise an +exception if the respective query erred. When `client.next_result` returns true, +call `client.store_result` to retrieve a result object. Exceptions are not +raised until `client.next_result` is called to find the status of the respective +query. Subsequent queries are not executed if an earlier query raised an +exception. Subsequent calls to `client.next_result` will return false. + +``` ruby +result = client.query('SELECT 1; SELECT 2; SELECT A; SELECT 3') +p result.first + +while client.next_result + result = client.store_result + p result.first +end +``` + +Yields: + +``` ruby +{"1"=>1} +{"2"=>2} +next_result: Unknown column 'A' in 'field list' (Mysql2::Error) +``` ## Cascading config @@ -164,6 +488,15 @@ c = Mysql2::Client.new c.query(sql, :symbolize_keys => true) ``` +or + +``` ruby +# this will set the options for the Mysql2::Result instance returned from the #execute method +c = Mysql2::Client.new +s = c.prepare(sql) +s.execute(arg1, args2, :symbolize_keys => true) +``` + ## Result types ### Array of Arrays @@ -172,12 +505,7 @@ Pass the `:as => :array` option to any of the above methods of configuration ### Array of Hashes -The default result type is set to :hash, but you can override a previous setting to something else with :as => :hash - -### Others... - -I may add support for `:as => :csv` or even `:as => :json` to allow for *much* more efficient generation of those data types from result sets. -If you'd like to see either of these (or others), open an issue and start bugging me about it ;) +The default result type is set to `:hash`, but you can override a previous setting to something else with `:as => :hash` ### Timezones @@ -202,9 +530,18 @@ client = Mysql2::Client.new result = client.query("SELECT * FROM table_with_boolean_field", :cast_booleans => true) ``` +Keep in mind that this works only with fields and not with computed values, e.g. this result will contain `1`, not `true`: + +``` ruby +client = Mysql2::Client.new +result = client.query("SELECT true", :cast_booleans => true) +``` + +CAST function wouldn't help here as there's no way to cast to TINYINT(1). Apparently the only way to solve this is to use a stored procedure with return type set to TINYINT(1). + ### Skipping casting -Mysql2 casting is fast, but not as fast as not casting data. In rare cases where typecasting is not needed, it will be faster to disable it by providing :cast => false. +Mysql2 casting is fast, but not as fast as not casting data. In rare cases where typecasting is not needed, it will be faster to disable it by providing :cast => false. (Note that :cast => false overrides :cast_booleans => true.) ``` ruby client = Mysql2::Client.new @@ -267,25 +604,55 @@ There are a few things that need to be kept in mind while using streaming: * `:cache_rows` is ignored currently. (if you want to use `:cache_rows` you probably don't want to be using `:stream`) * You must fetch all rows in the result set of your query before you can make new queries. (i.e. with `Mysql2::Result#each`) -Read more about the consequences of using `mysql_use_result` (what streaming is implemented with) here: http://dev.mysql.com/doc/refman/5.0/en/mysql-use-result.html. +Read more about the consequences of using `mysql_use_result` (what streaming is implemented with) here: [http://dev.mysql.com/doc/refman/5.0/en/mysql-use-result.html](http://dev.mysql.com/doc/refman/5.0/en/mysql-use-result.html). + +### Lazy Everything + +Well... almost ;) + +Field name strings/symbols are shared across all the rows so only one object is ever created to represent the field name for an entire dataset. + +Rows themselves are lazily created in ruby-land when an attempt to yield it is made via #each. +For example, if you were to yield 4 rows from a 100 row dataset, only 4 hashes will be created. The rest will sit and wait in C-land until you want them (or when the GC goes to cleanup your `Mysql2::Result` instance). +Now say you were to iterate over that same collection again, this time yielding 15 rows - the 4 previous rows that had already been turned into ruby hashes would be pulled from an internal cache, then 11 more would be created and stored in that cache. +Once the entire dataset has been converted into ruby objects, Mysql2::Result will free the Mysql C result object as it's no longer needed. + +This caching behavior can be disabled by setting the `:cache_rows` option to false. + +As for field values themselves, I'm workin on it - but expect that soon. + +## Compatibility + +This gem is tested with the following Ruby versions on Linux and Mac OS X: -## Active Record +* Ruby MRI 2.0 through 2.7 (all versions to date) +* Ruby MRI 3.0, 3.1, 3.2 (all versions to date) +* Rubinius 2.x and 3.x do work but may fail under some workloads -To use the Active Record driver (with or without rails), all you should need to do is have this gem installed and set the adapter in your database.yml to "mysql2". -That was easy right? :) +This gem is tested with the following MySQL and MariaDB versions: -NOTE: as of 0.3.0, and Active Record 3.1 - the Active Record adapter has been pulled out of this gem and into Active Record itself. If you need to use mysql2 with -Rails versions < 3.1 make sure and specify `gem "mysql2", "~> 0.2.7"` in your Gemfile +* MySQL 5.5, 5.6, 5.7, 8.0 +* MySQL Connector/C 6.0, 6.1, 8.0 (primarily on Windows) +* MariaDB 5.5, 10.x, with a focus on 10.6 LTS and 10.11 LTS +* MariaDB Connector/C 2.x, 3.x -## Asynchronous Active Record +### Ruby on Rails / Active Record + +* mysql2 0.5.x works with Rails / Active Record 4.2.11, 5.0.7, 5.1.6, and higher. +* mysql2 0.4.x works with Rails / Active Record 4.2.5 - 5.0 and higher. +* mysql2 0.3.x works with Rails / Active Record 3.1, 3.2, 4.x, 5.0. +* mysql2 0.2.x works with Rails / Active Record 2.3 - 3.0. + +### Asynchronous Active Record Please see the [em-synchrony](https://github.com/igrigorik/em-synchrony) project for details about using EventMachine with mysql2 and Rails. -## Sequel +### Sequel -The Sequel adapter was pulled out into Sequel core (will be part of the next release) and can be used by specifying the "mysql2://" prefix to your connection specification. +Sequel includes a mysql2 adapter in all releases since 3.15 (2010-09-01). +Use the prefix "mysql2://" in your connection specification. -## EventMachine +### EventMachine The mysql2 EventMachine deferrable api allows you to make async queries using EventMachine, while specifying callbacks for success for failure. Here's a simple example: @@ -308,65 +675,30 @@ EM.run do end ``` -## Lazy Everything - -Well... almost ;) - -Field name strings/symbols are shared across all the rows so only one object is ever created to represent the field name for an entire dataset. - -Rows themselves are lazily created in ruby-land when an attempt to yield it is made via #each. -For example, if you were to yield 4 rows from a 100 row dataset, only 4 hashes will be created. The rest will sit and wait in C-land until you want them (or when the GC goes to cleanup your `Mysql2::Result` instance). -Now say you were to iterate over that same collection again, this time yielding 15 rows - the 4 previous rows that had already been turned into ruby hashes would be pulled from an internal cache, then 11 more would be created and stored in that cache. -Once the entire dataset has been converted into ruby objects, Mysql2::Result will free the Mysql C result object as it's no longer needed. - -This caching behavior can be disabled by setting the :cache_rows option to false. - -As for field values themselves, I'm workin on it - but expect that soon. - -## Compatibility - -The specs pass on my system (SL 10.6.3, x86_64) in these rubies: +## Benchmarks and Comparison -* 1.8.7-p249 -* ree-1.8.7-2010.01 -* 1.9.1-p378 -* ruby-trunk -* rbx-head - broken at the moment, working with the rbx team for a solution +The mysql2 gem converts MySQL field types to Ruby data types in C code, providing a serious speed benefit. -The Active Record driver should work on 2.3.5 and 3.0 +The do_mysql gem also converts MySQL fields types, but has a considerably more complex API and is still ~2x slower than mysql2. -## Yeah... but why? +The mysql gem returns only nil or string data types, leaving you to convert field values to Ruby types in Ruby-land, which is much slower than mysql2's C code. -Someone: Dude, the Mysql gem works fiiiiiine. - -Me: It sure does, but it only hands you nil and strings for field values. Leaving you to convert -them into proper Ruby types in Ruby-land - which is slow as balls. - - -Someone: OK fine, but do_mysql can already give me back values with Ruby objects mapped to MySQL types. - -Me: Yep, but it's API is considerably more complex *and* can be ~2x slower. - -## Benchmarks - -Performing a basic "SELECT * FROM" query on a table with 30k rows and fields of nearly every Ruby-representable data type, -then iterating over every row using an #each like method yielding a block: - -These results are from the `query_with_mysql_casting.rb` script in the benchmarks folder +For a comparative benchmark, the script below performs a basic "SELECT * FROM" +query on a table with 30k rows and fields of nearly every Ruby-representable +data type, then iterating over every row using an #each like method yielding a +block: ``` sh - user system total real -Mysql2 - 0.750000 0.180000 0.930000 ( 1.821655) -do_mysql - 1.650000 0.200000 1.850000 ( 2.811357) -Mysql - 7.500000 0.210000 7.710000 ( 8.065871) + user system total real +Mysql2 0.750000 0.180000 0.930000 (1.821655) +do_mysql 1.650000 0.200000 1.850000 (2.811357) +Mysql 7.500000 0.210000 7.710000 (8.065871) ``` +These results are from the `query_with_mysql_casting.rb` script in the benchmarks folder. + ## Development -To run the tests, you can use RVM and Bundler to create a pristine environment for mysql2 development/hacking. Use 'bundle install' to install the necessary development and testing gems: ``` sh @@ -392,7 +724,12 @@ though. ## Special Thanks * Eric Wong - for the contribution (and the informative explanations) of some thread-safety, non-blocking I/O and cleanup patches. You rock dude -* Yury Korolev (http://github.com/yury) - for TONS of help testing the Active Record adapter -* Aaron Patterson (http://github.com/tenderlove) - tons of contributions, suggestions and general badassness -* Mike Perham (http://github.com/mperham) - Async Active Record adapter (uses Fibers and EventMachine) -* Aaron Stone (http://github.com/sodabrew) - additional client settings, local files, microsecond time, maintenance support. +* [Yury Korolev](http://github.com/yury) - for TONS of help testing the Active Record adapter +* [Aaron Patterson](http://github.com/tenderlove) - tons of contributions, suggestions and general badassness +* [Mike Perham](http://github.com/mperham) - Async Active Record adapter (uses Fibers and EventMachine) +* [Aaron Stone](http://github.com/sodabrew) - additional client settings, local files, microsecond time, maintenance support +* [Kouhei Ueno](https://github.com/nyaxt) - for the original work on Prepared Statements way back in 2012 +* [John Cant](http://github.com/johncant) - polishing and updating Prepared Statements support +* [Justin Case](http://github.com/justincase) - polishing and updating Prepared Statements support and getting it merged +* [Tamir Duberstein](http://github.com/tamird) - for help with timeouts and all around updates and cleanups +* [Jun Aruga](http://github.com/junaruga) - for migrating CI tests to GitHub Actions and other improvements diff --git a/Rakefile b/Rakefile index 7bb616b34..4164a7ad2 100644 --- a/Rakefile +++ b/Rakefile @@ -1,5 +1,17 @@ -# encoding: UTF-8 require 'rake' -# Load custom tasks -Dir['tasks/*.rake'].sort.each { |f| load f } +# Load custom tasks (careful attention to define tasks before prerequisites) +load 'tasks/vendor_mysql.rake' +load 'tasks/rspec.rake' +load 'tasks/compile.rake' +load 'tasks/generate.rake' +load 'tasks/benchmarks.rake' + +begin + require 'rubocop/rake_task' + RuboCop::RakeTask.new + task default: %i[spec rubocop] +rescue LoadError + warn 'RuboCop is not available' + task default: :spec +end diff --git a/benchmark/active_record.rb b/benchmark/active_record.rb index 5120ac7d8..0ae442c25 100644 --- a/benchmark/active_record.rb +++ b/benchmark/active_record.rb @@ -1,51 +1,32 @@ -# encoding: UTF-8 $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib') require 'rubygems' -require 'benchmark' +require 'benchmark/ips' require 'active_record' ActiveRecord::Base.default_timezone = :local ActiveRecord::Base.time_zone_aware_attributes = true -number_of = 10 -mysql2_opts = { - :adapter => 'mysql2', - :database => 'test' -} -mysql_opts = { - :adapter => 'mysql', - :database => 'test' -} - -class Mysql2Model < ActiveRecord::Base - set_table_name :mysql2_test -end +opts = { database: 'test' } -class MysqlModel < ActiveRecord::Base - set_table_name :mysql2_test +class TestModel < ActiveRecord::Base + self.table_name = 'mysql2_test' end -Benchmark.bmbm do |x| - x.report "Mysql2" do - Mysql2Model.establish_connection(mysql2_opts) - number_of.times do - Mysql2Model.all(:limit => 1000).each{ |r| - r.attributes.keys.each{ |k| - r.send(k.to_sym) - } - } - end - end +batch_size = 1000 - x.report "Mysql" do - MysqlModel.establish_connection(mysql_opts) - number_of.times do - MysqlModel.all(:limit => 1000).each{ |r| - r.attributes.keys.each{ |k| +Benchmark.ips do |x| + %w[mysql mysql2].each do |adapter| + TestModel.establish_connection(opts.merge(adapter: adapter)) + + x.report(adapter) do + TestModel.limit(batch_size).to_a.each do |r| + r.attributes.each_key do |k| r.send(k.to_sym) - } - } + end + end end end -end \ No newline at end of file + + x.compare! +end diff --git a/benchmark/active_record_threaded.rb b/benchmark/active_record_threaded.rb index 8c828bc27..22c3f01be 100644 --- a/benchmark/active_record_threaded.rb +++ b/benchmark/active_record_threaded.rb @@ -1,42 +1,22 @@ -# encoding: UTF-8 $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib') require 'rubygems' -require 'benchmark' +require 'benchmark/ips' require 'active_record' -times = 25 +number_of_threads = 25 +opts = { database: 'test', pool: number_of_threads } +Benchmark.ips do |x| + %w[mysql mysql2].each do |adapter| + ActiveRecord::Base.establish_connection(opts.merge(adapter: adapter)) -# mysql2 -mysql2_opts = { - :adapter => 'mysql2', - :database => 'test', - :pool => times -} -ActiveRecord::Base.establish_connection(mysql2_opts) -x = Benchmark.realtime do - threads = [] - times.times do - threads << Thread.new { ActiveRecord::Base.connection.execute("select sleep(1)") } + x.report(adapter) do + Array.new(number_of_threads) do + Thread.new { ActiveRecord::Base.connection.execute('SELECT SLEEP(1)') } + end.each(&:join) + end end - threads.each {|t| t.join } -end -puts "mysql2: #{x} seconds" - -# mysql -mysql2_opts = { - :adapter => 'mysql', - :database => 'test', - :pool => times -} -ActiveRecord::Base.establish_connection(mysql2_opts) -x = Benchmark.realtime do - threads = [] - times.times do - threads << Thread.new { ActiveRecord::Base.connection.execute("select sleep(1)") } - end - threads.each {|t| t.join } + x.compare! end -puts "mysql: #{x} seconds" diff --git a/benchmark/allocations.rb b/benchmark/allocations.rb index cf0c9313e..cc6df4d18 100644 --- a/benchmark/allocations.rb +++ b/benchmark/allocations.rb @@ -1,33 +1,29 @@ -# encoding: UTF-8 $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib') -raise Mysql2::Mysql2Error.new("GC allocation benchmarks only supported on Ruby 1.9!") unless RUBY_VERSION =~ /1\.9/ - require 'rubygems' -require 'benchmark' require 'active_record' ActiveRecord::Base.default_timezone = :local ActiveRecord::Base.time_zone_aware_attributes = true -class Mysql2Model < ActiveRecord::Base - set_table_name :mysql2_test +class TestModel < ActiveRecord::Base + self.table_name = 'mysql2_test' end -def bench_allocations(feature, iterations = 10, &blk) +def bench_allocations(feature, iterations = 10, batch_size = 1000) puts "GC overhead for #{feature}" - Mysql2Model.establish_connection(:adapter => 'mysql2', :database => 'test') + TestModel.establish_connection(adapter: 'mysql2', database: 'test') GC::Profiler.clear GC::Profiler.enable - iterations.times{ blk.call } - GC::Profiler.report(STDOUT) + iterations.times { yield batch_size } + GC::Profiler.report($stdout) GC::Profiler.disable end -bench_allocations('coercion') do - Mysql2Model.all(:limit => 1000).each{ |r| - r.attributes.keys.each{ |k| +bench_allocations('coercion') do |batch_size| + TestModel.limit(batch_size).to_a.each do |r| + r.attributes.each_key do |k| r.send(k.to_sym) - } - } -end \ No newline at end of file + end + end +end diff --git a/benchmark/escape.rb b/benchmark/escape.rb index 852d52ac6..0e2320e2b 100644 --- a/benchmark/escape.rb +++ b/benchmark/escape.rb @@ -1,36 +1,32 @@ -# encoding: UTF-8 $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib') require 'rubygems' -require 'benchmark' +require 'benchmark/ips' require 'mysql' require 'mysql2' require 'do_mysql' -def run_escape_benchmarks(str, number_of = 1000) - Benchmark.bmbm do |x| +def run_escape_benchmarks(str) + Benchmark.ips do |x| mysql = Mysql.new("localhost", "root") + x.report "Mysql #{str.inspect}" do - number_of.times do - mysql.quote str - end + mysql.quote str end - mysql2 = Mysql2::Client.new(:host => "localhost", :username => "root") + mysql2 = Mysql2::Client.new(host: "localhost", username: "root") x.report "Mysql2 #{str.inspect}" do - number_of.times do - mysql2.escape str - end + mysql2.escape str end do_mysql = DataObjects::Connection.new("mysql://localhost/test") x.report "do_mysql #{str.inspect}" do - number_of.times do - do_mysql.quote_string str - end + do_mysql.quote_string str end + + x.compare! end end run_escape_benchmarks "abc'def\"ghi\0jkl%mno" -run_escape_benchmarks "clean string" \ No newline at end of file +run_escape_benchmarks "clean string" diff --git a/benchmark/query_with_mysql_casting.rb b/benchmark/query_with_mysql_casting.rb index 5b460dd7e..e63bd2fb6 100644 --- a/benchmark/query_with_mysql_casting.rb +++ b/benchmark/query_with_mysql_casting.rb @@ -1,13 +1,11 @@ -# encoding: UTF-8 $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib') require 'rubygems' -require 'benchmark' +require 'benchmark/ips' require 'mysql' require 'mysql2' require 'do_mysql' -number_of = 100 database = 'test' sql = "SELECT * FROM mysql2_test LIMIT 100" @@ -17,64 +15,57 @@ class Mysql def mysql_cast(type, value) case type - when Mysql::Field::TYPE_NULL - nil - when Mysql::Field::TYPE_TINY, Mysql::Field::TYPE_SHORT, Mysql::Field::TYPE_LONG, - Mysql::Field::TYPE_INT24, Mysql::Field::TYPE_LONGLONG, Mysql::Field::TYPE_YEAR - value.to_i - when Mysql::Field::TYPE_DECIMAL, Mysql::Field::TYPE_NEWDECIMAL - BigDecimal.new(value) - when Mysql::Field::TYPE_DOUBLE, Mysql::Field::TYPE_FLOAT - value.to_f - when Mysql::Field::TYPE_DATE - Date.parse(value) - when Mysql::Field::TYPE_TIME, Mysql::Field::TYPE_DATETIME, Mysql::Field::TYPE_TIMESTAMP - Time.parse(value) - when Mysql::Field::TYPE_BLOB, Mysql::Field::TYPE_BIT, Mysql::Field::TYPE_STRING, - Mysql::Field::TYPE_VAR_STRING, Mysql::Field::TYPE_CHAR, Mysql::Field::TYPE_SET - Mysql::Field::TYPE_ENUM - value - else - value + when Mysql::Field::TYPE_NULL + nil + when Mysql::Field::TYPE_TINY, Mysql::Field::TYPE_SHORT, Mysql::Field::TYPE_LONG, + Mysql::Field::TYPE_INT24, Mysql::Field::TYPE_LONGLONG, Mysql::Field::TYPE_YEAR + value.to_i + when Mysql::Field::TYPE_DECIMAL, Mysql::Field::TYPE_NEWDECIMAL + BigDecimal(value) + when Mysql::Field::TYPE_DOUBLE, Mysql::Field::TYPE_FLOAT + value.to_f + when Mysql::Field::TYPE_DATE + Date.parse(value) + when Mysql::Field::TYPE_TIME, Mysql::Field::TYPE_DATETIME, Mysql::Field::TYPE_TIMESTAMP + Time.parse(value) + when Mysql::Field::TYPE_BLOB, Mysql::Field::TYPE_BIT, Mysql::Field::TYPE_STRING, + Mysql::Field::TYPE_VAR_STRING, Mysql::Field::TYPE_CHAR, Mysql::Field::TYPE_SET, + Mysql::Field::TYPE_ENUM + value + else + value end end -Benchmark.bmbm do |x| - mysql2 = Mysql2::Client.new(:host => "localhost", :username => "root") +debug = ENV['DEBUG'] + +Benchmark.ips do |x| + mysql2 = Mysql2::Client.new(host: "localhost", username: "root") mysql2.query "USE #{database}" x.report "Mysql2" do - number_of.times do - mysql2_result = mysql2.query sql, :symbolize_keys => true - mysql2_result.each do |res| - # puts res.inspect - end - end + mysql2_result = mysql2.query sql, symbolize_keys: true + mysql2_result.each { |res| puts res.inspect if debug } end mysql = Mysql.new("localhost", "root") mysql.query "USE #{database}" x.report "Mysql" do - number_of.times do - mysql_result = mysql.query sql - fields = mysql_result.fetch_fields - mysql_result.each do |row| - row_hash = {} - row.each_with_index do |f, j| - row_hash[fields[j].name.to_sym] = mysql_cast(fields[j].type, row[j]) - end - # puts row_hash.inspect + mysql_result = mysql.query sql + fields = mysql_result.fetch_fields + mysql_result.each do |row| + row_hash = row.each_with_index.each_with_object({}) do |(f, j), hash| + hash[fields[j].name.to_sym] = mysql_cast(fields[j].type, f) end + puts row_hash.inspect if debug end end do_mysql = DataObjects::Connection.new("mysql://localhost/#{database}") command = do_mysql.create_command sql x.report "do_mysql" do - number_of.times do - do_result = command.execute_reader - do_result.each do |res| - # puts res.inspect - end - end + do_result = command.execute_reader + do_result.each { |res| puts res.inspect if debug } end -end \ No newline at end of file + + x.compare! +end diff --git a/benchmark/query_without_mysql_casting.rb b/benchmark/query_without_mysql_casting.rb index 3929d15f2..4080e22d1 100644 --- a/benchmark/query_without_mysql_casting.rb +++ b/benchmark/query_without_mysql_casting.rb @@ -1,56 +1,42 @@ -# encoding: UTF-8 $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib') require 'rubygems' -require 'benchmark' +require 'benchmark/ips' require 'mysql' require 'mysql2' require 'do_mysql' -number_of = 100 database = 'test' sql = "SELECT * FROM mysql2_test LIMIT 100" -Benchmark.bmbm do |x| - mysql2 = Mysql2::Client.new(:host => "localhost", :username => "root") +debug = ENV['DEBUG'] + +Benchmark.ips do |x| + mysql2 = Mysql2::Client.new(host: "localhost", username: "root") mysql2.query "USE #{database}" x.report "Mysql2 (cast: true)" do - number_of.times do - mysql2_result = mysql2.query sql, :symbolize_keys => true, :cast => true - mysql2_result.each do |res| - # puts res.inspect - end - end + mysql2_result = mysql2.query sql, symbolize_keys: true, cast: true + mysql2_result.each { |res| puts res.inspect if debug } end x.report "Mysql2 (cast: false)" do - number_of.times do - mysql2_result = mysql2.query sql, :symbolize_keys => true, :cast => false - mysql2_result.each do |res| - # puts res.inspect - end - end + mysql2_result = mysql2.query sql, symbolize_keys: true, cast: false + mysql2_result.each { |res| puts res.inspect if debug } end mysql = Mysql.new("localhost", "root") mysql.query "USE #{database}" x.report "Mysql" do - number_of.times do - mysql_result = mysql.query sql - mysql_result.each_hash do |res| - # puts res.inspect - end - end + mysql_result = mysql.query sql + mysql_result.each_hash { |res| puts res.inspect if debug } end do_mysql = DataObjects::Connection.new("mysql://localhost/#{database}") command = DataObjects::Mysql::Command.new do_mysql, sql x.report "do_mysql" do - number_of.times do - do_result = command.execute_reader - do_result.each do |res| - # puts res.inspect - end - end + do_result = command.execute_reader + do_result.each { |res| puts res.inspect if debug } end -end \ No newline at end of file + + x.compare! +end diff --git a/benchmark/sequel.rb b/benchmark/sequel.rb index 0dac47fee..a9555fa6e 100644 --- a/benchmark/sequel.rb +++ b/benchmark/sequel.rb @@ -1,37 +1,31 @@ -# encoding: UTF-8 $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib') require 'rubygems' -require 'benchmark' +require 'benchmark/ips' require 'mysql2' require 'sequel' require 'sequel/adapters/do' -number_of = 10 -mysql2_opts = "mysql2://localhost/test" -mysql_opts = "mysql://localhost/test" -do_mysql_opts = "do:mysql://localhost/test" +mysql2_opts = "mysql2://root@localhost/test" +mysql_opts = "mysql://root@localhost/test" +do_mysql_opts = "do:mysql://root@localhost/test" class Mysql2Model < Sequel::Model(Sequel.connect(mysql2_opts)[:mysql2_test]); end class MysqlModel < Sequel::Model(Sequel.connect(mysql_opts)[:mysql2_test]); end class DOMysqlModel < Sequel::Model(Sequel.connect(do_mysql_opts)[:mysql2_test]); end -Benchmark.bmbm do |x| +Benchmark.ips do |x| x.report "Mysql2" do - number_of.times do - Mysql2Model.limit(1000).all - end + Mysql2Model.limit(1000).all end x.report "do:mysql" do - number_of.times do - DOMysqlModel.limit(1000).all - end + DOMysqlModel.limit(1000).all end x.report "Mysql" do - number_of.times do - MysqlModel.limit(1000).all - end + MysqlModel.limit(1000).all end -end \ No newline at end of file + + x.compare! +end diff --git a/benchmark/setup_db.rb b/benchmark/setup_db.rb index a5395b33d..497406700 100644 --- a/benchmark/setup_db.rb +++ b/benchmark/setup_db.rb @@ -1,4 +1,3 @@ -# encoding: UTF-8 $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib') # This script is for generating psudo-random data into a single table consisting of nearly every @@ -49,9 +48,10 @@ ] # connect to localhost by default, pass options as needed -@client = Mysql2::Client.new :host => "localhost", :username => "root", :database => "test" +@client = Mysql2::Client.new host: "localhost", username: "root", database: "test" @client.query create_table_sql +@client.query 'TRUNCATE mysql2_test' def insert_record(args) insert_sql = " @@ -79,41 +79,41 @@ def insert_record(args) five_words = Faker::Lorem.words(rand(5)) twenty5_paragraphs = Faker::Lorem.paragraphs(rand(25)) insert_record( - :bit_test => 1, - :tiny_int_test => rand(128), - :small_int_test => rand(32767), - :medium_int_test => rand(8388607), - :int_test => rand(2147483647), - :big_int_test => rand(9223372036854775807), - :float_test => rand(32767)/1.87, - :float_zero_test => 0.0, - :double_test => rand(8388607)/1.87, - :decimal_test => rand(8388607)/1.87, - :decimal_zero_test => 0, - :date_test => '2010-4-4', - :date_time_test => '2010-4-4 11:44:00', - :timestamp_test => '2010-4-4 11:44:00', - :time_test => '11:44:00', - :year_test => Time.now.year, - :char_test => five_words, - :varchar_test => five_words, - :binary_test => five_words, - :varbinary_test => five_words, - :tiny_blob_test => five_words, - :tiny_text_test => Faker::Lorem.paragraph(rand(5)), - :blob_test => twenty5_paragraphs, - :text_test => twenty5_paragraphs, - :medium_blob_test => twenty5_paragraphs, - :medium_text_test => twenty5_paragraphs, - :long_blob_test => twenty5_paragraphs, - :long_text_test => twenty5_paragraphs, - :enum_test => ['val1', 'val2'].rand, - :set_test => ['val1', 'val2', 'val1,val2'].rand + bit_test: 1, + tiny_int_test: rand(128), + small_int_test: rand(32767), + medium_int_test: rand(8388607), + int_test: rand(2147483647), + big_int_test: rand(9223372036854775807), + float_test: rand(32767) / 1.87, + float_zero_test: 0.0, + double_test: rand(8388607) / 1.87, + decimal_test: rand(8388607) / 1.87, + decimal_zero_test: 0, + date_test: '2010-4-4', + date_time_test: '2010-4-4 11:44:00', + timestamp_test: '2010-4-4 11:44:00', + time_test: '11:44:00', + year_test: Time.now.year, + char_test: five_words.join.slice(0, 10), # CHAR(10) + varchar_test: five_words.join.slice(0, 10), # VARCHAR(10) + binary_test: five_words.join.byteslice(0, 10), # BINARY(10) + varbinary_test: five_words.join.byteslice(0, 10), # VARBINARY(10) + tiny_blob_test: five_words.join.byteslice(0, 255), # TINYBLOB + tiny_text_test: Faker::Lorem.paragraph(rand(5)).byteslice(0, 255), # TINYTEXT + blob_test: twenty5_paragraphs, + text_test: twenty5_paragraphs, + medium_blob_test: twenty5_paragraphs, + medium_text_test: twenty5_paragraphs, + long_blob_test: twenty5_paragraphs, + long_text_test: twenty5_paragraphs, + enum_test: %w[val1 val2].sample, + set_test: %w[val1 val2 val1,val2].sample, ) - if n % 100 == 0 + if (n % 100).zero? $stdout.putc '.' $stdout.flush end end puts -puts "Done" \ No newline at end of file +puts "Done" diff --git a/benchmark/threaded.rb b/benchmark/threaded.rb deleted file mode 100644 index 7b989a2f5..000000000 --- a/benchmark/threaded.rb +++ /dev/null @@ -1,44 +0,0 @@ -# encoding: UTF-8 -$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib') - -require 'rubygems' -require 'benchmark' -require 'active_record' - -mysql2_opts = { - :adapter => 'mysql2', - :database => 'test', - :pool => 25 -} -ActiveRecord::Base.establish_connection(mysql2_opts) -x = Benchmark.realtime do - threads = [] - 25.times do - threads << Thread.new { ActiveRecord::Base.connection.execute("select sleep(1)") } - end - threads.each {|t| t.join } -end -puts x - -mysql2_opts = { - :adapter => 'mysql', - :database => 'test', - :pool => 25 -} -ActiveRecord::Base.establish_connection(mysql2_opts) -x = Benchmark.realtime do - threads = [] - 25.times do - threads << Thread.new { ActiveRecord::Base.connection.execute("select sleep(1)") } - end - threads.each {|t| t.join } -end -puts x - -# these results are similar on 1.8.7, 1.9.2 and rbx-head -# -# $ bundle exec ruby benchmarks/threaded.rb -# 1.0774750709533691 -# -# and using the mysql gem -# 25.099437952041626 \ No newline at end of file diff --git a/ci/Dockerfile_centos b/ci/Dockerfile_centos new file mode 100644 index 000000000..dd8434782 --- /dev/null +++ b/ci/Dockerfile_centos @@ -0,0 +1,34 @@ +ARG IMAGE=centos:7 +FROM ${IMAGE} + +WORKDIR /build +COPY . . + +# mirrorlist.centos.org no longer exists, see +# https://serverfault.com/questions/1161816/mirrorlist-centos-org-no-longer-resolve/1161847#1161847 +# +# The --setopt flags to yum enable faster installs +# +RUN cat /etc/redhat-release \ + && sed -i s/mirror.centos.org/vault.centos.org/g /etc/yum.repos.d/CentOS-*.repo \ + && sed -i s/^#.*baseurl=http/baseurl=http/g /etc/yum.repos.d/CentOS-*.repo \ + && sed -i s/^mirrorlist=http/#mirrorlist=http/g /etc/yum.repos.d/CentOS-*.repo \ + && yum -y -q update \ + && yum -y -q install epel-release \ + && yum -y -q install \ + --setopt=deltarpm=0 \ + --setopt=install_weak_deps=false \ + --setopt=tsflags=nodocs \ + gcc \ + gcc-c++ \ + git \ + make \ + mariadb-devel \ + mariadb-server \ + ruby-devel + +RUN gem install --no-document "rubygems-update:~>2.7" \ + && update_rubygems > /dev/null \ + && gem install --no-document "bundler:~>1.17" + +CMD bash ci/container.sh diff --git a/ci/Dockerfile_fedora b/ci/Dockerfile_fedora new file mode 100644 index 000000000..cc645d481 --- /dev/null +++ b/ci/Dockerfile_fedora @@ -0,0 +1,28 @@ +ARG IMAGE=fedora:latest +FROM ${IMAGE} + +WORKDIR /build +COPY . . + +RUN cat /etc/fedora-release +RUN dnf -yq update +# The options are to install faster. +RUN dnf -yq install \ + --setopt=deltarpm=0 \ + --setopt=install_weak_deps=false \ + --setopt=tsflags=nodocs \ + gcc \ + gcc-c++ \ + git \ + libyaml-devel \ + make \ + mariadb-connector-c-devel \ + mariadb-server \ + openssl \ + redhat-rpm-config \ + ruby-devel \ + rubygem-bigdecimal \ + rubygem-bundler \ + rubygem-json + +CMD bash ci/container.sh diff --git a/ci/container.sh b/ci/container.sh new file mode 100644 index 000000000..88764a389 --- /dev/null +++ b/ci/container.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -eux + +ruby -v +bundle install --path vendor/bundle --without development + +# Regenerate the SSL certification files from the specified host. +if [ -n "${TEST_RUBY_MYSQL2_SSL_CERT_HOST}" ]; then + pushd spec/ssl + bash gen_certs.sh + popd +fi + +# Start mysqld service. +bash ci/setup_container.sh + +bundle exec rake spec diff --git a/ci/mariadb1011.sh b/ci/mariadb1011.sh new file mode 100644 index 000000000..6eef2b5f3 --- /dev/null +++ b/ci/mariadb1011.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -eux + +apt purge -qq '^mysql*' '^libmysql*' +rm -fr /etc/mysql +rm -fr /var/lib/mysql + +RELEASE=$(lsb_release -cs) +VERSION=10.11 + +tee <<- EOF > /etc/apt/sources.list.d/mariadb.sources + X-Repolib-Name: MariaDB + Types: deb + # URIs: https://deb.mariadb.org/$VERSION/ubuntu + URIs: https://mirror.rackspace.com/mariadb/repo/$VERSION/ubuntu + Suites: $RELEASE + Components: main main/debug + Signed-By: /etc/apt/keyrings/mariadb-keyring.asc +EOF + +cp support/C74CD1D8.asc /etc/apt/keyrings/mariadb-keyring.asc +apt update +apt install -y -o Dpkg::Options::='--force-confnew' mariadb-server libmariadb-dev diff --git a/ci/mariadb106.sh b/ci/mariadb106.sh new file mode 100644 index 000000000..82b1db693 --- /dev/null +++ b/ci/mariadb106.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -eux + +apt purge -qq '^mysql*' '^libmysql*' +rm -fr /etc/mysql +rm -fr /var/lib/mysql + +RELEASE=$(lsb_release -cs) +VERSION=10.6 + +tee <<- EOF > /etc/apt/sources.list.d/mariadb.sources + X-Repolib-Name: MariaDB + Types: deb + # URIs: https://deb.mariadb.org/$VERSION/ubuntu + URIs: https://mirror.rackspace.com/mariadb/repo/$VERSION/ubuntu + Suites: $RELEASE + Components: main main/debug + Signed-By: /etc/apt/keyrings/mariadb-keyring.asc +EOF + +cp support/C74CD1D8.asc /etc/apt/keyrings/mariadb-keyring.asc +apt update +apt install -y -o Dpkg::Options::='--force-confnew' mariadb-server-$VERSION libmariadb-dev diff --git a/ci/mariadb114.sh b/ci/mariadb114.sh new file mode 100644 index 000000000..767dd451c --- /dev/null +++ b/ci/mariadb114.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -eux + +apt purge -qq '^mysql*' '^libmysql*' +rm -fr /etc/mysql +rm -fr /var/lib/mysql + +RELEASE=$(lsb_release -cs) +VERSION=11.4 + +tee <<- EOF > /etc/apt/sources.list.d/mariadb.sources + X-Repolib-Name: MariaDB + Types: deb + # URIs: https://deb.mariadb.org/$VERSION/ubuntu + URIs: https://mirror.rackspace.com/mariadb/repo/$VERSION/ubuntu + Suites: $RELEASE + Components: main main/debug + Signed-By: /etc/apt/keyrings/mariadb-keyring.asc +EOF + +cp support/C74CD1D8.asc /etc/apt/keyrings/mariadb-keyring.asc +apt update +apt install -y -o Dpkg::Options::='--force-confnew' mariadb-server libmariadb-dev diff --git a/ci/mysql55.sh b/ci/mysql55.sh new file mode 100644 index 000000000..ffc2f9571 --- /dev/null +++ b/ci/mysql55.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -eux + +apt-get purge -qq '^mysql*' '^libmysql*' +rm -fr /etc/mysql +rm -fr /var/lib/mysql +apt-get update -qq +apt-get install -qq mysql-server-5.5 mysql-client-core-5.5 mysql-client-5.5 libmysqlclient-dev diff --git a/ci/mysql57.sh b/ci/mysql57.sh new file mode 100644 index 000000000..9cd681ff3 --- /dev/null +++ b/ci/mysql57.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -eux + +apt-get purge -qq '^mysql*' '^libmysql*' +rm -fr /etc/mysql +rm -fr /var/lib/mysql +apt-key add support/5072E1F5.asc # old signing key +apt-key add support/3A79BD29.asc # 5.7.37 and higher +apt-key add support/B7B3B788A8D3785C.asc # 8.1 and higher +# Verify the repository as add-apt-repository does not. +wget -q --spider http://repo.mysql.com/apt/ubuntu/dists/$(lsb_release -cs)/mysql-5.7 +add-apt-repository 'http://repo.mysql.com/apt/ubuntu%20mysql-5.7' +apt-get update -qq +apt-get install -qq mysql-server libmysqlclient-dev diff --git a/ci/mysql80.sh b/ci/mysql80.sh new file mode 100644 index 000000000..814412c69 --- /dev/null +++ b/ci/mysql80.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -eux + +apt-get purge -qq '^mysql*' '^libmysql*' +rm -fr /etc/mysql +rm -fr /var/lib/mysql +apt-key add support/5072E1F5.asc # old signing key +apt-key add support/3A79BD29.asc # 8.0.28 and higher +apt-key add support/B7B3B788A8D3785C.asc # 8.1 and higher +# Verify the repository as add-apt-repository does not. +wget -q --spider http://repo.mysql.com/apt/ubuntu/dists/$(lsb_release -cs)/mysql-8.0 +add-apt-repository 'http://repo.mysql.com/apt/ubuntu%20mysql-8.0' +apt-get update -qq +apt-get install -qq mysql-server libmysqlclient-dev diff --git a/ci/mysql84.sh b/ci/mysql84.sh new file mode 100644 index 000000000..5c78da558 --- /dev/null +++ b/ci/mysql84.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -eux + +apt-get purge -qq '^mysql*' '^libmysql*' +rm -fr /etc/mysql +rm -fr /var/lib/mysql +apt-key add support/B7B3B788A8D3785C.asc # 8.1 and higher +# Verify the repository as add-apt-repository does not. +wget -q --spider http://repo.mysql.com/apt/ubuntu/dists/$(lsb_release -cs)/mysql-8.4-lts +add-apt-repository 'http://repo.mysql.com/apt/ubuntu%20mysql-8.4-lts' +apt-get update -qq +apt-get install -qq mysql-server libmysqlclient-dev diff --git a/ci/setup.sh b/ci/setup.sh new file mode 100644 index 000000000..0089a384b --- /dev/null +++ b/ci/setup.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash + +set -eux + +# Change the password to be empty. +CHANGED_PASSWORD=false +CHANGED_PASSWORD_SHA2=false +# Change the password to be empty, recreating the root user on mariadb < 10.2 +# where ALTER USER is not available. +# https://stackoverflow.com/questions/56052177/ +CHANGED_PASSWORD_BY_RECREATE=false + +# Install the default used DB if DB is not set. +if [[ -n ${GITHUB_ACTIONS-} && -z ${DB-} ]]; then + if command -v lsb_release > /dev/null; then + case "$(lsb_release -cs)" in + xenial | bionic) + sudo apt-get install -qq mysql-server-5.7 mysql-client-core-5.7 mysql-client-5.7 + CHANGED_PASSWORD=true + ;; + focal) + sudo apt-get install -qq mysql-server-8.0 mysql-client-core-8.0 mysql-client-8.0 + CHANGED_PASSWORD=true + ;; + jammy) + sudo apt-get install -qq mysql-server-8.0 mysql-client-core-8.0 mysql-client-8.0 + CHANGED_PASSWORD=true + ;; + *) + ;; + esac + fi +fi + +# Install MySQL 5.5 if DB=mysql55 +if [[ -n ${DB-} && x$DB =~ ^xmysql55 ]]; then + sudo bash ci/mysql55.sh +fi + +# Install MySQL 5.7 if DB=mysql57 +if [[ -n ${DB-} && x$DB =~ ^xmysql57 ]]; then + sudo bash ci/mysql57.sh + CHANGED_PASSWORD=true +fi + +# Install MySQL 8.0 if DB=mysql80 +if [[ -n ${DB-} && x$DB =~ ^xmysql80 ]]; then + sudo bash ci/mysql80.sh + CHANGED_PASSWORD_SHA2=true +fi + +# Install MySQL 8.4 if DB=mysql84 +if [[ -n ${DB-} && x$DB =~ ^xmysql84 ]]; then + sudo bash ci/mysql84.sh + CHANGED_PASSWORD_SHA2=true +fi + +# Install MariaDB 10.6 if DB=mariadb10.6 +if [[ -n ${GITHUB_ACTIONS-} && -n ${DB-} && x$DB =~ ^xmariadb10.6 ]]; then + sudo bash ci/mariadb106.sh + CHANGED_PASSWORD_BY_RECREATE=true +fi + +# Install MariaDB 10.11 if DB=mariadb10.11 +if [[ -n ${GITHUB_ACTIONS-} && -n ${DB-} && x$DB =~ ^xmariadb10.11 ]]; then + sudo bash ci/mariadb1011.sh + CHANGED_PASSWORD_BY_RECREATE=true +fi + +# Install MariaDB 11.4 if DB=mariadb11.4 +if [[ -n ${GITHUB_ACTIONS-} && -n ${DB-} && x$DB =~ ^xmariadb11.4 ]]; then + sudo bash ci/mariadb114.sh + CHANGED_PASSWORD_BY_RECREATE=true +fi + +# Install MySQL/MariaDB if OS=darwin +if [[ x$OSTYPE =~ ^xdarwin ]]; then + brew update > /dev/null + + # Check available packages. + for KEYWORD in mysql mariadb; do + brew search "${KEYWORD}" + done + + brew info "$DB" + brew install "$DB" zstd + brew link "$DB" # explicitly activate in case of kegged LTS versions + DB_PREFIX="$(brew --prefix "${DB}")" + export PATH="${DB_PREFIX}/bin:${PATH}" + export LDFLAGS="-L${DB_PREFIX}/lib" + export CPPFLAGS="-I${DB_PREFIX}/include" + + mysql.server start + CHANGED_PASSWORD_BY_RECREATE=true +fi + +# TODO: get SSL working on OS X in Travis +if ! [[ x$OSTYPE =~ ^xdarwin ]]; then + sudo bash ci/ssl.sh + sudo service mysql restart +fi + +mysqld --version + +MYSQL_OPTS='' +DB_SYS_USER=root +if ! [[ x$OSTYPE =~ ^xdarwin ]]; then + if [[ -n ${GITHUB_ACTIONS-} && -f /etc/mysql/debian.cnf ]]; then + MYSQL_OPTS='--defaults-extra-file=/etc/mysql/debian.cnf' + # Install from packages in OS official packages. + if sudo grep -q debian-sys-maint /etc/mysql/debian.cnf; then + # bionic, focal + DB_SYS_USER=debian-sys-maint + else + # xenial + DB_SYS_USER=root + fi + fi +fi + +if [ "${CHANGED_PASSWORD}" = true ]; then + # https://www.percona.com/blog/2016/03/16/change-user-password-in-mysql-5-7-with-plugin-auth_socket/ + sudo mysql ${MYSQL_OPTS} -u "${DB_SYS_USER}" \ + -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY ''" +elif [ "${CHANGED_PASSWORD_SHA2}" = true ]; then + # In MySQL 5.7, the default authentication plugin is mysql_native_password. + # As of MySQL 8.0, the default authentication plugin is changed to caching_sha2_password. + sudo mysql ${MYSQL_OPTS} -u "${DB_SYS_USER}" \ + -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY ''" +elif [ "${CHANGED_PASSWORD_BY_RECREATE}" = true ]; then + sudo mysql ${MYSQL_OPTS} -u "${DB_SYS_USER}" </dev/null | xargs dirname) + +# Put the configs into the server +echo " +[mysqld] +ssl-ca=${SSL_CERT_DIR}/ca-cert.pem +ssl-cert=${SSL_CERT_DIR}/server-cert.pem +ssl-key=${SSL_CERT_DIR}/server-key.pem +" >> my.cnf diff --git a/examples/eventmachine.rb b/examples/eventmachine.rb index a92ca4cb1..dd8419cf2 100644 --- a/examples/eventmachine.rb +++ b/examples/eventmachine.rb @@ -1,5 +1,3 @@ -# encoding: utf-8 - $LOAD_PATH.unshift 'lib' require 'rubygems' @@ -18,4 +16,4 @@ defer2.callback do |result| puts "Result: #{result.to_a.inspect}" end -end \ No newline at end of file +end diff --git a/examples/threaded.rb b/examples/threaded.rb index 7d3b9612a..440a0081b 100644 --- a/examples/threaded.rb +++ b/examples/threaded.rb @@ -1,20 +1,16 @@ -# encoding: utf-8 - $LOAD_PATH.unshift 'lib' require 'mysql2' require 'timeout' -threads = [] # Should never exceed worst case 3.5 secs across all 20 threads Timeout.timeout(3.5) do - 20.times do - threads << Thread.new do + Array.new(20) do + Thread.new do overhead = rand(3) puts ">> thread #{Thread.current.object_id} query, #{overhead} sec overhead" # 3 second overhead per query - Mysql2::Client.new(:host => "localhost", :username => "root").query("SELECT sleep(#{overhead}) as result") + Mysql2::Client.new(host: "localhost", username: "root").query("SELECT sleep(#{overhead}) as result") puts "<< thread #{Thread.current.object_id} result, #{overhead} sec overhead" end - end - threads.each{|t| t.join } -end \ No newline at end of file + end.each(&:join) +end diff --git a/ext/mysql2/client.c b/ext/mysql2/client.c index 5edfd244e..10e0c9253 100644 --- a/ext/mysql2/client.c +++ b/ext/mysql2/client.c @@ -1,52 +1,93 @@ #include -#include + +#include #include #ifndef _WIN32 +#include #include #endif +#ifndef _MSC_VER #include +#endif +#include #include "wait_for_single_fd.h" #include "mysql_enc_name_to_ruby.h" VALUE cMysql2Client; -extern VALUE mMysql2, cMysql2Error; -static VALUE sym_id, sym_version, sym_async, sym_symbolize_keys, sym_as, sym_array, sym_stream; -static ID intern_merge, intern_merge_bang, intern_error_number_eql, intern_sql_state_eql; - -#ifndef HAVE_RB_HASH_DUP -static VALUE rb_hash_dup(VALUE other) { - return rb_funcall(rb_cHash, rb_intern("[]"), 1, other); -} -#endif +extern VALUE mMysql2, cMysql2Error, cMysql2TimeoutError; +static VALUE sym_id, sym_version, sym_header_version, sym_async, sym_symbolize_keys, sym_as, sym_array, sym_stream; +static VALUE sym_no_good_index_used, sym_no_index_used, sym_query_was_slow; +static ID intern_brackets, intern_merge, intern_merge_bang, intern_new_with_args, + intern_current_query_options, intern_read_timeout; #define REQUIRE_INITIALIZED(wrapper) \ if (!wrapper->initialized) { \ rb_raise(cMysql2Error, "MySQL client is not initialized"); \ } +#if defined(HAVE_MYSQL_NET_VIO) || defined(HAVE_ST_NET_VIO) + #define CONNECTED(wrapper) (wrapper->client->net.vio != NULL && wrapper->client->net.fd != -1) +#elif defined(HAVE_MYSQL_NET_PVIO) || defined(HAVE_ST_NET_PVIO) + #define CONNECTED(wrapper) (wrapper->client->net.pvio != NULL && wrapper->client->net.fd != -1) +#endif + #define REQUIRE_CONNECTED(wrapper) \ REQUIRE_INITIALIZED(wrapper) \ - if (!wrapper->connected && !wrapper->reconnect_enabled) { \ - rb_raise(cMysql2Error, "closed MySQL connection"); \ + if (!CONNECTED(wrapper) && !wrapper->reconnect_enabled) { \ + rb_raise(cMysql2Error, "MySQL client is not connected"); \ } #define REQUIRE_NOT_CONNECTED(wrapper) \ REQUIRE_INITIALIZED(wrapper) \ - if (wrapper->connected) { \ + if (CONNECTED(wrapper)) { \ rb_raise(cMysql2Error, "MySQL connection is already open"); \ } -#define MARK_CONN_INACTIVE(conn) \ - wrapper->active_thread = Qnil; +/* + * compatibility with mysql-connector-c, where LIBMYSQL_VERSION is the correct + * variable to use, but MYSQL_SERVER_VERSION gives the correct numbers when + * linking against the server itself + * + * MariaDB exposes its client version independently to the server version as + * MARIADB_PACKAGE_VERSION. + */ +#if defined(MARIADB_PACKAGE_VERSION) + #define MYSQL_LINK_VERSION MARIADB_PACKAGE_VERSION +#elif defined(LIBMYSQL_VERSION) + #define MYSQL_LINK_VERSION LIBMYSQL_VERSION +#else + #define MYSQL_LINK_VERSION MYSQL_SERVER_VERSION +#endif -#define GET_CLIENT(self) \ - mysql_client_wrapper *wrapper; \ - Data_Get_Struct(self, mysql_client_wrapper, wrapper) +/* + * mariadb-connector-c defines CLIENT_SESSION_TRACKING and SESSION_TRACK_TRANSACTION_TYPE + * while mysql-connector-c defines CLIENT_SESSION_TRACK and SESSION_TRACK_TRANSACTION_STATE + * This is a hack to take care of both clients. + */ +#if defined(CLIENT_SESSION_TRACK) +#elif defined(CLIENT_SESSION_TRACKING) + #define CLIENT_SESSION_TRACK CLIENT_SESSION_TRACKING + #define SESSION_TRACK_TRANSACTION_STATE SESSION_TRACK_TRANSACTION_TYPE +#endif + +/* + * compatibility with mysql-connector-c 6.1.x, MySQL 5.7.3 - 5.7.10 & with MariaDB 10.x and later. + */ +#ifdef HAVE_CONST_MYSQL_OPT_SSL_VERIFY_SERVER_CERT + #define SSL_MODE_VERIFY_IDENTITY 5 + #define HAVE_CONST_SSL_MODE_VERIFY_IDENTITY +#endif +#ifdef HAVE_CONST_MYSQL_OPT_SSL_ENFORCE + #define SSL_MODE_DISABLED 1 + #define SSL_MODE_REQUIRED 3 + #define HAVE_CONST_SSL_MODE_DISABLED + #define HAVE_CONST_SSL_MODE_REQUIRED +#endif /* * used to pass all arguments to mysql_real_connect while inside - * rb_thread_blocking_region + * rb_thread_call_without_gvl */ struct nogvl_connect_args { MYSQL *mysql; @@ -61,7 +102,7 @@ struct nogvl_connect_args { /* * used to pass all arguments to mysql_send_query while inside - * rb_thread_blocking_region + * rb_thread_call_without_gvl */ struct nogvl_send_query_args { MYSQL *mysql; @@ -73,13 +114,89 @@ struct nogvl_send_query_args { /* * used to pass all arguments to mysql_select_db while inside - * rb_thread_blocking_region + * rb_thread_call_without_gvl */ struct nogvl_select_db_args { MYSQL *mysql; char *db; }; +static VALUE rb_set_ssl_mode_option(VALUE self, VALUE setting) { + unsigned long version = mysql_get_client_version(); + const char *version_str = mysql_get_client_info(); + + /* Warn about versions that are known to be incomplete; these are pretty + * ancient, we want people to upgrade if they need SSL/TLS to work + * + * MySQL 5.x before 5.6.30 -- ssl_mode introduced but not fully working until 5.6.36) + * MySQL 5.7 before 5.7.3 -- ssl_mode introduced but not fully working until 5.7.11) + */ + if ((version >= 50000 && version < 50630) || (version >= 50700 && version < 50703)) { + rb_warn("Your mysql client library version %s does not support setting ssl_mode; full support comes with 5.6.36+, 5.7.11+, 8.0+", version_str); + return Qnil; + } + + /* For these versions, map from the options we're exposing to Ruby to the constant available: + * ssl_mode: :verify_identity to MYSQL_OPT_SSL_VERIFY_SERVER_CERT = 1 + * ssl_mode: :required to MYSQL_OPT_SSL_ENFORCE = 1 + * ssl_mode: :disabled to MYSQL_OPT_SSL_ENFORCE = 0 + */ +#if defined(HAVE_CONST_MYSQL_OPT_SSL_VERIFY_SERVER_CERT) || defined(HAVE_CONST_MYSQL_OPT_SSL_ENFORCE) + GET_CLIENT(self); + int val = NUM2INT(setting); + + /* Expected code path for MariaDB 10.x and MariaDB Connector/C 3.x + * Workaround code path for MySQL 5.7.3 - 5.7.10 and MySQL Connector/C 6.1.3 - 6.1.x + */ + if (version >= 100000 // MariaDB (all versions numbered 10.x) + || (version >= 30000 && version < 40000) // MariaDB Connector/C (all versions numbered 3.x) + || (version >= 50703 && version < 50711) // Workaround for MySQL 5.7.3 - 5.7.10 + || (version >= 60103 && version < 60200)) { // Workaround for MySQL Connector/C 6.1.3 - 6.1.x +#ifdef HAVE_CONST_MYSQL_OPT_SSL_VERIFY_SERVER_CERT + if (val == SSL_MODE_VERIFY_IDENTITY) { + my_bool b = 1; + int result = mysql_options(wrapper->client, MYSQL_OPT_SSL_VERIFY_SERVER_CERT, &b); + return INT2NUM(result); + } +#endif +#ifdef HAVE_CONST_MYSQL_OPT_SSL_ENFORCE + if (val == SSL_MODE_DISABLED || val == SSL_MODE_REQUIRED) { + my_bool b = (val == SSL_MODE_REQUIRED); + int result = mysql_options(wrapper->client, MYSQL_OPT_SSL_ENFORCE, &b); + return INT2NUM(result); + } +#endif + rb_warn("Your mysql client library version %s does not support ssl_mode %d", version_str, val); + return Qnil; + } else { + rb_warn("Your mysql client library version %s does not support ssl_mode as expected", version_str); + return Qnil; + } +#endif + + /* For other versions -- known to be MySQL 5.6.36+, 5.7.11+, 8.0+ + * pass the value of the argument to MYSQL_OPT_SSL_MODE -- note the code + * mapping from atoms / constants is in the MySQL::Client Ruby class + */ +#ifdef FULL_SSL_MODE_SUPPORT + GET_CLIENT(self); + int val = NUM2INT(setting); + + if (val != SSL_MODE_DISABLED && val != SSL_MODE_PREFERRED && val != SSL_MODE_REQUIRED && val != SSL_MODE_VERIFY_CA && val != SSL_MODE_VERIFY_IDENTITY) { + rb_raise(cMysql2Error, "ssl_mode= takes DISABLED, PREFERRED, REQUIRED, VERIFY_CA, VERIFY_IDENTITY, you passed: %d", val ); + } + int result = mysql_options(wrapper->client, MYSQL_OPT_SSL_MODE, &val); + + return INT2NUM(result); +#endif + + // Warn if we get this far +#ifdef NO_SSL_MODE_SUPPORT + rb_warn("Your mysql client library does not support setting ssl_mode"); + return Qnil; +#endif +} + /* * non-blocking mysql_*() functions that we won't be wrapping since * they do not appear to hit the network nor issue any interruptible @@ -105,42 +222,76 @@ struct nogvl_select_db_args { static void rb_mysql_client_mark(void * wrapper) { mysql_client_wrapper * w = wrapper; if (w) { - rb_gc_mark(w->encoding); - rb_gc_mark(w->active_thread); + rb_gc_mark_movable(w->encoding); + rb_gc_mark_movable(w->active_fiber); } } -static VALUE rb_raise_mysql2_error(mysql_client_wrapper *wrapper) { - VALUE rb_error_msg = rb_str_new2(mysql_error(wrapper->client)); - VALUE rb_sql_state = rb_tainted_str_new2(mysql_sqlstate(wrapper->client)); -#ifdef HAVE_RUBY_ENCODING_H - rb_encoding *default_internal_enc = rb_default_internal_encoding(); - rb_encoding *conn_enc = rb_to_encoding(wrapper->encoding); +/* this is called during GC */ +static void rb_mysql_client_free(void *ptr) { + mysql_client_wrapper *wrapper = ptr; + decr_mysql2_client(wrapper); +} - rb_enc_associate(rb_error_msg, conn_enc); - rb_enc_associate(rb_sql_state, conn_enc); - if (default_internal_enc) { - rb_error_msg = rb_str_export_to_enc(rb_error_msg, default_internal_enc); - rb_sql_state = rb_str_export_to_enc(rb_sql_state, default_internal_enc); +static size_t rb_mysql_client_memsize(const void * wrapper) { + const mysql_client_wrapper * w = wrapper; + return sizeof(*w); +} + +static void rb_mysql_client_compact(void * wrapper) { + mysql_client_wrapper * w = wrapper; + if (w) { + rb_mysql2_gc_location(w->encoding); + rb_mysql2_gc_location(w->active_fiber); } +} + +const rb_data_type_t rb_mysql_client_type = { + "rb_mysql_client", + { + rb_mysql_client_mark, + rb_mysql_client_free, + rb_mysql_client_memsize, +#ifdef HAVE_RB_GC_MARK_MOVABLE + rb_mysql_client_compact, +#endif + }, + 0, + 0, +#ifdef RUBY_TYPED_FREE_IMMEDIATELY + RUBY_TYPED_FREE_IMMEDIATELY, #endif +}; + +static VALUE rb_raise_mysql2_error(mysql_client_wrapper *wrapper) { + VALUE rb_error_msg = rb_str_new2(mysql_error(wrapper->client)); + VALUE rb_sql_state = rb_str_new2(mysql_sqlstate(wrapper->client)); + VALUE e; + + rb_enc_associate(rb_error_msg, rb_utf8_encoding()); + rb_enc_associate(rb_sql_state, rb_usascii_encoding()); - VALUE e = rb_exc_new3(cMysql2Error, rb_error_msg); - rb_funcall(e, intern_error_number_eql, 1, UINT2NUM(mysql_errno(wrapper->client))); - rb_funcall(e, intern_sql_state_eql, 1, rb_sql_state); + e = rb_funcall(cMysql2Error, intern_new_with_args, 4, + rb_error_msg, + LONG2FIX(wrapper->server_version), + UINT2NUM(mysql_errno(wrapper->client)), + rb_sql_state); rb_exc_raise(e); - return Qnil; } -static VALUE nogvl_init(void *ptr) { +static void *nogvl_init(void *ptr) { MYSQL *client; + mysql_client_wrapper *wrapper = ptr; /* may initialize embedded server and read /etc/services off disk */ - client = mysql_init((MYSQL *)ptr); - return client ? Qtrue : Qfalse; + client = mysql_init(wrapper->client); + + if (client) mysql2_set_local_infile(client, wrapper); + + return (void*)(client ? Qtrue : Qfalse); } -static VALUE nogvl_connect(void *ptr) { +static void *nogvl_connect(void *ptr) { struct nogvl_connect_args *args = ptr; MYSQL *client; @@ -149,58 +300,123 @@ static VALUE nogvl_connect(void *ptr) { args->db, args->port, args->unix_socket, args->client_flag); - return client ? Qtrue : Qfalse; + return (void *)(client ? Qtrue : Qfalse); } -static VALUE nogvl_close(void *ptr) { - mysql_client_wrapper *wrapper; #ifndef _WIN32 - int flags; -#endif - wrapper = ptr; - if (wrapper->connected) { - wrapper->active_thread = Qnil; - wrapper->connected = 0; - /* - * we'll send a QUIT message to the server, but that message is more of a - * formality than a hard requirement since the socket is getting shutdown - * anyways, so ensure the socket write does not block our interpreter - * - * - * if the socket is dead we have no chance of blocking, - * so ignore any potential fcntl errors since they don't matter - */ -#ifndef _WIN32 - flags = fcntl(wrapper->client->net.fd, F_GETFL); - if (flags > 0 && !(flags & O_NONBLOCK)) - fcntl(wrapper->client->net.fd, F_SETFL, flags | O_NONBLOCK); +/* + * Redirect clientfd to /dev/null for mysql_close and SSL_close to write, + * shutdown, and close. The hack is needed to prevent shutdown() from breaking + * a socket that may be in use by the parent or other processes after fork. + * + * /dev/null is used to absorb writes; previously a dummy socket was used, but + * it could not absorb writes and caused openssl to go into an infinite loop. + * + * Returns Qtrue or Qfalse (success or failure) + * + * Note: if this function is needed on Windows, use "nul" instead of "/dev/null" + */ +static VALUE invalidate_fd(int clientfd) +{ +#ifdef O_CLOEXEC + /* Atomically set CLOEXEC on the new FD in case another thread forks */ + int sockfd = open("/dev/null", O_RDWR | O_CLOEXEC); +#else + /* Well we don't have O_CLOEXEC, trigger the fallback code below */ + int sockfd = -1; #endif + if (sockfd < 0) { + /* Either O_CLOEXEC wasn't defined at compile time, or it was defined at + * compile time, but isn't available at run-time. So we'll just be quick + * about setting FD_CLOEXEC now. + */ + int flags; + sockfd = open("/dev/null", O_RDWR); + flags = fcntl(sockfd, F_GETFD); + /* Do the flags dance in case there are more defined flags in the future */ + if (flags != -1) { + flags |= FD_CLOEXEC; + fcntl(sockfd, F_SETFD, flags); + } + } + + if (sockfd < 0) { + /* Cannot raise here, because one or both of the following may be true: + * a) we have no GVL (in C Ruby) + * b) are running as a GC finalizer + */ + return Qfalse; + } + + dup2(sockfd, clientfd); + close(sockfd); + + return Qtrue; +} +#endif /* _WIN32 */ + +static void *nogvl_close(void *ptr) { + mysql_client_wrapper *wrapper = ptr; + + if (wrapper->initialized && !wrapper->closed) { mysql_close(wrapper->client); + wrapper->closed = 1; + wrapper->reconnect_enabled = 0; + wrapper->active_fiber = Qnil; } - return Qnil; + return NULL; } -static void rb_mysql_client_free(void * ptr) { - mysql_client_wrapper *wrapper = (mysql_client_wrapper *)ptr; +void decr_mysql2_client(mysql_client_wrapper *wrapper) +{ + wrapper->refcount--; - nogvl_close(wrapper); + if (wrapper->refcount == 0) { +#ifndef _WIN32 + if (CONNECTED(wrapper) && !wrapper->automatic_close) { + /* The client is being garbage collected while connected. Prevent + * mysql_close() from sending a mysql-QUIT or from calling shutdown() on + * the socket by invalidating it. invalidate_fd() will drop this + * process's reference to the socket only, while a QUIT or shutdown() + * would render the underlying connection unusable, interrupting other + * processes which share this object across a fork(). + */ + if (invalidate_fd(wrapper->client->net.fd) == Qfalse) { + fprintf(stderr, "[WARN] mysql2 failed to invalidate FD safely\n"); + close(wrapper->client->net.fd); + } + wrapper->client->net.fd = -1; + } +#endif - xfree(wrapper->client); - xfree(ptr); + nogvl_close(wrapper); + xfree(wrapper->client); + xfree(wrapper); + } } static VALUE allocate(VALUE klass) { VALUE obj; mysql_client_wrapper * wrapper; +#ifdef NEW_TYPEDDATA_WRAPPER + obj = TypedData_Make_Struct(klass, mysql_client_wrapper, &rb_mysql_client_type, wrapper); +#else obj = Data_Make_Struct(klass, mysql_client_wrapper, rb_mysql_client_mark, rb_mysql_client_free, wrapper); +#endif wrapper->encoding = Qnil; - wrapper->active_thread = Qnil; + wrapper->active_fiber = Qnil; + wrapper->automatic_close = 1; + wrapper->server_version = 0; wrapper->reconnect_enabled = 0; - wrapper->connected = 0; /* means that a database connection is open */ - wrapper->initialized = 0; /* means that that the wrapper is initialized */ + wrapper->connect_timeout = 0; + wrapper->initialized = 0; /* will be set true after calling mysql_init */ + wrapper->closed = 1; /* will be set false after calling mysql_real_connect */ + wrapper->refcount = 1; + wrapper->affected_rows = -1; wrapper->client = (MYSQL*)xmalloc(sizeof(MYSQL)); + return obj; } @@ -221,16 +437,14 @@ static VALUE rb_mysql_client_escape(RB_MYSQL_UNUSED VALUE klass, VALUE str) { oldLen = RSTRING_LEN(str); newStr = xmalloc(oldLen*2+1); - newLen = mysql_escape_string((char *)newStr, StringValuePtr(str), oldLen); + newLen = mysql_escape_string((char *)newStr, RSTRING_PTR(str), oldLen); if (newLen == oldLen) { /* no need to return a new ruby string if nothing changed */ xfree(newStr); return str; } else { rb_str = rb_str_new((const char*)newStr, newLen); -#ifdef HAVE_RUBY_ENCODING_H rb_enc_copy(rb_str, str); -#endif xfree(newStr); return rb_str; } @@ -245,71 +459,157 @@ static VALUE rb_mysql_client_warning_count(VALUE self) { return UINT2NUM(warning_count); } -static VALUE rb_connect(VALUE self, VALUE user, VALUE pass, VALUE host, VALUE port, VALUE database, VALUE socket, VALUE flags) { +static VALUE rb_mysql_info(VALUE self) { + const char *info; + VALUE rb_str; + GET_CLIENT(self); + + info = mysql_info(wrapper->client); + + if (info == NULL) { + return Qnil; + } + + rb_str = rb_str_new2(info); + rb_enc_associate(rb_str, rb_utf8_encoding()); + + return rb_str; +} + +static VALUE rb_mysql_get_ssl_cipher(VALUE self) +{ + const char *cipher; + VALUE rb_str; + GET_CLIENT(self); + + cipher = mysql_get_ssl_cipher(wrapper->client); + + if (cipher == NULL) { + return Qnil; + } + + rb_str = rb_str_new2(cipher); + rb_enc_associate(rb_str, rb_utf8_encoding()); + + return rb_str; +} + +#ifdef CLIENT_CONNECT_ATTRS +static int opt_connect_attr_add_i(VALUE key, VALUE value, VALUE arg) +{ + mysql_client_wrapper *wrapper = (mysql_client_wrapper *)arg; + rb_encoding *enc = rb_to_encoding(wrapper->encoding); + key = rb_str_export_to_enc(key, enc); + value = rb_str_export_to_enc(value, enc); + + mysql_options4(wrapper->client, MYSQL_OPT_CONNECT_ATTR_ADD, StringValueCStr(key), StringValueCStr(value)); + return ST_CONTINUE; +} +#endif + +static VALUE rb_mysql_connect(VALUE self, VALUE user, VALUE pass, VALUE host, VALUE port, VALUE database, VALUE socket, VALUE flags, VALUE conn_attrs) { struct nogvl_connect_args args; + time_t start_time, end_time, elapsed_time, connect_timeout; VALUE rv; GET_CLIENT(self); - args.host = NIL_P(host) ? "localhost" : StringValuePtr(host); - args.unix_socket = NIL_P(socket) ? NULL : StringValuePtr(socket); - args.port = NIL_P(port) ? 3306 : NUM2INT(port); - args.user = NIL_P(user) ? NULL : StringValuePtr(user); - args.passwd = NIL_P(pass) ? NULL : StringValuePtr(pass); - args.db = NIL_P(database) ? NULL : StringValuePtr(database); - args.mysql = wrapper->client; + args.host = NIL_P(host) ? NULL : StringValueCStr(host); + args.unix_socket = NIL_P(socket) ? NULL : StringValueCStr(socket); + args.port = NIL_P(port) ? 0 : NUM2INT(port); + args.user = NIL_P(user) ? NULL : StringValueCStr(user); + args.passwd = NIL_P(pass) ? NULL : StringValueCStr(pass); + args.db = NIL_P(database) ? NULL : StringValueCStr(database); + args.mysql = wrapper->client; args.client_flag = NUM2ULONG(flags); - rv = rb_thread_blocking_region(nogvl_connect, &args, RUBY_UBF_IO, 0); +#ifdef CLIENT_CONNECT_ATTRS + mysql_options(wrapper->client, MYSQL_OPT_CONNECT_ATTR_RESET, 0); + rb_hash_foreach(conn_attrs, opt_connect_attr_add_i, (VALUE)wrapper); +#endif + + if (wrapper->connect_timeout) + time(&start_time); + rv = (VALUE) rb_thread_call_without_gvl(nogvl_connect, &args, RUBY_UBF_IO, 0); if (rv == Qfalse) { - while (rv == Qfalse && errno == EINTR && !mysql_errno(wrapper->client)) { + while (rv == Qfalse && errno == EINTR) { + if (wrapper->connect_timeout) { + time(&end_time); + /* avoid long connect timeout from system time changes */ + if (end_time < start_time) + start_time = end_time; + elapsed_time = end_time - start_time; + /* avoid an early timeout due to time truncating milliseconds off the start time */ + if (elapsed_time > 0) + elapsed_time--; + if (elapsed_time >= (time_t)wrapper->connect_timeout) + break; + connect_timeout = wrapper->connect_timeout - elapsed_time; + mysql_options(wrapper->client, MYSQL_OPT_CONNECT_TIMEOUT, &connect_timeout); + } errno = 0; - rv = rb_thread_blocking_region(nogvl_connect, &args, RUBY_UBF_IO, 0); + rv = (VALUE) rb_thread_call_without_gvl(nogvl_connect, &args, RUBY_UBF_IO, 0); } + /* restore the connect timeout for reconnecting */ + if (wrapper->connect_timeout) + mysql_options(wrapper->client, MYSQL_OPT_CONNECT_TIMEOUT, &wrapper->connect_timeout); if (rv == Qfalse) - return rb_raise_mysql2_error(wrapper); + rb_raise_mysql2_error(wrapper); } - wrapper->connected = 1; + wrapper->closed = 0; + wrapper->server_version = mysql_get_server_version(wrapper->client); return self; } /* - * Immediately disconnect from the server, normally the garbage collector + * Immediately disconnect from the server; normally the garbage collector * will disconnect automatically when a connection is no longer needed. * Explicitly closing this will free up server resources sooner than waiting * for the garbage collector. + * + * @return [nil] */ static VALUE rb_mysql_client_close(VALUE self) { GET_CLIENT(self); - if (wrapper->connected) { - rb_thread_blocking_region(nogvl_close, wrapper, RUBY_UBF_IO, 0); + if (wrapper->client) { + rb_thread_call_without_gvl(nogvl_close, wrapper, RUBY_UBF_IO, 0); } return Qnil; } +/* call-seq: + * client.closed? + * + * @return [Boolean] + */ +static VALUE rb_mysql_client_closed(VALUE self) { + GET_CLIENT(self); + return CONNECTED(wrapper) ? Qfalse : Qtrue; +} + /* * mysql_send_query is unlikely to block since most queries are small * enough to fit in a socket buffer, but sometimes large UPDATE and * INSERTs will cause the process to block */ -static VALUE nogvl_send_query(void *ptr) { +static void *nogvl_send_query(void *ptr) { struct nogvl_send_query_args *args = ptr; int rv; rv = mysql_send_query(args->mysql, args->sql_ptr, args->sql_len); - return rv == 0 ? Qtrue : Qfalse; + return (void*)(rv == 0 ? Qtrue : Qfalse); } -static VALUE do_send_query(void *args) { - struct nogvl_send_query_args *query_args = args; +static VALUE do_send_query(VALUE args) { + struct nogvl_send_query_args *query_args = (void *)args; mysql_client_wrapper *wrapper = query_args->wrapper; - if (rb_thread_blocking_region(nogvl_send_query, args, RUBY_UBF_IO, 0) == Qfalse) { + if ((VALUE)rb_thread_call_without_gvl(nogvl_send_query, query_args, RUBY_UBF_IO, 0) == Qfalse) { /* an error occurred, we're not active anymore */ - MARK_CONN_INACTIVE(self); - return rb_raise_mysql2_error(wrapper); + wrapper->active_fiber = Qnil; + rb_raise_mysql2_error(wrapper); } return Qnil; } @@ -319,19 +619,18 @@ static VALUE do_send_query(void *args) { * response can overflow the socket buffers and cause us to eventually * block while calling mysql_read_query_result */ -static VALUE nogvl_read_query_result(void *ptr) { +static void *nogvl_read_query_result(void *ptr) { MYSQL * client = ptr; my_bool res = mysql_read_query_result(client); - return res == 0 ? Qtrue : Qfalse; + return (void *)(res == 0 ? Qtrue : Qfalse); } -static VALUE nogvl_do_result(void *ptr, char use_result) { - mysql_client_wrapper *wrapper; +static void *nogvl_do_result(void *ptr, char use_result) { + mysql_client_wrapper *wrapper = ptr; MYSQL_RES *result; - wrapper = (mysql_client_wrapper *)ptr; - if(use_result) { + if (use_result) { result = mysql_use_result(wrapper->client); } else { result = mysql_store_result(wrapper->client); @@ -339,17 +638,17 @@ static VALUE nogvl_do_result(void *ptr, char use_result) { /* once our result is stored off, this connection is ready for another command to be issued */ - wrapper->active_thread = Qnil; + wrapper->active_fiber = Qnil; - return (VALUE)result; + return result; } /* mysql_store_result may (unlikely) read rows off the socket */ -static VALUE nogvl_store_result(void *ptr) { +static void *nogvl_store_result(void *ptr) { return nogvl_do_result(ptr, 0); } -static VALUE nogvl_use_result(void *ptr) { +static void *nogvl_use_result(void *ptr) { return nogvl_do_result(ptr, 1); } @@ -361,46 +660,46 @@ static VALUE nogvl_use_result(void *ptr) { static VALUE rb_mysql_client_async_result(VALUE self) { MYSQL_RES * result; VALUE resultObj; -#ifdef HAVE_RUBY_ENCODING_H - mysql2_result_wrapper * result_wrapper; -#endif + VALUE current, is_streaming; GET_CLIENT(self); /* if we're not waiting on a result, do nothing */ - if (NIL_P(wrapper->active_thread)) + if (NIL_P(wrapper->active_fiber)) return Qnil; REQUIRE_CONNECTED(wrapper); - if (rb_thread_blocking_region(nogvl_read_query_result, wrapper->client, RUBY_UBF_IO, 0) == Qfalse) { + if ((VALUE)rb_thread_call_without_gvl(nogvl_read_query_result, wrapper->client, RUBY_UBF_IO, 0) == Qfalse) { /* an error occurred, mark this connection inactive */ - MARK_CONN_INACTIVE(self); - return rb_raise_mysql2_error(wrapper); + wrapper->active_fiber = Qnil; + rb_raise_mysql2_error(wrapper); } - VALUE is_streaming = rb_hash_aref(rb_iv_get(self, "@current_query_options"), sym_stream); - if(is_streaming == Qtrue) { - result = (MYSQL_RES *)rb_thread_blocking_region(nogvl_use_result, wrapper, RUBY_UBF_IO, 0); + is_streaming = rb_hash_aref(rb_ivar_get(self, intern_current_query_options), sym_stream); + if (is_streaming == Qtrue) { + result = (MYSQL_RES *)rb_thread_call_without_gvl(nogvl_use_result, wrapper, RUBY_UBF_IO, 0); } else { - result = (MYSQL_RES *)rb_thread_blocking_region(nogvl_store_result, wrapper, RUBY_UBF_IO, 0); + result = (MYSQL_RES *)rb_thread_call_without_gvl(nogvl_store_result, wrapper, RUBY_UBF_IO, 0); } + wrapper->affected_rows = mysql_affected_rows(wrapper->client); + if (result == NULL) { if (mysql_errno(wrapper->client) != 0) { - MARK_CONN_INACTIVE(self); + wrapper->active_fiber = Qnil; rb_raise_mysql2_error(wrapper); } /* no data and no error, so query was not a SELECT */ return Qnil; } - resultObj = rb_mysql_result_to_obj(result); - /* pass-through query options for result construction later */ - rb_iv_set(resultObj, "@query_options", rb_hash_dup(rb_iv_get(self, "@current_query_options"))); + // Duplicate the options hash and put the copy in the Result object + current = rb_hash_dup(rb_ivar_get(self, intern_current_query_options)); + (void)RB_GC_GUARD(current); + Check_Type(current, T_HASH); + resultObj = rb_mysql_result_to_obj(self, wrapper->encoding, current, result, Qnil); + + rb_mysql_set_server_query_flags(wrapper->client, resultObj); -#ifdef HAVE_RUBY_ENCODING_H - GetMysql2Result(resultObj, result_wrapper); - result_wrapper->encoding = wrapper->encoding; -#endif return resultObj; } @@ -413,29 +712,31 @@ struct async_query_args { static VALUE disconnect_and_raise(VALUE self, VALUE error) { GET_CLIENT(self); - wrapper->active_thread = Qnil; - wrapper->connected = 0; + wrapper->active_fiber = Qnil; - /* manually close the socket for read/write - this feels dirty, but is there another way? */ - close(wrapper->client->net.fd); - wrapper->client->net.fd = -1; + /* Invalidate the MySQL socket to prevent further communication. + * The GC will come along later and call mysql_close to free it. + */ + if (CONNECTED(wrapper)) { + if (invalidate_fd(wrapper->client->net.fd) == Qfalse) { + fprintf(stderr, "[WARN] mysql2 failed to invalidate FD safely, closing unsafely\n"); + close(wrapper->client->net.fd); + } + wrapper->client->net.fd = -1; + } rb_exc_raise(error); - - return Qnil; } -static VALUE do_query(void *args) { - struct async_query_args *async_args; +static VALUE do_query(VALUE args) { + struct async_query_args *async_args = (void *)args; struct timeval tv; - struct timeval* tvp; + struct timeval *tvp; long int sec; int retval; VALUE read_timeout; - async_args = (struct async_query_args *)args; - read_timeout = rb_iv_get(async_args->self, "@read_timeout"); + read_timeout = rb_ivar_get(async_args->self, intern_read_timeout); tvp = NULL; if (!NIL_P(read_timeout)) { @@ -456,7 +757,7 @@ static VALUE do_query(void *args) { retval = rb_wait_for_single_fd(async_args->fd, RB_WAITFD_IN, tvp); if (retval == 0) { - rb_raise(cMysql2Error, "Timeout waiting for a response from the last query. (waited %d seconds)", FIX2INT(read_timeout)); + rb_raise(cMysql2TimeoutError, "Timeout waiting for a response from the last query. (waited %d seconds)", FIX2INT(read_timeout)); } if (retval < 0) { @@ -470,28 +771,50 @@ static VALUE do_query(void *args) { return Qnil; } +#endif + +static VALUE disconnect_and_mark_inactive(VALUE self) { + GET_CLIENT(self); + + /* Check if execution terminated while result was still being read. */ + if (!NIL_P(wrapper->active_fiber)) { + if (CONNECTED(wrapper)) { + /* Invalidate the MySQL socket to prevent further communication. */ +#ifndef _WIN32 + if (invalidate_fd(wrapper->client->net.fd) == Qfalse) { + rb_warn("mysql2 failed to invalidate FD safely, closing unsafely\n"); + close(wrapper->client->net.fd); + } #else -static VALUE finish_and_mark_inactive(void *args) { - VALUE self; - MYSQL_RES *result; + close(wrapper->client->net.fd); +#endif + wrapper->client->net.fd = -1; + } + /* Skip mysql client check performed before command execution. */ + wrapper->client->status = MYSQL_STATUS_READY; + wrapper->active_fiber = Qnil; + } - self = (VALUE)args; + return Qnil; +} +static void rb_mysql_client_set_active_fiber(VALUE self) { + VALUE fiber_current = rb_fiber_current(); GET_CLIENT(self); - if (!NIL_P(wrapper->active_thread)) { - /* if we got here, the result hasn't been read off the wire yet - so lets do that and then throw it away because we have no way - of getting it back up to the caller from here */ - result = (MYSQL_RES *)rb_thread_blocking_region(nogvl_store_result, wrapper, RUBY_UBF_IO, 0); - mysql_free_result(result); + // see if this connection is still waiting on a result from a previous query + if (NIL_P(wrapper->active_fiber)) { + // mark this connection active + wrapper->active_fiber = fiber_current; + } else if (wrapper->active_fiber == fiber_current) { + rb_raise(cMysql2Error, "This connection is still waiting for a result, try again once you have the result"); + } else { + VALUE inspect = rb_inspect(wrapper->active_fiber); + const char *thr = StringValueCStr(inspect); - wrapper->active_thread = Qnil; + rb_raise(cMysql2Error, "This connection is in use by: %s", thr); } - - return Qnil; } -#endif /* call-seq: * client.abandon_results! @@ -502,18 +825,18 @@ static VALUE finish_and_mark_inactive(void *args) { * again. */ static VALUE rb_mysql_client_abandon_results(VALUE self) { - GET_CLIENT(self); - MYSQL_RES *result; int ret; + GET_CLIENT(self); + while (mysql_more_results(wrapper->client) == 1) { ret = mysql_next_result(wrapper->client); if (ret > 0) { rb_raise_mysql2_error(wrapper); } - result = (MYSQL_RES *)rb_thread_blocking_region(nogvl_store_result, wrapper, RUBY_UBF_IO, 0); + result = (MYSQL_RES *)rb_thread_call_without_gvl(nogvl_store_result, wrapper, RUBY_UBF_IO, 0); if (result != NULL) { mysql_free_result(result); @@ -527,77 +850,51 @@ static VALUE rb_mysql_client_abandon_results(VALUE self) { * client.query(sql, options = {}) * * Query the database with +sql+, with optional +options+. For the possible - * options, see @@default_query_options on the Mysql2::Client class. + * options, see default_query_options on the Mysql2::Client class. */ -static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) { +static VALUE rb_mysql_query(VALUE self, VALUE sql, VALUE current) { #ifndef _WIN32 struct async_query_args async_args; #endif struct nogvl_send_query_args args; - int async = 0; - VALUE opts, current; - VALUE thread_current = rb_thread_current(); -#ifdef HAVE_RUBY_ENCODING_H - rb_encoding *conn_enc; -#endif GET_CLIENT(self); REQUIRE_CONNECTED(wrapper); args.mysql = wrapper->client; - rb_iv_set(self, "@current_query_options", rb_hash_dup(rb_iv_get(self, "@query_options"))); - current = rb_iv_get(self, "@current_query_options"); - if (rb_scan_args(argc, argv, "11", &args.sql, &opts) == 2) { - opts = rb_funcall(current, intern_merge_bang, 1, opts); - - if (rb_hash_aref(current, sym_async) == Qtrue) { - async = 1; - } - } + (void)RB_GC_GUARD(current); + Check_Type(current, T_HASH); + rb_ivar_set(self, intern_current_query_options, current); - Check_Type(args.sql, T_STRING); -#ifdef HAVE_RUBY_ENCODING_H - conn_enc = rb_to_encoding(wrapper->encoding); + Check_Type(sql, T_STRING); /* ensure the string is in the encoding the connection is expecting */ - args.sql = rb_str_export_to_enc(args.sql, conn_enc); -#endif - args.sql_ptr = StringValuePtr(args.sql); + args.sql = rb_str_export_to_enc(sql, rb_to_encoding(wrapper->encoding)); + args.sql_ptr = RSTRING_PTR(args.sql); args.sql_len = RSTRING_LEN(args.sql); - - /* see if this connection is still waiting on a result from a previous query */ - if (NIL_P(wrapper->active_thread)) { - /* mark this connection active */ - wrapper->active_thread = thread_current; - } else if (wrapper->active_thread == thread_current) { - rb_raise(cMysql2Error, "This connection is still waiting for a result, try again once you have the result"); - } else { - VALUE inspect = rb_inspect(wrapper->active_thread); - const char *thr = StringValueCStr(inspect); - - rb_raise(cMysql2Error, "This connection is in use by: %s", thr); - RB_GC_GUARD(inspect); - } - args.wrapper = wrapper; + rb_mysql_client_set_active_fiber(self); + #ifndef _WIN32 rb_rescue2(do_send_query, (VALUE)&args, disconnect_and_raise, self, rb_eException, (VALUE)0); + (void)RB_GC_GUARD(sql); - if (!async) { + if (rb_hash_aref(current, sym_async) == Qtrue) { + return Qnil; + } else { async_args.fd = wrapper->client->net.fd; async_args.self = self; rb_rescue2(do_query, (VALUE)&async_args, disconnect_and_raise, self, rb_eException, (VALUE)0); - return rb_mysql_client_async_result(self); - } else { - return Qnil; + return rb_ensure(rb_mysql_client_async_result, self, disconnect_and_mark_inactive, self); } #else - do_send_query(&args); + do_send_query((VALUE)&args); + (void)RB_GC_GUARD(sql); /* this will just block until the result is ready */ - return rb_ensure(rb_mysql_client_async_result, self, finish_and_mark_inactive, self); + return rb_ensure(rb_mysql_client_async_result, self, disconnect_and_mark_inactive, self); #endif } @@ -610,37 +907,34 @@ static VALUE rb_mysql_client_real_escape(VALUE self, VALUE str) { unsigned char *newStr; VALUE rb_str; unsigned long newLen, oldLen; -#ifdef HAVE_RUBY_ENCODING_H rb_encoding *default_internal_enc; rb_encoding *conn_enc; -#endif GET_CLIENT(self); REQUIRE_CONNECTED(wrapper); Check_Type(str, T_STRING); -#ifdef HAVE_RUBY_ENCODING_H default_internal_enc = rb_default_internal_encoding(); conn_enc = rb_to_encoding(wrapper->encoding); /* ensure the string is in the encoding the connection is expecting */ str = rb_str_export_to_enc(str, conn_enc); -#endif oldLen = RSTRING_LEN(str); newStr = xmalloc(oldLen*2+1); - newLen = mysql_real_escape_string(wrapper->client, (char *)newStr, StringValuePtr(str), oldLen); + newLen = mysql_real_escape_string(wrapper->client, (char *)newStr, RSTRING_PTR(str), oldLen); if (newLen == oldLen) { /* no need to return a new ruby string if nothing changed */ + if (default_internal_enc) { + str = rb_str_export_to_enc(str, default_internal_enc); + } xfree(newStr); return str; } else { rb_str = rb_str_new((const char*)newStr, newLen); -#ifdef HAVE_RUBY_ENCODING_H rb_enc_associate(rb_str, conn_enc); if (default_internal_enc) { rb_str = rb_str_export_to_enc(rb_str, default_internal_enc); } -#endif xfree(newStr); return rb_str; } @@ -648,8 +942,9 @@ static VALUE rb_mysql_client_real_escape(VALUE self, VALUE str) { static VALUE _mysql_client_options(VALUE self, int opt, VALUE value) { int result; - void *retval = NULL; + const void *retval = NULL; unsigned int intval = 0; + const char * charval = NULL; my_bool boolval; GET_CLIENT(self); @@ -661,17 +956,17 @@ static VALUE _mysql_client_options(VALUE self, int opt, VALUE value) { switch(opt) { case MYSQL_OPT_CONNECT_TIMEOUT: - intval = NUM2INT(value); + intval = NUM2UINT(value); retval = &intval; break; case MYSQL_OPT_READ_TIMEOUT: - intval = NUM2INT(value); + intval = NUM2UINT(value); retval = &intval; break; case MYSQL_OPT_WRITE_TIMEOUT: - intval = NUM2INT(value); + intval = NUM2UINT(value); retval = &intval; break; @@ -685,6 +980,49 @@ static VALUE _mysql_client_options(VALUE self, int opt, VALUE value) { retval = &boolval; break; +#ifdef MYSQL_SECURE_AUTH + case MYSQL_SECURE_AUTH: + boolval = (value == Qfalse ? 0 : 1); + retval = &boolval; + break; +#endif + + case MYSQL_READ_DEFAULT_FILE: + charval = (const char *)StringValueCStr(value); + retval = charval; + break; + + case MYSQL_READ_DEFAULT_GROUP: + charval = (const char *)StringValueCStr(value); + retval = charval; + break; + + case MYSQL_INIT_COMMAND: + charval = (const char *)StringValueCStr(value); + retval = charval; + break; + +#ifdef HAVE_CONST_MYSQL_OPT_GET_SERVER_PUBLIC_KEY + case MYSQL_OPT_GET_SERVER_PUBLIC_KEY: + boolval = (value == Qfalse ? 0 : 1); + retval = &boolval; + break; +#endif + +#ifdef HAVE_MYSQL_DEFAULT_AUTH + case MYSQL_DEFAULT_AUTH: + charval = (const char *)StringValueCStr(value); + retval = charval; + break; +#endif + +#ifdef HAVE_CONST_MYSQL_ENABLE_CLEARTEXT_PLUGIN + case MYSQL_ENABLE_CLEARTEXT_PLUGIN: + boolval = (value == Qfalse ? 0 : 1); + retval = &boolval; + break; +#endif + default: return Qfalse; } @@ -695,9 +1033,15 @@ static VALUE _mysql_client_options(VALUE self, int opt, VALUE value) { if (result != 0) { rb_warn("%s\n", mysql_error(wrapper->client)); } else { - /* Special case for reconnect, this option is also stored in the wrapper struct */ - if (opt == MYSQL_OPT_RECONNECT) - wrapper->reconnect_enabled = boolval; + /* Special case for options that are stored in the wrapper struct */ + switch (opt) { + case MYSQL_OPT_RECONNECT: + wrapper->reconnect_enabled = boolval; + break; + case MYSQL_OPT_CONNECT_TIMEOUT: + wrapper->connect_timeout = intval; + break; + } } return (result == 0) ? Qtrue : Qfalse; @@ -708,30 +1052,21 @@ static VALUE _mysql_client_options(VALUE self, int opt, VALUE value) { * * Returns a string that represents the client library version. */ -static VALUE rb_mysql_client_info(VALUE self) { - VALUE version, client_info; -#ifdef HAVE_RUBY_ENCODING_H - rb_encoding *default_internal_enc; - rb_encoding *conn_enc; -#endif - GET_CLIENT(self); - version = rb_hash_new(); +static VALUE rb_mysql_client_info(RB_MYSQL_UNUSED VALUE klass) { + VALUE version_info, version, header_version; + version_info = rb_hash_new(); -#ifdef HAVE_RUBY_ENCODING_H - default_internal_enc = rb_default_internal_encoding(); - conn_enc = rb_to_encoding(wrapper->encoding); -#endif + version = rb_str_new2(mysql_get_client_info()); + header_version = rb_str_new2(MYSQL_LINK_VERSION); - rb_hash_aset(version, sym_id, LONG2NUM(mysql_get_client_version())); - client_info = rb_str_new2(mysql_get_client_info()); -#ifdef HAVE_RUBY_ENCODING_H - rb_enc_associate(client_info, conn_enc); - if (default_internal_enc) { - client_info = rb_str_export_to_enc(client_info, default_internal_enc); - } -#endif - rb_hash_aset(version, sym_version, client_info); - return version; + rb_enc_associate(version, rb_usascii_encoding()); + rb_enc_associate(header_version, rb_usascii_encoding()); + + rb_hash_aset(version_info, sym_id, LONG2NUM(mysql_get_client_version())); + rb_hash_aset(version_info, sym_version, version); + rb_hash_aset(version_info, sym_header_version, header_version); + + return version_info; } /* call-seq: @@ -741,27 +1076,21 @@ static VALUE rb_mysql_client_info(VALUE self) { */ static VALUE rb_mysql_client_server_info(VALUE self) { VALUE version, server_info; -#ifdef HAVE_RUBY_ENCODING_H rb_encoding *default_internal_enc; rb_encoding *conn_enc; -#endif GET_CLIENT(self); REQUIRE_CONNECTED(wrapper); -#ifdef HAVE_RUBY_ENCODING_H default_internal_enc = rb_default_internal_encoding(); conn_enc = rb_to_encoding(wrapper->encoding); -#endif version = rb_hash_new(); rb_hash_aset(version, sym_id, LONG2FIX(mysql_get_server_version(wrapper->client))); server_info = rb_str_new2(mysql_get_server_info(wrapper->client)); -#ifdef HAVE_RUBY_ENCODING_H rb_enc_associate(server_info, conn_enc); if (default_internal_enc) { server_info = rb_str_export_to_enc(server_info, default_internal_enc); } -#endif rb_hash_aset(version, sym_version, server_info); return version; } @@ -771,16 +1100,17 @@ static VALUE rb_mysql_client_server_info(VALUE self) { * * Return the file descriptor number for this client. */ +#ifndef _WIN32 static VALUE rb_mysql_client_socket(VALUE self) { GET_CLIENT(self); -#ifndef _WIN32 REQUIRE_CONNECTED(wrapper); - int fd_set_fd = wrapper->client->net.fd; - return INT2NUM(fd_set_fd); + return INT2NUM(wrapper->client->net.fd); +} #else +static VALUE rb_mysql_client_socket(RB_MYSQL_UNUSED VALUE self) { rb_raise(cMysql2Error, "Raw access to the mysql file descriptor isn't supported on Windows"); -#endif } +#endif /* call-seq: * client.last_id @@ -794,6 +1124,36 @@ static VALUE rb_mysql_client_last_id(VALUE self) { return ULL2NUM(mysql_insert_id(wrapper->client)); } +/* call-seq: + * client.session_track + * + * Returns information about changes to the session state on the server. + */ +static VALUE rb_mysql_client_session_track(VALUE self, VALUE type) { +#ifdef CLIENT_SESSION_TRACK + const char *data; + size_t length; + my_ulonglong retVal; + GET_CLIENT(self); + + REQUIRE_CONNECTED(wrapper); + retVal = mysql_session_track_get_first(wrapper->client, NUM2INT(type), &data, &length); + if (retVal != 0) { + return Qnil; + } + VALUE rbAry = rb_ary_new(); + VALUE rbFirst = rb_str_new(data, length); + rb_ary_push(rbAry, rbFirst); + while(mysql_session_track_get_next(wrapper->client, NUM2INT(type), &data, &length) == 0) { + VALUE rbNext = rb_str_new(data, length); + rb_ary_push(rbAry, rbNext); + } + return rbAry; +#else + return Qnil; +#endif +} + /* call-seq: * client.affected_rows * @@ -801,11 +1161,11 @@ static VALUE rb_mysql_client_last_id(VALUE self) { * if it was an UPDATE, DELETE, or INSERT. */ static VALUE rb_mysql_client_affected_rows(VALUE self) { - my_ulonglong retVal; + uint64_t retVal; GET_CLIENT(self); REQUIRE_CONNECTED(wrapper); - retVal = mysql_affected_rows(wrapper->client); + retVal = wrapper->affected_rows; if (retVal == (my_ulonglong)-1) { rb_raise_mysql2_error(wrapper); } @@ -826,13 +1186,13 @@ static VALUE rb_mysql_client_thread_id(VALUE self) { return ULL2NUM(retVal); } -static VALUE nogvl_select_db(void *ptr) { +static void *nogvl_select_db(void *ptr) { struct nogvl_select_db_args *args = ptr; if (mysql_select_db(args->mysql, args->db) == 0) - return Qtrue; + return (void *)Qtrue; else - return Qfalse; + return (void *)Qfalse; } /* call-seq: @@ -849,18 +1209,18 @@ static VALUE rb_mysql_client_select_db(VALUE self, VALUE db) REQUIRE_CONNECTED(wrapper); args.mysql = wrapper->client; - args.db = StringValuePtr(db); + args.db = StringValueCStr(db); - if (rb_thread_blocking_region(nogvl_select_db, &args, RUBY_UBF_IO, 0) == Qfalse) + if (rb_thread_call_without_gvl(nogvl_select_db, &args, RUBY_UBF_IO, 0) == Qfalse) rb_raise_mysql2_error(wrapper); return db; } -static VALUE nogvl_ping(void *ptr) { +static void *nogvl_ping(void *ptr) { MYSQL *client = ptr; - return mysql_ping(client) == 0 ? Qtrue : Qfalse; + return (void *)(mysql_ping(client) == 0 ? Qtrue : Qfalse); } /* call-seq: @@ -874,10 +1234,27 @@ static VALUE nogvl_ping(void *ptr) { static VALUE rb_mysql_client_ping(VALUE self) { GET_CLIENT(self); - if (!wrapper->connected) { + if (!CONNECTED(wrapper)) { return Qfalse; } else { - return rb_thread_blocking_region(nogvl_ping, wrapper->client, RUBY_UBF_IO, 0); + return (VALUE)rb_thread_call_without_gvl(nogvl_ping, wrapper->client, RUBY_UBF_IO, 0); + } +} + +/* call-seq: + * client.set_server_option(value) + * + * Enables or disables an option for the connection. + * Read https://dev.mysql.com/doc/refman/5.7/en/mysql-set-server-option.html + * for more information. + */ +static VALUE rb_mysql_client_set_server_option(VALUE self, VALUE value) { + GET_CLIENT(self); + + if (mysql_set_server_option(wrapper->client, NUM2INT(value)) == 0) { + return Qtrue; + } else { + return Qfalse; } } @@ -889,10 +1266,10 @@ static VALUE rb_mysql_client_ping(VALUE self) { static VALUE rb_mysql_client_more_results(VALUE self) { GET_CLIENT(self); - if (mysql_more_results(wrapper->client) == 0) - return Qfalse; - else - return Qtrue; + if (mysql_more_results(wrapper->client) == 0) + return Qfalse; + else + return Qtrue; } /* call-seq: @@ -903,9 +1280,10 @@ static VALUE rb_mysql_client_more_results(VALUE self) */ static VALUE rb_mysql_client_next_result(VALUE self) { - GET_CLIENT(self); int ret; + GET_CLIENT(self); ret = mysql_next_result(wrapper->client); + wrapper->affected_rows = mysql_affected_rows(wrapper->client); if (ret > 0) { rb_raise_mysql2_error(wrapper); return Qfalse; @@ -926,13 +1304,10 @@ static VALUE rb_mysql_client_store_result(VALUE self) { MYSQL_RES * result; VALUE resultObj; -#ifdef HAVE_RUBY_ENCODING_H - mysql2_result_wrapper * result_wrapper; -#endif - + VALUE current; GET_CLIENT(self); - result = (MYSQL_RES *)rb_thread_blocking_region(nogvl_store_result, wrapper, RUBY_UBF_IO, 0); + result = (MYSQL_RES *)rb_thread_call_without_gvl(nogvl_store_result, wrapper, RUBY_UBF_IO, 0); if (result == NULL) { if (mysql_errno(wrapper->client) != 0) { @@ -942,19 +1317,15 @@ static VALUE rb_mysql_client_store_result(VALUE self) return Qnil; } - resultObj = rb_mysql_result_to_obj(result); - /* pass-through query options for result construction later */ - rb_iv_set(resultObj, "@query_options", rb_hash_dup(rb_iv_get(self, "@current_query_options"))); + // Duplicate the options hash and put the copy in the Result object + current = rb_hash_dup(rb_ivar_get(self, intern_current_query_options)); + (void)RB_GC_GUARD(current); + Check_Type(current, T_HASH); + resultObj = rb_mysql_result_to_obj(self, wrapper->encoding, current, result, Qnil); -#ifdef HAVE_RUBY_ENCODING_H - GetMysql2Result(resultObj, result_wrapper); - result_wrapper->encoding = wrapper->encoding; -#endif return resultObj; - } -#ifdef HAVE_RUBY_ENCODING_H /* call-seq: * client.encoding * @@ -964,7 +1335,59 @@ static VALUE rb_mysql_client_encoding(VALUE self) { GET_CLIENT(self); return wrapper->encoding; } + +/* call-seq: + * client.database + * + * Returns the currently selected database. + * + * The result may be stale if `session_track_schema` is disabled. Read + * https://dev.mysql.com/doc/refman/5.7/en/session-state-tracking.html for more + * information. + */ +static VALUE rb_mysql_client_database(VALUE self) { + GET_CLIENT(self); + + char *db = wrapper->client->db; + if (!db) { + return Qnil; + } + + return rb_str_new_cstr(wrapper->client->db); +} + +/* call-seq: + * client.automatic_close? + * + * @return [Boolean] + */ +static VALUE get_automatic_close(VALUE self) { + GET_CLIENT(self); + return wrapper->automatic_close ? Qtrue : Qfalse; +} + +/* call-seq: + * client.automatic_close = false + * + * Set this to +false+ to leave the connection open after it is garbage + * collected. To avoid "Aborted connection" errors on the server, explicitly + * call +close+ when the connection is no longer needed. + * + * @see http://dev.mysql.com/doc/en/communication-errors.html + */ +static VALUE set_automatic_close(VALUE self, VALUE value) { + GET_CLIENT(self); + if (RTEST(value)) { + wrapper->automatic_close = 1; + } else { +#ifndef _WIN32 + wrapper->automatic_close = 0; +#else + rb_warn("Connections are always closed by garbage collector on Windows"); #endif + } + return value; +} /* call-seq: * client.reconnect = true @@ -1001,7 +1424,7 @@ static VALUE set_read_timeout(VALUE self, VALUE value) { /* Set the instance variable here even though _mysql_client_options might not succeed, because the timeout is used in other ways elsewhere */ - rb_iv_set(self, "@read_timeout", value); + rb_ivar_set(self, intern_read_timeout, value); return _mysql_client_options(self, MYSQL_OPT_READ_TIMEOUT, value); } @@ -1017,19 +1440,15 @@ static VALUE set_write_timeout(VALUE self, VALUE value) { static VALUE set_charset_name(VALUE self, VALUE value) { char *charset_name; - size_t charset_name_len; const struct mysql2_mysql_enc_name_to_rb_map *mysql2rb; -#ifdef HAVE_RUBY_ENCODING_H rb_encoding *enc; VALUE rb_enc; -#endif GET_CLIENT(self); + Check_Type(value, T_STRING); charset_name = RSTRING_PTR(value); - charset_name_len = RSTRING_LEN(value); -#ifdef HAVE_RUBY_ENCODING_H - mysql2rb = mysql2_mysql_enc_name_to_rb(charset_name, charset_name_len); + mysql2rb = mysql2_mysql_enc_name_to_rb(charset_name, (unsigned int)RSTRING_LEN(value)); if (mysql2rb == NULL || mysql2rb->rb_name == NULL) { VALUE inspect = rb_inspect(value); rb_raise(cMysql2Error, "Unsupported charset: '%s'", RSTRING_PTR(inspect)); @@ -1038,7 +1457,6 @@ static VALUE set_charset_name(VALUE self, VALUE value) { rb_enc = rb_enc_from_encoding(enc); wrapper->encoding = rb_enc; } -#endif if (mysql_options(wrapper->client, MYSQL_SET_CHARSET_NAME, charset_name)) { /* TODO: warning - unable to set charset */ @@ -1051,104 +1469,212 @@ static VALUE set_charset_name(VALUE self, VALUE value) { static VALUE set_ssl_options(VALUE self, VALUE key, VALUE cert, VALUE ca, VALUE capath, VALUE cipher) { GET_CLIENT(self); - if(!NIL_P(ca) || !NIL_P(key)) { - mysql_ssl_set(wrapper->client, - NIL_P(key) ? NULL : StringValuePtr(key), - NIL_P(cert) ? NULL : StringValuePtr(cert), - NIL_P(ca) ? NULL : StringValuePtr(ca), - NIL_P(capath) ? NULL : StringValuePtr(capath), - NIL_P(cipher) ? NULL : StringValuePtr(cipher)); +#ifdef HAVE_MYSQL_SSL_SET + mysql_ssl_set(wrapper->client, + NIL_P(key) ? NULL : StringValueCStr(key), + NIL_P(cert) ? NULL : StringValueCStr(cert), + NIL_P(ca) ? NULL : StringValueCStr(ca), + NIL_P(capath) ? NULL : StringValueCStr(capath), + NIL_P(cipher) ? NULL : StringValueCStr(cipher)); +#else + /* mysql 8.3 does not provide mysql_ssl_set */ + if (!NIL_P(key)) { + mysql_options(wrapper->client, MYSQL_OPT_SSL_KEY, StringValueCStr(key)); + } + if (!NIL_P(cert)) { + mysql_options(wrapper->client, MYSQL_OPT_SSL_CERT, StringValueCStr(cert)); + } + if (!NIL_P(ca)) { + mysql_options(wrapper->client, MYSQL_OPT_SSL_CA, StringValueCStr(ca)); + } + if (!NIL_P(capath)) { + mysql_options(wrapper->client, MYSQL_OPT_SSL_CAPATH, StringValueCStr(capath)); + } + if (!NIL_P(cipher)) { + mysql_options(wrapper->client, MYSQL_OPT_SSL_CIPHER, StringValueCStr(cipher)); } +#endif return self; } +static VALUE set_secure_auth(VALUE self, VALUE value) { +/* This option was deprecated in MySQL 5.x and removed in MySQL 8.0 */ +#ifdef MYSQL_SECURE_AUTH + return _mysql_client_options(self, MYSQL_SECURE_AUTH, value); +#else + return Qfalse; +#endif +} + +static VALUE set_read_default_file(VALUE self, VALUE value) { + return _mysql_client_options(self, MYSQL_READ_DEFAULT_FILE, value); +} + +static VALUE set_read_default_group(VALUE self, VALUE value) { + return _mysql_client_options(self, MYSQL_READ_DEFAULT_GROUP, value); +} + +static VALUE set_init_command(VALUE self, VALUE value) { + return _mysql_client_options(self, MYSQL_INIT_COMMAND, value); +} + +static VALUE set_get_server_public_key(VALUE self, VALUE value) { +#ifdef HAVE_CONST_MYSQL_OPT_GET_SERVER_PUBLIC_KEY + return _mysql_client_options(self, MYSQL_OPT_GET_SERVER_PUBLIC_KEY, value); +#else + rb_raise(cMysql2Error, "get-server-public-key is not available, you may need a newer MySQL client library"); +#endif +} + +static VALUE set_default_auth(VALUE self, VALUE value) { +#ifdef HAVE_MYSQL_DEFAULT_AUTH + return _mysql_client_options(self, MYSQL_DEFAULT_AUTH, value); +#else + rb_raise(cMysql2Error, "pluggable authentication is not available, you may need a newer MySQL client library"); +#endif +} + +static VALUE set_enable_cleartext_plugin(VALUE self, VALUE value) { +#ifdef HAVE_CONST_MYSQL_ENABLE_CLEARTEXT_PLUGIN + return _mysql_client_options(self, MYSQL_ENABLE_CLEARTEXT_PLUGIN, value); +#else + rb_raise(cMysql2Error, "enable-cleartext-plugin is not available, you may need a newer MySQL client library"); +#endif +} + static VALUE initialize_ext(VALUE self) { GET_CLIENT(self); - if (rb_thread_blocking_region(nogvl_init, wrapper->client, RUBY_UBF_IO, 0) == Qfalse) { + if ((VALUE)rb_thread_call_without_gvl(nogvl_init, wrapper, RUBY_UBF_IO, 0) == Qfalse) { /* TODO: warning - not enough memory? */ - return rb_raise_mysql2_error(wrapper); + rb_raise_mysql2_error(wrapper); } wrapper->initialized = 1; return self; } +/* call-seq: client.prepare # => Mysql2::Statement + * + * Create a new prepared statement. + */ +static VALUE rb_mysql_client_prepare_statement(VALUE self, VALUE sql) { + GET_CLIENT(self); + REQUIRE_CONNECTED(wrapper); + + return rb_mysql_stmt_new(self, sql); +} + void init_mysql2_client() { +#ifdef _WIN32 /* verify the libmysql we're about to use was the version we were built against https://github.com/luislavena/mysql-gem/commit/a600a9c459597da0712f70f43736e24b484f8a99 */ int i; int dots = 0; const char *lib = mysql_get_client_info(); - for (i = 0; lib[i] != 0 && MYSQL_SERVER_VERSION[i] != 0; i++) { + + for (i = 0; lib[i] != 0 && MYSQL_LINK_VERSION[i] != 0; i++) { if (lib[i] == '.') { dots++; /* we only compare MAJOR and MINOR */ if (dots == 2) break; } - if (lib[i] != MYSQL_SERVER_VERSION[i]) { - rb_raise(rb_eRuntimeError, "Incorrect MySQL client library version! This gem was compiled for %s but the client library is %s.", MYSQL_SERVER_VERSION, lib); - return; + if (lib[i] != MYSQL_LINK_VERSION[i]) { + rb_raise(rb_eRuntimeError, "Incorrect MySQL client library version! This gem was compiled for %s but the client library is %s.", MYSQL_LINK_VERSION, lib); } } +#endif + + /* Initializing mysql library, so different threads could call Client.new */ + /* without race condition in the library */ + if (mysql_library_init(0, NULL, NULL) != 0) { + rb_raise(rb_eRuntimeError, "Could not initialize MySQL client library"); + } #if 0 mMysql2 = rb_define_module("Mysql2"); Teach RDoc about Mysql2 constant. #endif cMysql2Client = rb_define_class_under(mMysql2, "Client", rb_cObject); + rb_global_variable(&cMysql2Client); rb_define_alloc_func(cMysql2Client, allocate); rb_define_singleton_method(cMysql2Client, "escape", rb_mysql_client_escape, 1); + rb_define_singleton_method(cMysql2Client, "info", rb_mysql_client_info, 0); rb_define_method(cMysql2Client, "close", rb_mysql_client_close, 0); - rb_define_method(cMysql2Client, "query", rb_mysql_client_query, -1); + rb_define_method(cMysql2Client, "closed?", rb_mysql_client_closed, 0); rb_define_method(cMysql2Client, "abandon_results!", rb_mysql_client_abandon_results, 0); rb_define_method(cMysql2Client, "escape", rb_mysql_client_real_escape, 1); - rb_define_method(cMysql2Client, "info", rb_mysql_client_info, 0); rb_define_method(cMysql2Client, "server_info", rb_mysql_client_server_info, 0); rb_define_method(cMysql2Client, "socket", rb_mysql_client_socket, 0); rb_define_method(cMysql2Client, "async_result", rb_mysql_client_async_result, 0); rb_define_method(cMysql2Client, "last_id", rb_mysql_client_last_id, 0); rb_define_method(cMysql2Client, "affected_rows", rb_mysql_client_affected_rows, 0); + rb_define_method(cMysql2Client, "prepare", rb_mysql_client_prepare_statement, 1); rb_define_method(cMysql2Client, "thread_id", rb_mysql_client_thread_id, 0); rb_define_method(cMysql2Client, "ping", rb_mysql_client_ping, 0); rb_define_method(cMysql2Client, "select_db", rb_mysql_client_select_db, 1); + rb_define_method(cMysql2Client, "set_server_option", rb_mysql_client_set_server_option, 1); rb_define_method(cMysql2Client, "more_results?", rb_mysql_client_more_results, 0); rb_define_method(cMysql2Client, "next_result", rb_mysql_client_next_result, 0); rb_define_method(cMysql2Client, "store_result", rb_mysql_client_store_result, 0); + rb_define_method(cMysql2Client, "automatic_close?", get_automatic_close, 0); + rb_define_method(cMysql2Client, "automatic_close=", set_automatic_close, 1); rb_define_method(cMysql2Client, "reconnect=", set_reconnect, 1); rb_define_method(cMysql2Client, "warning_count", rb_mysql_client_warning_count, 0); -#ifdef HAVE_RUBY_ENCODING_H + rb_define_method(cMysql2Client, "query_info_string", rb_mysql_info, 0); + rb_define_method(cMysql2Client, "ssl_cipher", rb_mysql_get_ssl_cipher, 0); rb_define_method(cMysql2Client, "encoding", rb_mysql_client_encoding, 0); -#endif + rb_define_method(cMysql2Client, "session_track", rb_mysql_client_session_track, 1); + rb_define_method(cMysql2Client, "database", rb_mysql_client_database, 0); rb_define_private_method(cMysql2Client, "connect_timeout=", set_connect_timeout, 1); rb_define_private_method(cMysql2Client, "read_timeout=", set_read_timeout, 1); rb_define_private_method(cMysql2Client, "write_timeout=", set_write_timeout, 1); rb_define_private_method(cMysql2Client, "local_infile=", set_local_infile, 1); rb_define_private_method(cMysql2Client, "charset_name=", set_charset_name, 1); + rb_define_private_method(cMysql2Client, "secure_auth=", set_secure_auth, 1); + rb_define_private_method(cMysql2Client, "default_file=", set_read_default_file, 1); + rb_define_private_method(cMysql2Client, "default_group=", set_read_default_group, 1); + rb_define_private_method(cMysql2Client, "init_command=", set_init_command, 1); + rb_define_private_method(cMysql2Client, "get_server_public_key=", set_get_server_public_key, 1); + rb_define_private_method(cMysql2Client, "default_auth=", set_default_auth, 1); rb_define_private_method(cMysql2Client, "ssl_set", set_ssl_options, 5); + rb_define_private_method(cMysql2Client, "ssl_mode=", rb_set_ssl_mode_option, 1); + rb_define_private_method(cMysql2Client, "enable_cleartext_plugin=", set_enable_cleartext_plugin, 1); rb_define_private_method(cMysql2Client, "initialize_ext", initialize_ext, 0); - rb_define_private_method(cMysql2Client, "connect", rb_connect, 7); + rb_define_private_method(cMysql2Client, "connect", rb_mysql_connect, 8); + rb_define_private_method(cMysql2Client, "_query", rb_mysql_query, 2); sym_id = ID2SYM(rb_intern("id")); sym_version = ID2SYM(rb_intern("version")); + sym_header_version = ID2SYM(rb_intern("header_version")); sym_async = ID2SYM(rb_intern("async")); sym_symbolize_keys = ID2SYM(rb_intern("symbolize_keys")); sym_as = ID2SYM(rb_intern("as")); sym_array = ID2SYM(rb_intern("array")); sym_stream = ID2SYM(rb_intern("stream")); + sym_no_good_index_used = ID2SYM(rb_intern("no_good_index_used")); + sym_no_index_used = ID2SYM(rb_intern("no_index_used")); + sym_query_was_slow = ID2SYM(rb_intern("query_was_slow")); + + intern_brackets = rb_intern("[]"); intern_merge = rb_intern("merge"); intern_merge_bang = rb_intern("merge!"); - intern_error_number_eql = rb_intern("error_number="); - intern_sql_state_eql = rb_intern("sql_state="); + intern_new_with_args = rb_intern("new_with_args"); + intern_current_query_options = rb_intern("@current_query_options"); + intern_read_timeout = rb_intern("@read_timeout"); #ifdef CLIENT_LONG_PASSWORD rb_const_set(cMysql2Client, rb_intern("LONG_PASSWORD"), LONG2NUM(CLIENT_LONG_PASSWORD)); +#else + /* HACK because MariaDB 10.2 no longer defines this constant, + * but we're using it in our default connection flags. */ + rb_const_set(cMysql2Client, rb_intern("LONG_PASSWORD"), INT2NUM(0)); #endif #ifdef CLIENT_FOUND_ROWS @@ -1220,6 +1746,20 @@ void init_mysql2_client() { #ifdef CLIENT_SECURE_CONNECTION rb_const_set(cMysql2Client, rb_intern("SECURE_CONNECTION"), LONG2NUM(CLIENT_SECURE_CONNECTION)); +#else + /* HACK because MySQL5.7 no longer defines this constant, + * but we're using it in our default connection flags. */ + rb_const_set(cMysql2Client, rb_intern("SECURE_CONNECTION"), LONG2NUM(0)); +#endif + +#ifdef HAVE_CONST_MYSQL_OPTION_MULTI_STATEMENTS_ON + rb_const_set(cMysql2Client, rb_intern("OPTION_MULTI_STATEMENTS_ON"), + LONG2NUM(MYSQL_OPTION_MULTI_STATEMENTS_ON)); +#endif + +#ifdef HAVE_CONST_MYSQL_OPTION_MULTI_STATEMENTS_OFF + rb_const_set(cMysql2Client, rb_intern("OPTION_MULTI_STATEMENTS_OFF"), + LONG2NUM(MYSQL_OPTION_MULTI_STATEMENTS_OFF)); #endif #ifdef CLIENT_MULTI_STATEMENTS @@ -1251,4 +1791,83 @@ void init_mysql2_client() { rb_const_set(cMysql2Client, rb_intern("BASIC_FLAGS"), LONG2NUM(CLIENT_BASIC_FLAGS)); #endif + +#ifdef CLIENT_CONNECT_ATTRS + rb_const_set(cMysql2Client, rb_intern("CONNECT_ATTRS"), + LONG2NUM(CLIENT_CONNECT_ATTRS)); +#else + /* HACK because MySQL 5.5 and earlier don't define this constant, + * but we're using it in our default connection flags. */ + rb_const_set(cMysql2Client, rb_intern("CONNECT_ATTRS"), + INT2NUM(0)); +#endif + +#ifdef CLIENT_SESSION_TRACK + rb_const_set(cMysql2Client, rb_intern("SESSION_TRACK"), INT2NUM(CLIENT_SESSION_TRACK)); + /* From mysql_com.h -- but stable from at least 5.7.4 through 8.0.20 */ + rb_const_set(cMysql2Client, rb_intern("SESSION_TRACK_SYSTEM_VARIABLES"), INT2NUM(SESSION_TRACK_SYSTEM_VARIABLES)); + rb_const_set(cMysql2Client, rb_intern("SESSION_TRACK_SCHEMA"), INT2NUM(SESSION_TRACK_SCHEMA)); + rb_const_set(cMysql2Client, rb_intern("SESSION_TRACK_STATE_CHANGE"), INT2NUM(SESSION_TRACK_STATE_CHANGE)); + rb_const_set(cMysql2Client, rb_intern("SESSION_TRACK_GTIDS"), INT2NUM(SESSION_TRACK_GTIDS)); + rb_const_set(cMysql2Client, rb_intern("SESSION_TRACK_TRANSACTION_CHARACTERISTICS"), INT2NUM(SESSION_TRACK_TRANSACTION_CHARACTERISTICS)); + rb_const_set(cMysql2Client, rb_intern("SESSION_TRACK_TRANSACTION_STATE"), INT2NUM(SESSION_TRACK_TRANSACTION_STATE)); +#endif + +#if defined(FULL_SSL_MODE_SUPPORT) // MySQL 5.6.36 and MySQL 5.7.11 and above + rb_const_set(cMysql2Client, rb_intern("SSL_MODE_DISABLED"), INT2NUM(SSL_MODE_DISABLED)); + rb_const_set(cMysql2Client, rb_intern("SSL_MODE_PREFERRED"), INT2NUM(SSL_MODE_PREFERRED)); + rb_const_set(cMysql2Client, rb_intern("SSL_MODE_REQUIRED"), INT2NUM(SSL_MODE_REQUIRED)); + rb_const_set(cMysql2Client, rb_intern("SSL_MODE_VERIFY_CA"), INT2NUM(SSL_MODE_VERIFY_CA)); + rb_const_set(cMysql2Client, rb_intern("SSL_MODE_VERIFY_IDENTITY"), INT2NUM(SSL_MODE_VERIFY_IDENTITY)); +#else +#ifdef HAVE_CONST_MYSQL_OPT_SSL_VERIFY_SERVER_CERT // MySQL 5.7.3 - 5.7.10 & MariaDB 10.x and later + rb_const_set(cMysql2Client, rb_intern("SSL_MODE_VERIFY_IDENTITY"), INT2NUM(SSL_MODE_VERIFY_IDENTITY)); +#endif +#ifdef HAVE_CONST_MYSQL_OPT_SSL_ENFORCE // MySQL 5.7.3 - 5.7.10 & MariaDB 10.x and later + rb_const_set(cMysql2Client, rb_intern("SSL_MODE_DISABLED"), INT2NUM(SSL_MODE_DISABLED)); + rb_const_set(cMysql2Client, rb_intern("SSL_MODE_REQUIRED"), INT2NUM(SSL_MODE_REQUIRED)); +#endif +#endif + +#ifndef HAVE_CONST_SSL_MODE_DISABLED + rb_const_set(cMysql2Client, rb_intern("SSL_MODE_DISABLED"), INT2NUM(0)); +#endif +#ifndef HAVE_CONST_SSL_MODE_PREFERRED + rb_const_set(cMysql2Client, rb_intern("SSL_MODE_PREFERRED"), INT2NUM(0)); +#endif +#ifndef HAVE_CONST_SSL_MODE_REQUIRED + rb_const_set(cMysql2Client, rb_intern("SSL_MODE_REQUIRED"), INT2NUM(0)); +#endif +#ifndef HAVE_CONST_SSL_MODE_VERIFY_CA + rb_const_set(cMysql2Client, rb_intern("SSL_MODE_VERIFY_CA"), INT2NUM(0)); +#endif +#ifndef HAVE_CONST_SSL_MODE_VERIFY_IDENTITY + rb_const_set(cMysql2Client, rb_intern("SSL_MODE_VERIFY_IDENTITY"), INT2NUM(0)); +#endif +} + +#define flag_to_bool(f) ((client->server_status & f) ? Qtrue : Qfalse) + +void rb_mysql_set_server_query_flags(MYSQL *client, VALUE result) { + VALUE server_flags = rb_hash_new(); + +#ifdef HAVE_CONST_SERVER_QUERY_NO_GOOD_INDEX_USED + rb_hash_aset(server_flags, sym_no_good_index_used, flag_to_bool(SERVER_QUERY_NO_GOOD_INDEX_USED)); +#else + rb_hash_aset(server_flags, sym_no_good_index_used, Qnil); +#endif + +#ifdef HAVE_CONST_SERVER_QUERY_NO_INDEX_USED + rb_hash_aset(server_flags, sym_no_index_used, flag_to_bool(SERVER_QUERY_NO_INDEX_USED)); +#else + rb_hash_aset(server_flags, sym_no_index_used, Qnil); +#endif + +#ifdef HAVE_CONST_SERVER_QUERY_WAS_SLOW + rb_hash_aset(server_flags, sym_query_was_slow, flag_to_bool(SERVER_QUERY_WAS_SLOW)); +#else + rb_hash_aset(server_flags, sym_query_was_slow, Qnil); +#endif + + rb_iv_set(result, "@server_flags", server_flags); } diff --git a/ext/mysql2/client.h b/ext/mysql2/client.h index 49a1f0e3e..6a8227bd1 100644 --- a/ext/mysql2/client.h +++ b/ext/mysql2/client.h @@ -1,44 +1,36 @@ #ifndef MYSQL2_CLIENT_H #define MYSQL2_CLIENT_H -/* - * partial emulation of the 1.9 rb_thread_blocking_region under 1.8, - * this is enough for dealing with blocking I/O functions in the - * presence of threads. - */ -#ifndef HAVE_RB_THREAD_BLOCKING_REGION - -#include -#define RUBY_UBF_IO ((rb_unblock_function_t *)-1) -typedef void rb_unblock_function_t(void *); -typedef VALUE rb_blocking_function_t(void *); -static VALUE -rb_thread_blocking_region( - rb_blocking_function_t *func, void *data1, - RB_MYSQL_UNUSED rb_unblock_function_t *ubf, - RB_MYSQL_UNUSED void *data2) -{ - VALUE rv; - - TRAP_BEG; - rv = func(data1); - TRAP_END; - - return rv; -} - -#endif /* ! HAVE_RB_THREAD_BLOCKING_REGION */ - -void init_mysql2_client(); - typedef struct { VALUE encoding; - VALUE active_thread; /* rb_thread_current() or Qnil */ + VALUE active_fiber; /* rb_fiber_current() or Qnil */ + long server_version; int reconnect_enabled; + unsigned int connect_timeout; int active; - int connected; + int automatic_close; int initialized; + int refcount; + int closed; + uint64_t affected_rows; MYSQL *client; } mysql_client_wrapper; +void rb_mysql_set_server_query_flags(MYSQL *client, VALUE result); + +extern const rb_data_type_t rb_mysql_client_type; + +#ifdef NEW_TYPEDDATA_WRAPPER +#define GET_CLIENT(self) \ + mysql_client_wrapper *wrapper; \ + TypedData_Get_Struct(self, mysql_client_wrapper, &rb_mysql_client_type, wrapper); +#else +#define GET_CLIENT(self) \ + mysql_client_wrapper *wrapper; \ + Data_Get_Struct(self, mysql_client_wrapper, wrapper); +#endif + +void init_mysql2_client(void); +void decr_mysql2_client(mysql_client_wrapper *wrapper); + #endif diff --git a/ext/mysql2/extconf.rb b/ext/mysql2/extconf.rb index 0eabe2836..449147298 100644 --- a/ext/mysql2/extconf.rb +++ b/ext/mysql2/extconf.rb @@ -1,76 +1,324 @@ -# encoding: UTF-8 require 'mkmf' +require 'English' -def asplode lib - abort "-----\n#{lib} is missing. please check your installation of mysql and try again.\n-----" +### Some helper functions + +def asplode(lib) + if RUBY_PLATFORM =~ /mingw|mswin/ + abort "-----\n#{lib} is missing. Check your installation of MySQL or Connector/C, and try again.\n-----" + elsif RUBY_PLATFORM =~ /darwin/ + abort "-----\n#{lib} is missing. You may need to 'brew install mysql' or 'port install mysql', and try again.\n-----" + else + abort "-----\n#{lib} is missing. You may need to 'sudo apt-get install libmariadb-dev', 'sudo apt-get install libmysqlclient-dev' or 'sudo yum install mysql-devel', and try again.\n-----" + end end -# 1.9-only -have_func('rb_thread_blocking_region') +def add_ssl_defines(header) + all_modes_found = %w[SSL_MODE_DISABLED SSL_MODE_PREFERRED SSL_MODE_REQUIRED SSL_MODE_VERIFY_CA SSL_MODE_VERIFY_IDENTITY].inject(true) do |m, ssl_mode| + m && have_const(ssl_mode, header) + end + if all_modes_found + $CFLAGS << ' -DFULL_SSL_MODE_SUPPORT' + else + # if we only have ssl toggle (--ssl,--disable-ssl) from 5.7.3 to 5.7.10 + # and the verify server cert option. This is also the case for MariaDB. + has_verify_support = have_const('MYSQL_OPT_SSL_VERIFY_SERVER_CERT', header) + has_enforce_support = have_const('MYSQL_OPT_SSL_ENFORCE', header) + $CFLAGS << ' -DNO_SSL_MODE_SUPPORT' if !has_verify_support && !has_enforce_support + end +end + +### Check for Ruby C extension interfaces + +# 2.1+ +have_func('rb_absint_size') +have_func('rb_absint_singlebit_p') + +# 2.7+ +have_func('rb_gc_mark_movable') + +# Missing in RBX (https://github.com/rubinius/rubinius/issues/3771) have_func('rb_wait_for_single_fd') -have_func('rb_hash_dup') + +# 3.0+ +have_func('rb_enc_interned_str', 'ruby.h') + +### Find OpenSSL library + +# User-specified OpenSSL if explicitly specified +if with_config('openssl-dir') + _, lib = dir_config('openssl') + if lib + # Ruby versions below 2.0 on Unix and below 2.1 on Windows + # do not properly search for lib directories, and must be corrected: + # https://bugs.ruby-lang.org/projects/ruby-trunk/repository/revisions/39717 + unless lib && lib[-3, 3] == 'lib' + @libdir_basename = 'lib' + _, lib = dir_config('openssl') + end + abort "-----\nCannot find library dir(s) #{lib}\n-----" unless lib && lib.split(File::PATH_SEPARATOR).any? { |dir| File.directory?(dir) } + warn "-----\nUsing --with-openssl-dir=#{File.dirname lib}\n-----" + $LDFLAGS << " -L#{lib}" + end +# Homebrew OpenSSL on MacOS +elsif RUBY_PLATFORM =~ /darwin/ && system('command -v brew') + openssl_location = `brew --prefix openssl`.strip + $LIBPATH << "#{openssl_location}/lib" unless openssl_location.empty? +end + +if RUBY_PLATFORM =~ /darwin/ && system('command -v brew') + zstd_location = `brew --prefix zstd`.strip + $LIBPATH << "#{zstd_location}/lib" unless zstd_location.empty? +end + +### Find MySQL client library # borrowed from mysqlplus # http://github.com/oldmoe/mysqlplus/blob/master/ext/extconf.rb -dirs = ENV['PATH'].split(File::PATH_SEPARATOR) + %w[ +dirs = ENV.fetch('http://github.com/PATH').split(File::PATH_SEPARATOR) + %w[ /opt /opt/local /opt/local/mysql - /opt/local/lib/mysql5 + /opt/local/lib/mysql5* + /opt/homebrew/opt/mysql* /usr /usr/mysql /usr/local /usr/local/mysql /usr/local/mysql-* - /usr/local/lib/mysql5 -].map{|dir| "#{dir}/bin" } + /usr/local/lib/mysql5* + /usr/local/opt/mysql5* + /usr/local/opt/mysql@* + /usr/local/opt/mysql-client + /usr/local/opt/mysql-client@* +].map { |dir| "#{dir}/bin" } + +# For those without HOMEBREW_ROOT in PATH +dirs << "#{ENV['HOMEBREW_ROOT']}/bin" if ENV['HOMEBREW_ROOT'] -GLOB = "{#{dirs.join(',')}}/{mysql_config,mysql_config5}" +GLOB = "{#{dirs.join(',')}}/{mysql_config,mysql_config5,mariadb_config}".freeze -if RUBY_PLATFORM =~ /mswin|mingw/ - inc, lib = dir_config('mysql') - exit 1 unless have_library("libmysql") -elsif mc = (with_config('mysql-config') || Dir[GLOB].first) then +# If the user has provided a --with-mysql-dir argument, we must respect it or fail. +inc, lib = dir_config('mysql') +if inc && lib + # Ruby versions below 2.0 on Unix and below 2.1 on Windows + # do not properly search for lib directories, and must be corrected: + # https://bugs.ruby-lang.org/projects/ruby-trunk/repository/revisions/39717 + unless lib && lib[-3, 3] == 'lib' + @libdir_basename = 'lib' + inc, lib = dir_config('mysql') + end + abort "-----\nCannot find include dir(s) #{inc}\n-----" unless inc && inc.split(File::PATH_SEPARATOR).any? { |dir| File.directory?(dir) } + abort "-----\nCannot find library dir(s) #{lib}\n-----" unless lib && lib.split(File::PATH_SEPARATOR).any? { |dir| File.directory?(dir) } + warn "-----\nUsing --with-mysql-dir=#{File.dirname inc}\n-----" + rpath_dir = lib + have_library('mysqlclient') +elsif (mc = with_config('mysql-config') || Dir[GLOB].first) + # If the user has provided a --with-mysql-config argument, we must respect it or fail. + # If the user gave --with-mysql-config with no argument means we should try to find it. mc = Dir[GLOB].first if mc == true + abort "-----\nCannot find mysql_config at #{mc}\n-----" unless mc && File.exist?(mc) + abort "-----\nCannot execute mysql_config at #{mc}\n-----" unless File.executable?(mc) + warn "-----\nUsing mysql_config at #{mc}\n-----" ver = `#{mc} --version`.chomp.to_f - cflags = `#{mc} --cflags`.chomp - exit 1 if $? != 0 + includes = `#{mc} --include`.chomp + abort unless $CHILD_STATUS.success? libs = `#{mc} --libs_r`.chomp # MySQL 5.5 and above already have re-entrant code in libmysqlclient (no _r). - if ver >= 5.5 || libs.empty? - libs = `#{mc} --libs`.chomp - end - exit 1 if $? != 0 - $CPPFLAGS += ' ' + cflags + libs = `#{mc} --libs`.chomp if ver >= 5.5 || libs.empty? + abort unless $CHILD_STATUS.success? + $INCFLAGS += ' ' + includes $libs = libs + " " + $libs + rpath_dir = libs else - inc, lib = dir_config('mysql', '/usr/local') - libs = ['m', 'z', 'socket', 'nsl', 'mygcc'] - while not find_library('mysqlclient', 'mysql_query', lib, "#{lib}/mysql") do - exit 1 if libs.empty? - have_library(libs.shift) - end + _, usr_local_lib = dir_config('mysql', '/usr/local') + + asplode("mysql client") unless find_library('mysqlclient', nil, usr_local_lib, "#{usr_local_lib}/mysql") + + rpath_dir = usr_local_lib end -if have_header('mysql.h') then +if have_header('mysql.h') prefix = nil -elsif have_header('mysql/mysql.h') then +elsif have_header('mysql/mysql.h') prefix = 'mysql' else asplode 'mysql.h' end -%w{ errmsg.h mysqld_error.h }.each do |h| - header = [prefix, h].compact.join '/' - asplode h unless have_header h +%w[errmsg.h].each do |h| + header = [prefix, h].compact.join('/') + asplode h unless have_header header end -# GCC specific flags -if RbConfig::MAKEFILE_CONFIG['CC'] =~ /gcc/ - $CFLAGS << ' -Wall -funroll-loops' +mysql_h = [prefix, 'mysql.h'].compact.join('/') +add_ssl_defines(mysql_h) +have_struct_member('MYSQL', 'net.vio', mysql_h) +have_struct_member('MYSQL', 'net.pvio', mysql_h) + +# These constants are actually enums, so they cannot be detected by #ifdef in C code. +have_const('MYSQL_DEFAULT_AUTH', mysql_h) +have_const('MYSQL_ENABLE_CLEARTEXT_PLUGIN', mysql_h) +have_const('SERVER_QUERY_NO_GOOD_INDEX_USED', mysql_h) +have_const('SERVER_QUERY_NO_INDEX_USED', mysql_h) +have_const('SERVER_QUERY_WAS_SLOW', mysql_h) +have_const('MYSQL_OPTION_MULTI_STATEMENTS_ON', mysql_h) +have_const('MYSQL_OPTION_MULTI_STATEMENTS_OFF', mysql_h) +have_const('MYSQL_OPT_GET_SERVER_PUBLIC_KEY', mysql_h) + +# my_bool is replaced by C99 bool in MySQL 8.0, but we want +# to retain compatibility with the typedef in earlier MySQLs. +have_type('my_bool', mysql_h) + +# detect mysql functions +have_func('mysql_ssl_set', mysql_h) + +### Compiler flags to help catch errors - if hard_mysql_path = $libs[%r{-L(/[^ ]+)}, 1] - $LDFLAGS << " -Wl,-rpath,#{hard_mysql_path}" +# This is our wishlist. We use whichever flags work on the host. +# -Wall and -Wextra are included by default. +wishlist = [ + '-Weverything', + '-Wno-compound-token-split-by-macro', # Fixed in Ruby 2.7+ at https://bugs.ruby-lang.org/issues/17865 + '-Wno-bad-function-cast', # rb_thread_call_without_gvl returns void * that we cast to VALUE + '-Wno-conditional-uninitialized', # false positive in client.c + '-Wno-covered-switch-default', # result.c -- enum_field_types (when fully covered, e.g. mysql 5.5) + '-Wno-declaration-after-statement', # GET_CLIENT followed by GET_STATEMENT in statement.c + '-Wno-disabled-macro-expansion', # rubby :( + '-Wno-documentation-unknown-command', # rubby :( + '-Wno-missing-field-initializers', # gperf generates bad code + '-Wno-missing-variable-declarations', # missing symbols due to ruby native ext initialization + '-Wno-padded', # mysql :( + '-Wno-reserved-id-macro', # rubby :( + '-Wno-sign-conversion', # gperf generates bad code + '-Wno-static-in-inline', # gperf generates bad code + '-Wno-switch-enum', # result.c -- enum_field_types (when not fully covered, e.g. mysql 5.6+) + '-Wno-undef', # rubinius :( + '-Wno-unreachable-code', # rubby :( + '-Wno-used-but-marked-unused', # rubby :( +] + +usable_flags = wishlist.select do |flag| + try_link('int main() {return 0;}', "-Werror #{flag}") +end + +$CFLAGS << ' ' << usable_flags.join(' ') + +### Sanitizers to help with debugging -- many are available on both Clang/LLVM and GCC + +enabled_sanitizers = disabled_sanitizers = [] +# Specify a comma-separated list of sanitizers, or try them all by default +sanitizers = with_config('sanitize') +case sanitizers +when true + # Try them all, turn on whatever we can + enabled_sanitizers = %w[address cfi integer memory thread undefined].select do |s| + try_link('int main() {return 0;}', "-Werror -fsanitize=#{s}") + end + abort "-----\nCould not enable any sanitizers!\n-----" if enabled_sanitizers.empty? +when String + # Figure out which sanitizers are supported + enabled_sanitizers, disabled_sanitizers = sanitizers.split(',').partition do |s| + try_link('int main() {return 0;}', "-Werror -fsanitize=#{s}") + end +end + +unless disabled_sanitizers.empty? # rubocop:disable Style/IfUnlessModifier + abort "-----\nCould not enable requested sanitizers: #{disabled_sanitizers.join(',')}\n-----" +end + +unless enabled_sanitizers.empty? + warn "-----\nEnabling sanitizers: #{enabled_sanitizers.join(',')}\n-----" + enabled_sanitizers.each do |s| + # address sanitizer requires runtime support + if s == 'address' # rubocop:disable Style/IfUnlessModifier + have_library('asan') || $LDFLAGS << ' -fsanitize=address' + end + $CFLAGS << " -fsanitize=#{s}" + end + # Options for line numbers in backtraces + $CFLAGS << ' -g -fno-omit-frame-pointer' +end + +### Find MySQL Client on Windows, set RPATH to find the library at runtime + +if RUBY_PLATFORM =~ /mswin|mingw/ && !defined?(RubyInstaller) + # Build libmysql.a interface link library + require 'rake' + + # Build libmysql.a interface link library + # Use rake to rebuild only if these files change + deffile = File.expand_path('../../../support/libmysql.def', __FILE__) + libfile = File.expand_path(File.join(rpath_dir, 'libmysql.lib')) + file 'libmysql.a' => [deffile, libfile] do + when_writing 'building libmysql.a' do + # Ruby kindly shows us where dllwrap is, but that tool does more than we want. + # Maybe in the future Ruby could provide RbConfig::CONFIG['DLLTOOL'] directly. + dlltool = RbConfig::CONFIG['DLLWRAP'].gsub('dllwrap', 'dlltool') + sh dlltool, '--kill-at', + '--dllname', 'libmysql.dll', + '--output-lib', 'libmysql.a', + '--input-def', deffile, libfile + end + end + + Rake::Task['libmysql.a'].invoke + $LOCAL_LIBS << ' ' << 'libmysql.a' + + # Make sure the generated interface library works (if cross-compiling, trust without verifying) + unless RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ + abort "-----\nCannot find libmysql.a\n-----" unless have_library('libmysql') + abort "-----\nCannot link to libmysql.a (my_init)\n-----" unless have_func('my_init') + end + + # Vendor libmysql.dll + vendordir = File.expand_path('../../../vendor/', __FILE__) + directory vendordir + + vendordll = File.join(vendordir, 'libmysql.dll') + dllfile = File.expand_path(File.join(rpath_dir, 'libmysql.dll')) + file vendordll => [dllfile, vendordir] do + when_writing 'copying libmysql.dll' do + cp dllfile, vendordll + end + end + + # Copy libmysql.dll to the local vendor directory by default + if arg_config('--no-vendor-libmysql') + # Fine, don't. + puts "--no-vendor-libmysql" + else # Default: arg_config('--vendor-libmysql') + # Let's do it! + Rake::Task[vendordll].invoke + end +else + case explicit_rpath = with_config('mysql-rpath') + when true + abort "-----\nOption --with-mysql-rpath must have an argument\n-----" + when false + warn "-----\nOption --with-mysql-rpath has been disabled at your request\n-----" + when String + # The user gave us a value so use it + rpath_flags = " -Wl,-rpath,#{explicit_rpath}" + warn "-----\nSetting mysql rpath to #{explicit_rpath}\n-----" + $LDFLAGS << rpath_flags + else + if (libdir = rpath_dir[%r{(-L)?(/[^ ]+)}, 2]) + rpath_flags = " -Wl,-rpath,#{libdir}" + if RbConfig::CONFIG["RPATHFLAG"].to_s.empty? && try_link('int main() {return 0;}', rpath_flags) + # Usually Ruby sets RPATHFLAG the right way for each system, but not on OS X. + warn "-----\nSetting rpath to #{libdir}\n-----" + $LDFLAGS << rpath_flags + else + if RbConfig::CONFIG["RPATHFLAG"].to_s.empty? + # If we got here because try_link failed, warn the user + warn "-----\nDon't know how to set rpath on your system, if MySQL libraries are not in path mysql2 may not load\n-----" + end + # Make sure that LIBPATH gets set if we didn't explicitly set the rpath. + warn "-----\nSetting libpath to #{libdir}\n-----" + $LIBPATH << libdir unless $LIBPATH.include?(libdir) + end + end end end diff --git a/ext/mysql2/infile.c b/ext/mysql2/infile.c new file mode 100644 index 000000000..b25934d15 --- /dev/null +++ b/ext/mysql2/infile.c @@ -0,0 +1,122 @@ +#include + +#include +#ifndef _MSC_VER +#include +#endif +#include + +#define ERROR_LEN 1024 +typedef struct +{ + int fd; + char *filename; + char error[ERROR_LEN]; + mysql_client_wrapper *wrapper; +} mysql2_local_infile_data; + +/* MySQL calls this function when a user begins a LOAD DATA LOCAL INFILE query. + * + * Allocate a data struct and pass it back through the data pointer. + * + * Returns: + * 0 on success + * 1 on error + */ +static int +mysql2_local_infile_init(void **ptr, const char *filename, void *userdata) +{ + mysql2_local_infile_data *data = malloc(sizeof(mysql2_local_infile_data)); + if (!data) return 1; + + *ptr = data; + data->error[0] = 0; + data->wrapper = userdata; + + data->filename = strdup(filename); + if (!data->filename) { + snprintf(data->error, ERROR_LEN, "%s: %s", strerror(errno), filename); + return 1; + } + + data->fd = open(filename, O_RDONLY); + if (data->fd < 0) { + snprintf(data->error, ERROR_LEN, "%s: %s", strerror(errno), filename); + return 1; + } + + return 0; +} + +/* MySQL calls this function to read data from the local file. + * + * Returns: + * > 0 number of bytes read + * == 0 end of file + * < 0 error + */ +static int +mysql2_local_infile_read(void *ptr, char *buf, unsigned int buf_len) +{ + int count; + mysql2_local_infile_data *data = (mysql2_local_infile_data *)ptr; + + count = (int)read(data->fd, buf, buf_len); + if (count < 0) { + snprintf(data->error, ERROR_LEN, "%s: %s", strerror(errno), data->filename); + } + + return count; +} + +/* MySQL calls this function when we're done with the LOCAL INFILE query. + * + * ptr will be null if the init function failed. + */ +static void +mysql2_local_infile_end(void *ptr) +{ + mysql2_local_infile_data *data = (mysql2_local_infile_data *)ptr; + if (data) { + if (data->fd >= 0) + close(data->fd); + if (data->filename) + free(data->filename); + free(data); + } +} + +/* MySQL calls this function if any of the functions above returned an error. + * + * This function is called even if init failed, with whatever ptr value + * init has set, regardless of the return value of the init function. + * + * Returns: + * Error message number (see http://dev.mysql.com/doc/refman/5.0/en/error-messages-client.html) + */ +static int +mysql2_local_infile_error(void *ptr, char *error_msg, unsigned int error_msg_len) +{ + mysql2_local_infile_data *data = (mysql2_local_infile_data *) ptr; + + if (data) { + snprintf(error_msg, error_msg_len, "%s", data->error); + return CR_UNKNOWN_ERROR; + } + + snprintf(error_msg, error_msg_len, "Out of memory"); + return CR_OUT_OF_MEMORY; +} + +/* Tell MySQL Client to use our own local_infile functions. + * This is both due to bugginess in the default handlers, + * and to improve the Rubyness of the handlers here. + */ +void mysql2_set_local_infile(MYSQL *mysql, void *userdata) +{ + mysql_set_local_infile_handler(mysql, + mysql2_local_infile_init, + mysql2_local_infile_read, + mysql2_local_infile_end, + mysql2_local_infile_error, userdata); +} diff --git a/ext/mysql2/infile.h b/ext/mysql2/infile.h new file mode 100644 index 000000000..ffc4aa023 --- /dev/null +++ b/ext/mysql2/infile.h @@ -0,0 +1 @@ +void mysql2_set_local_infile(MYSQL *mysql, void *userdata); diff --git a/ext/mysql2/mysql2_ext.c b/ext/mysql2/mysql2_ext.c index dcb72f377..8c887a867 100644 --- a/ext/mysql2/mysql2_ext.c +++ b/ext/mysql2/mysql2_ext.c @@ -1,12 +1,19 @@ #include -VALUE mMysql2, cMysql2Error; +VALUE mMysql2, cMysql2Error, cMysql2TimeoutError; /* Ruby Extension initializer */ void Init_mysql2() { - mMysql2 = rb_define_module("Mysql2"); + mMysql2 = rb_define_module("Mysql2"); + rb_global_variable(&mMysql2); + cMysql2Error = rb_const_get(mMysql2, rb_intern("Error")); + rb_global_variable(&cMysql2Error); + + cMysql2TimeoutError = rb_const_get(cMysql2Error, rb_intern("TimeoutError")); + rb_global_variable(&cMysql2TimeoutError); init_mysql2_client(); init_mysql2_result(); + init_mysql2_statement(); } diff --git a/ext/mysql2/mysql2_ext.h b/ext/mysql2/mysql2_ext.h index 01bd8840c..f82c47e5e 100644 --- a/ext/mysql2/mysql2_ext.h +++ b/ext/mysql2/mysql2_ext.h @@ -1,42 +1,57 @@ #ifndef MYSQL2_EXT #define MYSQL2_EXT +void Init_mysql2(void); + /* tell rbx not to use it's caching compat layer by doing this we're making a promise to RBX that we'll never modify the pointers we get back from RSTRING_PTR */ #define RSTRING_NOT_MODIFIED #include -#include - -#ifndef HAVE_UINT -#define HAVE_UINT -typedef unsigned short ushort; -typedef unsigned int uint; -#endif #ifdef HAVE_MYSQL_H #include -#include #include -#include #else #include -#include #include -#include #endif -#ifdef HAVE_RUBY_ENCODING_H #include -#endif +#include #if defined(__GNUC__) && (__GNUC__ >= 3) +#define RB_MYSQL_NORETURN __attribute__ ((noreturn)) #define RB_MYSQL_UNUSED __attribute__ ((unused)) #else +#define RB_MYSQL_NORETURN #define RB_MYSQL_UNUSED #endif +/* MySQL 8.0 replaces my_bool with C99 bool. Earlier versions of MySQL had + * a typedef to char. Gem users reported failures on big endian systems when + * using C99 bool types with older MySQLs due to mismatched behavior. */ +#ifndef HAVE_TYPE_MY_BOOL +#include +typedef bool my_bool; +#endif + +// ruby 2.7+ +#ifdef HAVE_RB_GC_MARK_MOVABLE +#define rb_mysql2_gc_location(ptr) ptr = rb_gc_location(ptr) +#else +#define rb_gc_mark_movable(ptr) rb_gc_mark(ptr) +#define rb_mysql2_gc_location(ptr) +#endif + +// ruby 2.2+ +#ifdef TypedData_Make_Struct +#define NEW_TYPEDDATA_WRAPPER 1 +#endif + #include +#include #include +#include #endif diff --git a/ext/mysql2/mysql_enc_name_to_ruby.h b/ext/mysql2/mysql_enc_name_to_ruby.h index dfabeef1f..95609a7fe 100644 --- a/ext/mysql2/mysql_enc_name_to_ruby.h +++ b/ext/mysql2/mysql_enc_name_to_ruby.h @@ -1,4 +1,4 @@ -/* C code produced by gperf version 3.0.3 */ +/* C code produced by gperf version 3.0.4 */ /* Command-line: gperf */ /* Computed positions: -k'1,3,$' */ @@ -30,7 +30,7 @@ error "gperf generated tables don't work with this execution character set. Plea #endif struct mysql2_mysql_enc_name_to_rb_map { const char *name; const char *rb_name; }; -/* maximum key range = 66, duplicates = 0 */ +/* maximum key range = 71, duplicates = 0 */ #ifdef __GNUC__ __inline @@ -46,39 +46,39 @@ mysql2_mysql_enc_name_to_rb_hash (str, len) { static const unsigned char asso_values[] = { - 69, 69, 69, 69, 69, 69, 69, 69, 69, 69, - 69, 69, 69, 69, 69, 69, 69, 69, 69, 69, - 69, 69, 69, 69, 69, 69, 69, 69, 69, 69, - 69, 69, 69, 69, 69, 69, 69, 69, 69, 69, - 69, 69, 69, 69, 69, 69, 69, 69, 40, 5, - 0, 69, 0, 40, 25, 20, 10, 55, 69, 69, - 69, 69, 69, 69, 69, 69, 69, 69, 69, 69, - 69, 69, 69, 69, 69, 69, 69, 69, 69, 69, - 69, 69, 69, 69, 69, 69, 69, 69, 69, 69, - 69, 69, 69, 69, 69, 69, 69, 35, 5, 0, - 10, 0, 20, 0, 5, 5, 69, 0, 10, 15, - 0, 0, 69, 69, 25, 5, 5, 0, 69, 30, - 69, 0, 69, 69, 69, 69, 69, 69, 69, 69, - 69, 69, 69, 69, 69, 69, 69, 69, 69, 69, - 69, 69, 69, 69, 69, 69, 69, 69, 69, 69, - 69, 69, 69, 69, 69, 69, 69, 69, 69, 69, - 69, 69, 69, 69, 69, 69, 69, 69, 69, 69, - 69, 69, 69, 69, 69, 69, 69, 69, 69, 69, - 69, 69, 69, 69, 69, 69, 69, 69, 69, 69, - 69, 69, 69, 69, 69, 69, 69, 69, 69, 69, - 69, 69, 69, 69, 69, 69, 69, 69, 69, 69, - 69, 69, 69, 69, 69, 69, 69, 69, 69, 69, - 69, 69, 69, 69, 69, 69, 69, 69, 69, 69, - 69, 69, 69, 69, 69, 69, 69, 69, 69, 69, - 69, 69, 69, 69, 69, 69, 69, 69, 69, 69, - 69, 69, 69, 69, 69, 69 + 74, 74, 74, 74, 74, 74, 74, 74, 74, 74, + 74, 74, 74, 74, 74, 74, 74, 74, 74, 74, + 74, 74, 74, 74, 74, 74, 74, 74, 74, 74, + 74, 74, 74, 74, 74, 74, 74, 74, 74, 74, + 74, 74, 74, 74, 74, 74, 74, 74, 15, 5, + 0, 30, 5, 25, 40, 10, 20, 50, 74, 74, + 74, 74, 74, 74, 74, 74, 74, 74, 74, 74, + 74, 74, 74, 74, 74, 74, 74, 74, 74, 74, + 74, 74, 74, 74, 74, 74, 74, 74, 74, 74, + 74, 74, 74, 74, 74, 74, 74, 40, 5, 0, + 15, 10, 0, 0, 0, 5, 74, 0, 25, 5, + 0, 5, 74, 74, 20, 5, 5, 0, 74, 45, + 74, 0, 74, 74, 74, 74, 74, 74, 74, 74, + 74, 74, 74, 74, 74, 74, 74, 74, 74, 74, + 74, 74, 74, 74, 74, 74, 74, 74, 74, 74, + 74, 74, 74, 74, 74, 74, 74, 74, 74, 74, + 74, 74, 74, 74, 74, 74, 74, 74, 74, 74, + 74, 74, 74, 74, 74, 74, 74, 74, 74, 74, + 74, 74, 74, 74, 74, 74, 74, 74, 74, 74, + 74, 74, 74, 74, 74, 74, 74, 74, 74, 74, + 74, 74, 74, 74, 74, 74, 74, 74, 74, 74, + 74, 74, 74, 74, 74, 74, 74, 74, 74, 74, + 74, 74, 74, 74, 74, 74, 74, 74, 74, 74, + 74, 74, 74, 74, 74, 74, 74, 74, 74, 74, + 74, 74, 74, 74, 74, 74, 74, 74, 74, 74, + 74, 74, 74, 74, 74, 74 }; return len + asso_values[(unsigned char)str[2]] + asso_values[(unsigned char)str[0]] + asso_values[(unsigned char)str[len - 1]]; } #ifdef __GNUC__ __inline -#ifdef __GNUC_STDC_INLINE__ +#if defined __GNUC_STDC_INLINE__ || defined __GNUC_GNU_INLINE__ __attribute__ ((__gnu_inline__)) #endif #endif @@ -89,11 +89,11 @@ mysql2_mysql_enc_name_to_rb (str, len) { enum { - TOTAL_KEYWORDS = 39, + TOTAL_KEYWORDS = 42, MIN_WORD_LENGTH = 3, MAX_WORD_LENGTH = 8, MIN_HASH_VALUE = 3, - MAX_HASH_VALUE = 68 + MAX_HASH_VALUE = 73 }; static const struct mysql2_mysql_enc_name_to_rb_map wordlist[] = @@ -101,54 +101,59 @@ mysql2_mysql_enc_name_to_rb (str, len) {""}, {""}, {""}, {"gbk", "GBK"}, {""}, - {"greek", "ISO-8859-7"}, + {"utf32", "UTF-32"}, {"gb2312", "GB2312"}, {"keybcs2", NULL}, {""}, {"ucs2", "UTF-16BE"}, {"koi8u", "KOI8-R"}, {"binary", "ASCII-8BIT"}, - {"eucjpms", "eucJP-ms"}, - {""}, + {"utf8mb4", "UTF-8"}, + {"macroman", "macRoman"}, {"ujis", "eucJP-ms"}, - {"cp852", "CP852"}, + {"greek", "ISO-8859-7"}, {"cp1251", "Windows-1251"}, - {"geostd8", NULL}, + {"utf16le", "UTF-16LE"}, {""}, {"sjis", "Shift_JIS"}, {"macce", "macCentEuro"}, + {"cp1257", "Windows-1257"}, + {"eucjpms", "eucJP-ms"}, + {""}, + {"utf8", "UTF-8"}, + {"cp852", "CP852"}, + {"cp1250", "Windows-1250"}, + {"gb18030", "GB18030"}, + {""}, + {"swe7", NULL}, + {"koi8r", "KOI8-R"}, + {"tis620", "TIS-620"}, + {"geostd8", NULL}, + {""}, + {"big5", "Big5"}, + {"euckr", "EUC-KR"}, {"latin2", "ISO-8859-2"}, + {"utf8mb3", "UTF-8"}, {""}, - {"macroman", "macRoman"}, {"dec8", NULL}, - {"utf32", "UTF-32"}, + {"cp850", "CP850"}, {"latin1", "ISO-8859-1"}, - {"utf8mb4", "UTF-8"}, + {""}, {"hp8", NULL}, - {"swe7", NULL}, - {"euckr", "EUC-KR"}, - {"cp1257", "Windows-1257"}, - {""}, {""}, - {"utf8", "UTF-8"}, - {"koi8r", "KOI8-R"}, - {"cp1256", "Windows-1256"}, - {""}, {""}, {""}, - {"cp866", "IBM866"}, + {""}, + {"utf16", "UTF-16"}, {"latin7", "ISO-8859-13"}, {""}, {""}, {""}, {"ascii", "US-ASCII"}, - {"hebrew", "ISO-8859-8"}, - {""}, {""}, - {"big5", "Big5"}, - {"utf16", "UTF-16"}, - {"cp1250", "Windows-1250"}, - {""}, {""}, {""}, - {"cp850", "CP850"}, - {"tis620", "TIS-620"}, + {"cp1256", "Windows-1256"}, {""}, {""}, {""}, {"cp932", "Windows-31J"}, + {"hebrew", "ISO-8859-8"}, + {""}, {""}, {""}, {""}, {"latin5", "ISO-8859-9"}, - {""}, {""}, {""}, {""}, {""}, {""}, + {""}, {""}, {""}, + {"cp866", "IBM866"}, + {""}, {""}, {""}, {""}, {""}, {""}, {""}, {"armscii8", NULL} }; diff --git a/ext/mysql2/mysql_enc_to_ruby.h b/ext/mysql2/mysql_enc_to_ruby.h index 37dbf6f73..915d9db03 100644 --- a/ext/mysql2/mysql_enc_to_ruby.h +++ b/ext/mysql2/mysql_enc_to_ruby.h @@ -1,4 +1,4 @@ -const char *mysql2_mysql_enc_to_rb[] = { +static const char *mysql2_mysql_enc_to_rb[] = { "Big5", "ISO-8859-2", NULL, @@ -54,13 +54,13 @@ const char *mysql2_mysql_enc_to_rb[] = { "macRoman", "UTF-16", "UTF-16", - NULL, + "UTF-16LE", "Windows-1256", "Windows-1257", "Windows-1257", "UTF-32", "UTF-32", - NULL, + "UTF-16LE", "ASCII-8BIT", NULL, "US-ASCII", @@ -74,7 +74,7 @@ const char *mysql2_mysql_enc_to_rb[] = { NULL, "KOI8-R", "KOI8-R", - NULL, + "UTF-8", "ISO-8859-2", "ISO-8859-9", "ISO-8859-13", @@ -119,10 +119,10 @@ const char *mysql2_mysql_enc_to_rb[] = { "UTF-16", "UTF-16", "UTF-16", - NULL, - NULL, - NULL, - NULL, + "UTF-16", + "UTF-16", + "UTF-16", + "UTF-16", NULL, NULL, NULL, @@ -146,6 +146,10 @@ const char *mysql2_mysql_enc_to_rb[] = { "UTF-16BE", "UTF-16BE", "UTF-16BE", + "UTF-16BE", + "UTF-16BE", + "UTF-16BE", + "UTF-16BE", NULL, NULL, NULL, @@ -153,11 +157,11 @@ const char *mysql2_mysql_enc_to_rb[] = { NULL, NULL, NULL, - NULL, - NULL, - NULL, - NULL, - NULL, + "UTF-16BE", + "UTF-32", + "UTF-32", + "UTF-32", + "UTF-32", "UTF-32", "UTF-32", "UTF-32", @@ -186,6 +190,33 @@ const char *mysql2_mysql_enc_to_rb[] = { NULL, NULL, NULL, + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + NULL, + NULL, + NULL, NULL, NULL, NULL, @@ -210,18 +241,67 @@ const char *mysql2_mysql_enc_to_rb[] = { "UTF-8", "UTF-8", "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "GB18030", + "GB18030", + "GB18030", NULL, NULL, NULL, NULL, + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", NULL, + "UTF-8", + "UTF-8", + "UTF-8", NULL, + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", + "UTF-8", NULL, + "UTF-8", + "UTF-8", + "UTF-8", NULL, + "UTF-8", NULL, NULL, - NULL, - NULL, + "UTF-8", "UTF-8", "UTF-8", "UTF-8", @@ -243,4 +323,3 @@ const char *mysql2_mysql_enc_to_rb[] = { "UTF-8", "UTF-8" }; - diff --git a/ext/mysql2/result.c b/ext/mysql2/result.c index e4f3d3666..9f9b473b8 100644 --- a/ext/mysql2/result.c +++ b/ext/mysql2/result.c @@ -1,103 +1,209 @@ #include -#include #include "mysql_enc_to_ruby.h" +#define MYSQL2_CHARSETNR_SIZE (sizeof(mysql2_mysql_enc_to_rb)/sizeof(mysql2_mysql_enc_to_rb[0])) -#ifdef HAVE_RUBY_ENCODING_H static rb_encoding *binaryEncoding; -#endif -#if (SIZEOF_INT < SIZEOF_LONG) || defined(HAVE_RUBY_ENCODING_H) /* on 64bit platforms we can handle dates way outside 2038-01-19T03:14:07 * * (9999*31557600) + (12*2592000) + (31*86400) + (11*3600) + (59*60) + 59 */ #define MYSQL2_MAX_TIME 315578267999ULL -#else -/** - * On 32bit platforms the maximum date the Time class can handle is 2038-01-19T03:14:07 - * 2038 years + 1 month + 19 days + 3 hours + 14 minutes + 7 seconds = 64318634047 seconds - * - * (2038*31557600) + (1*2592000) + (19*86400) + (3*3600) + (14*60) + 7 - */ -#define MYSQL2_MAX_TIME 64318634047ULL -#endif -#if defined(HAVE_RUBY_ENCODING_H) /* 0000-1-1 00:00:00 UTC * * (0*31557600) + (1*2592000) + (1*86400) + (0*3600) + (0*60) + 0 */ #define MYSQL2_MIN_TIME 2678400ULL -#elif SIZEOF_INT < SIZEOF_LONG /* 64bit Ruby 1.8 */ -/* 0139-1-1 00:00:00 UTC - * - * (139*31557600) + (1*2592000) + (1*86400) + (0*3600) + (0*60) + 0 - */ -#define MYSQL2_MIN_TIME 4389184800ULL -#elif defined(NEGATIVE_TIME_T) -/* 1901-12-13 20:45:52 UTC : The oldest time in 32-bit signed time_t. - * - * (1901*31557600) + (12*2592000) + (13*86400) + (20*3600) + (45*60) + 52 - */ -#define MYSQL2_MIN_TIME 60023299552ULL -#else -/* 1970-01-01 00:00:01 UTC : The Unix epoch - the oldest time in portable time_t. - * - * (1970*31557600) + (1*2592000) + (1*86400) + (0*3600) + (0*60) + 1 + +#define MYSQL2_MAX_BYTES_PER_CHAR 3 + +/* From Mysql documentations: + * To distinguish between binary and nonbinary data for string data types, + * check whether the charsetnr value is 63. If so, the character set is binary, + * which indicates binary rather than nonbinary data. This enables you to distinguish BINARY + * from CHAR, VARBINARY from VARCHAR, and the BLOB types from the TEXT types. */ -#define MYSQL2_MIN_TIME 62171150401ULL +#define MYSQL2_BINARY_CHARSET 63 + +#ifndef MYSQL_TYPE_VECTOR +#define MYSQL_TYPE_VECTOR 242 #endif -static VALUE cMysql2Result; -static VALUE cBigDecimal, cDate, cDateTime; -static VALUE opt_decimal_zero, opt_float_zero, opt_time_year, opt_time_month, opt_utc_offset; -extern VALUE mMysql2, cMysql2Client, cMysql2Error; -static ID intern_new, intern_utc, intern_local, intern_localtime, intern_local_offset, intern_civil, intern_new_offset; -static VALUE sym_symbolize_keys, sym_as, sym_array, sym_database_timezone, sym_application_timezone, - sym_local, sym_utc, sym_cast_booleans, sym_cache_rows, sym_cast, sym_stream, sym_name; -static ID intern_merge; +#ifndef MYSQL_TYPE_JSON +#define MYSQL_TYPE_JSON 245 +#endif + +#ifndef NEW_TYPEDDATA_WRAPPER +#define TypedData_Get_Struct(obj, type, ignore, sval) Data_Get_Struct(obj, type, sval) +#endif +#define GET_RESULT(self) \ + mysql2_result_wrapper *wrapper; \ + TypedData_Get_Struct(self, mysql2_result_wrapper, &rb_mysql_result_type, wrapper); + +typedef struct { + int symbolizeKeys; + int asArray; + int castBool; + int cacheRows; + int cast; + int streaming; + ID db_timezone; + ID app_timezone; + int block_given; /* boolean */ +} result_each_args; + +extern VALUE mMysql2, cMysql2Client, cMysql2Error; +static VALUE cMysql2Result, cDateTime, cDate; +static VALUE opt_decimal_zero, opt_float_zero, opt_time_year, opt_time_month, opt_utc_offset; +static ID intern_new, intern_utc, intern_local, intern_localtime, intern_local_offset, + intern_civil, intern_new_offset, intern_merge, intern_BigDecimal, + intern_query_options; +static VALUE sym_symbolize_keys, sym_as, sym_array, sym_database_timezone, + sym_application_timezone, sym_local, sym_utc, sym_cast_booleans, + sym_cache_rows, sym_cast, sym_stream, sym_name; + +/* Mark any VALUEs that are only referenced in C, so the GC won't get them. */ static void rb_mysql_result_mark(void * wrapper) { mysql2_result_wrapper * w = wrapper; if (w) { - rb_gc_mark(w->fields); - rb_gc_mark(w->rows); - rb_gc_mark(w->encoding); + rb_gc_mark_movable(w->fields); + rb_gc_mark_movable(w->rows); + rb_gc_mark_movable(w->encoding); + rb_gc_mark_movable(w->client); + rb_gc_mark_movable(w->statement); } } /* this may be called manually or during GC */ static void rb_mysql_result_free_result(mysql2_result_wrapper * wrapper) { - if (wrapper && wrapper->resultFreed != 1) { + if (!wrapper) return; + + if (wrapper->resultFreed != 1) { + if (wrapper->stmt_wrapper) { + if (!wrapper->stmt_wrapper->closed) { + mysql_stmt_free_result(wrapper->stmt_wrapper->stmt); + + /* MySQL BUG? If the statement handle was previously used, and so + * mysql_stmt_bind_result was called, and if that result set and bind buffers were freed, + * MySQL still thinks the result set buffer is available and will prefetch the + * first result in mysql_stmt_execute. This will corrupt or crash the program. + * By setting bind_result_done back to 0, we make MySQL think that a result set + * has never been bound to this statement handle before to prevent the prefetch. + */ + wrapper->stmt_wrapper->stmt->bind_result_done = 0; + } + + if (wrapper->statement != Qnil) { + decr_mysql2_stmt(wrapper->stmt_wrapper); + } + + if (wrapper->result_buffers) { + unsigned int i; + for (i = 0; i < wrapper->numberOfFields; i++) { + if (wrapper->result_buffers[i].buffer) { + xfree(wrapper->result_buffers[i].buffer); + } + } + xfree(wrapper->result_buffers); + xfree(wrapper->is_null); + xfree(wrapper->error); + xfree(wrapper->length); + } + /* Clue that the next statement execute will need to allocate a new result buffer. */ + wrapper->result_buffers = NULL; + } + /* FIXME: this may call flush_use_result, which can hit the socket */ + /* For prepared statements, wrapper->result is the result metadata */ mysql_free_result(wrapper->result); wrapper->resultFreed = 1; } } /* this is called during GC */ -static void rb_mysql_result_free(void * wrapper) { - mysql2_result_wrapper * w = wrapper; - /* FIXME: this may call flush_use_result, which can hit the socket */ - rb_mysql_result_free_result(w); +static void rb_mysql_result_free(void *ptr) { + mysql2_result_wrapper *wrapper = ptr; + rb_mysql_result_free_result(wrapper); + + // If the GC gets to client first it will be nil + if (wrapper->client != Qnil) { + decr_mysql2_client(wrapper->client_wrapper); + } + xfree(wrapper); } +static size_t rb_mysql_result_memsize(const void * wrapper) { + const mysql2_result_wrapper * w = wrapper; + size_t memsize = sizeof(*w); + if (w->stmt_wrapper) { + memsize += sizeof(*w->stmt_wrapper); + } + if (w->client_wrapper) { + memsize += sizeof(*w->client_wrapper); + } + return memsize; +} + +#ifdef HAVE_RB_GC_MARK_MOVABLE +static void rb_mysql_result_compact(void * wrapper) { + mysql2_result_wrapper * w = wrapper; + if (w) { + rb_mysql2_gc_location(w->fields); + rb_mysql2_gc_location(w->rows); + rb_mysql2_gc_location(w->encoding); + rb_mysql2_gc_location(w->client); + rb_mysql2_gc_location(w->statement); + } +} +#endif + +static const rb_data_type_t rb_mysql_result_type = { + "rb_mysql_result", + { + rb_mysql_result_mark, + rb_mysql_result_free, + rb_mysql_result_memsize, +#ifdef HAVE_RB_GC_MARK_MOVABLE + rb_mysql_result_compact, +#endif + }, + 0, + 0, +#ifdef RUBY_TYPED_FREE_IMMEDIATELY + RUBY_TYPED_FREE_IMMEDIATELY, +#endif +}; + +static VALUE rb_mysql_result_free_(VALUE self) { + GET_RESULT(self); + rb_mysql_result_free_result(wrapper); + return Qnil; +} + /* * for small results, this won't hit the network, but there's no * reliable way for us to tell this so we'll always release the GVL * to be safe */ -static VALUE nogvl_fetch_row(void *ptr) { +static void *nogvl_fetch_row(void *ptr) { MYSQL_RES *result = ptr; - return (VALUE)mysql_fetch_row(result); + return mysql_fetch_row(result); } -static VALUE rb_mysql_result_fetch_field(VALUE self, unsigned int idx, short int symbolize_keys) { - mysql2_result_wrapper * wrapper; +static void *nogvl_stmt_fetch(void *ptr) { + MYSQL_STMT *stmt = ptr; + uintptr_t r = mysql_stmt_fetch(stmt); + + return (void *)r; +} + +static VALUE rb_mysql_result_fetch_field(VALUE self, unsigned int idx, int symbolize_keys) { VALUE rb_field; - GetMysql2Result(self, wrapper); + GET_RESULT(self); if (wrapper->fields == Qnil) { wrapper->numberOfFields = mysql_num_fields(wrapper->result); @@ -107,29 +213,25 @@ static VALUE rb_mysql_result_fetch_field(VALUE self, unsigned int idx, short int rb_field = rb_ary_entry(wrapper->fields, idx); if (rb_field == Qnil) { MYSQL_FIELD *field = NULL; -#ifdef HAVE_RUBY_ENCODING_H rb_encoding *default_internal_enc = rb_default_internal_encoding(); rb_encoding *conn_enc = rb_to_encoding(wrapper->encoding); -#endif field = mysql_fetch_field_direct(wrapper->result, idx); if (symbolize_keys) { - VALUE colStr; - char buf[field->name_length+1]; - memcpy(buf, field->name, field->name_length); - buf[field->name_length] = 0; - colStr = rb_str_new2(buf); -#ifdef HAVE_RUBY_ENCODING_H - rb_enc_associate(colStr, rb_utf8_encoding()); -#endif - rb_field = ID2SYM(rb_to_id(colStr)); + rb_field = rb_intern3(field->name, field->name_length, rb_utf8_encoding()); + rb_field = ID2SYM(rb_field); } else { - rb_field = rb_str_new(field->name, field->name_length); -#ifdef HAVE_RUBY_ENCODING_H - rb_enc_associate(rb_field, conn_enc); - if (default_internal_enc) { +#ifdef HAVE_RB_ENC_INTERNED_STR + rb_field = rb_enc_interned_str(field->name, field->name_length, conn_enc); + if (default_internal_enc && default_internal_enc != conn_enc) { + rb_field = rb_str_to_interned_str(rb_str_export_to_enc(rb_field, default_internal_enc)); + } +#else + rb_field = rb_enc_str_new(field->name, field->name_length, conn_enc); + if (default_internal_enc && default_internal_enc != conn_enc) { rb_field = rb_str_export_to_enc(rb_field, default_internal_enc); } + rb_obj_freeze(rb_field); #endif } rb_ary_store(wrapper->fields, idx, rb_field); @@ -138,17 +240,185 @@ static VALUE rb_mysql_result_fetch_field(VALUE self, unsigned int idx, short int return rb_field; } -#ifdef HAVE_RUBY_ENCODING_H +static VALUE rb_mysql_result_fetch_field_type(VALUE self, unsigned int idx) { + VALUE rb_field_type; + GET_RESULT(self); + + if (wrapper->fieldTypes == Qnil) { + wrapper->numberOfFields = mysql_num_fields(wrapper->result); + wrapper->fieldTypes = rb_ary_new2(wrapper->numberOfFields); + } + + rb_field_type = rb_ary_entry(wrapper->fieldTypes, idx); + if (rb_field_type == Qnil) { + MYSQL_FIELD *field = NULL; + rb_encoding *default_internal_enc = rb_default_internal_encoding(); + rb_encoding *conn_enc = rb_to_encoding(wrapper->encoding); + int precision; + + field = mysql_fetch_field_direct(wrapper->result, idx); + + switch(field->type) { + case MYSQL_TYPE_NULL: // NULL + rb_field_type = rb_str_new_cstr("null"); + break; + case MYSQL_TYPE_TINY: // signed char + rb_field_type = rb_sprintf("tinyint(%ld)", field->length); + break; + case MYSQL_TYPE_SHORT: // short int + rb_field_type = rb_sprintf("smallint(%ld)", field->length); + break; + case MYSQL_TYPE_YEAR: // short int + rb_field_type = rb_sprintf("year(%ld)", field->length); + break; + case MYSQL_TYPE_INT24: // int + rb_field_type = rb_sprintf("mediumint(%ld)", field->length); + break; + case MYSQL_TYPE_LONG: // int + rb_field_type = rb_sprintf("int(%ld)", field->length); + break; + case MYSQL_TYPE_LONGLONG: // long long int + rb_field_type = rb_sprintf("bigint(%ld)", field->length); + break; + case MYSQL_TYPE_FLOAT: // float + rb_field_type = rb_sprintf("float(%ld,%d)", field->length, field->decimals); + break; + case MYSQL_TYPE_DOUBLE: // double + rb_field_type = rb_sprintf("double(%ld,%d)", field->length, field->decimals); + break; + case MYSQL_TYPE_TIME: // MYSQL_TIME + rb_field_type = rb_str_new_cstr("time"); + break; + case MYSQL_TYPE_DATE: // MYSQL_TIME + case MYSQL_TYPE_NEWDATE: // MYSQL_TIME + rb_field_type = rb_str_new_cstr("date"); + break; + case MYSQL_TYPE_DATETIME: // MYSQL_TIME + rb_field_type = rb_str_new_cstr("datetime"); + break; + case MYSQL_TYPE_TIMESTAMP: // MYSQL_TIME + rb_field_type = rb_str_new_cstr("timestamp"); + break; + case MYSQL_TYPE_DECIMAL: // char[] + case MYSQL_TYPE_NEWDECIMAL: // char[] + /* + Handle precision similar to this line from mysql's code: + https://github.com/mysql/mysql-server/blob/ea7d2e2d16ac03afdd9cb72a972a95981107bf51/sql/field.cc#L2246 + */ + precision = field->length - (field->decimals > 0 ? 2 : 1); + rb_field_type = rb_sprintf("decimal(%d,%d)", precision, field->decimals); + break; + case MYSQL_TYPE_STRING: // char[] + if (field->flags & ENUM_FLAG) { + rb_field_type = rb_str_new_cstr("enum"); + } else if (field->flags & SET_FLAG) { + rb_field_type = rb_str_new_cstr("set"); + } else { + if (field->charsetnr == MYSQL2_BINARY_CHARSET) { + rb_field_type = rb_sprintf("binary(%ld)", field->length); + } else { + rb_field_type = rb_sprintf("char(%ld)", field->length / MYSQL2_MAX_BYTES_PER_CHAR); + } + } + break; + case MYSQL_TYPE_VAR_STRING: // char[] + if (field->charsetnr == MYSQL2_BINARY_CHARSET) { + rb_field_type = rb_sprintf("varbinary(%ld)", field->length); + } else { + rb_field_type = rb_sprintf("varchar(%ld)", field->length / MYSQL2_MAX_BYTES_PER_CHAR); + } + break; + case MYSQL_TYPE_VARCHAR: // char[] + rb_field_type = rb_sprintf("varchar(%ld)", field->length / MYSQL2_MAX_BYTES_PER_CHAR); + break; + case MYSQL_TYPE_TINY_BLOB: // char[] + rb_field_type = rb_str_new_cstr("tinyblob"); + break; + case MYSQL_TYPE_BLOB: // char[] + if (field->charsetnr == MYSQL2_BINARY_CHARSET) { + switch(field->length) { + case 255: + rb_field_type = rb_str_new_cstr("tinyblob"); + break; + case 65535: + rb_field_type = rb_str_new_cstr("blob"); + break; + case 16777215: + rb_field_type = rb_str_new_cstr("mediumblob"); + break; + case 4294967295: + rb_field_type = rb_str_new_cstr("longblob"); + default: + break; + } + } else { + if (field->length == (255 * MYSQL2_MAX_BYTES_PER_CHAR)) { + rb_field_type = rb_str_new_cstr("tinytext"); + } else if (field->length == (65535 * MYSQL2_MAX_BYTES_PER_CHAR)) { + rb_field_type = rb_str_new_cstr("text"); + } else if (field->length == (16777215 * MYSQL2_MAX_BYTES_PER_CHAR)) { + rb_field_type = rb_str_new_cstr("mediumtext"); + } else if (field->length == 4294967295) { + rb_field_type = rb_str_new_cstr("longtext"); + } else { + rb_field_type = rb_sprintf("text(%ld)", field->length); + } + } + break; + case MYSQL_TYPE_MEDIUM_BLOB: // char[] + rb_field_type = rb_str_new_cstr("mediumblob"); + break; + case MYSQL_TYPE_LONG_BLOB: // char[] + rb_field_type = rb_str_new_cstr("longblob"); + break; + case MYSQL_TYPE_BIT: // char[] + rb_field_type = rb_sprintf("bit(%ld)", field->length); + break; + case MYSQL_TYPE_SET: // char[] + rb_field_type = rb_str_new_cstr("set"); + break; + case MYSQL_TYPE_ENUM: // char[] + rb_field_type = rb_str_new_cstr("enum"); + break; + case MYSQL_TYPE_GEOMETRY: // char[] + rb_field_type = rb_str_new_cstr("geometry"); + break; + case MYSQL_TYPE_JSON: // json + rb_field_type = rb_str_new_cstr("json"); + break; + case MYSQL_TYPE_VECTOR: // vector + rb_field_type = rb_str_new_cstr("vector"); + break; + default: + rb_field_type = rb_str_new_cstr("unknown"); + break; + } + + rb_enc_associate(rb_field_type, conn_enc); + if (default_internal_enc) { + rb_field_type = rb_str_export_to_enc(rb_field_type, default_internal_enc); + } + + rb_ary_store(wrapper->fieldTypes, idx, rb_field_type); + } + + return rb_field_type; +} + static VALUE mysql2_set_field_string_encoding(VALUE val, MYSQL_FIELD field, rb_encoding *default_internal_enc, rb_encoding *conn_enc) { - /* if binary flag is set, respect it's wishes */ - if (field.flags & BINARY_FLAG && field.charsetnr == 63) { + /* if binary flag is set, respect its wishes */ + if (field.flags & BINARY_FLAG && field.charsetnr == MYSQL2_BINARY_CHARSET) { + rb_enc_associate(val, binaryEncoding); + } else if (!field.charsetnr) { + /* MySQL 4.x may not provide an encoding, binary will get the bytes through */ rb_enc_associate(val, binaryEncoding); } else { /* lookup the encoding configured on this field */ const char *enc_name; int enc_index; - enc_name = mysql2_mysql_enc_to_rb[field.charsetnr-1]; + enc_name = (field.charsetnr-1 < MYSQL2_CHARSETNR_SIZE) ? mysql2_mysql_enc_to_rb[field.charsetnr-1] : NULL; + if (enc_name != NULL) { /* use the field encoding we were able to match */ enc_index = rb_enc_find_index(enc_name); @@ -164,58 +434,327 @@ static VALUE mysql2_set_field_string_encoding(VALUE val, MYSQL_FIELD field, rb_e } return val; } -#endif +/* Interpret microseconds digits left-aligned in fixed-width field. + * e.g. 10.123 seconds means 10 seconds and 123000 microseconds, + * because the microseconds are to the right of the decimal point. + */ +static unsigned int msec_char_to_uint(char *msec_char, size_t len) +{ + size_t i; + for (i = 0; i < (len - 1); i++) { + if (msec_char[i] == '\0') { + msec_char[i] = '0'; + } + } + return (unsigned int)strtoul(msec_char, NULL, 10); +} -static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezone, int symbolizeKeys, int asArray, int castBool, int cast, MYSQL_FIELD * fields) { +static void rb_mysql_result_alloc_result_buffers(VALUE self, MYSQL_FIELD *fields) { + unsigned int i; + GET_RESULT(self); + + if (wrapper->result_buffers != NULL) return; + + wrapper->result_buffers = xcalloc(wrapper->numberOfFields, sizeof(MYSQL_BIND)); + wrapper->is_null = xcalloc(wrapper->numberOfFields, sizeof(my_bool)); + wrapper->error = xcalloc(wrapper->numberOfFields, sizeof(my_bool)); + wrapper->length = xcalloc(wrapper->numberOfFields, sizeof(unsigned long)); + + for (i = 0; i < wrapper->numberOfFields; i++) { + wrapper->result_buffers[i].buffer_type = fields[i].type; + + // mysql type | C type + switch(fields[i].type) { + case MYSQL_TYPE_NULL: // NULL + break; + case MYSQL_TYPE_TINY: // signed char + wrapper->result_buffers[i].buffer = xcalloc(1, sizeof(signed char)); + wrapper->result_buffers[i].buffer_length = sizeof(signed char); + break; + case MYSQL_TYPE_SHORT: // short int + case MYSQL_TYPE_YEAR: // short int + wrapper->result_buffers[i].buffer = xcalloc(1, sizeof(short int)); + wrapper->result_buffers[i].buffer_length = sizeof(short int); + break; + case MYSQL_TYPE_INT24: // int + case MYSQL_TYPE_LONG: // int + wrapper->result_buffers[i].buffer = xcalloc(1, sizeof(int)); + wrapper->result_buffers[i].buffer_length = sizeof(int); + break; + case MYSQL_TYPE_LONGLONG: // long long int + wrapper->result_buffers[i].buffer = xcalloc(1, sizeof(long long int)); + wrapper->result_buffers[i].buffer_length = sizeof(long long int); + break; + case MYSQL_TYPE_FLOAT: // float + case MYSQL_TYPE_DOUBLE: // double + wrapper->result_buffers[i].buffer = xcalloc(1, sizeof(double)); + wrapper->result_buffers[i].buffer_length = sizeof(double); + break; + case MYSQL_TYPE_TIME: // MYSQL_TIME + case MYSQL_TYPE_DATE: // MYSQL_TIME + case MYSQL_TYPE_NEWDATE: // MYSQL_TIME + case MYSQL_TYPE_DATETIME: // MYSQL_TIME + case MYSQL_TYPE_TIMESTAMP: // MYSQL_TIME + wrapper->result_buffers[i].buffer = xcalloc(1, sizeof(MYSQL_TIME)); + wrapper->result_buffers[i].buffer_length = sizeof(MYSQL_TIME); + break; + case MYSQL_TYPE_DECIMAL: // char[] + case MYSQL_TYPE_NEWDECIMAL: // char[] + case MYSQL_TYPE_STRING: // char[] + case MYSQL_TYPE_VAR_STRING: // char[] + case MYSQL_TYPE_VARCHAR: // char[] + case MYSQL_TYPE_TINY_BLOB: // char[] + case MYSQL_TYPE_BLOB: // char[] + case MYSQL_TYPE_MEDIUM_BLOB: // char[] + case MYSQL_TYPE_LONG_BLOB: // char[] + case MYSQL_TYPE_BIT: // char[] + case MYSQL_TYPE_SET: // char[] + case MYSQL_TYPE_ENUM: // char[] + case MYSQL_TYPE_GEOMETRY: // char[] + default: + wrapper->result_buffers[i].buffer = xmalloc(fields[i].max_length); + wrapper->result_buffers[i].buffer_length = fields[i].max_length; + break; + } + + wrapper->result_buffers[i].is_null = &wrapper->is_null[i]; + wrapper->result_buffers[i].length = &wrapper->length[i]; + wrapper->result_buffers[i].error = &wrapper->error[i]; + wrapper->result_buffers[i].is_unsigned = ((fields[i].flags & UNSIGNED_FLAG) != 0); + } +} + +static VALUE rb_mysql_result_fetch_row_stmt(VALUE self, MYSQL_FIELD * fields, const result_each_args *args) +{ + VALUE rowVal; + unsigned int i = 0; + + rb_encoding *default_internal_enc; + rb_encoding *conn_enc; + GET_RESULT(self); + + default_internal_enc = rb_default_internal_encoding(); + conn_enc = rb_to_encoding(wrapper->encoding); + + if (wrapper->fields == Qnil) { + wrapper->numberOfFields = mysql_num_fields(wrapper->result); + wrapper->fields = rb_ary_new2(wrapper->numberOfFields); + } + if (args->asArray) { + rowVal = rb_ary_new2(wrapper->numberOfFields); + } else { + rowVal = rb_hash_new(); + } + + if (wrapper->result_buffers == NULL) { + rb_mysql_result_alloc_result_buffers(self, fields); + } + + if (mysql_stmt_bind_result(wrapper->stmt_wrapper->stmt, wrapper->result_buffers)) { + rb_raise_mysql2_stmt_error(wrapper->stmt_wrapper); + } + + { + switch((uintptr_t)rb_thread_call_without_gvl(nogvl_stmt_fetch, wrapper->stmt_wrapper->stmt, RUBY_UBF_IO, 0)) { + case 0: + /* success */ + break; + + case 1: + /* error */ + rb_raise_mysql2_stmt_error(wrapper->stmt_wrapper); + + case MYSQL_NO_DATA: + /* no more row */ + return Qnil; + + case MYSQL_DATA_TRUNCATED: + rb_raise(cMysql2Error, "IMPLBUG: caught MYSQL_DATA_TRUNCATED. should not come here as buffer_length is set to fields[i].max_length."); + } + } + + for (i = 0; i < wrapper->numberOfFields; i++) { + VALUE field = rb_mysql_result_fetch_field(self, i, args->symbolizeKeys); + VALUE val = Qnil; + MYSQL_TIME *ts; + + if (wrapper->is_null[i]) { + val = Qnil; + } else { + const MYSQL_BIND* const result_buffer = &wrapper->result_buffers[i]; + + switch(result_buffer->buffer_type) { + case MYSQL_TYPE_TINY: // signed char + if (args->castBool && fields[i].length == 1) { + val = (*((unsigned char*)result_buffer->buffer) != 0) ? Qtrue : Qfalse; + break; + } + if (result_buffer->is_unsigned) { + val = UINT2NUM(*((unsigned char*)result_buffer->buffer)); + } else { + val = INT2NUM(*((signed char*)result_buffer->buffer)); + } + break; + case MYSQL_TYPE_BIT: /* BIT field (MySQL 5.0.3 and up) */ + if (args->castBool && fields[i].length == 1) { + val = (*((unsigned char*)result_buffer->buffer) != 0) ? Qtrue : Qfalse; + }else{ + val = rb_str_new(result_buffer->buffer, *(result_buffer->length)); + } + break; + case MYSQL_TYPE_SHORT: // short int + case MYSQL_TYPE_YEAR: // short int + if (result_buffer->is_unsigned) { + val = UINT2NUM(*((unsigned short int*)result_buffer->buffer)); + } else { + val = INT2NUM(*((short int*)result_buffer->buffer)); + } + break; + case MYSQL_TYPE_INT24: // int + case MYSQL_TYPE_LONG: // int + if (result_buffer->is_unsigned) { + val = UINT2NUM(*((unsigned int*)result_buffer->buffer)); + } else { + val = INT2NUM(*((int*)result_buffer->buffer)); + } + break; + case MYSQL_TYPE_LONGLONG: // long long int + if (result_buffer->is_unsigned) { + val = ULL2NUM(*((unsigned long long int*)result_buffer->buffer)); + } else { + val = LL2NUM(*((long long int*)result_buffer->buffer)); + } + break; + case MYSQL_TYPE_FLOAT: // float + val = rb_float_new((double)(*((float*)result_buffer->buffer))); + break; + case MYSQL_TYPE_DOUBLE: // double + val = rb_float_new((double)(*((double*)result_buffer->buffer))); + break; + case MYSQL_TYPE_DATE: // MYSQL_TIME + case MYSQL_TYPE_NEWDATE: // MYSQL_TIME + ts = (MYSQL_TIME*)result_buffer->buffer; + val = rb_funcall(cDate, intern_new, 3, INT2NUM(ts->year), INT2NUM(ts->month), INT2NUM(ts->day)); + break; + case MYSQL_TYPE_TIME: // MYSQL_TIME + ts = (MYSQL_TIME*)result_buffer->buffer; + val = rb_funcall(rb_cTime, args->db_timezone, 7, opt_time_year, opt_time_month, opt_time_month, UINT2NUM(ts->hour), UINT2NUM(ts->minute), UINT2NUM(ts->second), ULONG2NUM(ts->second_part)); + if (!NIL_P(args->app_timezone)) { + if (args->app_timezone == intern_local) { + val = rb_funcall(val, intern_localtime, 0); + } else { // utc + val = rb_funcall(val, intern_utc, 0); + } + } + break; + case MYSQL_TYPE_DATETIME: // MYSQL_TIME + case MYSQL_TYPE_TIMESTAMP: { // MYSQL_TIME + uint64_t seconds; + + ts = (MYSQL_TIME*)result_buffer->buffer; + seconds = (ts->year*31557600ULL) + (ts->month*2592000ULL) + (ts->day*86400ULL) + (ts->hour*3600ULL) + (ts->minute*60ULL) + ts->second; + + if (seconds < MYSQL2_MIN_TIME || seconds > MYSQL2_MAX_TIME) { // use DateTime instead + VALUE offset = INT2NUM(0); + if (args->db_timezone == intern_local) { + offset = rb_funcall(cMysql2Client, intern_local_offset, 0); + } + val = rb_funcall(cDateTime, intern_civil, 7, UINT2NUM(ts->year), UINT2NUM(ts->month), UINT2NUM(ts->day), UINT2NUM(ts->hour), UINT2NUM(ts->minute), UINT2NUM(ts->second), offset); + if (!NIL_P(args->app_timezone)) { + if (args->app_timezone == intern_local) { + offset = rb_funcall(cMysql2Client, intern_local_offset, 0); + val = rb_funcall(val, intern_new_offset, 1, offset); + } else { // utc + val = rb_funcall(val, intern_new_offset, 1, opt_utc_offset); + } + } + } else { + val = rb_funcall(rb_cTime, args->db_timezone, 7, UINT2NUM(ts->year), UINT2NUM(ts->month), UINT2NUM(ts->day), UINT2NUM(ts->hour), UINT2NUM(ts->minute), UINT2NUM(ts->second), ULONG2NUM(ts->second_part)); + if (!NIL_P(args->app_timezone)) { + if (args->app_timezone == intern_local) { + val = rb_funcall(val, intern_localtime, 0); + } else { // utc + val = rb_funcall(val, intern_utc, 0); + } + } + } + break; + } + case MYSQL_TYPE_DECIMAL: // char[] + case MYSQL_TYPE_NEWDECIMAL: // char[] + val = rb_funcall(rb_mKernel, intern_BigDecimal, 1, rb_str_new(result_buffer->buffer, *(result_buffer->length))); + break; + case MYSQL_TYPE_STRING: // char[] + case MYSQL_TYPE_VAR_STRING: // char[] + case MYSQL_TYPE_VARCHAR: // char[] + case MYSQL_TYPE_TINY_BLOB: // char[] + case MYSQL_TYPE_BLOB: // char[] + case MYSQL_TYPE_MEDIUM_BLOB: // char[] + case MYSQL_TYPE_LONG_BLOB: // char[] + case MYSQL_TYPE_SET: // char[] + case MYSQL_TYPE_ENUM: // char[] + case MYSQL_TYPE_GEOMETRY: // char[] + default: + val = rb_str_new(result_buffer->buffer, *(result_buffer->length)); + val = mysql2_set_field_string_encoding(val, fields[i], default_internal_enc, conn_enc); + break; + } + } + + if (args->asArray) { + rb_ary_push(rowVal, val); + } else { + rb_hash_aset(rowVal, field, val); + } + } + + return rowVal; +} + +static VALUE rb_mysql_result_fetch_row(VALUE self, MYSQL_FIELD * fields, const result_each_args *args) +{ VALUE rowVal; - mysql2_result_wrapper * wrapper; MYSQL_ROW row; unsigned int i = 0; unsigned long * fieldLengths; void * ptr; -#ifdef HAVE_RUBY_ENCODING_H rb_encoding *default_internal_enc; rb_encoding *conn_enc; -#endif - GetMysql2Result(self, wrapper); + GET_RESULT(self); -#ifdef HAVE_RUBY_ENCODING_H default_internal_enc = rb_default_internal_encoding(); conn_enc = rb_to_encoding(wrapper->encoding); -#endif ptr = wrapper->result; - row = (MYSQL_ROW)rb_thread_blocking_region(nogvl_fetch_row, ptr, RUBY_UBF_IO, 0); + row = (MYSQL_ROW)rb_thread_call_without_gvl(nogvl_fetch_row, ptr, RUBY_UBF_IO, 0); if (row == NULL) { return Qnil; } - if (asArray) { + if (wrapper->fields == Qnil) { + wrapper->numberOfFields = mysql_num_fields(wrapper->result); + wrapper->fields = rb_ary_new2(wrapper->numberOfFields); + } + if (args->asArray) { rowVal = rb_ary_new2(wrapper->numberOfFields); } else { rowVal = rb_hash_new(); } fieldLengths = mysql_fetch_lengths(wrapper->result); - if (wrapper->fields == Qnil) { - wrapper->numberOfFields = mysql_num_fields(wrapper->result); - wrapper->fields = rb_ary_new2(wrapper->numberOfFields); - } for (i = 0; i < wrapper->numberOfFields; i++) { - VALUE field = rb_mysql_result_fetch_field(self, i, symbolizeKeys); + VALUE field = rb_mysql_result_fetch_field(self, i, args->symbolizeKeys); if (row[i]) { VALUE val = Qnil; enum enum_field_types type = fields[i].type; - if(!cast) { + if (!args->cast) { if (type == MYSQL_TYPE_NULL) { val = Qnil; } else { val = rb_str_new(row[i], fieldLengths[i]); -#ifdef HAVE_RUBY_ENCODING_H val = mysql2_set_field_string_encoding(val, fields[i], default_internal_enc, conn_enc); -#endif } } else { switch(type) { @@ -223,10 +762,14 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo val = Qnil; break; case MYSQL_TYPE_BIT: /* BIT field (MySQL 5.0.3 and up) */ - val = rb_str_new(row[i], fieldLengths[i]); + if (args->castBool && fields[i].length == 1) { + val = *row[i] == 1 ? Qtrue : Qfalse; + }else{ + val = rb_str_new(row[i], fieldLengths[i]); + } break; case MYSQL_TYPE_TINY: /* TINYINT field */ - if (castBool && fields[i].length == 1) { + if (args->castBool && fields[i].length == 1) { val = *row[i] != '0' ? Qtrue : Qfalse; break; } @@ -242,9 +785,9 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo if (fields[i].decimals == 0) { val = rb_cstr2inum(row[i], 10); } else if (strtod(row[i], NULL) == 0.000000){ - val = rb_funcall(cBigDecimal, intern_new, 1, opt_decimal_zero); + val = rb_funcall(rb_mKernel, intern_BigDecimal, 1, opt_decimal_zero); }else{ - val = rb_funcall(cBigDecimal, intern_new, 1, rb_str_new(row[i], fieldLengths[i])); + val = rb_funcall(rb_mKernel, intern_BigDecimal, 1, rb_str_new(row[i], fieldLengths[i])); } break; case MYSQL_TYPE_FLOAT: /* FLOAT field */ @@ -260,11 +803,18 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo } case MYSQL_TYPE_TIME: { /* TIME field */ int tokens; - unsigned int hour=0, min=0, sec=0; - tokens = sscanf(row[i], "%2u:%2u:%2u", &hour, &min, &sec); - val = rb_funcall(rb_cTime, db_timezone, 6, opt_time_year, opt_time_month, opt_time_month, UINT2NUM(hour), UINT2NUM(min), UINT2NUM(sec)); - if (!NIL_P(app_timezone)) { - if (app_timezone == intern_local) { + unsigned int hour=0, min=0, sec=0, msec=0; + char msec_char[7] = {'0','0','0','0','0','0','\0'}; + + tokens = sscanf(row[i], "%2u:%2u:%2u.%6s", &hour, &min, &sec, msec_char); + if (tokens < 3) { + val = Qnil; + break; + } + msec = msec_char_to_uint(msec_char, sizeof(msec_char)); + val = rb_funcall(rb_cTime, args->db_timezone, 7, opt_time_year, opt_time_month, opt_time_month, UINT2NUM(hour), UINT2NUM(min), UINT2NUM(sec), UINT2NUM(msec)); + if (!NIL_P(args->app_timezone)) { + if (args->app_timezone == intern_local) { val = rb_funcall(val, intern_localtime, 0); } else { /* utc */ val = rb_funcall(val, intern_utc, 0); @@ -276,36 +826,42 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo case MYSQL_TYPE_DATETIME: { /* DATETIME field */ int tokens; unsigned int year=0, month=0, day=0, hour=0, min=0, sec=0, msec=0; + char msec_char[7] = {'0','0','0','0','0','0','\0'}; uint64_t seconds; - tokens = sscanf(row[i], "%4u-%2u-%2u %2u:%2u:%2u.%6u", &year, &month, &day, &hour, &min, &sec, &msec); + tokens = sscanf(row[i], "%4u-%2u-%2u %2u:%2u:%2u.%6s", &year, &month, &day, &hour, &min, &sec, msec_char); + if (tokens < 6) { /* msec might be empty */ + val = Qnil; + break; + } seconds = (year*31557600ULL) + (month*2592000ULL) + (day*86400ULL) + (hour*3600ULL) + (min*60ULL) + sec; if (seconds == 0) { val = Qnil; } else { if (month < 1 || day < 1) { - rb_raise(cMysql2Error, "Invalid date: %s", row[i]); + rb_raise(cMysql2Error, "Invalid date in field '%.*s': %s", fields[i].name_length, fields[i].name, row[i]); val = Qnil; } else { if (seconds < MYSQL2_MIN_TIME || seconds > MYSQL2_MAX_TIME) { /* use DateTime for larger date range, does not support microseconds */ VALUE offset = INT2NUM(0); - if (db_timezone == intern_local) { + if (args->db_timezone == intern_local) { offset = rb_funcall(cMysql2Client, intern_local_offset, 0); } val = rb_funcall(cDateTime, intern_civil, 7, UINT2NUM(year), UINT2NUM(month), UINT2NUM(day), UINT2NUM(hour), UINT2NUM(min), UINT2NUM(sec), offset); - if (!NIL_P(app_timezone)) { - if (app_timezone == intern_local) { + if (!NIL_P(args->app_timezone)) { + if (args->app_timezone == intern_local) { offset = rb_funcall(cMysql2Client, intern_local_offset, 0); val = rb_funcall(val, intern_new_offset, 1, offset); } else { /* utc */ val = rb_funcall(val, intern_new_offset, 1, opt_utc_offset); } } - } else { /* use Time, supports microseconds */ - val = rb_funcall(rb_cTime, db_timezone, 7, UINT2NUM(year), UINT2NUM(month), UINT2NUM(day), UINT2NUM(hour), UINT2NUM(min), UINT2NUM(sec), UINT2NUM(msec)); - if (!NIL_P(app_timezone)) { - if (app_timezone == intern_local) { + } else { + msec = msec_char_to_uint(msec_char, sizeof(msec_char)); + val = rb_funcall(rb_cTime, args->db_timezone, 7, UINT2NUM(year), UINT2NUM(month), UINT2NUM(day), UINT2NUM(hour), UINT2NUM(min), UINT2NUM(sec), UINT2NUM(msec)); + if (!NIL_P(args->app_timezone)) { + if (args->app_timezone == intern_local) { val = rb_funcall(val, intern_localtime, 0); } else { /* utc */ val = rb_funcall(val, intern_utc, 0); @@ -321,11 +877,15 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo int tokens; unsigned int year=0, month=0, day=0; tokens = sscanf(row[i], "%4u-%2u-%2u", &year, &month, &day); + if (tokens < 3) { + val = Qnil; + break; + } if (year+month+day == 0) { val = Qnil; } else { if (month < 1 || day < 1) { - rb_raise(cMysql2Error, "Invalid date: %s", row[i]); + rb_raise(cMysql2Error, "Invalid date in field '%.*s': %s", fields[i].name_length, fields[i].name, row[i]); val = Qnil; } else { val = rb_funcall(cDate, intern_new, 3, UINT2NUM(year), UINT2NUM(month), UINT2NUM(day)); @@ -345,19 +905,17 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo case MYSQL_TYPE_GEOMETRY: /* Spatial fielda */ default: val = rb_str_new(row[i], fieldLengths[i]); -#ifdef HAVE_RUBY_ENCODING_H val = mysql2_set_field_string_encoding(val, fields[i], default_internal_enc, conn_enc); -#endif break; } } - if (asArray) { + if (args->asArray) { rb_ary_push(rowVal, val); } else { rb_hash_aset(rowVal, field, val); } } else { - if (asArray) { + if (args->asArray) { rb_ary_push(rowVal, Qnil); } else { rb_hash_aset(rowVal, field, Qnil); @@ -368,14 +926,14 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo } static VALUE rb_mysql_result_fetch_fields(VALUE self) { - mysql2_result_wrapper * wrapper; unsigned int i = 0; short int symbolizeKeys = 0; VALUE defaults; - GetMysql2Result(self, wrapper); + GET_RESULT(self); - defaults = rb_iv_get(self, "@query_options"); + defaults = rb_ivar_get(self, intern_query_options); + Check_Type(defaults, T_HASH); if (rb_hash_aref(defaults, sym_symbolize_keys) == Qtrue) { symbolizeKeys = 1; } @@ -385,7 +943,7 @@ static VALUE rb_mysql_result_fetch_fields(VALUE self) { wrapper->fields = rb_ary_new2(wrapper->numberOfFields); } - if (RARRAY_LEN(wrapper->fields) != wrapper->numberOfFields) { + if ((my_ulonglong)RARRAY_LEN(wrapper->fields) != wrapper->numberOfFields) { for (i=0; inumberOfFields; i++) { rb_mysql_result_fetch_field(self, i, symbolizeKeys); } @@ -394,112 +952,70 @@ static VALUE rb_mysql_result_fetch_fields(VALUE self) { return wrapper->fields; } -static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) { - VALUE defaults, opts, block; - ID db_timezone, app_timezone, dbTz, appTz; - mysql2_result_wrapper * wrapper; - unsigned long i; - int symbolizeKeys = 0, asArray = 0, castBool = 0, cacheRows = 1, cast = 1, streaming = 0; - MYSQL_FIELD * fields = NULL; - - GetMysql2Result(self, wrapper); - - defaults = rb_iv_get(self, "@query_options"); - if (rb_scan_args(argc, argv, "01&", &opts, &block) == 1) { - opts = rb_funcall(defaults, intern_merge, 1, opts); - } else { - opts = defaults; - } - - if (rb_hash_aref(opts, sym_symbolize_keys) == Qtrue) { - symbolizeKeys = 1; - } - - if (rb_hash_aref(opts, sym_as) == sym_array) { - asArray = 1; - } +static VALUE rb_mysql_result_fetch_field_types(VALUE self) { + unsigned int i = 0; - if (rb_hash_aref(opts, sym_cast_booleans) == Qtrue) { - castBool = 1; - } + GET_RESULT(self); - if (rb_hash_aref(opts, sym_cache_rows) == Qfalse) { - cacheRows = 0; - } - - if (rb_hash_aref(opts, sym_cast) == Qfalse) { - cast = 0; + if (wrapper->fieldTypes == Qnil) { + wrapper->numberOfFields = mysql_num_fields(wrapper->result); + wrapper->fieldTypes = rb_ary_new2(wrapper->numberOfFields); } - if(rb_hash_aref(opts, sym_stream) == Qtrue) { - streaming = 1; + if ((my_ulonglong)RARRAY_LEN(wrapper->fieldTypes) != wrapper->numberOfFields) { + for (i=0; inumberOfFields; i++) { + rb_mysql_result_fetch_field_type(self, i); + } } - if(streaming && cacheRows) { - rb_warn("cacheRows is ignored if streaming is true"); - } + return wrapper->fieldTypes; +} - dbTz = rb_hash_aref(opts, sym_database_timezone); - if (dbTz == sym_local) { - db_timezone = intern_local; - } else if (dbTz == sym_utc) { - db_timezone = intern_utc; - } else { - if (!NIL_P(dbTz)) { - rb_warn(":database_timezone option must be :utc or :local - defaulting to :local"); - } - db_timezone = intern_local; - } +static VALUE rb_mysql_result_each_(VALUE self, + VALUE(*fetch_row_func)(VALUE, MYSQL_FIELD *fields, const result_each_args *args), + const result_each_args *args) +{ + unsigned long i; + const char *errstr; + MYSQL_FIELD *fields = NULL; - appTz = rb_hash_aref(opts, sym_application_timezone); - if (appTz == sym_local) { - app_timezone = intern_local; - } else if (appTz == sym_utc) { - app_timezone = intern_utc; - } else { - app_timezone = Qnil; - } + GET_RESULT(self); - if (wrapper->lastRowProcessed == 0) { - if(streaming) { - /* We can't get number of rows if we're streaming, */ - /* until we've finished fetching all rows */ - wrapper->numberOfRows = 0; + if (wrapper->is_streaming) { + /* When streaming, we will only yield rows, not return them. */ + if (wrapper->rows == Qnil) { wrapper->rows = rb_ary_new(); - } else { - wrapper->numberOfRows = mysql_num_rows(wrapper->result); - if (wrapper->numberOfRows == 0) { - wrapper->rows = rb_ary_new(); - return wrapper->rows; - } - wrapper->rows = rb_ary_new2(wrapper->numberOfRows); } - } - if (streaming) { - if(!wrapper->streamingComplete) { + if (!wrapper->streamingComplete) { VALUE row; fields = mysql_fetch_fields(wrapper->result); do { - row = rb_mysql_result_fetch_row(self, db_timezone, app_timezone, symbolizeKeys, asArray, castBool, cast, fields); - - if (block != Qnil && row != Qnil) { - rb_yield(row); - wrapper->lastRowProcessed++; + row = fetch_row_func(self, fields, args); + if (row != Qnil) { + wrapper->numberOfRows++; + if (args->block_given) { + rb_yield(row); + } } } while(row != Qnil); rb_mysql_result_free_result(wrapper); - - wrapper->numberOfRows = wrapper->lastRowProcessed; wrapper->streamingComplete = 1; + + // Check for errors, the connection might have gone out from under us + // mysql_error returns an empty string if there is no error + errstr = mysql_error(wrapper->client_wrapper->client); + if (errstr[0]) { + rb_raise(cMysql2Error, "%s", errstr); + } } else { rb_raise(cMysql2Error, "You have already fetched all the rows for this query and streaming is true. (to reiterate you must requery)."); } } else { - if (cacheRows && wrapper->lastRowProcessed == wrapper->numberOfRows) { + if (args->cacheRows && wrapper->resultFreed) { /* we've already read the entire dataset from the C result into our */ /* internal array. Lets hand that over to the user since it's ready to go */ for (i = 0; i < wrapper->numberOfRows; i++) { @@ -512,11 +1028,11 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) { for (i = 0; i < wrapper->numberOfRows; i++) { VALUE row; - if (cacheRows && i < rowsProcessed) { + if (args->cacheRows && i < rowsProcessed) { row = rb_ary_entry(wrapper->rows, i); } else { - row = rb_mysql_result_fetch_row(self, db_timezone, app_timezone, symbolizeKeys, asArray, castBool, cast, fields); - if (cacheRows) { + row = fetch_row_func(self, fields, args); + if (args->cacheRows) { rb_ary_store(wrapper->rows, i, row); } wrapper->lastRowProcessed++; @@ -524,65 +1040,204 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) { if (row == Qnil) { /* we don't need the mysql C dataset around anymore, peace it */ - rb_mysql_result_free_result(wrapper); + if (args->cacheRows) { + rb_mysql_result_free_result(wrapper); + } return Qnil; } - if (block != Qnil) { + if (args->block_given) { rb_yield(row); } } - if (wrapper->lastRowProcessed == wrapper->numberOfRows) { + if (wrapper->lastRowProcessed == wrapper->numberOfRows && args->cacheRows) { /* we don't need the mysql C dataset around anymore, peace it */ rb_mysql_result_free_result(wrapper); } } } + // FIXME return Enumerator instead? + // return rb_ary_each(wrapper->rows); return wrapper->rows; } +static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) { + result_each_args args; + VALUE defaults, opts, (*fetch_row_func)(VALUE, MYSQL_FIELD *fields, const result_each_args *args); + ID db_timezone, app_timezone, dbTz, appTz; + int symbolizeKeys, asArray, castBool, cacheRows, cast; + + GET_RESULT(self); + + if (wrapper->stmt_wrapper && wrapper->stmt_wrapper->closed) { + rb_raise(cMysql2Error, "Statement handle already closed"); + } + + defaults = rb_ivar_get(self, intern_query_options); + Check_Type(defaults, T_HASH); + + // A block can be passed to this method, but since we don't call the block directly from C, + // we don't need to capture it into a variable here with the "&" scan arg. + if (rb_scan_args(argc, argv, "01", &opts) == 1) { + opts = rb_funcall(defaults, intern_merge, 1, opts); + } else { + opts = defaults; + } + + symbolizeKeys = RTEST(rb_hash_aref(opts, sym_symbolize_keys)); + asArray = rb_hash_aref(opts, sym_as) == sym_array; + castBool = RTEST(rb_hash_aref(opts, sym_cast_booleans)); + cacheRows = RTEST(rb_hash_aref(opts, sym_cache_rows)); + cast = RTEST(rb_hash_aref(opts, sym_cast)); + + if (wrapper->is_streaming && cacheRows) { + rb_warn(":cache_rows is ignored if :stream is true"); + } + + if (wrapper->stmt_wrapper && !cacheRows && !wrapper->is_streaming) { + rb_warn(":cache_rows is forced for prepared statements (if not streaming)"); + cacheRows = 1; + } + + if (wrapper->stmt_wrapper && !cast) { + rb_warn(":cast is forced for prepared statements"); + } + + dbTz = rb_hash_aref(opts, sym_database_timezone); + if (dbTz == sym_local) { + db_timezone = intern_local; + } else if (dbTz == sym_utc) { + db_timezone = intern_utc; + } else { + if (!NIL_P(dbTz)) { + rb_warn(":database_timezone option must be :utc or :local - defaulting to :local"); + } + db_timezone = intern_local; + } + + appTz = rb_hash_aref(opts, sym_application_timezone); + if (appTz == sym_local) { + app_timezone = intern_local; + } else if (appTz == sym_utc) { + app_timezone = intern_utc; + } else { + app_timezone = Qnil; + } + + if (wrapper->rows == Qnil && !wrapper->is_streaming) { + wrapper->numberOfRows = wrapper->stmt_wrapper ? mysql_stmt_num_rows(wrapper->stmt_wrapper->stmt) : mysql_num_rows(wrapper->result); + wrapper->rows = rb_ary_new2(wrapper->numberOfRows); + } else if (wrapper->rows && !cacheRows) { + if (wrapper->resultFreed) { + rb_raise(cMysql2Error, "Result set has already been freed"); + } + mysql_data_seek(wrapper->result, 0); + wrapper->lastRowProcessed = 0; + wrapper->rows = rb_ary_new2(wrapper->numberOfRows); + } + + // Backward compat + args.symbolizeKeys = symbolizeKeys; + args.asArray = asArray; + args.castBool = castBool; + args.cacheRows = cacheRows; + args.cast = cast; + args.db_timezone = db_timezone; + args.app_timezone = app_timezone; + args.block_given = rb_block_given_p(); + + if (wrapper->stmt_wrapper) { + fetch_row_func = rb_mysql_result_fetch_row_stmt; + } else { + fetch_row_func = rb_mysql_result_fetch_row; + } + + return rb_mysql_result_each_(self, fetch_row_func, &args); +} + static VALUE rb_mysql_result_count(VALUE self) { - mysql2_result_wrapper *wrapper; + GET_RESULT(self); + + if (wrapper->is_streaming) { + /* This is an unsigned long per result.h */ + return ULONG2NUM(wrapper->numberOfRows); + } - GetMysql2Result(self, wrapper); - if(wrapper->resultFreed) { - if (wrapper->streamingComplete){ - return LONG2NUM(wrapper->numberOfRows); + if (wrapper->resultFreed) { + /* Ruby arrays have platform signed long length */ + return LONG2NUM(RARRAY_LEN(wrapper->rows)); + } else { + /* MySQL returns an unsigned 64-bit long here */ + if (wrapper->stmt_wrapper) { + return ULL2NUM(mysql_stmt_num_rows(wrapper->stmt_wrapper->stmt)); } else { - return LONG2NUM(RARRAY_LEN(wrapper->rows)); + return ULL2NUM(mysql_num_rows(wrapper->result)); } - } else { - return INT2FIX(mysql_num_rows(wrapper->result)); } } /* Mysql2::Result */ -VALUE rb_mysql_result_to_obj(MYSQL_RES * r) { +VALUE rb_mysql_result_to_obj(VALUE client, VALUE encoding, VALUE options, MYSQL_RES *r, VALUE statement) { VALUE obj; mysql2_result_wrapper * wrapper; + +#ifdef NEW_TYPEDDATA_WRAPPER + obj = TypedData_Make_Struct(cMysql2Result, mysql2_result_wrapper, &rb_mysql_result_type, wrapper); +#else obj = Data_Make_Struct(cMysql2Result, mysql2_result_wrapper, rb_mysql_result_mark, rb_mysql_result_free, wrapper); +#endif wrapper->numberOfFields = 0; wrapper->numberOfRows = 0; wrapper->lastRowProcessed = 0; wrapper->resultFreed = 0; wrapper->result = r; wrapper->fields = Qnil; + wrapper->fieldTypes = Qnil; wrapper->rows = Qnil; - wrapper->encoding = Qnil; + wrapper->encoding = encoding; wrapper->streamingComplete = 0; + wrapper->client = client; + wrapper->client_wrapper = DATA_PTR(client); + wrapper->client_wrapper->refcount++; + wrapper->result_buffers = NULL; + wrapper->is_null = NULL; + wrapper->error = NULL; + wrapper->length = NULL; + + /* Keep a handle to the Statement to ensure it doesn't get garbage collected first */ + wrapper->statement = statement; + if (statement != Qnil) { + wrapper->stmt_wrapper = DATA_PTR(statement); + wrapper->stmt_wrapper->refcount++; + } else { + wrapper->stmt_wrapper = NULL; + } + rb_obj_call_init(obj, 0, NULL); + rb_ivar_set(obj, intern_query_options, options); + + /* Options that cannot be changed in results.each(...) { |row| } + * should be processed here. */ + wrapper->is_streaming = (rb_hash_aref(options, sym_stream) == Qtrue ? 1 : 0); + return obj; } void init_mysql2_result() { - cBigDecimal = rb_const_get(rb_cObject, rb_intern("BigDecimal")); cDate = rb_const_get(rb_cObject, rb_intern("Date")); + rb_global_variable(&cDate); cDateTime = rb_const_get(rb_cObject, rb_intern("DateTime")); + rb_global_variable(&cDateTime); cMysql2Result = rb_define_class_under(mMysql2, "Result", rb_cObject); + rb_undef_alloc_func(cMysql2Result); + rb_global_variable(&cMysql2Result); + rb_define_method(cMysql2Result, "each", rb_mysql_result_each, -1); rb_define_method(cMysql2Result, "fields", rb_mysql_result_fetch_fields, 0); + rb_define_method(cMysql2Result, "field_types", rb_mysql_result_fetch_field_types, 0); + rb_define_method(cMysql2Result, "free", rb_mysql_result_free_, 0); rb_define_method(cMysql2Result, "count", rb_mysql_result_count, 0); rb_define_alias(cMysql2Result, "size", "count"); @@ -594,6 +1249,8 @@ void init_mysql2_result() { intern_local_offset = rb_intern("local_offset"); intern_civil = rb_intern("civil"); intern_new_offset = rb_intern("new_offset"); + intern_BigDecimal = rb_intern("BigDecimal"); + intern_query_options = rb_intern("@query_options"); sym_symbolize_keys = ID2SYM(rb_intern("symbolize_keys")); sym_as = ID2SYM(rb_intern("as")); @@ -616,7 +1273,5 @@ void init_mysql2_result() { opt_time_month = INT2NUM(1); opt_utc_offset = INT2NUM(0); -#ifdef HAVE_RUBY_ENCODING_H binaryEncoding = rb_enc_find("binary"); -#endif } diff --git a/ext/mysql2/result.h b/ext/mysql2/result.h index 396007254..3f58b1005 100644 --- a/ext/mysql2/result.h +++ b/ext/mysql2/result.h @@ -1,21 +1,30 @@ #ifndef MYSQL2_RESULT_H #define MYSQL2_RESULT_H -void init_mysql2_result(); -VALUE rb_mysql_result_to_obj(MYSQL_RES * r); +void init_mysql2_result(void); +VALUE rb_mysql_result_to_obj(VALUE client, VALUE encoding, VALUE options, MYSQL_RES *r, VALUE statement); typedef struct { VALUE fields; + VALUE fieldTypes; VALUE rows; + VALUE client; VALUE encoding; - unsigned int numberOfFields; - unsigned long numberOfRows; + VALUE statement; + my_ulonglong numberOfFields; + my_ulonglong numberOfRows; unsigned long lastRowProcessed; + char is_streaming; char streamingComplete; char resultFreed; MYSQL_RES *result; + mysql_stmt_wrapper *stmt_wrapper; + mysql_client_wrapper *client_wrapper; + /* statement result bind buffers */ + MYSQL_BIND *result_buffers; + my_bool *is_null; + my_bool *error; + unsigned long *length; } mysql2_result_wrapper; -#define GetMysql2Result(obj, sval) (sval = (mysql2_result_wrapper*)DATA_PTR(obj)); - #endif diff --git a/ext/mysql2/statement.c b/ext/mysql2/statement.c new file mode 100644 index 000000000..fa3b660cd --- /dev/null +++ b/ext/mysql2/statement.c @@ -0,0 +1,670 @@ +#include + +extern VALUE mMysql2, cMysql2Error; +static VALUE cMysql2Statement, cBigDecimal, cDateTime, cDate; +static VALUE sym_stream, intern_new_with_args, intern_each, intern_to_s, intern_merge_bang; +static VALUE intern_sec_fraction, intern_usec, intern_sec, intern_min, intern_hour, intern_day, intern_month, intern_year, + intern_query_options; + +#ifndef NEW_TYPEDDATA_WRAPPER +#define TypedData_Get_Struct(obj, type, ignore, sval) Data_Get_Struct(obj, type, sval) +#endif + +#define RAW_GET_STATEMENT(self) \ + mysql_stmt_wrapper *stmt_wrapper; \ + TypedData_Get_Struct(self, mysql_stmt_wrapper, &rb_mysql_statement_type, stmt_wrapper); \ + +#define GET_STATEMENT(self) \ + RAW_GET_STATEMENT(self) \ + if (!stmt_wrapper->stmt) { rb_raise(cMysql2Error, "Invalid statement handle"); } \ + if (stmt_wrapper->closed) { rb_raise(cMysql2Error, "Statement handle already closed"); } + +static void rb_mysql_stmt_mark(void * ptr) { + mysql_stmt_wrapper *stmt_wrapper = ptr; + if (!stmt_wrapper) return; + + rb_gc_mark_movable(stmt_wrapper->client); +} + +static void rb_mysql_stmt_free(void *ptr) { + mysql_stmt_wrapper *stmt_wrapper = ptr; + decr_mysql2_stmt(stmt_wrapper); +} + +static size_t rb_mysql_stmt_memsize(const void * ptr) { + const mysql_stmt_wrapper *stmt_wrapper = ptr; + return sizeof(*stmt_wrapper); +} + +#ifdef HAVE_RB_GC_MARK_MOVABLE +static void rb_mysql_stmt_compact(void * ptr) { + mysql_stmt_wrapper *stmt_wrapper = ptr; + if (!stmt_wrapper) return; + + rb_mysql2_gc_location(stmt_wrapper->client); +} +#endif + +static const rb_data_type_t rb_mysql_statement_type = { + "rb_mysql_statement", + { + rb_mysql_stmt_mark, + rb_mysql_stmt_free, + rb_mysql_stmt_memsize, +#ifdef HAVE_RB_GC_MARK_MOVABLE + rb_mysql_stmt_compact, +#endif + }, + 0, + 0, +#ifdef RUBY_TYPED_FREE_IMMEDIATELY + RUBY_TYPED_FREE_IMMEDIATELY, +#endif +}; + +static void *nogvl_stmt_close(void *ptr) { + mysql_stmt_wrapper *stmt_wrapper = ptr; + if (stmt_wrapper->stmt) { + mysql_stmt_close(stmt_wrapper->stmt); + stmt_wrapper->stmt = NULL; + } + return NULL; +} + +void decr_mysql2_stmt(mysql_stmt_wrapper *stmt_wrapper) { + stmt_wrapper->refcount--; + + if (stmt_wrapper->refcount == 0) { + nogvl_stmt_close(stmt_wrapper); + xfree(stmt_wrapper); + } +} + +void rb_raise_mysql2_stmt_error(mysql_stmt_wrapper *stmt_wrapper) { + VALUE e; + GET_CLIENT(stmt_wrapper->client); + VALUE rb_error_msg = rb_str_new2(mysql_stmt_error(stmt_wrapper->stmt)); + VALUE rb_sql_state = rb_str_new2(mysql_stmt_sqlstate(stmt_wrapper->stmt)); + + rb_encoding *conn_enc; + conn_enc = rb_to_encoding(wrapper->encoding); + + rb_encoding *default_internal_enc = rb_default_internal_encoding(); + + rb_enc_associate(rb_error_msg, conn_enc); + rb_enc_associate(rb_sql_state, conn_enc); + if (default_internal_enc) { + rb_error_msg = rb_str_export_to_enc(rb_error_msg, default_internal_enc); + rb_sql_state = rb_str_export_to_enc(rb_sql_state, default_internal_enc); + } + + e = rb_funcall(cMysql2Error, intern_new_with_args, 4, + rb_error_msg, + LONG2FIX(wrapper->server_version), + UINT2NUM(mysql_stmt_errno(stmt_wrapper->stmt)), + rb_sql_state); + rb_exc_raise(e); +} + +/* + * used to pass all arguments to mysql_stmt_prepare while inside + * nogvl_prepare_statement_args + */ +struct nogvl_prepare_statement_args { + MYSQL_STMT *stmt; + VALUE sql; + const char *sql_ptr; + unsigned long sql_len; +}; + +static void *nogvl_prepare_statement(void *ptr) { + struct nogvl_prepare_statement_args *args = ptr; + + if (mysql_stmt_prepare(args->stmt, args->sql_ptr, args->sql_len)) { + return (void*)Qfalse; + } else { + return (void*)Qtrue; + } +} + +VALUE rb_mysql_stmt_new(VALUE rb_client, VALUE sql) { + mysql_stmt_wrapper *stmt_wrapper; + VALUE rb_stmt; + rb_encoding *conn_enc; + + Check_Type(sql, T_STRING); + +#ifdef NEW_TYPEDDATA_WRAPPER + rb_stmt = TypedData_Make_Struct(cMysql2Statement, mysql_stmt_wrapper, &rb_mysql_statement_type, stmt_wrapper); +#else + rb_stmt = Data_Make_Struct(cMysql2Statement, mysql_stmt_wrapper, rb_mysql_stmt_mark, rb_mysql_stmt_free, stmt_wrapper); +#endif + { + stmt_wrapper->client = rb_client; + stmt_wrapper->refcount = 1; + stmt_wrapper->closed = 0; + stmt_wrapper->stmt = NULL; + } + + // instantiate stmt + { + GET_CLIENT(rb_client); + stmt_wrapper->stmt = mysql_stmt_init(wrapper->client); + conn_enc = rb_to_encoding(wrapper->encoding); + } + if (stmt_wrapper->stmt == NULL) { + rb_raise(cMysql2Error, "Unable to initialize prepared statement: out of memory"); + } + + // set STMT_ATTR_UPDATE_MAX_LENGTH attr + { + my_bool truth = 1; + if (mysql_stmt_attr_set(stmt_wrapper->stmt, STMT_ATTR_UPDATE_MAX_LENGTH, &truth)) { + rb_raise(cMysql2Error, "Unable to initialize prepared statement: set STMT_ATTR_UPDATE_MAX_LENGTH"); + } + } + + // call mysql_stmt_prepare w/o gvl + { + struct nogvl_prepare_statement_args args; + args.stmt = stmt_wrapper->stmt; + // ensure the string is in the encoding the connection is expecting + args.sql = rb_str_export_to_enc(sql, conn_enc); + args.sql_ptr = RSTRING_PTR(sql); + args.sql_len = RSTRING_LEN(sql); + + if ((VALUE)rb_thread_call_without_gvl(nogvl_prepare_statement, &args, RUBY_UBF_IO, 0) == Qfalse) { + rb_raise_mysql2_stmt_error(stmt_wrapper); + } + } + + return rb_stmt; +} + +/* call-seq: stmt.param_count # => Numeric + * + * Returns the number of parameters the prepared statement expects. + */ +static VALUE rb_mysql_stmt_param_count(VALUE self) { + GET_STATEMENT(self); + + return ULL2NUM(mysql_stmt_param_count(stmt_wrapper->stmt)); +} + +/* call-seq: stmt.field_count # => Numeric + * + * Returns the number of fields the prepared statement returns. + */ +static VALUE rb_mysql_stmt_field_count(VALUE self) { + GET_STATEMENT(self); + + return UINT2NUM(mysql_stmt_field_count(stmt_wrapper->stmt)); +} + +static void *nogvl_stmt_execute(void *ptr) { + MYSQL_STMT *stmt = ptr; + + if (mysql_stmt_execute(stmt)) { + return (void*)Qfalse; + } else { + return (void*)Qtrue; + } +} + +static void set_buffer_for_string(MYSQL_BIND* bind_buffer, unsigned long *length_buffer, VALUE string) { + unsigned long length; + + bind_buffer->buffer = RSTRING_PTR(string); + + length = RSTRING_LEN(string); + bind_buffer->buffer_length = length; + *length_buffer = length; + + bind_buffer->length = length_buffer; +} + +/* Free each bind_buffer[i].buffer except when params_enc is non-nil, this means + * the buffer is a Ruby string pointer and not our memory to manage. + */ +#define FREE_BINDS \ + for (i = 0; i < bind_count; i++) { \ + if (bind_buffers[i].buffer && NIL_P(params_enc[i])) { \ + xfree(bind_buffers[i].buffer); \ + } \ + } \ + if (argc > 0) { \ + xfree(bind_buffers); \ + xfree(length_buffers); \ + } + +/* return 0 if the given bignum can cast as LONG_LONG, otherwise 1 */ +static int my_big2ll(VALUE bignum, LONG_LONG *ptr) +{ + unsigned LONG_LONG num; + size_t len; +// rb_absint_size was added in 2.1.0. See: +// https://github.com/ruby/ruby/commit/9fea875 +#ifdef HAVE_RB_ABSINT_SIZE + int nlz_bits = 0; + len = rb_absint_size(bignum, &nlz_bits); +#else + len = RBIGNUM_LEN(bignum) * SIZEOF_BDIGITS; +#endif + if (len > sizeof(LONG_LONG)) goto overflow; + if (RBIGNUM_POSITIVE_P(bignum)) { + num = rb_big2ull(bignum); + if (num > LLONG_MAX) + goto overflow; + *ptr = num; + } + else { + if (len == 8 && +#ifdef HAVE_RB_ABSINT_SIZE + nlz_bits == 0 && +#endif +// rb_absint_singlebit_p was added in 2.1.0. See: +// https://github.com/ruby/ruby/commit/e5ff9d5 +#if defined(HAVE_RB_ABSINT_SIZE) && defined(HAVE_RB_ABSINT_SINGLEBIT_P) + /* Optimized to avoid object allocation for Ruby 2.1+ + * only -0x8000000000000000 is safe if `len == 8 && nlz_bits == 0` + */ + !rb_absint_singlebit_p(bignum) +#else + rb_big_cmp(bignum, LL2NUM(LLONG_MIN)) == INT2FIX(-1) +#endif + ) { + goto overflow; + } + *ptr = rb_big2ll(bignum); + } + return 0; +overflow: + return 1; +} + +/* call-seq: stmt.execute + * + * Executes the current prepared statement, returns +result+. + */ +static VALUE rb_mysql_stmt_execute(int argc, VALUE *argv, VALUE self) { + MYSQL_BIND *bind_buffers = NULL; + unsigned long *length_buffers = NULL; + unsigned long bind_count; + unsigned long i; + MYSQL_STMT *stmt; + MYSQL_RES *metadata; + VALUE opts; + VALUE current; + VALUE resultObj; + VALUE *params_enc = NULL; + int is_streaming; + rb_encoding *conn_enc; + + GET_STATEMENT(self); + GET_CLIENT(stmt_wrapper->client); + + conn_enc = rb_to_encoding(wrapper->encoding); + + stmt = stmt_wrapper->stmt; + bind_count = mysql_stmt_param_count(stmt); + + // Get count of ordinary arguments, and extract hash opts/keyword arguments + // Use a local scope to avoid leaking the temporary count variable + { + int c = rb_scan_args(argc, argv, "*:", NULL, &opts); + if (c != (long)bind_count) { + rb_raise(cMysql2Error, "Bind parameter count (%ld) doesn't match number of arguments (%d)", bind_count, c); + } + } + + // setup any bind variables in the query + if (bind_count > 0) { + // Scratch space for string encoding exports, allocate on the stack + params_enc = alloca(sizeof(VALUE) * bind_count); + bind_buffers = xcalloc(bind_count, sizeof(MYSQL_BIND)); + length_buffers = xcalloc(bind_count, sizeof(unsigned long)); + + for (i = 0; i < bind_count; i++) { + bind_buffers[i].buffer = NULL; + params_enc[i] = Qnil; + + switch (TYPE(argv[i])) { + case T_NIL: + bind_buffers[i].buffer_type = MYSQL_TYPE_NULL; + break; + case T_FIXNUM: +#if SIZEOF_INT < SIZEOF_LONG + bind_buffers[i].buffer_type = MYSQL_TYPE_LONGLONG; + bind_buffers[i].buffer = xmalloc(sizeof(long long int)); + *(long*)(bind_buffers[i].buffer) = FIX2LONG(argv[i]); +#else + bind_buffers[i].buffer_type = MYSQL_TYPE_LONG; + bind_buffers[i].buffer = xmalloc(sizeof(int)); + *(long*)(bind_buffers[i].buffer) = FIX2INT(argv[i]); +#endif + break; + case T_BIGNUM: + { + LONG_LONG num; + if (my_big2ll(argv[i], &num) == 0) { + bind_buffers[i].buffer_type = MYSQL_TYPE_LONGLONG; + bind_buffers[i].buffer = xmalloc(sizeof(long long int)); + *(LONG_LONG*)(bind_buffers[i].buffer) = num; + } else { + /* The bignum was larger than we can fit in LONG_LONG, send it as a string */ + bind_buffers[i].buffer_type = MYSQL_TYPE_NEWDECIMAL; + params_enc[i] = rb_str_export_to_enc(rb_big2str(argv[i], 10), conn_enc); + set_buffer_for_string(&bind_buffers[i], &length_buffers[i], params_enc[i]); + } + } + break; + case T_FLOAT: + bind_buffers[i].buffer_type = MYSQL_TYPE_DOUBLE; + bind_buffers[i].buffer = xmalloc(sizeof(double)); + *(double*)(bind_buffers[i].buffer) = NUM2DBL(argv[i]); + break; + case T_STRING: + bind_buffers[i].buffer_type = MYSQL_TYPE_STRING; + + params_enc[i] = argv[i]; + params_enc[i] = rb_str_export_to_enc(params_enc[i], conn_enc); + set_buffer_for_string(&bind_buffers[i], &length_buffers[i], params_enc[i]); + break; + case T_TRUE: + bind_buffers[i].buffer_type = MYSQL_TYPE_TINY; + bind_buffers[i].buffer = xmalloc(sizeof(signed char)); + *(signed char*)(bind_buffers[i].buffer) = 1; + break; + case T_FALSE: + bind_buffers[i].buffer_type = MYSQL_TYPE_TINY; + bind_buffers[i].buffer = xmalloc(sizeof(signed char)); + *(signed char*)(bind_buffers[i].buffer) = 0; + break; + default: + // TODO: what Ruby type should support MYSQL_TYPE_TIME + if (CLASS_OF(argv[i]) == rb_cTime || CLASS_OF(argv[i]) == cDateTime) { + MYSQL_TIME t; + VALUE rb_time = argv[i]; + + bind_buffers[i].buffer_type = MYSQL_TYPE_DATETIME; + bind_buffers[i].buffer = xmalloc(sizeof(MYSQL_TIME)); + + memset(&t, 0, sizeof(MYSQL_TIME)); + t.neg = 0; + + if (CLASS_OF(argv[i]) == rb_cTime) { + t.second_part = FIX2INT(rb_funcall(rb_time, intern_usec, 0)); + } else if (CLASS_OF(argv[i]) == cDateTime) { + t.second_part = NUM2DBL(rb_funcall(rb_time, intern_sec_fraction, 0)) * 1000000; + } + + t.second = FIX2INT(rb_funcall(rb_time, intern_sec, 0)); + t.minute = FIX2INT(rb_funcall(rb_time, intern_min, 0)); + t.hour = FIX2INT(rb_funcall(rb_time, intern_hour, 0)); + t.day = FIX2INT(rb_funcall(rb_time, intern_day, 0)); + t.month = FIX2INT(rb_funcall(rb_time, intern_month, 0)); + t.year = FIX2INT(rb_funcall(rb_time, intern_year, 0)); + + *(MYSQL_TIME*)(bind_buffers[i].buffer) = t; + } else if (CLASS_OF(argv[i]) == cDate) { + MYSQL_TIME t; + VALUE rb_time = argv[i]; + + bind_buffers[i].buffer_type = MYSQL_TYPE_DATE; + bind_buffers[i].buffer = xmalloc(sizeof(MYSQL_TIME)); + + memset(&t, 0, sizeof(MYSQL_TIME)); + t.second_part = 0; + t.neg = 0; + t.day = FIX2INT(rb_funcall(rb_time, intern_day, 0)); + t.month = FIX2INT(rb_funcall(rb_time, intern_month, 0)); + t.year = FIX2INT(rb_funcall(rb_time, intern_year, 0)); + + *(MYSQL_TIME*)(bind_buffers[i].buffer) = t; + } else if (CLASS_OF(argv[i]) == cBigDecimal) { + bind_buffers[i].buffer_type = MYSQL_TYPE_NEWDECIMAL; + + // DECIMAL are represented with the "string representation of the + // original server-side value", see + // https://dev.mysql.com/doc/refman/5.7/en/c-api-prepared-statement-type-conversions.html + // This should be independent of the locale used both on the server + // and the client side. + VALUE rb_val_as_string = rb_funcall(argv[i], intern_to_s, 0); + + params_enc[i] = rb_val_as_string; + params_enc[i] = rb_str_export_to_enc(params_enc[i], conn_enc); + set_buffer_for_string(&bind_buffers[i], &length_buffers[i], params_enc[i]); + } + break; + } + } + + // copies bind_buffers into internal storage + if (mysql_stmt_bind_param(stmt, bind_buffers)) { + FREE_BINDS; + rb_raise_mysql2_stmt_error(stmt_wrapper); + } + } + + // Duplicate the options hash, merge! extra opts, put the copy into the Result object + current = rb_hash_dup(rb_ivar_get(stmt_wrapper->client, intern_query_options)); + (void)RB_GC_GUARD(current); + Check_Type(current, T_HASH); + + // Merge in hash opts/keyword arguments + if (!NIL_P(opts)) { + rb_funcall(current, intern_merge_bang, 1, opts); + } + + is_streaming = (Qtrue == rb_hash_aref(current, sym_stream)); + + // From stmt_execute to mysql_stmt_result_metadata to stmt_store_result, no + // Ruby API calls are allowed so that GC is not invoked. If the connection is + // in results-streaming-mode for Statement A, and in the middle Statement B + // gets garbage collected, a message will be sent to the server notifying it + // to release Statement B, resulting in the following error: + // Commands out of sync; you can't run this command now + // + // In streaming mode, statement execute must return a cursor because we + // cannot prevent other Statement objects from being garbage collected + // between fetches of each row of the result set. The following error + // occurs if cursor mode is not set: + // Row retrieval was canceled by mysql_stmt_close + + if (is_streaming) { + unsigned long type = CURSOR_TYPE_READ_ONLY; + if (mysql_stmt_attr_set(stmt, STMT_ATTR_CURSOR_TYPE, &type)) { + FREE_BINDS; + rb_raise(cMysql2Error, "Unable to stream prepared statement, could not set CURSOR_TYPE_READ_ONLY"); + } + } + + if ((VALUE)rb_thread_call_without_gvl(nogvl_stmt_execute, stmt, RUBY_UBF_IO, 0) == Qfalse) { + FREE_BINDS; + rb_raise_mysql2_stmt_error(stmt_wrapper); + } + + FREE_BINDS; + + metadata = mysql_stmt_result_metadata(stmt); + if (metadata == NULL) { + if (mysql_stmt_errno(stmt) != 0) { + // either CR_OUT_OF_MEMORY or CR_UNKNOWN_ERROR. both fatal. + wrapper->active_fiber = Qnil; + rb_raise_mysql2_stmt_error(stmt_wrapper); + } + // no data and no error, so query was not a SELECT + return Qnil; + } + + if (!is_streaming) { + // receive the whole result set from the server + if (mysql_stmt_store_result(stmt)) { + mysql_free_result(metadata); + rb_raise_mysql2_stmt_error(stmt_wrapper); + } + wrapper->active_fiber = Qnil; + } + + resultObj = rb_mysql_result_to_obj(stmt_wrapper->client, wrapper->encoding, current, metadata, self); + + rb_mysql_set_server_query_flags(wrapper->client, resultObj); + + if (!is_streaming) { + // cache all result + rb_funcall(resultObj, intern_each, 0); + } + + return resultObj; +} + +/* call-seq: stmt.fields # => array + * + * Returns a list of fields that will be returned by this statement. + */ +static VALUE rb_mysql_stmt_fields(VALUE self) { + MYSQL_FIELD *fields; + MYSQL_RES *metadata; + unsigned int field_count; + unsigned int i; + VALUE field_list; + MYSQL_STMT* stmt; + rb_encoding *default_internal_enc, *conn_enc; + GET_STATEMENT(self); + GET_CLIENT(stmt_wrapper->client); + stmt = stmt_wrapper->stmt; + + default_internal_enc = rb_default_internal_encoding(); + { + GET_CLIENT(stmt_wrapper->client); + conn_enc = rb_to_encoding(wrapper->encoding); + } + + metadata = mysql_stmt_result_metadata(stmt); + if (metadata == NULL) { + if (mysql_stmt_errno(stmt) != 0) { + // either CR_OUT_OF_MEMORY or CR_UNKNOWN_ERROR. both fatal. + wrapper->active_fiber = Qnil; + rb_raise_mysql2_stmt_error(stmt_wrapper); + } + // no data and no error, so query was not a SELECT + return Qnil; + } + + fields = mysql_fetch_fields(metadata); + field_count = mysql_stmt_field_count(stmt); + field_list = rb_ary_new2((long)field_count); + + for (i = 0; i < field_count; i++) { + VALUE rb_field; + + rb_field = rb_str_new(fields[i].name, fields[i].name_length); + rb_enc_associate(rb_field, conn_enc); + if (default_internal_enc) { + rb_field = rb_str_export_to_enc(rb_field, default_internal_enc); + } + + rb_ary_store(field_list, (long)i, rb_field); + } + + mysql_free_result(metadata); + return field_list; +} + +/* call-seq: + * stmt.last_id + * + * Returns the AUTO_INCREMENT value from the executed INSERT or UPDATE. + */ +static VALUE rb_mysql_stmt_last_id(VALUE self) { + GET_STATEMENT(self); + return ULL2NUM(mysql_stmt_insert_id(stmt_wrapper->stmt)); +} + +/* call-seq: + * stmt.affected_rows + * + * Returns the number of rows changed, deleted, or inserted. + */ +static VALUE rb_mysql_stmt_affected_rows(VALUE self) { + my_ulonglong affected; + GET_STATEMENT(self); + + affected = mysql_stmt_affected_rows(stmt_wrapper->stmt); + if (affected == (my_ulonglong)-1) { + rb_raise_mysql2_stmt_error(stmt_wrapper); + } + + return ULL2NUM(affected); +} + +/* call-seq: + * stmt.close + * + * Explicitly closing this will free up server resources immediately rather + * than waiting for the garbage collector. Useful if you're managing your + * own prepared statement cache. + */ +static VALUE rb_mysql_stmt_close(VALUE self) { + RAW_GET_STATEMENT(self); + + if (!stmt_wrapper->closed) { + stmt_wrapper->closed = 1; + rb_thread_call_without_gvl(nogvl_stmt_close, stmt_wrapper, RUBY_UBF_IO, 0); + } + + return Qnil; +} + +/* call-seq: + * stmt.closed? + * + * Returns wheter or not the statement have been closed. + */ +static VALUE rb_mysql_stmt_closed_p(VALUE self) { + RAW_GET_STATEMENT(self); + + return stmt_wrapper->closed ? Qtrue : Qfalse; +} + +void init_mysql2_statement() { + cDate = rb_const_get(rb_cObject, rb_intern("Date")); + rb_global_variable(&cDate); + + cDateTime = rb_const_get(rb_cObject, rb_intern("DateTime")); + rb_global_variable(&cDateTime); + + cBigDecimal = rb_const_get(rb_cObject, rb_intern("BigDecimal")); + rb_global_variable(&cBigDecimal); + + cMysql2Statement = rb_define_class_under(mMysql2, "Statement", rb_cObject); + rb_undef_alloc_func(cMysql2Statement); + rb_global_variable(&cMysql2Statement); + + rb_define_method(cMysql2Statement, "param_count", rb_mysql_stmt_param_count, 0); + rb_define_method(cMysql2Statement, "field_count", rb_mysql_stmt_field_count, 0); + rb_define_method(cMysql2Statement, "_execute", rb_mysql_stmt_execute, -1); + rb_define_method(cMysql2Statement, "fields", rb_mysql_stmt_fields, 0); + rb_define_method(cMysql2Statement, "last_id", rb_mysql_stmt_last_id, 0); + rb_define_method(cMysql2Statement, "affected_rows", rb_mysql_stmt_affected_rows, 0); + rb_define_method(cMysql2Statement, "close", rb_mysql_stmt_close, 0); + rb_define_method(cMysql2Statement, "closed?", rb_mysql_stmt_closed_p, 0); + + sym_stream = ID2SYM(rb_intern("stream")); + + intern_new_with_args = rb_intern("new_with_args"); + intern_each = rb_intern("each"); + + intern_sec_fraction = rb_intern("sec_fraction"); + intern_usec = rb_intern("usec"); + intern_sec = rb_intern("sec"); + intern_min = rb_intern("min"); + intern_hour = rb_intern("hour"); + intern_day = rb_intern("day"); + intern_month = rb_intern("month"); + intern_year = rb_intern("year"); + + intern_to_s = rb_intern("to_s"); + intern_merge_bang = rb_intern("merge!"); + intern_query_options = rb_intern("@query_options"); +} diff --git a/ext/mysql2/statement.h b/ext/mysql2/statement.h new file mode 100644 index 000000000..e48510676 --- /dev/null +++ b/ext/mysql2/statement.h @@ -0,0 +1,17 @@ +#ifndef MYSQL2_STATEMENT_H +#define MYSQL2_STATEMENT_H + +typedef struct { + VALUE client; + MYSQL_STMT *stmt; + int refcount; + int closed; +} mysql_stmt_wrapper; + +void init_mysql2_statement(void); +void decr_mysql2_stmt(mysql_stmt_wrapper *stmt_wrapper); + +VALUE rb_mysql_stmt_new(VALUE rb_client, VALUE sql); +void rb_raise_mysql2_stmt_error(mysql_stmt_wrapper *stmt_wrapper) RB_MYSQL_NORETURN; + +#endif diff --git a/ext/mysql2/wait_for_single_fd.h b/ext/mysql2/wait_for_single_fd.h index c99eec76f..8f74ad05e 100644 --- a/ext/mysql2/wait_for_single_fd.h +++ b/ext/mysql2/wait_for_single_fd.h @@ -1,5 +1,6 @@ /* - * backwards compatibility for pre-1.9.3 C API + * backwards compatibility for Rubinius. See + * https://github.com/rubinius/rubinius/issues/3771. * * Ruby 1.9.3 provides this API which allows the use of ppoll() on Linux * to minimize select() and malloc() overhead on high-numbered FDs. diff --git a/lib/mysql2.rb b/lib/mysql2.rb index 45d0a54ba..9461846e9 100644 --- a/lib/mysql2.rb +++ b/lib/mysql2.rb @@ -1,13 +1,43 @@ -# encoding: UTF-8 require 'date' require 'bigdecimal' -require 'rational' unless RUBY_VERSION >= '1.9.2' + +# Load libmysql.dll before requiring mysql2/mysql2.so +# This gives a chance to be flexible about the load path +# Or to bomb out with a clear error message instead of a linker crash +if RUBY_PLATFORM =~ /mswin|mingw/ + dll_path = if ENV['RUBY_MYSQL2_LIBMYSQL_DLL'] + # If this environment variable is set, it overrides any other paths + # The user is advised to use backslashes not forward slashes + ENV['RUBY_MYSQL2_LIBMYSQL_DLL'] + elsif File.exist?(File.expand_path('../vendor/libmysql.dll', File.dirname(__FILE__))) + # Use vendor/libmysql.dll if it exists, convert slashes for Win32 LoadLibrary + File.expand_path('../vendor/libmysql.dll', File.dirname(__FILE__)) + elsif defined?(RubyInstaller) + # RubyInstaller-2.4+ native build doesn't need DLL preloading + else + # This will use default / system library paths + 'libmysql.dll' + end + + if dll_path + require 'fiddle' + kernel32 = Fiddle.dlopen 'kernel32' + load_library = Fiddle::Function.new( + kernel32['LoadLibraryW'], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT, + ) + if load_library.call(dll_path.encode('utf-16le')).zero? + abort "Failed to load libmysql.dll from #{dll_path}" + end + end +end require 'mysql2/version' unless defined? Mysql2::VERSION require 'mysql2/error' require 'mysql2/mysql2' require 'mysql2/result' require 'mysql2/client' +require 'mysql2/field' +require 'mysql2/statement' # = Mysql2 # @@ -28,14 +58,31 @@ module Mysql2 end # For holding utility methods -module Mysql2::Util - - # - # Rekey a string-keyed hash with equivalent symbols. - # - def self.key_hash_as_symbols(hash) - return nil unless hash - Hash[hash.map { |k,v| [k.to_sym, v] }] - end +module Mysql2 + module Util + # + # Rekey a string-keyed hash with equivalent symbols. + # + def self.key_hash_as_symbols(hash) + return nil unless hash + Hash[hash.map { |k, v| [k.to_sym, v] }] + end + + # + # In Mysql2::Client#query and Mysql2::Statement#execute, + # Thread#handle_interrupt is used to prevent Timeout#timeout + # from interrupting query execution. + # + # Timeout::ExitException was removed in Ruby 2.3.0, 2.2.3, and 2.1.8, + # but is present in earlier 2.1.x and 2.2.x, so we provide a shim. + # + require 'timeout' + TIMEOUT_ERROR_CLASS = if defined?(::Timeout::ExitException) + ::Timeout::ExitException + else + ::Timeout::Error + end + TIMEOUT_ERROR_NEVER = { TIMEOUT_ERROR_CLASS => :never }.freeze + end end diff --git a/lib/mysql2/client.rb b/lib/mysql2/client.rb index a24d34db2..2bb81a87f 100644 --- a/lib/mysql2/client.rb +++ b/lib/mysql2/client.rb @@ -1,68 +1,186 @@ module Mysql2 class Client - attr_reader :query_options - @@default_query_options = { - :as => :hash, # the type of object you want each row back as; also supports :array (an array of values) - :async => false, # don't wait for a result after sending the query, you'll have to monitor the socket yourself then eventually call Mysql2::Client#async_result - :cast_booleans => false, # cast tinyint(1) fields as true/false in ruby - :symbolize_keys => false, # return field names as symbols instead of strings - :database_timezone => :local, # timezone Mysql2 will assume datetime objects are stored in - :application_timezone => nil, # timezone Mysql2 will convert to before handing the object back to the caller - :cache_rows => true, # tells Mysql2 to use it's internal row cache for results - :connect_flags => REMEMBER_OPTIONS | LONG_PASSWORD | LONG_FLAG | TRANSACTIONS | PROTOCOL_41 | SECURE_CONNECTION, - :cast => true - } + attr_reader :query_options, :read_timeout + + def self.default_query_options + @default_query_options ||= { + as: :hash, # the type of object you want each row back as; also supports :array (an array of values) + async: false, # don't wait for a result after sending the query, you'll have to monitor the socket yourself then eventually call Mysql2::Client#async_result + cast_booleans: false, # cast tinyint(1) fields as true/false in ruby + symbolize_keys: false, # return field names as symbols instead of strings + database_timezone: :local, # timezone Mysql2 will assume datetime objects are stored in + application_timezone: nil, # timezone Mysql2 will convert to before handing the object back to the caller + cache_rows: true, # tells Mysql2 to use its internal row cache for results + connect_flags: REMEMBER_OPTIONS | LONG_PASSWORD | LONG_FLAG | TRANSACTIONS | PROTOCOL_41 | SECURE_CONNECTION | CONNECT_ATTRS, + cast: true, + default_file: nil, + default_group: nil, + } + end def initialize(opts = {}) - opts = Mysql2::Util.key_hash_as_symbols( opts ) - @query_options = @@default_query_options.dup + raise Mysql2::Error, "Options parameter must be a Hash" unless opts.is_a? Hash + + opts = Mysql2::Util.key_hash_as_symbols(opts) + @read_timeout = nil + @query_options = self.class.default_query_options.dup @query_options.merge! opts initialize_ext - # Set MySQL connection options (each one is a call to mysql_options()) - [:reconnect, :connect_timeout, :local_infile, :read_timeout, :write_timeout].each do |key| + # Set default connect_timeout to avoid unlimited retries from signal interruption + opts[:connect_timeout] = 120 unless opts.key?(:connect_timeout) + + # TODO: stricter validation rather than silent massaging + %i[reconnect connect_timeout local_infile read_timeout write_timeout default_file default_group secure_auth init_command automatic_close enable_cleartext_plugin default_auth get_server_public_key].each do |key| next unless opts.key?(key) + case key - when :reconnect, :local_infile - send(:"#{key}=", !!opts[key]) + when :reconnect, :local_infile, :secure_auth, :automatic_close, :enable_cleartext_plugin, :get_server_public_key + send(:"#{key}=", !!opts[key]) # rubocop:disable Style/DoubleNegation when :connect_timeout, :read_timeout, :write_timeout - send(:"#{key}=", opts[key].to_i) + send(:"#{key}=", Integer(opts[key])) unless opts[key].nil? else send(:"#{key}=", opts[key]) end end - # force the encoding to utf8 - self.charset_name = opts[:encoding] || 'utf8' + # force the encoding to utf8mb4 + self.charset_name = opts[:encoding] || 'utf8mb4' - ssl_set(*opts.values_at(:sslkey, :sslcert, :sslca, :sslcapath, :sslcipher)) + mode = parse_ssl_mode(opts[:ssl_mode]) if opts[:ssl_mode] + if (mode == SSL_MODE_VERIFY_CA || mode == SSL_MODE_VERIFY_IDENTITY) && !opts[:sslca] + opts[:sslca] = find_default_ca_path + end - if [:user,:pass,:hostname,:dbname,:db,:sock].any?{|k| @query_options.has_key?(k) } - warn "============= WARNING FROM mysql2 =============" - warn "The options :user, :pass, :hostname, :dbname, :db, and :sock will be deprecated at some point in the future." - warn "Instead, please use :username, :password, :host, :port, :database, :socket, :flags for the options." - warn "============= END WARNING FROM mysql2 =========" + ssl_options = opts.values_at(:sslkey, :sslcert, :sslca, :sslcapath, :sslcipher) + ssl_set(*ssl_options) if ssl_options.any? || opts.key?(:sslverify) + self.ssl_mode = mode if mode + + flags = case opts[:flags] + when Array + parse_flags_array(opts[:flags], @query_options[:connect_flags]) + when String + parse_flags_array(opts[:flags].split(' '), @query_options[:connect_flags]) + when Integer + @query_options[:connect_flags] | opts[:flags] + else + @query_options[:connect_flags] end + # SSL verify is a connection flag rather than a mysql_ssl_set option + flags |= SSL_VERIFY_SERVER_CERT if opts[:sslverify] + + check_and_clean_query_options + user = opts[:username] || opts[:user] pass = opts[:password] || opts[:pass] - host = opts[:host] || opts[:hostname] || 'localhost' - port = opts[:port] || 3306 + host = opts[:host] || opts[:hostname] + port = opts[:port] database = opts[:database] || opts[:dbname] || opts[:db] socket = opts[:socket] || opts[:sock] - flags = opts[:flags] ? opts[:flags] | @query_options[:connect_flags] : @query_options[:connect_flags] - connect user, pass, host, port, database, socket, flags + # Correct the data types before passing these values down to the C level + user = user.to_s unless user.nil? + pass = pass.to_s unless pass.nil? + host = host.to_s unless host.nil? + port = port.to_i unless port.nil? + database = database.to_s unless database.nil? + socket = socket.to_s unless socket.nil? + conn_attrs = parse_connect_attrs(opts[:connect_attrs]) + + connect user, pass, host, port, database, socket, flags, conn_attrs end - def self.default_query_options - @@default_query_options + def parse_ssl_mode(mode) + m = mode.to_s.upcase + if m.start_with?('SSL_MODE_') + return Mysql2::Client.const_get(m) if Mysql2::Client.const_defined?(m) + else + x = 'SSL_MODE_' + m + return Mysql2::Client.const_get(x) if Mysql2::Client.const_defined?(x) + end + warn "Unknown MySQL ssl_mode flag: #{mode}" + end + + def parse_flags_array(flags, initial = 0) + flags.reduce(initial) do |memo, f| + fneg = f.start_with?('-') ? f[1..-1] : nil + if fneg && fneg =~ /^\w+$/ && Mysql2::Client.const_defined?(fneg) + memo & ~ Mysql2::Client.const_get(fneg) + elsif f && f =~ /^\w+$/ && Mysql2::Client.const_defined?(f) + memo | Mysql2::Client.const_get(f) + else + warn "Unknown MySQL connection flag: '#{f}'" + memo + end + end + end + + # Find any default system CA paths to handle system roots + # by default if stricter validation is requested and no + # path is provide. + def find_default_ca_path + [ + "/etc/ssl/certs/ca-certificates.crt", + "/etc/pki/tls/certs/ca-bundle.crt", + "/etc/ssl/ca-bundle.pem", + "/etc/ssl/cert.pem", + ].find { |f| File.exist?(f) } + end + + # Set default program_name in performance_schema.session_connect_attrs + # and performance_schema.session_account_connect_attrs + def parse_connect_attrs(conn_attrs) + return {} if Mysql2::Client::CONNECT_ATTRS.zero? + + conn_attrs ||= {} + conn_attrs[:program_name] ||= $PROGRAM_NAME + conn_attrs.each_with_object({}) do |(key, value), hash| + hash[key.to_s] = value.to_s + end + end + + def query(sql, options = {}) + Thread.handle_interrupt(::Mysql2::Util::TIMEOUT_ERROR_NEVER) do + _query(sql, @query_options.merge(options)) + end + end + + def query_info + info = query_info_string + return {} unless info + + info_hash = {} + info.split.each_slice(2) { |s| info_hash[s[0].downcase.delete(':').to_sym] = s[1].to_i } + info_hash + end + + def info + self.class.info end private - def self.local_offset + + def check_and_clean_query_options + if %i[user pass hostname dbname db sock].any? { |k| @query_options.key?(k) } + warn "============= WARNING FROM mysql2 =============" + warn "The options :user, :pass, :hostname, :dbname, :db, and :sock are deprecated and will be removed at some point in the future." + warn "Instead, please use :username, :password, :host, :port, :database, :socket, :flags for the options." + warn "============= END WARNING FROM mysql2 =========" + end + + # avoid logging sensitive data via #inspect + @query_options.delete(:password) + @query_options.delete(:pass) + end + + class << self + private + + def local_offset ::Time.local(2010).utc_offset.to_r / 86400 end + end end end diff --git a/lib/mysql2/console.rb b/lib/mysql2/console.rb new file mode 100644 index 000000000..d8fb9e324 --- /dev/null +++ b/lib/mysql2/console.rb @@ -0,0 +1,5 @@ +# Loaded by script/console. Land helpers here. + +Pry.config.prompt = lambda do |context, *| + "[mysql2] #{context}> " +end diff --git a/lib/mysql2/em.rb b/lib/mysql2/em.rb index 5a6449901..329b3080e 100644 --- a/lib/mysql2/em.rb +++ b/lib/mysql2/em.rb @@ -1,5 +1,3 @@ -# encoding: utf-8 - require 'eventmachine' require 'mysql2' @@ -10,32 +8,40 @@ module Watcher def initialize(client, deferable) @client = client @deferable = deferable + @is_watching = true end def notify_readable detach begin result = @client.async_result - rescue Exception => e + rescue StandardError => e @deferable.fail(e) else @deferable.succeed(result) end end + + def watching? + @is_watching + end + + def unbind + @is_watching = false + end end def close(*args) - if @watch - @watch.detach - end + @watch.detach if @watch && @watch.watching? + super(*args) end - def query(sql, opts={}) + def query(sql, opts = {}) if ::EM.reactor_running? - super(sql, opts.merge(:async => true)) + super(sql, opts.merge(async: true)) deferable = ::EM::DefaultDeferrable.new - @watch = ::EM.watch(self.socket, Watcher, self, deferable) + @watch = ::EM.watch(socket, Watcher, self, deferable) @watch.notify_readable = true deferable else diff --git a/lib/mysql2/error.rb b/lib/mysql2/error.rb index e195c2cbb..8cfa23ffb 100644 --- a/lib/mysql2/error.rb +++ b/lib/mysql2/error.rb @@ -1,15 +1,101 @@ module Mysql2 class Error < StandardError - attr_accessor :error_number, :sql_state + ENCODE_OPTS = { + undef: :replace, + invalid: :replace, + replace: '?'.freeze, + }.freeze - def initialize msg - super - @error_number = nil - @sql_state = nil - end + ConnectionError = Class.new(Error) + TimeoutError = Class.new(Error) + + CODES = { + 1205 => TimeoutError, # ER_LOCK_WAIT_TIMEOUT + + 1044 => ConnectionError, # ER_DBACCESS_DENIED_ERROR + 1045 => ConnectionError, # ER_ACCESS_DENIED_ERROR + 1152 => ConnectionError, # ER_ABORTING_CONNECTION + 1153 => ConnectionError, # ER_NET_PACKET_TOO_LARGE + 1154 => ConnectionError, # ER_NET_READ_ERROR_FROM_PIPE + 1155 => ConnectionError, # ER_NET_FCNTL_ERROR + 1156 => ConnectionError, # ER_NET_PACKETS_OUT_OF_ORDER + 1157 => ConnectionError, # ER_NET_UNCOMPRESS_ERROR + 1158 => ConnectionError, # ER_NET_READ_ERROR + 1159 => ConnectionError, # ER_NET_READ_INTERRUPTED + 1160 => ConnectionError, # ER_NET_ERROR_ON_WRITE + 1161 => ConnectionError, # ER_NET_WRITE_INTERRUPTED + 1927 => ConnectionError, # ER_CONNECTION_KILLED + + 2001 => ConnectionError, # CR_SOCKET_CREATE_ERROR + 2002 => ConnectionError, # CR_CONNECTION_ERROR + 2003 => ConnectionError, # CR_CONN_HOST_ERROR + 2004 => ConnectionError, # CR_IPSOCK_ERROR + 2005 => ConnectionError, # CR_UNKNOWN_HOST + 2006 => ConnectionError, # CR_SERVER_GONE_ERROR + 2007 => ConnectionError, # CR_VERSION_ERROR + 2009 => ConnectionError, # CR_WRONG_HOST_INFO + 2012 => ConnectionError, # CR_SERVER_HANDSHAKE_ERR + 2013 => ConnectionError, # CR_SERVER_LOST + 2020 => ConnectionError, # CR_NET_PACKET_TOO_LARGE + 2026 => ConnectionError, # CR_SSL_CONNECTION_ERROR + 2027 => ConnectionError, # CR_MALFORMED_PACKET + 2047 => ConnectionError, # CR_CONN_UNKNOW_PROTOCOL + 2048 => ConnectionError, # CR_INVALID_CONN_HANDLE + 2049 => ConnectionError, # CR_UNUSED_1 + }.freeze + + attr_reader :error_number, :sql_state # Mysql gem compatibility - alias_method :errno, :error_number - alias_method :error, :message + alias errno error_number + alias error message + + def initialize(msg, server_version = nil, error_number = nil, sql_state = nil) + @server_version = server_version + @error_number = error_number + @sql_state = sql_state ? sql_state.encode(**ENCODE_OPTS) : nil + + super(clean_message(msg)) + end + + def self.new_with_args(msg, server_version, error_number, sql_state) + error_class = CODES.fetch(error_number, self) + error_class.new(msg, server_version, error_number, sql_state) + end + + private + + # In MySQL 5.5+ error messages are always constructed server-side as UTF-8 + # then returned in the encoding set by the `character_set_results` system + # variable. + # + # See http://dev.mysql.com/doc/refman/5.5/en/charset-errors.html for + # more context. + # + # Before MySQL 5.5 error message template strings are in whatever encoding + # is associated with the error message language. + # See http://dev.mysql.com/doc/refman/5.1/en/error-message-language.html + # for more information. + # + # The issue is that the user-data inserted in the message could potentially + # be in any encoding MySQL supports and is insert into the latin1, euckr or + # koi8r string raw. Meaning there's a high probability the string will be + # corrupt encoding-wise. + # + # See http://dev.mysql.com/doc/refman/5.1/en/charset-errors.html for + # more information. + # + # So in an attempt to make sure the error message string is always in a valid + # encoding, we'll assume UTF-8 and clean the string of anything that's not a + # valid UTF-8 character. + # + # Returns a valid UTF-8 string. + def clean_message(message) + if @server_version && @server_version > 50500 + message.encode(**ENCODE_OPTS) + else + message.encode(Encoding::UTF_8, **ENCODE_OPTS) + end + end end end diff --git a/lib/mysql2/field.rb b/lib/mysql2/field.rb new file mode 100644 index 000000000..516ec17c2 --- /dev/null +++ b/lib/mysql2/field.rb @@ -0,0 +1,3 @@ +module Mysql2 + Field = Struct.new(:name, :type) +end diff --git a/lib/mysql2/result.rb b/lib/mysql2/result.rb index e005e817c..585104e0b 100644 --- a/lib/mysql2/result.rb +++ b/lib/mysql2/result.rb @@ -1,5 +1,7 @@ module Mysql2 class Result + attr_reader :server_flags + include Enumerable end end diff --git a/lib/mysql2/statement.rb b/lib/mysql2/statement.rb new file mode 100644 index 000000000..c50ed1f96 --- /dev/null +++ b/lib/mysql2/statement.rb @@ -0,0 +1,9 @@ +module Mysql2 + class Statement + def execute(*args, **kwargs) + Thread.handle_interrupt(::Mysql2::Util::TIMEOUT_ERROR_NEVER) do + _execute(*args, **kwargs) + end + end + end +end diff --git a/lib/mysql2/version.rb b/lib/mysql2/version.rb index bd9267d64..cf7beba92 100644 --- a/lib/mysql2/version.rb +++ b/lib/mysql2/version.rb @@ -1,3 +1,3 @@ module Mysql2 - VERSION = "0.3.12b6" + VERSION = "0.5.7".freeze end diff --git a/mysql2.gemspec b/mysql2.gemspec index 148b33ed8..28fd1c301 100644 --- a/mysql2.gemspec +++ b/mysql2.gemspec @@ -1,21 +1,27 @@ require File.expand_path('../lib/mysql2/version', __FILE__) -Gem::Specification.new do |s| - s.name = %q{mysql2} +Mysql2::GEMSPEC = Gem::Specification.new do |s| + s.name = 'mysql2' s.version = Mysql2::VERSION - s.authors = ["Brian Lopez"] - s.email = %q{seniorlopez@gmail.com} + s.authors = ['Brian Lopez', 'Aaron Stone'] + s.license = "MIT" + s.email = ['seniorlopez@gmail.com', 'aaron@serendipity.cx'] s.extensions = ["ext/mysql2/extconf.rb"] - s.homepage = %q{http://github.com/brianmario/mysql2} + s.homepage = 'https://github.com/brianmario/mysql2' s.rdoc_options = ["--charset=UTF-8"] - s.summary = %q{A simple, fast Mysql library for Ruby, binding to libmysql} + s.summary = 'A simple, fast Mysql library for Ruby, binding to libmysql' + s.metadata = { + 'bug_tracker_uri' => "#{s.homepage}/issues", + 'changelog_uri' => "#{s.homepage}/releases/tag/#{s.version}", + 'documentation_uri' => "https://www.rubydoc.info/gems/mysql2/#{s.version}", + 'homepage_uri' => s.homepage, + 'source_code_uri' => "#{s.homepage}/tree/#{s.version}", + } + s.required_ruby_version = '>= 2.0.0' - s.files = `git ls-files README.md CHANGELOG.md MIT-LICENSE ext lib support`.split - s.test_files = `git ls-files spec examples`.split + s.files = `git ls-files README.md CHANGELOG.md LICENSE ext lib support`.split - # tests - s.add_development_dependency 'eventmachine' - s.add_development_dependency 'rake-compiler', '~> 0.8.1' - s.add_development_dependency 'rake', '~> 0.9.3' - s.add_development_dependency 'rspec', '~> 2.8.0' + s.metadata['msys2_mingw_dependencies'] = 'libmariadbclient' + + s.add_dependency 'bigdecimal' end diff --git a/script/bootstrap b/script/bootstrap new file mode 100755 index 000000000..500005c6b --- /dev/null +++ b/script/bootstrap @@ -0,0 +1,5 @@ +#!/bin/sh +set -e + +cd "$(dirname "$0")/.." +exec bundle install --binstubs --path vendor/gems "$@" diff --git a/script/console b/script/console new file mode 100755 index 000000000..9145f1691 --- /dev/null +++ b/script/console @@ -0,0 +1,7 @@ +#!/bin/sh +# Run a Ruby REPL. + +set -e + +cd $(dirname "$0")/.. +exec ruby -S bin/pry -Ilib -r mysql2 -r mysql2/console diff --git a/spec/em/em_spec.rb b/spec/em/em_spec.rb index 5a6b42ee2..c57d474dd 100644 --- a/spec/em/em_spec.rb +++ b/spec/em/em_spec.rb @@ -1,10 +1,9 @@ -# encoding: UTF-8 require 'spec_helper' begin require 'eventmachine' require 'mysql2/em' - describe Mysql2::EM::Client do + RSpec.describe Mysql2::EM::Client do it "should support async queries" do results = [] EM.run do @@ -12,6 +11,7 @@ defer1 = client1.query "SELECT sleep(0.1) as first_query" defer1.callback do |result| results << result.first + client1.close EM.stop_event_loop end @@ -19,11 +19,12 @@ defer2 = client2.query "SELECT sleep(0.025) second_query" defer2.callback do |result| results << result.first + client2.close end end - results[0].keys.should include("second_query") - results[1].keys.should include("first_query") + expect(results[0].keys).to include("second_query") + expect(results[1].keys).to include("first_query") end it "should support queries in callbacks" do @@ -34,44 +35,47 @@ defer1.callback do |result| results << result.first defer2 = client.query "SELECT sleep(0.025) as second_query" - defer2.callback do |result| - results << result.first + defer2.callback do |r| + results << r.first + client.close EM.stop_event_loop end end end - results[0].keys.should include("first_query") - results[1].keys.should include("second_query") + expect(results[0].keys).to include("first_query") + expect(results[1].keys).to include("second_query") end it "should not swallow exceptions raised in callbacks" do - lambda { + expect do EM.run do client = Mysql2::EM::Client.new DatabaseCredentials['root'] defer = client.query "SELECT sleep(0.1) as first_query" - defer.callback do |result| + defer.callback do + client.close raise 'some error' end - defer.errback do |err| + defer.errback do # This _shouldn't_ be run, but it needed to prevent the specs from # freezing if this test fails. EM.stop_event_loop end end - }.should raise_error + end.to raise_error('some error') end context 'when an exception is raised by the client' do let(:client) { Mysql2::EM::Client.new DatabaseCredentials['root'] } let(:error) { StandardError.new('some error') } - before { client.stub(:async_result).and_raise(error) } + before { allow(client).to receive(:async_result).and_raise(error) } + after { client.close } it "should swallow exceptions raised in by the client" do errors = [] EM.run do defer = client.query "SELECT sleep(0.1) as first_query" - defer.callback do |result| + defer.callback do # This _shouldn't_ be run, but it is needed to prevent the specs from # freezing if this test fails. EM.stop_event_loop @@ -81,7 +85,7 @@ EM.stop_event_loop end end - errors.should == [error] + expect(errors).to eq([error]) end it "should fail the deferrable" do @@ -89,19 +93,40 @@ EM.run do defer = client.query "SELECT sleep(0.025) as first_query" EM.add_timer(0.1) do - defer.callback do |result| + defer.callback do callbacks_run << :callback # This _shouldn't_ be run, but it is needed to prevent the specs from # freezing if this test fails. EM.stop_event_loop end - defer.errback do |err| + defer.errback do callbacks_run << :errback EM.stop_event_loop end end end - callbacks_run.should == [:errback] + expect(callbacks_run).to eq([:errback]) + end + end + + it "should not raise error when closing client with no query running" do + callbacks_run = [] + EM.run do + client = Mysql2::EM::Client.new DatabaseCredentials['root'] + defer = client.query("select sleep(0.025)") + defer.callback do + callbacks_run << :callback + end + defer.errback do + callbacks_run << :errback + end + EM.add_timer(0.1) do + expect(callbacks_run).to eq([:callback]) + expect do + client.close + end.not_to raise_error + EM.stop_event_loop + end end end end diff --git a/spec/my.cnf.example b/spec/my.cnf.example new file mode 100644 index 000000000..5e7792d35 --- /dev/null +++ b/spec/my.cnf.example @@ -0,0 +1,9 @@ +[root] +host=localhost +user=LOCALUSERNAME +password= + +[client] +host=localhost +user=LOCALUSERNAME +password= diff --git a/spec/mysql2/client_spec.rb b/spec/mysql2/client_spec.rb index afdec3308..ad7d25323 100644 --- a/spec/mysql2/client_spec.rb +++ b/spec/mysql2/client_spec.rb @@ -1,642 +1,1165 @@ -# encoding: UTF-8 require 'spec_helper' -describe Mysql2::Client do - before(:each) do - @client = Mysql2::Client.new DatabaseCredentials['root'] +RSpec.describe Mysql2::Client do # rubocop:disable Metrics/BlockLength + let(:performance_schema_enabled) do + performance_schema = @client.query "SHOW VARIABLES LIKE 'performance_schema'" + performance_schema.any? { |x| x['Value'] == 'ON' } end - it "should raise an exception upon connection failure" do - lambda { + context "using defaults file" do + let(:cnf_file) { File.expand_path('../../my.cnf', __FILE__) } + + it "should not raise an exception for valid defaults group" do + expect do + new_client(default_file: cnf_file, default_group: "test") + end.not_to raise_error + end + + it "should not raise an exception without default group" do + expect do + new_client(default_file: cnf_file) + end.not_to raise_error + end + end + + it "should raise a Mysql::Error::ConnectionError upon connection failure" do + expect do # The odd local host IP address forces the mysql client library to # use a TCP socket rather than a domain socket. - Mysql2::Client.new DatabaseCredentials['root'].merge('host' => '127.0.0.2', 'port' => 999999) - }.should raise_error(Mysql2::Error) + new_client('host' => '127.0.0.2', 'port' => 999999) + end.to raise_error(Mysql2::Error::ConnectionError) end - if defined? Encoding - it "should raise an exception on create for invalid encodings" do - lambda { - c = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => "fake")) - }.should raise_error(Mysql2::Error) - end + it "should raise an exception on create for invalid encodings" do + expect do + new_client(encoding: "fake") + end.to raise_error(Mysql2::Error) + end - it "should not raise an exception on create for a valid encoding" do - lambda { - c = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => "utf8")) - }.should_not raise_error(Mysql2::Error) + it "should raise an exception on non-string encodings" do + expect do + new_client(encoding: :fake) + end.to raise_error(TypeError) + end - lambda { - c = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => "big5")) - }.should_not raise_error(Mysql2::Error) + it "should not raise an exception on create for a valid encoding" do + expect do + new_client(encoding: "utf8") + end.not_to raise_error + + expect do + new_client(DatabaseCredentials['root'].merge(encoding: "big5")) + end.not_to raise_error + end + + Klient = Class.new(Mysql2::Client) do + attr_reader :connect_args + + def connect(*args) + @connect_args ||= [] + @connect_args << args end end it "should accept connect flags and pass them to #connect" do - klient = Class.new(Mysql2::Client) do - attr_reader :connect_args - def connect *args - @connect_args ||= [] - @connect_args << args + client = Klient.new flags: Mysql2::Client::FOUND_ROWS + expect(client.connect_args.last[6] & Mysql2::Client::FOUND_ROWS).to be > 0 + end + + it "should parse flags array" do + client = Klient.new flags: %w[FOUND_ROWS -PROTOCOL_41] + expect(client.connect_args.last[6] & Mysql2::Client::FOUND_ROWS).to eql(Mysql2::Client::FOUND_ROWS) + expect(client.connect_args.last[6] & Mysql2::Client::PROTOCOL_41).to eql(0) + end + + it "should parse flags string" do + client = Klient.new flags: "FOUND_ROWS -PROTOCOL_41" + expect(client.connect_args.last[6] & Mysql2::Client::FOUND_ROWS).to eql(Mysql2::Client::FOUND_ROWS) + expect(client.connect_args.last[6] & Mysql2::Client::PROTOCOL_41).to eql(0) + end + + it "should default flags to (REMEMBER_OPTIONS, LONG_PASSWORD, LONG_FLAG, TRANSACTIONS, PROTOCOL_41, SECURE_CONNECTION)" do + client = Klient.new + client_flags = Mysql2::Client::REMEMBER_OPTIONS | + Mysql2::Client::LONG_PASSWORD | + Mysql2::Client::LONG_FLAG | + Mysql2::Client::TRANSACTIONS | + Mysql2::Client::PROTOCOL_41 | + Mysql2::Client::SECURE_CONNECTION | + Mysql2::Client::CONNECT_ATTRS + expect(client.connect_args.last[6]).to eql(client_flags) + end + + it "should execute init command" do + options = DatabaseCredentials['root'].dup + options[:init_command] = "SET @something = 'setting_value';" + client = new_client(options) + result = client.query("SELECT @something;") + expect(result.first['@something']).to eq('setting_value') + end + + it "should send init_command after reconnect" do + options = DatabaseCredentials['root'].dup + options[:init_command] = "SET @something = 'setting_value';" + options[:reconnect] = true + client = new_client(options) + + result = client.query("SELECT @something;") + expect(result.first['@something']).to eq('setting_value') + + # get the current connection id + result = client.query("SELECT CONNECTION_ID()") + first_conn_id = result.first['CONNECTION_ID()'] + + # break the current connection + expect { client.query("KILL #{first_conn_id}") }.to raise_error(Mysql2::Error) + + client.ping # reconnect now + + # get the new connection id + result = client.query("SELECT CONNECTION_ID()") + second_conn_id = result.first['CONNECTION_ID()'] + + # confirm reconnect by checking the new connection id + expect(first_conn_id).not_to eq(second_conn_id) + + # At last, check that the init command executed + result = client.query("SELECT @something;") + expect(result.first['@something']).to eq('setting_value') + end + + it "should have a global default_query_options hash" do + expect(Mysql2::Client).to respond_to(:default_query_options) + end + + context "SSL" do + before(:example) do + ssl = @client.query "SHOW VARIABLES LIKE 'have_ssl'" + ssl_uncompiled = ssl.any? { |x| x['Value'] == 'OFF' } + ssl_disabled = ssl.any? { |x| x['Value'] == 'DISABLED' } + if ssl_uncompiled + skip("DON'T WORRY, THIS TEST PASSES - but SSL is not compiled into your MySQL daemon.") + elsif ssl_disabled + skip("DON'T WORRY, THIS TEST PASSES - but SSL is not enabled in your MySQL daemon.") + else + %i[sslkey sslcert sslca].each do |item| + unless File.exist?(option_overrides[item]) + skip("DON'T WORRY, THIS TEST PASSES - but #{option_overrides[item]} does not exist.") + break + end + end end end - client = klient.new :flags => Mysql2::Client::FOUND_ROWS - (client.connect_args.last.last & Mysql2::Client::FOUND_ROWS).should be_true + + let(:option_overrides) do + { + 'host' => ssl_cert_host, # must match the certificates + :sslkey => "#{ssl_cert_dir}/client-key.pem", + :sslcert => "#{ssl_cert_dir}/client-cert.pem", + :sslca => "#{ssl_cert_dir}/ca-cert.pem", + :sslcipher => 'DHE-RSA-AES256-SHA', + :sslverify => true, + } + end + + let(:ssl_client) do + new_client(option_overrides) + end + + # 'preferred' or 'verify_ca' are only in MySQL 5.6.36+, 5.7.11+, 8.0+ + version = Mysql2::Client.info + ssl_modes = case version + when 50636...50700, 50711...50800, 80000...90000 + %i[disabled preferred required verifa_ca verify_identity] + else + %i[disabled required verify_identity] + end + + # MySQL and MariaDB and all versions of Connector/C + ssl_modes.each do |ssl_mode| + it "should set ssl_mode option #{ssl_mode}" do + options = { + ssl_mode: ssl_mode, + } + options.merge!(option_overrides) + expect do + expect do + new_client(options) + end.not_to output(/does not support ssl_mode/).to_stderr + end.not_to raise_error + end + end + + it "should be able to connect via SSL options" do + # You may need to adjust the lines below to match your SSL certificate paths + results = Hash[ssl_client.query('SHOW STATUS WHERE Variable_name LIKE "Ssl_%"').map { |x| x.values_at('Variable_name', 'Value') }] + expect(results['Ssl_cipher']).not_to be_empty + expect(results['Ssl_version']).not_to be_empty + + expect(ssl_client.ssl_cipher).not_to be_empty + expect(results['Ssl_cipher']).to eql(ssl_client.ssl_cipher) + end end - it "should default flags to (REMEMBER_OPTIONS, LONG_PASSWORD, LONG_FLAG, TRANSACTIONS, PROTOCOL_41, SECURE_CONNECTION)" do - klient = Class.new(Mysql2::Client) do - attr_reader :connect_args - def connect *args - @connect_args ||= [] - @connect_args << args + def run_gc + if defined?(Rubinius) + GC.run(true) + else + GC.start + end + sleep(0.5) + end + + it "should terminate connections when calling close" do + # rubocop:disable Lint/AmbiguousBlockAssociation + expect do + client = Mysql2::Client.new(DatabaseCredentials['root']) + connection_id = client.thread_id + client.close + + # mysql_close sends a quit command without waiting for a response + # so give the server some time to handle the detect the closed connection + closed = false + 10.times do + closed = @client.query("SHOW PROCESSLIST").none? { |row| row['Id'] == connection_id } + break if closed + + sleep(0.1) + end + expect(closed).to eq(true) + end.to_not change { + @client.query("SHOW STATUS LIKE 'Aborted_%'").to_a + } + # rubocop:enable Lint/AmbiguousBlockAssociation + end + + it "should not leave dangling connections after garbage collection" do + run_gc + # rubocop:disable Lint/AmbiguousBlockAssociation + expect do + expect do + 10.times do + Mysql2::Client.new(DatabaseCredentials['root']).query('SELECT 1') + end + end.to change { + @client.query("SHOW STATUS LIKE 'Threads_connected'").first['Value'].to_i + }.by(10) + + run_gc + end.to_not change { + @client.query("SHOW STATUS LIKE 'Aborted_%'").to_a + + @client.query("SHOW STATUS LIKE 'Threads_connected'").to_a + } + # rubocop:enable Lint/AmbiguousBlockAssociation + end + + context "#set_server_option" do + let(:client) do + new_client.tap do |client| + client.set_server_option(Mysql2::Client::OPTION_MULTI_STATEMENTS_ON) end end - client = klient.new - (client.connect_args.last.last & (Mysql2::Client::REMEMBER_OPTIONS | - Mysql2::Client::LONG_PASSWORD | - Mysql2::Client::LONG_FLAG | - Mysql2::Client::TRANSACTIONS | - Mysql2::Client::PROTOCOL_41 | - Mysql2::Client::SECURE_CONNECTION)).should be_true + + it 'returns true when multi_statements is enable' do + expect(client.set_server_option(Mysql2::Client::OPTION_MULTI_STATEMENTS_ON)).to be true + end + + it 'returns true when multi_statements is disable' do + expect(client.set_server_option(Mysql2::Client::OPTION_MULTI_STATEMENTS_OFF)).to be true + end + + it 'returns false when multi_statements is neither OPTION_MULTI_STATEMENTS_OFF or OPTION_MULTI_STATEMENTS_ON' do + expect(client.set_server_option(344)).to be false + end + + it 'enables multiple-statement' do + client.query("SELECT 1;SELECT 2;") + + expect(client.next_result).to be true + expect(client.store_result.first).to eql('2' => 2) + expect(client.next_result).to be false + end + + it 'disables multiple-statement' do + client.set_server_option(Mysql2::Client::OPTION_MULTI_STATEMENTS_OFF) + + expect { client.query("SELECT 1;SELECT 2;") }.to raise_error(Mysql2::Error) + end end - it "should have a global default_query_options hash" do - Mysql2::Client.should respond_to(:default_query_options) + context "#automatic_close" do + it "is enabled by default" do + expect(new_client.automatic_close?).to be(true) + end + + if RUBY_PLATFORM =~ /mingw|mswin/ + it "cannot be disabled" do + expect do + client = new_client(automatic_close: false) + expect(client.automatic_close?).to be(true) + end.to output(/always closed by garbage collector/).to_stderr + + expect do + client = new_client(automatic_close: true) + expect(client.automatic_close?).to be(true) + end.to_not output(/always closed by garbage collector/).to_stderr + + expect do + client = new_client(automatic_close: true) + client.automatic_close = false + expect(client.automatic_close?).to be(true) + end.to output(/always closed by garbage collector/).to_stderr + end + else + it "can be configured" do + client = new_client(automatic_close: false) + expect(client.automatic_close?).to be(false) + end + + it "can be assigned" do + client = new_client + client.automatic_close = false + expect(client.automatic_close?).to be(false) + + client.automatic_close = true + expect(client.automatic_close?).to be(true) + + client.automatic_close = nil + expect(client.automatic_close?).to be(false) + + client.automatic_close = 9 + expect(client.automatic_close?).to be(true) + end + + it "should not close connections when running in a child process" do + run_gc + client = Mysql2::Client.new(DatabaseCredentials['root']) + client.automatic_close = false + + child = fork do + client.query('SELECT 1') + client = nil + run_gc + end + + Process.wait(child) + + # this will throw an error if the underlying socket was shutdown by the + # child's GC + expect { client.query('SELECT 1') }.to_not raise_exception + client.close + end + end end - it "should be able to connect via SSL options" do - pending("DON'T WORRY, THIS TEST PASSES :) - but is machine-specific. You need to have MySQL running with SSL configured and enabled. Then update the paths in this test to your needs and remove the pending state.") - ssl_client = nil - lambda { - ssl_client = Mysql2::Client.new( - :sslkey => '/path/to/client-key.pem', - :sslcert => '/path/to/client-cert.pem', - :sslca => '/path/to/ca-cert.pem', - :sslcapath => '/path/to/newcerts/', - :sslcipher => 'DHE-RSA-AES256-SHA' - ) - }.should_not raise_error(Mysql2::Error) + it "should be able to connect to database with numeric-only name" do + database = 1235 + @client.query "CREATE DATABASE IF NOT EXISTS `#{database}`" - results = ssl_client.query("SHOW STATUS WHERE Variable_name = \"Ssl_version\" OR Variable_name = \"Ssl_cipher\"").to_a - results[0]['Variable_name'].should eql('Ssl_cipher') - results[0]['Value'].should_not be_nil - results[0]['Value'].class.should eql(String) + expect do + new_client('database' => database) + end.not_to raise_error - results[1]['Variable_name'].should eql('Ssl_version') - results[1]['Value'].should_not be_nil - results[1]['Value'].class.should eql(String) + @client.query "DROP DATABASE IF EXISTS `#{database}`" end it "should respond to #close" do - @client.should respond_to(:close) + expect(@client).to respond_to(:close) end it "should be able to close properly" do - @client.close.should be_nil - lambda { + expect(@client.close).to be_nil + expect do @client.query "SELECT 1" - }.should raise_error(Mysql2::Error) + end.to raise_error(Mysql2::Error) + end + + context "#closed?" do + it "should return false when connected" do + expect(@client.closed?).to eql(false) + end + + it "should return true after close" do + @client.close + expect(@client.closed?).to eql(true) + end + end + + it "should not try to query closed mysql connection" do + client = new_client(reconnect: true) + expect(client.close).to be_nil + expect do + client.query "SELECT 1" + end.to raise_error(Mysql2::Error) end it "should respond to #query" do - @client.should respond_to(:query) + expect(@client).to respond_to(:query) end it "should respond to #warning_count" do - @client.should respond_to(:warning_count) + expect(@client).to respond_to(:warning_count) end context "#warning_count" do context "when no warnings" do - before(:each) do - @client.query('select 1') - end it "should 0" do - @client.warning_count.should == 0 + @client.query('select 1') + expect(@client.warning_count).to eq(0) end end context "when has a warnings" do - before(:each) do + it "should > 0" do # "the statement produces extra information that can be viewed by issuing a SHOW WARNINGS" - # http://dev.mysql.com/doc/refman/5.0/en/explain-extended.html - @client.query("explain extended select 1") + # https://dev.mysql.com/doc/refman/5.7/en/show-warnings.html + @client.query('DROP TABLE IF EXISTS test.no_such_table') + expect(@client.warning_count).to be > 0 end - it "should > 0" do - @client.warning_count.should > 0 + end + end + + it "should respond to #query_info" do + expect(@client).to respond_to(:query_info) + end + + context "#query_info" do + context "when no info present" do + it "should 0" do + @client.query('select 1') + expect(@client.query_info).to be_empty + expect(@client.query_info_string).to be_nil + end + end + context "when has some info" do + it "should retrieve it" do + @client.query "USE test" + @client.query "CREATE TABLE IF NOT EXISTS infoTest (`id` int(11) NOT NULL AUTO_INCREMENT, blah INT(11), PRIMARY KEY (`id`))" + + # http://dev.mysql.com/doc/refman/5.0/en/mysql-info.html says + # # Note that mysql_info() returns a non-NULL value for INSERT ... VALUES only for the multiple-row form of the statement (that is, only if multiple value lists are specified). + @client.query("INSERT INTO infoTest (blah) VALUES (1234),(4535)") + + expect(@client.query_info).to eql(records: 2, duplicates: 0, warnings: 0) + expect(@client.query_info_string).to eq('Records: 2 Duplicates: 0 Warnings: 0') + + @client.query "DROP TABLE infoTest" end end end + context ":local_infile" do + before(:context) do + new_client(local_infile: true) do |client| + local = client.query "SHOW VARIABLES LIKE 'local_infile'" + local_enabled = local.any? { |x| x['Value'] == 'ON' } + skip("DON'T WORRY, THIS TEST PASSES - but LOCAL INFILE is not enabled in your MySQL daemon.") unless local_enabled + + client.query %[ + CREATE TABLE IF NOT EXISTS infileTest ( + id MEDIUMINT NOT NULL AUTO_INCREMENT PRIMARY KEY, + foo VARCHAR(10), + bar MEDIUMTEXT + ) + ] + end + end + + after(:context) do + new_client do |client| + client.query "DROP TABLE IF EXISTS infileTest" + end + end + + it "should raise an error when local_infile is disabled" do + client = new_client(local_infile: false) + expect do + client.query "LOAD DATA LOCAL INFILE 'spec/test_data' INTO TABLE infileTest" + end.to raise_error(Mysql2::Error, /command is not allowed/) + end + + it "should raise an error when a non-existent file is loaded" do + client = new_client(local_infile: true) + expect do + client.query "LOAD DATA LOCAL INFILE 'this/file/is/not/here' INTO TABLE infileTest" + end.to raise_error(Mysql2::Error, 'No such file or directory: this/file/is/not/here') + end + + it "should LOAD DATA LOCAL INFILE" do + client = new_client(local_infile: true) + client.query "LOAD DATA LOCAL INFILE 'spec/test_data' INTO TABLE infileTest" + info = client.query_info + expect(info).to eql(records: 1, deleted: 0, skipped: 0, warnings: 0) + + result = client.query "SELECT * FROM infileTest" + expect(result.first).to eql('id' => 1, 'foo' => 'Hello', 'bar' => 'World') + end + end + it "should expect connect_timeout to be a positive integer" do - lambda { - Mysql2::Client.new(:connect_timeout => -1) - }.should raise_error(Mysql2::Error) + expect do + new_client(connect_timeout: -1) + end.to raise_error(Mysql2::Error) end it "should expect read_timeout to be a positive integer" do - lambda { - Mysql2::Client.new(:read_timeout => -1) - }.should raise_error(Mysql2::Error) + expect do + new_client(read_timeout: -1) + end.to raise_error(Mysql2::Error) end it "should expect write_timeout to be a positive integer" do - lambda { - Mysql2::Client.new(:write_timeout => -1) - }.should raise_error(Mysql2::Error) + expect do + new_client(write_timeout: -1) + end.to raise_error(Mysql2::Error) + end + + it "should allow nil read_timeout" do + client = new_client(read_timeout: nil) + + expect(client.read_timeout).to be_nil + end + + it "should set default program_name in connect_attrs" do + skip("DON'T WORRY, THIS TEST PASSES - but PERFORMANCE SCHEMA is not enabled in your MySQL daemon.") unless performance_schema_enabled + client = new_client + if Mysql2::Client::CONNECT_ATTRS.zero? || client.server_info[:version].match(/10.[01].\d+-MariaDB/) + pending('Both client and server versions must be MySQL 5.6 or MariaDB 10.2 or later.') + end + result = client.query("SELECT attr_value FROM performance_schema.session_account_connect_attrs WHERE processlist_id = connection_id() AND attr_name = 'program_name'") + expect(result.first['attr_value']).to eq($PROGRAM_NAME) + end + + it "should set custom connect_attrs" do + skip("DON'T WORRY, THIS TEST PASSES - but PERFORMANCE SCHEMA is not enabled in your MySQL daemon.") unless performance_schema_enabled + client = new_client(connect_attrs: { program_name: 'my_program_name', foo: 'fooval', bar: 'barval' }) + if Mysql2::Client::CONNECT_ATTRS.zero? || client.server_info[:version].match(/10.[01].\d+-MariaDB/) + pending('Both client and server versions must be MySQL 5.6 or MariaDB 10.2 or later.') + end + results = Hash[client.query("SELECT * FROM performance_schema.session_account_connect_attrs WHERE processlist_id = connection_id()").map { |x| x.values_at('ATTR_NAME', 'ATTR_VALUE') }] + expect(results['program_name']).to eq('my_program_name') + expect(results['foo']).to eq('fooval') + expect(results['bar']).to eq('barval') end context "#query" do it "should let you query again if iterating is finished when streaming" do - @client.query("SELECT 1 UNION SELECT 2", :stream => true, :cache_rows => false).each {} + @client.query("SELECT 1 UNION SELECT 2", stream: true, cache_rows: false).each.to_a - expect { - @client.query("SELECT 1 UNION SELECT 2", :stream => true, :cache_rows => false) - }.to_not raise_exception(Mysql2::Error) + expect do + @client.query("SELECT 1 UNION SELECT 2", stream: true, cache_rows: false) + end.to_not raise_error end it "should not let you query again if iterating is not finished when streaming" do - @client.query("SELECT 1 UNION SELECT 2", :stream => true, :cache_rows => false).first + @client.query("SELECT 1 UNION SELECT 2", stream: true, cache_rows: false).first - expect { - @client.query("SELECT 1 UNION SELECT 2", :stream => true, :cache_rows => false) - }.to raise_exception(Mysql2::Error) + expect do + @client.query("SELECT 1 UNION SELECT 2", stream: true, cache_rows: false) + end.to raise_exception(Mysql2::Error) end it "should only accept strings as the query parameter" do - lambda { + expect do @client.query ["SELECT 'not right'"] - }.should raise_error(TypeError) + end.to raise_error(TypeError) end it "should not retain query options set on a query for subsequent queries, but should retain it in the result" do - result = @client.query "SELECT 1", :something => :else - @client.query_options[:something].should be_nil - result.instance_variable_get('@query_options').should eql(@client.query_options.merge(:something => :else)) - @client.instance_variable_get('@current_query_options').should eql(@client.query_options.merge(:something => :else)) + result = @client.query "SELECT 1", something: :else + expect(@client.query_options[:something]).to be_nil + expect(result.instance_variable_get('@query_options')).to eql(@client.query_options.merge(something: :else)) + expect(@client.instance_variable_get('@current_query_options')).to eql(@client.query_options.merge(something: :else)) result = @client.query "SELECT 1" - result.instance_variable_get('@query_options').should eql(@client.query_options) - @client.instance_variable_get('@current_query_options').should eql(@client.query_options) + expect(result.instance_variable_get('@query_options')).to eql(@client.query_options) + expect(@client.instance_variable_get('@current_query_options')).to eql(@client.query_options) end it "should allow changing query options for subsequent queries" do - @client.query_options.merge!(:something => :else) + @client.query_options[:something] = :else result = @client.query "SELECT 1" - @client.query_options[:something].should eql(:else) - result.instance_variable_get('@query_options')[:something].should eql(:else) + expect(@client.query_options[:something]).to eql(:else) + expect(result.instance_variable_get('@query_options')[:something]).to eql(:else) # Clean up after this test @client.query_options.delete(:something) - @client.query_options[:something].should be_nil + expect(@client.query_options[:something]).to be_nil end it "should return results as a hash by default" do - @client.query("SELECT 1").first.class.should eql(Hash) + expect(@client.query("SELECT 1").first).to be_an_instance_of(Hash) end it "should be able to return results as an array" do - @client.query("SELECT 1", :as => :array).first.class.should eql(Array) - @client.query("SELECT 1").each(:as => :array) + expect(@client.query("SELECT 1", as: :array).first).to be_an_instance_of(Array) + @client.query("SELECT 1").each(as: :array) end it "should be able to return results with symbolized keys" do - @client.query("SELECT 1", :symbolize_keys => true).first.keys[0].class.should eql(Symbol) + expect(@client.query("SELECT 1", symbolize_keys: true).first.keys[0]).to be_an_instance_of(Symbol) end it "should require an open connection" do @client.close - lambda { + expect do @client.query "SELECT 1" - }.should raise_error(Mysql2::Error) + end.to raise_error(Mysql2::Error) + end + + it "should detect closed connection on query read error" do + connection_id = @client.thread_id + Thread.new do + sleep(0.1) + Mysql2::Client.new(DatabaseCredentials['root']).tap do |supervisor| + supervisor.query("KILL #{connection_id}") + end.close + end + expect do + @client.query("SELECT SLEEP(1)") + end.to raise_error(Mysql2::Error, /Lost connection/) + + if RUBY_PLATFORM !~ /mingw|mswin/ + expect do + @client.socket + end.to raise_error(Mysql2::Error, 'MySQL client is not connected') + end end if RUBY_PLATFORM !~ /mingw|mswin/ it "should not allow another query to be sent without fetching a result first" do - @client.query("SELECT 1", :async => true) - lambda { + @client.query("SELECT 1", async: true) + expect do @client.query("SELECT 1") - }.should raise_error(Mysql2::Error) + end.to raise_error(Mysql2::Error) end it "should describe the thread holding the active query" do - thr = Thread.new { @client.query("SELECT 1", :async => true) } - - thr.join - begin - @client.query("SELECT 1") - rescue Mysql2::Error => e - message = e.message + thr = Thread.new do + @client.query("SELECT 1", async: true) + Fiber.current end - re = Regexp.escape(thr.inspect) - message.should match(Regexp.new(re)) + + fiber = thr.value + expect { @client.query('SELECT 1') }.to raise_error(Mysql2::Error, Regexp.new(Regexp.escape(fiber.inspect))) end it "should timeout if we wait longer than :read_timeout" do - client = Mysql2::Client.new(DatabaseCredentials['root'].merge(:read_timeout => 1)) - lambda { - client.query("SELECT sleep(2)") - }.should raise_error(Mysql2::Error) + client = new_client(read_timeout: 0) + expect do + client.query('SELECT SLEEP(0.1)') + end.to raise_error(Mysql2::Error::TimeoutError) end # XXX this test is not deterministic (because Unix signal handling is not) # and may fail on a loaded system it "should run signal handlers while waiting for a response" do + kill_time = 0.25 + query_time = 4 * kill_time + mark = {} - trap(:USR1) { mark[:USR1] = Time.now } + begin - mark[:START] = Time.now + trap(:USR1) { mark.store(:USR1, clock_time) } pid = fork do - sleep 1 # wait for client "SELECT sleep(2)" query to start + sleep kill_time # wait for client query to start Process.kill(:USR1, Process.ppid) sleep # wait for explicit kill to prevent GC disconnect end - @client.query("SELECT sleep(2)") - mark[:END] = Time.now - mark.include?(:USR1).should be_true - (mark[:USR1] - mark[:START]).should >= 1 - (mark[:USR1] - mark[:START]).should < 1.1 - (mark[:END] - mark[:USR1]).should > 0.9 - (mark[:END] - mark[:START]).should >= 2 - (mark[:END] - mark[:START]).should < 2.1 + mark.store(:QUERY_START, clock_time) + @client.query("SELECT SLEEP(#{query_time})") + mark.store(:QUERY_END, clock_time) + ensure Process.kill(:TERM, pid) Process.waitpid2(pid) - ensure trap(:USR1, 'DEFAULT') end + + # the query ran uninterrupted + expect(mark.fetch(:QUERY_END) - mark.fetch(:QUERY_START)).to be_within(0.2).of(query_time) + # signals fired while the query was running + expect(mark.fetch(:USR1)).to be_between(mark.fetch(:QUERY_START), mark.fetch(:QUERY_END)) end it "#socket should return a Fixnum (file descriptor from C)" do - @client.socket.class.should eql(Fixnum) - @client.socket.should_not eql(0) + expect(@client.socket).to be_an_instance_of(0.class) + expect(@client.socket).not_to eql(0) end it "#socket should require an open connection" do @client.close - lambda { + expect do @client.socket - }.should raise_error(Mysql2::Error) + end.to raise_error(Mysql2::Error) end - it "should close the connection when an exception is raised" do - begin - Timeout.timeout(1) do - @client.query("SELECT sleep(2)") - end - rescue Timeout::Error - end + it 'should be impervious to connection-corrupting timeouts in #execute' do + # attempt to break the connection + stmt = @client.prepare('SELECT SLEEP(?)') + expect { Timeout.timeout(0.1) { stmt.execute(0.2) } }.to raise_error(Timeout::Error) + stmt.close - lambda { - @client.query("SELECT 1") - }.should raise_error(Mysql2::Error, 'closed MySQL connection') + # expect the connection to not be broken + expect { @client.query('SELECT 1') }.to_not raise_error end - it "should handle Timeouts without leaving the connection hanging if reconnect is true" do - client = Mysql2::Client.new(DatabaseCredentials['root'].merge(:reconnect => true)) - begin - Timeout.timeout(1) do - client.query("SELECT sleep(2)") - end - rescue Timeout::Error + context 'when a non-standard exception class is raised' do + it "should close the connection when an exception is raised" do + expect { Timeout.timeout(0.1, ArgumentError) { @client.query('SELECT SLEEP(1)') } }.to raise_error(ArgumentError) + expect { @client.query('SELECT 1') }.to raise_error(Mysql2::Error, 'MySQL client is not connected') end - lambda { - client.query("SELECT 1") - }.should_not raise_error(Mysql2::Error) - end - - it "should handle Timeouts without leaving the connection hanging if reconnect is set to true after construction true" do - client = Mysql2::Client.new(DatabaseCredentials['root']) - begin - Timeout.timeout(1) do - client.query("SELECT sleep(2)") + it "should handle Timeouts without leaving the connection hanging if reconnect is true" do + if RUBY_PLATFORM.include?('darwin') && @client.server_info.fetch(:version).start_with?('5.5') + pending('MySQL 5.5 on OSX is afflicted by an unknown bug that breaks this test. See #633 and #634.') end - rescue Timeout::Error - end - lambda { - client.query("SELECT 1") - }.should raise_error(Mysql2::Error) + client = new_client(reconnect: true) - client.reconnect = true + expect { Timeout.timeout(0.1, ArgumentError) { client.query('SELECT SLEEP(1)') } }.to raise_error(ArgumentError) + expect { client.query('SELECT 1') }.to_not raise_error + end - begin - Timeout.timeout(1) do - client.query("SELECT sleep(2)") + it "should handle Timeouts without leaving the connection hanging if reconnect is set to true after construction" do + if RUBY_PLATFORM.include?('darwin') && @client.server_info.fetch(:version).start_with?('5.5') + pending('MySQL 5.5 on OSX is afflicted by an unknown bug that breaks this test. See #633 and #634.') end - rescue Timeout::Error - end - lambda { - client.query("SELECT 1") - }.should_not raise_error(Mysql2::Error) + client = new_client + + expect { Timeout.timeout(0.1, ArgumentError) { client.query('SELECT SLEEP(1)') } }.to raise_error(ArgumentError) + expect { client.query('SELECT 1') }.to raise_error(Mysql2::Error) + client.reconnect = true + + expect { Timeout.timeout(0.1, ArgumentError) { client.query('SELECT SLEEP(1)') } }.to raise_error(ArgumentError) + expect { client.query('SELECT 1') }.to_not raise_error + end end it "threaded queries should be supported" do - threads, results = [], {} - lock = Mutex.new - connect = lambda{ - Mysql2::Client.new(DatabaseCredentials['root']) - } - Timeout.timeout(0.7) do - 5.times { - threads << Thread.new do - result = connect.call.query("SELECT sleep(0.5) as result") - lock.synchronize do - results[Thread.current.object_id] = result - end + sleep_time = 0.5 + + # Note that each thread opens its own database connection + start = clock_time + threads = Array.new(5) do + Thread.new do + new_client do |client| + client.query("SELECT SLEEP(#{sleep_time})") end - } + Thread.current.object_id + end end - threads.each{|t| t.join } - results.keys.sort.should eql(threads.map{|t| t.object_id }.sort) + values = threads.map(&:value) + stop = clock_time + + # This check demonstrates that the threads are sleeping concurrently: + # In the serial case, the difference would be a multiple of sleep time + expect(stop - start).to be_within(0.2).of(sleep_time) + + expect(values).to match_array(threads.map(&:object_id)) end it "evented async queries should be supported" do # should immediately return nil - @client.query("SELECT sleep(0.1)", :async => true).should eql(nil) + expect(@client.query("SELECT sleep(0.1)", async: true)).to eql(nil) - io_wrapper = IO.for_fd(@client.socket) + io_wrapper = IO.for_fd(@client.socket, autoclose: false) loops = 0 - loop do - if IO.select([io_wrapper], nil, nil, 0.05) - break - else - loops += 1 - end - end + loops += 1 until IO.select([io_wrapper], nil, nil, 0.05) # make sure we waited some period of time - (loops >= 1).should be_true + expect(loops >= 1).to be true result = @client.async_result - result.class.should eql(Mysql2::Result) + expect(result).to be_an_instance_of(Mysql2::Result) end end context "Multiple results sets" do - before(:each) do - @multi_client = Mysql2::Client.new(DatabaseCredentials['root'].merge(:flags => Mysql2::Client::MULTI_STATEMENTS)) + before(:example) do + @multi_client = new_client(flags: Mysql2::Client::MULTI_STATEMENTS) + end + + it "should raise an exception when one of multiple statements fails" do + result = @multi_client.query("SELECT 1 AS 'set_1'; SELECT * FROM invalid_table_name; SELECT 2 AS 'set_2';") + expect(result.first['set_1']).to be(1) + expect do + @multi_client.next_result + end.to raise_error(Mysql2::Error) + expect(@multi_client.next_result).to be false end it "returns multiple result sets" do - @multi_client.query( "select 1 as 'set_1'; select 2 as 'set_2'").first.should == { 'set_1' => 1 } + expect(@multi_client.query("SELECT 1 AS 'set_1'; SELECT 2 AS 'set_2'").first).to eql('set_1' => 1) - @multi_client.next_result.should == true - @multi_client.store_result.first.should == { 'set_2' => 2 } + expect(@multi_client.next_result).to be true + expect(@multi_client.store_result.first).to eql('set_2' => 2) - @multi_client.next_result.should == false + expect(@multi_client.next_result).to be false end it "does not interfere with other statements" do - @multi_client.query( "select 1 as 'set_1'; select 2 as 'set_2'") - while( @multi_client.next_result ) - @multi_client.store_result - end + @multi_client.query("SELECT 1 AS 'set_1'; SELECT 2 AS 'set_2'") + @multi_client.store_result while @multi_client.next_result - @multi_client.query( "select 3 as 'next'").first.should == { 'next' => 3 } + expect(@multi_client.query("SELECT 3 AS 'next'").first).to eq('next' => 3) end it "will raise on query if there are outstanding results to read" do @multi_client.query("SELECT 1; SELECT 2; SELECT 3") - lambda { + expect do @multi_client.query("SELECT 4") - }.should raise_error(Mysql2::Error) + end.to raise_error(Mysql2::Error) end it "#abandon_results! should work" do @multi_client.query("SELECT 1; SELECT 2; SELECT 3") @multi_client.abandon_results! - lambda { + expect do @multi_client.query("SELECT 4") - }.should_not raise_error(Mysql2::Error) + end.not_to raise_error end it "#more_results? should work" do - @multi_client.query( "select 1 as 'set_1'; select 2 as 'set_2'") - @multi_client.more_results?.should == true + @multi_client.query("SELECT 1 AS 'set_1'; SELECT 2 AS 'set_2'") + expect(@multi_client.more_results?).to be true @multi_client.next_result @multi_client.store_result - @multi_client.more_results?.should == false + expect(@multi_client.more_results?).to be false + end + + it "#more_results? should work with stored procedures" do + @multi_client.query("DROP PROCEDURE IF EXISTS test_proc") + @multi_client.query("CREATE PROCEDURE test_proc() BEGIN SELECT 1 AS 'set_1'; SELECT 2 AS 'set_2'; END") + expect(@multi_client.query("CALL test_proc()").first).to eql('set_1' => 1) + expect(@multi_client.more_results?).to be true + + @multi_client.next_result + expect(@multi_client.store_result.first).to eql('set_2' => 2) + + @multi_client.next_result + expect(@multi_client.store_result).to be_nil # this is the result from CALL itself + + expect(@multi_client.more_results?).to be false end end end it "should respond to #socket" do - @client.should respond_to(:socket) + expect(@client).to respond_to(:socket) end if RUBY_PLATFORM =~ /mingw|mswin/ it "#socket should raise as it's not supported" do - lambda { + expect do @client.socket - }.should raise_error(Mysql2::Error) + end.to raise_error(Mysql2::Error, /Raw access to the mysql file descriptor isn't supported on Windows/) end end it "should respond to escape" do - Mysql2::Client.should respond_to(:escape) + expect(Mysql2::Client).to respond_to(:escape) end context "escape" do it "should return a new SQL-escape version of the passed string" do - Mysql2::Client.escape("abc'def\"ghi\0jkl%mno").should eql("abc\\'def\\\"ghi\\0jkl%mno") + expect(Mysql2::Client.escape("abc'def\"ghi\0jkl%mno")).to eql("abc\\'def\\\"ghi\\0jkl%mno") end it "should return the passed string if nothing was escaped" do str = "plain" - Mysql2::Client.escape(str).object_id.should eql(str.object_id) + expect(Mysql2::Client.escape(str).object_id).to eql(str.object_id) end it "should not overflow the thread stack" do - lambda { + expect do Thread.new { Mysql2::Client.escape("'" * 256 * 1024) }.join - }.should_not raise_error(SystemStackError) + end.not_to raise_error end it "should not overflow the process stack" do - lambda { + expect do Thread.new { Mysql2::Client.escape("'" * 1024 * 1024 * 4) }.join - }.should_not raise_error(SystemStackError) + end.not_to raise_error end - unless RUBY_VERSION =~ /1.8/ - it "should carry over the original string's encoding" do - str = "abc'def\"ghi\0jkl%mno" - escaped = Mysql2::Client.escape(str) - escaped.encoding.should eql(str.encoding) + it "should carry over the original string's encoding" do + str = "abc'def\"ghi\0jkl%mno".dup + escaped = Mysql2::Client.escape(str) + expect(escaped.encoding).to eql(str.encoding) - str.encode!('us-ascii') - escaped = Mysql2::Client.escape(str) - escaped.encoding.should eql(str.encoding) - end + str.encode!('us-ascii') + escaped = Mysql2::Client.escape(str) + expect(escaped.encoding).to eql(str.encoding) end end it "should respond to #escape" do - @client.should respond_to(:escape) + expect(@client).to respond_to(:escape) end context "#escape" do it "should return a new SQL-escape version of the passed string" do - @client.escape("abc'def\"ghi\0jkl%mno").should eql("abc\\'def\\\"ghi\\0jkl%mno") + expect(@client.escape("abc'def\"ghi\0jkl%mno")).to eql("abc\\'def\\\"ghi\\0jkl%mno") end it "should return the passed string if nothing was escaped" do str = "plain" - @client.escape(str).object_id.should eql(str.object_id) + expect(@client.escape(str).object_id).to eql(str.object_id) end it "should not overflow the thread stack" do - lambda { + expect do Thread.new { @client.escape("'" * 256 * 1024) }.join - }.should_not raise_error(SystemStackError) + end.not_to raise_error end it "should not overflow the process stack" do - lambda { + expect do Thread.new { @client.escape("'" * 1024 * 1024 * 4) }.join - }.should_not raise_error(SystemStackError) + end.not_to raise_error end it "should require an open connection" do @client.close - lambda { + expect do @client.escape "" - }.should raise_error(Mysql2::Error) + end.to raise_error(Mysql2::Error) + end + + context 'when mysql encoding is not utf8' do + let(:client) { new_client(encoding: "ujis") } + + it 'should return a internal encoding string if Encoding.default_internal is set' do + with_internal_encoding Encoding::UTF_8 do + expect(client.escape("\u{30C6}\u{30B9}\u{30C8}")).to eq "\u{30C6}\u{30B9}\u{30C8}" + expect(client.escape("\u{30C6}'\u{30B9}\"\u{30C8}")).to eq "\u{30C6}\\'\u{30B9}\\\"\u{30C8}" + end + end end end it "should respond to #info" do - @client.should respond_to(:info) + expect(@client).to respond_to(:info) end it "#info should return a hash containing the client version ID and String" do info = @client.info - info.class.should eql(Hash) - info.should have_key(:id) - info[:id].class.should eql(Fixnum) - info.should have_key(:version) - info[:version].class.should eql(String) + expect(info).to be_an_instance_of(Hash) + expect(info).to have_key(:id) + expect(info[:id]).to be_an_instance_of(0.class) + expect(info).to have_key(:version) + expect(info[:version]).to be_an_instance_of(String) end - if defined? Encoding - context "strings returned by #info" do - it "should default to the connection's encoding if Encoding.default_internal is nil" do - Encoding.default_internal = nil - @client.info[:version].encoding.should eql(Encoding.find('utf-8')) - - client2 = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => 'ascii')) - client2.info[:version].encoding.should eql(Encoding.find('us-ascii')) - end + context "strings returned by #info" do + it "should be tagged as ascii" do + expect(@client.info[:version].encoding).to eql(Encoding::US_ASCII) + expect(@client.info[:header_version].encoding).to eql(Encoding::US_ASCII) + end + end - it "should use Encoding.default_internal" do - Encoding.default_internal = Encoding.find('utf-8') - @client.info[:version].encoding.should eql(Encoding.default_internal) - Encoding.default_internal = Encoding.find('us-ascii') - @client.info[:version].encoding.should eql(Encoding.default_internal) - end + context "strings returned by .info" do + it "should be tagged as ascii" do + expect(Mysql2::Client.info[:version].encoding).to eql(Encoding::US_ASCII) + expect(Mysql2::Client.info[:header_version].encoding).to eql(Encoding::US_ASCII) end end it "should respond to #server_info" do - @client.should respond_to(:server_info) + expect(@client).to respond_to(:server_info) end it "#server_info should return a hash containing the client version ID and String" do server_info = @client.server_info - server_info.class.should eql(Hash) - server_info.should have_key(:id) - server_info[:id].class.should eql(Fixnum) - server_info.should have_key(:version) - server_info[:version].class.should eql(String) + expect(server_info).to be_an_instance_of(Hash) + expect(server_info).to have_key(:id) + expect(server_info[:id]).to be_an_instance_of(0.class) + expect(server_info).to have_key(:version) + expect(server_info[:version]).to be_an_instance_of(String) end it "#server_info should require an open connection" do @client.close - lambda { + expect do @client.server_info - }.should raise_error(Mysql2::Error) + end.to raise_error(Mysql2::Error) end - if defined? Encoding - context "strings returned by #server_info" do - it "should default to the connection's encoding if Encoding.default_internal is nil" do - Encoding.default_internal = nil - @client.server_info[:version].encoding.should eql(Encoding.find('utf-8')) + context "strings returned by #server_info" do + it "should default to the connection's encoding if Encoding.default_internal is nil" do + with_internal_encoding nil do + expect(@client.server_info[:version].encoding).to eql(Encoding::UTF_8) - client2 = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => 'ascii')) - client2.server_info[:version].encoding.should eql(Encoding.find('us-ascii')) + client2 = new_client(encoding: 'ascii') + expect(client2.server_info[:version].encoding).to eql(Encoding::ASCII) + end + end + + it "should use Encoding.default_internal" do + with_internal_encoding Encoding::UTF_8 do + expect(@client.server_info[:version].encoding).to eql(Encoding.default_internal) end - it "should use Encoding.default_internal" do - Encoding.default_internal = Encoding.find('utf-8') - @client.server_info[:version].encoding.should eql(Encoding.default_internal) - Encoding.default_internal = Encoding.find('us-ascii') - @client.server_info[:version].encoding.should eql(Encoding.default_internal) + with_internal_encoding Encoding::ASCII do + expect(@client.server_info[:version].encoding).to eql(Encoding.default_internal) end end end - it "should raise a Mysql2::Error exception upon connection failure" do - lambda { - bad_client = Mysql2::Client.new :host => "localhost", :username => 'asdfasdf8d2h', :password => 'asdfasdfw42' - }.should raise_error(Mysql2::Error) + it "should raise a Mysql2::Error::ConnectionError exception upon connection failure due to invalid credentials" do + expect do + new_client(host: 'localhost', username: 'asdfasdf8d2h', password: 'asdfasdfw42') + end.to raise_error(Mysql2::Error::ConnectionError) - lambda { - good_client = Mysql2::Client.new DatabaseCredentials['root'] - }.should_not raise_error(Mysql2::Error) + expect do + new_client(DatabaseCredentials['root']) + end.not_to raise_error end context 'write operations api' do - before(:each) do + before(:example) do @client.query "USE test" - @client.query "CREATE TABLE IF NOT EXISTS lastIdTest (`id` int(11) NOT NULL AUTO_INCREMENT, blah INT(11), PRIMARY KEY (`id`))" + @client.query "CREATE TABLE IF NOT EXISTS lastIdTest (`id` BIGINT NOT NULL AUTO_INCREMENT, blah INT(11), PRIMARY KEY (`id`))" end - after(:each) do + after(:example) do @client.query "DROP TABLE lastIdTest" end it "should respond to #last_id" do - @client.should respond_to(:last_id) + expect(@client).to respond_to(:last_id) end - it "#last_id should return a Fixnum, the from the last INSERT/UPDATE" do - @client.last_id.should eql(0) + it "#last_id should return a Fixnum, from the last INSERT/UPDATE" do + expect(@client.last_id).to eql(0) @client.query "INSERT INTO lastIdTest (blah) VALUES (1234)" - @client.last_id.should eql(1) + expect(@client.last_id).to eql(1) end it "should respond to #last_id" do - @client.should respond_to(:last_id) + expect(@client).to respond_to(:last_id) + end + + it "#last_id should handle BIGINT auto-increment ids above 32 bits" do + # The id column type must be BIGINT. Surprise: INT(x) is limited to 32-bits for all values of x. + # Insert a row with a given ID, this should raise the auto-increment state + @client.query "INSERT INTO lastIdTest (id, blah) VALUES (5000000000, 5000)" + expect(@client.last_id).to eql(5000000000) + @client.query "INSERT INTO lastIdTest (blah) VALUES (5001)" + expect(@client.last_id).to eql(5000000001) end - it "#last_id should return a Fixnum, the from the last INSERT/UPDATE" do + it "#last_id isn't cleared by Statement#close" do + stmt = @client.prepare("INSERT INTO lastIdTest (blah) VALUES (1234)") + @client.query "INSERT INTO lastIdTest (blah) VALUES (1234)" - @client.affected_rows.should eql(1) + expect(@client.last_id).to eql(1) + + stmt.close + + expect(@client.last_id).to eql(1) + end + + it "#affected_rows should return a Fixnum, from the last INSERT/UPDATE" do + @client.query "INSERT INTO lastIdTest (blah) VALUES (1234), (5678)" + expect(@client.affected_rows).to eql(2) @client.query "UPDATE lastIdTest SET blah=4321 WHERE id=1" - @client.affected_rows.should eql(1) + expect(@client.affected_rows).to eql(1) + end + + it "#affected_rows with multi statements returns the last result's affected_rows" do + @client.set_server_option(Mysql2::Client::OPTION_MULTI_STATEMENTS_ON) + + @client.query("INSERT INTO lastIdTest (blah) VALUES (1234), (5678); UPDATE lastIdTest SET blah=4321 WHERE id=1") + expect(@client.affected_rows).to eq(2) + expect(@client.next_result).to eq(true) + expect(@client.affected_rows).to eq(1) + ensure + @client.set_server_option(Mysql2::Client::OPTION_MULTI_STATEMENTS_OFF) end + + it "#affected_rows isn't cleared by Statement#close" do + stmt = @client.prepare("INSERT INTO lastIdTest (blah) VALUES (1234)") + + @client.query "INSERT INTO lastIdTest (blah) VALUES (1234)" + expect(@client.affected_rows).to eql(1) + + stmt.close + + expect(@client.affected_rows).to eql(1) + end + end + + it "#affected_rows when no rows were affected returns 1" do + @client.query "SELECT sleep(0.01)" + expect(@client.affected_rows).to eq(1) end it "should respond to #thread_id" do - @client.should respond_to(:thread_id) + expect(@client).to respond_to(:thread_id) end it "#thread_id should be a Fixnum" do - @client.thread_id.class.should eql(Fixnum) + expect(@client.thread_id).to be_an_instance_of(0.class) end it "should respond to #ping" do - @client.should respond_to(:ping) + expect(@client).to respond_to(:ping) + end + + context "session_track" do + before(:example) do + unless Mysql2::Client.const_defined?(:SESSION_TRACK) + skip('Server versions must be MySQL 5.7 later.') + end + @client.query("SET @@SESSION.session_track_system_variables='*';") + end + + it "returns changes system variables for SESSION_TRACK_SYSTEM_VARIABLES" do + @client.query("SET @@SESSION.session_track_state_change=ON;") + res = @client.session_track(Mysql2::Client::SESSION_TRACK_SYSTEM_VARIABLES) + expect(res).to include("session_track_state_change", "ON") + end + + it "returns database name for SESSION_TRACK_SCHEMA" do + @client.query("USE information_schema") + res = @client.session_track(Mysql2::Client::SESSION_TRACK_SCHEMA) + expect(res).to eq(["information_schema"]) + end + + it "returns multiple session track type values when available" do + @client.query("SET @@SESSION.session_track_transaction_info='CHARACTERISTICS';") + + res = @client.session_track(Mysql2::Client::SESSION_TRACK_SYSTEM_VARIABLES) + expect(res).to include("session_track_transaction_info", "CHARACTERISTICS") + + res = @client.session_track(Mysql2::Client::SESSION_TRACK_STATE_CHANGE) + expect(res).to be_nil + + res = @client.session_track(Mysql2::Client::SESSION_TRACK_TRANSACTION_CHARACTERISTICS) + expect(res).to include("") + end + + it "returns valid transaction state inside a transaction" do + @client.query("SET @@SESSION.session_track_transaction_info='CHARACTERISTICS'") + @client.query("START TRANSACTION") + + res = @client.session_track(Mysql2::Client::SESSION_TRACK_TRANSACTION_STATE) + expect(res).to include("T_______") + end + + it "returns empty array if session track type not found" do + @client.query("SET @@SESSION.session_track_state_change=ON;") + res = @client.session_track(Mysql2::Client::SESSION_TRACK_TRANSACTION_CHARACTERISTICS) + expect(res).to be_nil + end end context "select_db" do - before(:each) do + before(:example) do 2.times do |i| @client.query("CREATE DATABASE test_selectdb_#{i}") @client.query("USE test_selectdb_#{i}") @@ -644,45 +1167,109 @@ def connect *args end end - after(:each) do + after(:example) do 2.times do |i| @client.query("DROP DATABASE test_selectdb_#{i}") end end it "should respond to #select_db" do - @client.should respond_to(:select_db) + expect(@client).to respond_to(:select_db) end it "should switch databases" do @client.select_db("test_selectdb_0") - @client.query("SHOW TABLES").first.values.first.should eql("test0") + expect(@client.query("SHOW TABLES").first.values.first).to eql("test0") @client.select_db("test_selectdb_1") - @client.query("SHOW TABLES").first.values.first.should eql("test1") + expect(@client.query("SHOW TABLES").first.values.first).to eql("test1") @client.select_db("test_selectdb_0") - @client.query("SHOW TABLES").first.values.first.should eql("test0") + expect(@client.query("SHOW TABLES").first.values.first).to eql("test0") end it "should raise a Mysql2::Error when the database doesn't exist" do - lambda { + expect do @client.select_db("nopenothere") - }.should raise_error(Mysql2::Error) + end.to raise_error(Mysql2::Error) end it "should return the database switched to" do - @client.select_db("test_selectdb_1").should eq("test_selectdb_1") + expect(@client.select_db("test_selectdb_1")).to eq("test_selectdb_1") + end + end + + context 'database' do + before(:example) do + 2.times do |i| + @client.query("CREATE DATABASE test_db#{i}") + end + end + + after(:example) do + 2.times do |i| + @client.query("DROP DATABASE test_db#{i}") + end + end + + it "should be `nil` when no database is selected" do + client = new_client(database: nil) + expect(client.database).to eq(nil) + end + + it "should reflect the initially connected database" do + client = new_client(database: 'test_db0') + expect(client.database).to eq('test_db0') + end + + context "when session tracking is on" do + it "should change to reflect currently selected database" do + client = new_client(database: 'test_db0') + client.query('SET session_track_schema=on') + expect { client.query('USE test_db1') }.to change { + client.database + }.from('test_db0').to('test_db1') + end + end + + context "when session tracking is off" do + it "does not change when a new database is selected" do + client = new_client(database: 'test_db0') + client.query('SET session_track_schema=off') + expect(client.database).to eq('test_db0') + expect { client.query('USE test_db1') }.not_to(change { client.database }) + end end end it "#thread_id should return a boolean" do - @client.ping.should eql(true) + expect(@client.ping).to eql(true) @client.close - @client.ping.should eql(false) + expect(@client.ping).to eql(false) end - unless RUBY_VERSION =~ /1.8/ - it "should respond to #encoding" do - @client.should respond_to(:encoding) + it "should be able to connect using plaintext password" do + client = new_client(enable_cleartext_plugin: true) + client.query('SELECT 1') + end + + it "should respond to #encoding" do + expect(@client).to respond_to(:encoding) + end + + it "should not include the password in the output of #inspect" do + client_class = Class.new(Mysql2::Client) do + def connect(*args); end end + + client = client_class.new(password: "secretsecret") + + expect(client.inspect).not_to include("password") + expect(client.inspect).not_to include("secretsecret") + + expect do + client = client_class.new(pass: "secretsecret") + end.to output(/WARNING/).to_stderr + + expect(client.inspect).not_to include("pass") + expect(client.inspect).not_to include("secretsecret") end end diff --git a/spec/mysql2/error_spec.rb b/spec/mysql2/error_spec.rb index 3e0fcab2e..efa3437f8 100644 --- a/spec/mysql2/error_spec.rb +++ b/spec/mysql2/error_spec.rb @@ -1,68 +1,89 @@ -# encoding: UTF-8 require 'spec_helper' -describe Mysql2::Error do - before(:each) do - @client = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => "utf8")) +RSpec.describe Mysql2::Error do + let(:error) do begin @client.query("HAHAHA") rescue Mysql2::Error => e - @error = e + error = e end - @client2 = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => "big5")) - begin - @client2.query("HAHAHA") - rescue Mysql2::Error => e - @error2 = e - end - end - - it "should respond to #error_number" do - @error.should respond_to(:error_number) + error end - it "should respond to #sql_state" do - @error.should respond_to(:sql_state) - end + it "responds to error_number and sql_state, with aliases" do + expect(error).to respond_to(:error_number) + expect(error).to respond_to(:sql_state) - # Mysql gem compatibility - it "should alias #error_number to #errno" do - @error.should respond_to(:errno) + # Mysql gem compatibility + expect(error).to respond_to(:errno) + expect(error).to respond_to(:error) end - it "should alias #message to #error" do - @error.should respond_to(:error) - end + context 'encoding' do + let(:valid_utf8) { '造字' } + let(:error) do + begin + @client.query(valid_utf8) + rescue Mysql2::Error => e + e + end + end - unless RUBY_VERSION =~ /1.8/ - it "#message encoding should match the connection's encoding, or Encoding.default_internal if set" do - if Encoding.default_internal.nil? - @error.message.encoding.should eql(@client.encoding) - @error2.message.encoding.should eql(@client2.encoding) - else - @error.message.encoding.should eql(Encoding.default_internal) - @error2.message.encoding.should eql(Encoding.default_internal) + let(:invalid_utf8) { ["e5c67d1f"].pack('H*').force_encoding(Encoding::UTF_8) } + let(:bad_err) do + begin + @client.query(invalid_utf8) + rescue Mysql2::Error => e + e end end - it "#error encoding should match the connection's encoding, or Encoding.default_internal if set" do - if Encoding.default_internal.nil? - @error.error.encoding.should eql(@client.encoding) - @error2.error.encoding.should eql(@client2.encoding) - else - @error.error.encoding.should eql(Encoding.default_internal) - @error2.error.encoding.should eql(Encoding.default_internal) + let(:server_info) do + @client.server_info + end + + before do + # sanity check + expect(valid_utf8.encoding).to eql(Encoding::UTF_8) + expect(valid_utf8).to be_valid_encoding + + expect(invalid_utf8.encoding).to eql(Encoding::UTF_8) + expect(invalid_utf8).to_not be_valid_encoding + end + + it "returns error messages as UTF-8 by default" do + with_internal_encoding nil do + expect(error.message.encoding).to eql(Encoding::UTF_8) + expect(error.message).to be_valid_encoding + + expect(bad_err.message.encoding).to eql(Encoding::UTF_8) + expect(bad_err.message).to be_valid_encoding + + # MariaDB 10.5 returns a little different error message unlike MySQL + # and other old MariaDBs. + # https://jira.mariadb.org/browse/MDEV-25400 + err_str = if server_info[:version].match(/MariaDB/) && server_info[:id] >= 100500 + "??}\\001F" + else + "??}\u001F" + end + expect(bad_err.message).to include(err_str) end end - it "#sql_state encoding should match the connection's encoding, or Encoding.default_internal if set" do - if Encoding.default_internal.nil? - @error.sql_state.encoding.should eql(@client.encoding) - @error2.sql_state.encoding.should eql(@client2.encoding) - else - @error.sql_state.encoding.should eql(Encoding.default_internal) - @error2.sql_state.encoding.should eql(Encoding.default_internal) + it "returns sql state as ASCII" do + expect(error.sql_state.encoding).to eql(Encoding::US_ASCII) + expect(error.sql_state).to be_valid_encoding + end + + it "returns error messages and sql state in Encoding.default_internal if set" do + with_internal_encoding Encoding::UTF_16LE do + expect(error.message.encoding).to eql(Encoding.default_internal) + expect(error.message).to be_valid_encoding + + expect(bad_err.message.encoding).to eql(Encoding.default_internal) + expect(bad_err.message).to be_valid_encoding end end end diff --git a/spec/mysql2/result_spec.rb b/spec/mysql2/result_spec.rb index c0e50acf5..ac42f2dea 100644 --- a/spec/mysql2/result_spec.rb +++ b/spec/mysql2/result_spec.rb @@ -1,72 +1,47 @@ -# encoding: UTF-8 require 'spec_helper' -describe Mysql2::Result do - before(:each) do - @client = Mysql2::Client.new DatabaseCredentials['root'] - end - - before(:each) do +RSpec.describe Mysql2::Result do + before(:example) do @result = @client.query "SELECT 1" end - it "should maintain a count while streaming" do - result = @client.query('SELECT 1') - - result.count.should eql(1) - result.each { |r| } - result.count.should eql(1) - end - - it "should set the actual count of rows after streaming" do - @client.query "USE test" - result = @client.query("SELECT * FROM mysql2_test", :stream => true, :cache_rows => false) - result.count.should eql(0) - result.each {|r| } - result.count.should eql(1) - end - - it "should not yield nil at the end of streaming" do - result = @client.query('SELECT * FROM mysql2_test', :stream => true) - result.each { |r| r.should_not be_nil} - end - - it "#count should be zero for rows after streaming when there were no results " do - @client.query "USE test" - result = @client.query("SELECT * FROM mysql2_test WHERE null_test IS NOT NULL", :stream => true, :cache_rows => false) - result.count.should eql(0) - result.each {|r| } - result.count.should eql(0) + it "should raise a TypeError exception when it doesn't wrap a result set" do + expect { Mysql2::Result.new }.to raise_error(TypeError) + expect { Mysql2::Result.allocate }.to raise_error(TypeError) end it "should have included Enumerable" do - Mysql2::Result.ancestors.include?(Enumerable).should be_true + expect(Mysql2::Result.ancestors.include?(Enumerable)).to be true end it "should respond to #each" do - @result.should respond_to(:each) + expect(@result).to respond_to(:each) + end + + it "should respond to #free" do + expect(@result).to respond_to(:free) end it "should raise a Mysql2::Error exception upon a bad query" do - lambda { + expect do @client.query "bad sql" - }.should raise_error(Mysql2::Error) + end.to raise_error(Mysql2::Error) - lambda { + expect do @client.query "SELECT 1" - }.should_not raise_error(Mysql2::Error) + end.not_to raise_error end it "should respond to #count, which is aliased as #size" do r = @client.query "SELECT 1" - r.should respond_to :count - r.should respond_to :size + expect(r).to respond_to :count + expect(r).to respond_to :size end it "should be able to return the number of rows in the result set" do r = @client.query "SELECT 1" - r.count.should eql(1) - r.size.should eql(1) + expect(r.count).to eql(1) + expect(r.size).to eql(1) end context "metadata queries" do @@ -78,363 +53,526 @@ context "#each" do it "should yield rows as hash's" do @result.each do |row| - row.class.should eql(Hash) + expect(row).to be_an_instance_of(Hash) end end it "should yield rows as hash's with symbol keys if :symbolize_keys was set to true" do - @result.each(:symbolize_keys => true) do |row| - row.keys.first.class.should eql(Symbol) + @result.each(symbolize_keys: true) do |row| + expect(row.keys.first).to be_an_instance_of(Symbol) end end it "should be able to return results as an array" do - @result.each(:as => :array) do |row| - row.class.should eql(Array) + @result.each(as: :array) do |row| + expect(row).to be_an_instance_of(Array) end end it "should cache previously yielded results by default" do - @result.first.object_id.should eql(@result.first.object_id) + expect(@result.first.object_id).to eql(@result.first.object_id) end it "should not cache previously yielded results if cache_rows is disabled" do - result = @client.query "SELECT 1", :cache_rows => false - result.first.object_id.should_not eql(result.first.object_id) + result = @client.query "SELECT 1", cache_rows: false + expect(result.first.object_id).not_to eql(result.first.object_id) + end + + it "should be able to iterate a second time even if cache_rows is disabled" do + result = @client.query "SELECT 1 UNION SELECT 2", cache_rows: false + expect(result.to_a).to eql(result.to_a) end it "should yield different value for #first if streaming" do - result = @client.query "SELECT 1 UNION SELECT 2", :stream => true, :cache_rows => false - result.first.should_not eql(result.first) + result = @client.query "SELECT 1 UNION SELECT 2", stream: true, cache_rows: false + expect(result.first).not_to eql(result.first) end it "should yield the same value for #first if streaming is disabled" do - result = @client.query "SELECT 1 UNION SELECT 2", :stream => false - result.first.should eql(result.first) + result = @client.query "SELECT 1 UNION SELECT 2", stream: false + expect(result.first).to eql(result.first) end it "should throw an exception if we try to iterate twice when streaming is enabled" do - result = @client.query "SELECT 1 UNION SELECT 2", :stream => true, :cache_rows => false + result = @client.query "SELECT 1 UNION SELECT 2", stream: true, cache_rows: false - expect { - result.each {} - result.each {} - }.to raise_exception(Mysql2::Error) + expect do + result.each.to_a + result.each.to_a + end.to raise_exception(Mysql2::Error) end end context "#fields" do - before(:each) do - @client.query "USE test" - @test_result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1") - end + let(:test_result) { @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1") } it "method should exist" do - @test_result.should respond_to(:fields) + expect(test_result).to respond_to(:fields) end it "should return an array of field names in proper order" do result = @client.query "SELECT 'a', 'b', 'c'" - result.fields.should eql(['a', 'b', 'c']) + expect(result.fields).to eql(%w[a b c]) + end + + it "should return an array of frozen strings" do + result = @client.query "SELECT 'a', 'b', 'c'" + result.fields.each do |f| + expect(f).to be_frozen + end end end - context "row data type mapping" do - before(:each) do + context "#field_types" do + let(:test_result) { @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1") } + + it "method should exist" do + expect(test_result).to respond_to(:field_types) + end + + it "should return correct types" do + expected_types = %w[ + mediumint(9) + varchar(13) + bit(64) + bit(1) + tinyint(4) + tinyint(1) + smallint(6) + mediumint(9) + int(11) + bigint(20) + float(10,3) + float(10,3) + double(10,3) + decimal(10,3) + decimal(10,3) + date + datetime + timestamp + time + year(4) + char(13) + varchar(13) + binary(10) + varbinary(10) + tinyblob + text(1020) + blob + text(262140) + mediumblob + text(67108860) + longblob + longtext + enum + set + ] + + expect(test_result.field_types).to eql(expected_types) + end + + it "should return an array of field types in proper order" do + result = @client.query( + "SELECT cast('a' as char), " \ + "cast(1.2 as decimal(15, 2)), " \ + "cast(1.2 as decimal(15, 5)), " \ + "cast(1.2 as decimal(15, 4)), " \ + "cast(1.2 as decimal(15, 10)), " \ + "cast(1.2 as decimal(14, 0)), " \ + "cast(1.2 as decimal(15, 0)), " \ + "cast(1.2 as decimal(16, 0)), " \ + "cast(1.0 as decimal(16, 1))", + ) + + expected_types = %w[ + varchar(1) + decimal(15,2) + decimal(15,5) + decimal(15,4) + decimal(15,10) + decimal(14,0) + decimal(15,0) + decimal(16,0) + decimal(16,1) + ] + + expect(result.field_types).to eql(expected_types) + end + + it "should return json type on mysql 8.0" do + next unless /8.\d+.\d+/ =~ @client.server_info[:version] + + result = @client.query("SELECT JSON_OBJECT('key', 'value')") + expect(result.field_types).to eql(['json']) + end + end + + context "streaming" do + it "should maintain a count while streaming" do + result = @client.query('SELECT 1', stream: true, cache_rows: false) + expect(result.count).to eql(0) + result.each.to_a + expect(result.count).to eql(1) + end + + it "should retain the count when mixing first and each" do + result = @client.query("SELECT 1 UNION SELECT 2", stream: true, cache_rows: false) + expect(result.count).to eql(0) + result.first + expect(result.count).to eql(1) + result.each.to_a + expect(result.count).to eql(2) + end + + it "should not yield nil at the end of streaming" do + result = @client.query('SELECT * FROM mysql2_test', stream: true, cache_rows: false) + result.each { |r| expect(r).not_to be_nil } + end + + it "#count should be zero for rows after streaming when there were no results" do @client.query "USE test" - @test_result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first + result = @client.query("SELECT * FROM mysql2_test WHERE null_test IS NOT NULL", stream: true, cache_rows: false) + expect(result.count).to eql(0) + result.each.to_a + expect(result.count).to eql(0) + end + + it "should raise an exception if streaming ended due to a timeout" do + @client.query "CREATE TEMPORARY TABLE streamingTest (val BINARY(255)) ENGINE=MEMORY" + + # Insert enough records to force the result set into multiple reads + # (the BINARY type is used simply because it forces full width results) + 10000.times do |i| + @client.query "INSERT INTO streamingTest (val) VALUES ('Foo #{i}')" + end + + @client.query "SET net_write_timeout = 1" + res = @client.query "SELECT * FROM streamingTest", stream: true, cache_rows: false + + expect do + res.each_with_index do |_, i| + # Exhaust the first result packet then trigger a timeout + sleep 4 if i > 0 && i % 1000 == 0 + end + end.to raise_error(Mysql2::Error, /Lost connection/) end + end + + context "row data type mapping" do + let(:test_result) { @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first } it "should return nil values for NULL and strings for everything else when :cast is false" do - result = @client.query('SELECT null_test, tiny_int_test, bool_cast_test, int_test, date_test, enum_test FROM mysql2_test WHERE bool_cast_test = 1 LIMIT 1', :cast => false).first - result["null_test"].should be_nil - result["tiny_int_test"].should == "1" - result["bool_cast_test"].should == "1" - result["int_test"].should == "10" - result["date_test"].should == "2010-04-04" - result["enum_test"].should == "val1" + result = @client.query('SELECT null_test, tiny_int_test, bool_cast_test, int_test, date_test, enum_test FROM mysql2_test WHERE bool_cast_test = 1 LIMIT 1', cast: false).first + expect(result["null_test"]).to be_nil + expect(result["tiny_int_test"]).to eql("1") + expect(result["bool_cast_test"]).to eql("1") + expect(result["int_test"]).to eql("10") + expect(result["date_test"]).to eql("2010-04-04") + expect(result["enum_test"]).to eql("val1") end it "should return nil for a NULL value" do - @test_result['null_test'].class.should eql(NilClass) - @test_result['null_test'].should eql(nil) + expect(test_result['null_test']).to be_an_instance_of(NilClass) + expect(test_result['null_test']).to eql(nil) + end + + it "should return String for a BIT(64) value" do + expect(test_result['bit_test']).to be_an_instance_of(String) + expect(test_result['bit_test']).to eql("\000\000\000\000\000\000\000\005") end - it "should return Fixnum for a BIT value" do - @test_result['bit_test'].class.should eql(String) - @test_result['bit_test'].should eql("\000\000\000\000\000\000\000\005") + it "should return String for a BIT(1) value" do + expect(test_result['single_bit_test']).to be_an_instance_of(String) + expect(test_result['single_bit_test']).to eql("\001") end it "should return Fixnum for a TINYINT value" do - [Fixnum, Bignum].should include(@test_result['tiny_int_test'].class) - @test_result['tiny_int_test'].should eql(1) + expect(num_classes).to include(test_result['tiny_int_test'].class) + expect(test_result['tiny_int_test']).to eql(1) end - it "should return TrueClass or FalseClass for a TINYINT value if :cast_booleans is enabled" do - @client.query 'INSERT INTO mysql2_test (bool_cast_test) VALUES (1)' - id1 = @client.last_id - @client.query 'INSERT INTO mysql2_test (bool_cast_test) VALUES (0)' - id2 = @client.last_id - @client.query 'INSERT INTO mysql2_test (bool_cast_test) VALUES (-1)' - id3 = @client.last_id + context "cast booleans for TINYINT if :cast_booleans is enabled" do + # rubocop:disable Style/Semicolon + let(:id1) { @client.query 'INSERT INTO mysql2_test (bool_cast_test) VALUES ( 1)'; @client.last_id } + let(:id2) { @client.query 'INSERT INTO mysql2_test (bool_cast_test) VALUES ( 0)'; @client.last_id } + let(:id3) { @client.query 'INSERT INTO mysql2_test (bool_cast_test) VALUES (-1)'; @client.last_id } + # rubocop:enable Style/Semicolon - result1 = @client.query 'SELECT bool_cast_test FROM mysql2_test WHERE bool_cast_test = 1 LIMIT 1', :cast_booleans => true - result2 = @client.query 'SELECT bool_cast_test FROM mysql2_test WHERE bool_cast_test = 0 LIMIT 1', :cast_booleans => true - result3 = @client.query 'SELECT bool_cast_test FROM mysql2_test WHERE bool_cast_test = -1 LIMIT 1', :cast_booleans => true - result1.first['bool_cast_test'].should be_true - result2.first['bool_cast_test'].should be_false - result3.first['bool_cast_test'].should be_true + after do + @client.query "DELETE from mysql2_test WHERE id IN(#{id1},#{id2},#{id3})" + end - @client.query "DELETE from mysql2_test WHERE id IN(#{id1},#{id2},#{id3})" + it "should return TrueClass or FalseClass for a TINYINT value if :cast_booleans is enabled" do + result1 = @client.query "SELECT bool_cast_test FROM mysql2_test WHERE id = #{id1} LIMIT 1", cast_booleans: true + result2 = @client.query "SELECT bool_cast_test FROM mysql2_test WHERE id = #{id2} LIMIT 1", cast_booleans: true + result3 = @client.query "SELECT bool_cast_test FROM mysql2_test WHERE id = #{id3} LIMIT 1", cast_booleans: true + expect(result1.first['bool_cast_test']).to be true + expect(result2.first['bool_cast_test']).to be false + expect(result3.first['bool_cast_test']).to be true + end + end + + context "cast booleans for BIT(1) if :cast_booleans is enabled" do + # rubocop:disable Style/Semicolon + let(:id1) { @client.query 'INSERT INTO mysql2_test (single_bit_test) VALUES (1)'; @client.last_id } + let(:id2) { @client.query 'INSERT INTO mysql2_test (single_bit_test) VALUES (0)'; @client.last_id } + # rubocop:enable Style/Semicolon + + after do + @client.query "DELETE from mysql2_test WHERE id IN(#{id1},#{id2})" + end + + it "should return TrueClass or FalseClass for a BIT(1) value if :cast_booleans is enabled" do + result1 = @client.query "SELECT single_bit_test FROM mysql2_test WHERE id = #{id1}", cast_booleans: true + result2 = @client.query "SELECT single_bit_test FROM mysql2_test WHERE id = #{id2}", cast_booleans: true + expect(result1.first['single_bit_test']).to be true + expect(result2.first['single_bit_test']).to be false + end end it "should return Fixnum for a SMALLINT value" do - [Fixnum, Bignum].should include(@test_result['small_int_test'].class) - @test_result['small_int_test'].should eql(10) + expect(num_classes).to include(test_result['small_int_test'].class) + expect(test_result['small_int_test']).to eql(10) end it "should return Fixnum for a MEDIUMINT value" do - [Fixnum, Bignum].should include(@test_result['medium_int_test'].class) - @test_result['medium_int_test'].should eql(10) + expect(num_classes).to include(test_result['medium_int_test'].class) + expect(test_result['medium_int_test']).to eql(10) end it "should return Fixnum for an INT value" do - [Fixnum, Bignum].should include(@test_result['int_test'].class) - @test_result['int_test'].should eql(10) + expect(num_classes).to include(test_result['int_test'].class) + expect(test_result['int_test']).to eql(10) end it "should return Fixnum for a BIGINT value" do - [Fixnum, Bignum].should include(@test_result['big_int_test'].class) - @test_result['big_int_test'].should eql(10) + expect(num_classes).to include(test_result['big_int_test'].class) + expect(test_result['big_int_test']).to eql(10) end it "should return Fixnum for a YEAR value" do - [Fixnum, Bignum].should include(@test_result['year_test'].class) - @test_result['year_test'].should eql(2009) + expect(num_classes).to include(test_result['year_test'].class) + expect(test_result['year_test']).to eql(2009) end it "should return BigDecimal for a DECIMAL value" do - @test_result['decimal_test'].class.should eql(BigDecimal) - @test_result['decimal_test'].should eql(10.3) + expect(test_result['decimal_test']).to be_an_instance_of(BigDecimal) + expect(test_result['decimal_test']).to eql(10.3) end it "should return Float for a FLOAT value" do - @test_result['float_test'].class.should eql(Float) - @test_result['float_test'].should eql(10.3) + expect(test_result['float_test']).to be_an_instance_of(Float) + expect(test_result['float_test']).to eql(10.3) end it "should return Float for a DOUBLE value" do - @test_result['double_test'].class.should eql(Float) - @test_result['double_test'].should eql(10.3) + expect(test_result['double_test']).to be_an_instance_of(Float) + expect(test_result['double_test']).to eql(10.3) end it "should return Time for a DATETIME value when within the supported range" do - @test_result['date_time_test'].class.should eql(Time) - @test_result['date_time_test'].strftime("%Y-%m-%d %H:%M:%S").should eql('2010-04-04 11:44:00') + expect(test_result['date_time_test']).to be_an_instance_of(Time) + expect(test_result['date_time_test'].strftime("%Y-%m-%d %H:%M:%S")).to eql('2010-04-04 11:44:00') end - if 1.size == 4 # 32bit - unless RUBY_VERSION =~ /1.8/ - klass = Time - else - klass = DateTime - end - - it "should return DateTime when timestamp is < 1901-12-13 20:45:52" do - # 1901-12-13T20:45:52 is the min for 32bit Ruby 1.8 - r = @client.query("SELECT CAST('1901-12-13 20:45:51' AS DATETIME) as test") - r.first['test'].class.should eql(klass) - end - - it "should return DateTime when timestamp is > 2038-01-19T03:14:07" do - # 2038-01-19T03:14:07 is the max for 32bit Ruby 1.8 - r = @client.query("SELECT CAST('2038-01-19 03:14:08' AS DATETIME) as test") - r.first['test'].class.should eql(klass) - end - elsif 1.size == 8 # 64bit - unless RUBY_VERSION =~ /1.8/ - it "should return Time when timestamp is < 1901-12-13 20:45:52" do - r = @client.query("SELECT CAST('1901-12-13 20:45:51' AS DATETIME) as test") - r.first['test'].class.should eql(Time) - end - - it "should return Time when timestamp is > 2038-01-19T03:14:07" do - r = @client.query("SELECT CAST('2038-01-19 03:14:08' AS DATETIME) as test") - r.first['test'].class.should eql(Time) - end - else - it "should return Time when timestamp is > 0138-12-31 11:59:59" do - r = @client.query("SELECT CAST('0139-1-1 00:00:00' AS DATETIME) as test") - r.first['test'].class.should eql(Time) - end - - it "should return DateTime when timestamp is < 0139-1-1T00:00:00" do - r = @client.query("SELECT CAST('0138-12-31 11:59:59' AS DATETIME) as test") - r.first['test'].class.should eql(DateTime) - end + it "should return Time when timestamp is < 1901-12-13 20:45:52" do + r = @client.query("SELECT CAST('1901-12-13 20:45:51' AS DATETIME) as test") + expect(r.first['test']).to be_an_instance_of(Time) + end - it "should return Time when timestamp is > 2038-01-19T03:14:07" do - r = @client.query("SELECT CAST('2038-01-19 03:14:08' AS DATETIME) as test") - r.first['test'].class.should eql(Time) - end - end + it "should return Time when timestamp is > 2038-01-19T03:14:07" do + r = @client.query("SELECT CAST('2038-01-19 03:14:08' AS DATETIME) as test") + expect(r.first['test']).to be_an_instance_of(Time) end it "should return Time for a TIMESTAMP value when within the supported range" do - @test_result['timestamp_test'].class.should eql(Time) - @test_result['timestamp_test'].strftime("%Y-%m-%d %H:%M:%S").should eql('2010-04-04 11:44:00') + expect(test_result['timestamp_test']).to be_an_instance_of(Time) + expect(test_result['timestamp_test'].strftime("%Y-%m-%d %H:%M:%S")).to eql('2010-04-04 11:44:00') end it "should return Time for a TIME value" do - @test_result['time_test'].class.should eql(Time) - @test_result['time_test'].strftime("%Y-%m-%d %H:%M:%S").should eql('2000-01-01 11:44:00') + expect(test_result['time_test']).to be_an_instance_of(Time) + expect(test_result['time_test'].strftime("%Y-%m-%d %H:%M:%S")).to eql('2000-01-01 11:44:00') end it "should return Date for a DATE value" do - @test_result['date_test'].class.should eql(Date) - @test_result['date_test'].strftime("%Y-%m-%d").should eql('2010-04-04') + expect(test_result['date_test']).to be_an_instance_of(Date) + expect(test_result['date_test'].strftime("%Y-%m-%d")).to eql('2010-04-04') end it "should return String for an ENUM value" do - @test_result['enum_test'].class.should eql(String) - @test_result['enum_test'].should eql('val1') + expect(test_result['enum_test']).to be_an_instance_of(String) + expect(test_result['enum_test']).to eql('val1') end - if defined? Encoding - context "string encoding for ENUM values" do - it "should default to the connection's encoding if Encoding.default_internal is nil" do - Encoding.default_internal = nil + it "should raise an error given an invalid DATETIME" do + if @client.info[:version] < "8.0" + expect { @client.query("SELECT CAST('1972-00-27 00:00:00' AS DATETIME) as bad_datetime").each }.to \ + raise_error(Mysql2::Error, "Invalid date in field 'bad_datetime': 1972-00-27 00:00:00") + else + expect(@client.query("SELECT CAST('1972-00-27 00:00:00' AS DATETIME) as bad_datetime").to_a.first).to \ + eql("bad_datetime" => nil) + end + end + + context "string encoding for ENUM values" do + it "should default to the connection's encoding if Encoding.default_internal is nil" do + with_internal_encoding nil do result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first - result['enum_test'].encoding.should eql(Encoding.find('utf-8')) + expect(result['enum_test'].encoding).to eql(Encoding::UTF_8) - client2 = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => 'ascii')) - client2.query "USE test" + client2 = new_client(encoding: 'ascii') result = client2.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first - result['enum_test'].encoding.should eql(Encoding.find('us-ascii')) + expect(result['enum_test'].encoding).to eql(Encoding::ASCII) end + end - it "should use Encoding.default_internal" do - Encoding.default_internal = Encoding.find('utf-8') + it "should use Encoding.default_internal" do + with_internal_encoding Encoding::UTF_8 do result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first - result['enum_test'].encoding.should eql(Encoding.default_internal) - Encoding.default_internal = Encoding.find('us-ascii') + expect(result['enum_test'].encoding).to eql(Encoding.default_internal) + end + + with_internal_encoding Encoding::ASCII do result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first - result['enum_test'].encoding.should eql(Encoding.default_internal) + expect(result['enum_test'].encoding).to eql(Encoding.default_internal) end end end it "should return String for a SET value" do - @test_result['set_test'].class.should eql(String) - @test_result['set_test'].should eql('val1,val2') + expect(test_result['set_test']).to be_an_instance_of(String) + expect(test_result['set_test']).to eql('val1,val2') end - if defined? Encoding - context "string encoding for SET values" do - it "should default to the connection's encoding if Encoding.default_internal is nil" do - Encoding.default_internal = nil + context "string encoding for SET values" do + it "should default to the connection's encoding if Encoding.default_internal is nil" do + with_internal_encoding nil do result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first - result['set_test'].encoding.should eql(Encoding.find('utf-8')) + expect(result['set_test'].encoding).to eql(Encoding::UTF_8) - client2 = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => 'ascii')) - client2.query "USE test" + client2 = new_client(encoding: 'ascii') result = client2.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first - result['set_test'].encoding.should eql(Encoding.find('us-ascii')) + expect(result['set_test'].encoding).to eql(Encoding::ASCII) end + end - it "should use Encoding.default_internal" do - Encoding.default_internal = Encoding.find('utf-8') + it "should use Encoding.default_internal" do + with_internal_encoding Encoding::UTF_8 do result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first - result['set_test'].encoding.should eql(Encoding.default_internal) - Encoding.default_internal = Encoding.find('us-ascii') + expect(result['set_test'].encoding).to eql(Encoding.default_internal) + end + + with_internal_encoding Encoding::ASCII do result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first - result['set_test'].encoding.should eql(Encoding.default_internal) + expect(result['set_test'].encoding).to eql(Encoding.default_internal) end end end it "should return String for a BINARY value" do - @test_result['binary_test'].class.should eql(String) - @test_result['binary_test'].should eql("test#{"\000"*6}") + expect(test_result['binary_test']).to be_an_instance_of(String) + expect(test_result['binary_test']).to eql("test#{"\000" * 6}") end - if defined? Encoding - context "string encoding for BINARY values" do - it "should default to binary if Encoding.default_internal is nil" do - Encoding.default_internal = nil + context "string encoding for BINARY values" do + it "should default to binary if Encoding.default_internal is nil" do + with_internal_encoding nil do result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first - result['binary_test'].encoding.should eql(Encoding.find('binary')) + expect(result['binary_test'].encoding).to eql(Encoding::BINARY) end + end - it "should not use Encoding.default_internal" do - Encoding.default_internal = Encoding.find('utf-8') + it "should not use Encoding.default_internal" do + with_internal_encoding Encoding::UTF_8 do result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first - result['binary_test'].encoding.should eql(Encoding.find('binary')) - Encoding.default_internal = Encoding.find('us-ascii') + expect(result['binary_test'].encoding).to eql(Encoding::BINARY) + end + + with_internal_encoding Encoding::ASCII do result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first - result['binary_test'].encoding.should eql(Encoding.find('binary')) + expect(result['binary_test'].encoding).to eql(Encoding::BINARY) end end end { - 'char_test' => 'CHAR', - 'varchar_test' => 'VARCHAR', - 'varbinary_test' => 'VARBINARY', - 'tiny_blob_test' => 'TINYBLOB', - 'tiny_text_test' => 'TINYTEXT', - 'blob_test' => 'BLOB', - 'text_test' => 'TEXT', + 'char_test' => 'CHAR', + 'varchar_test' => 'VARCHAR', + 'varbinary_test' => 'VARBINARY', + 'tiny_blob_test' => 'TINYBLOB', + 'tiny_text_test' => 'TINYTEXT', + 'blob_test' => 'BLOB', + 'text_test' => 'TEXT', 'medium_blob_test' => 'MEDIUMBLOB', 'medium_text_test' => 'MEDIUMTEXT', - 'long_blob_test' => 'LONGBLOB', - 'long_text_test' => 'LONGTEXT' + 'long_blob_test' => 'LONGBLOB', + 'long_text_test' => 'LONGTEXT', }.each do |field, type| it "should return a String for #{type}" do - @test_result[field].class.should eql(String) - @test_result[field].should eql("test") + expect(test_result[field]).to be_an_instance_of(String) + expect(test_result[field]).to eql("test") end - if defined? Encoding - context "string encoding for #{type} values" do - if ['VARBINARY', 'TINYBLOB', 'BLOB', 'MEDIUMBLOB', 'LONGBLOB'].include?(type) - it "should default to binary if Encoding.default_internal is nil" do - Encoding.default_internal = nil + context "string encoding for #{type} values" do + if %w[VARBINARY TINYBLOB BLOB MEDIUMBLOB LONGBLOB].include?(type) + it "should default to binary if Encoding.default_internal is nil" do + with_internal_encoding nil do result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first - result['binary_test'].encoding.should eql(Encoding.find('binary')) + expect(result['binary_test'].encoding).to eql(Encoding::BINARY) end + end - it "should not use Encoding.default_internal" do - Encoding.default_internal = Encoding.find('utf-8') + it "should not use Encoding.default_internal" do + with_internal_encoding Encoding::UTF_8 do result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first - result['binary_test'].encoding.should eql(Encoding.find('binary')) - Encoding.default_internal = Encoding.find('us-ascii') + expect(result['binary_test'].encoding).to eql(Encoding::BINARY) + end + + with_internal_encoding Encoding::ASCII do result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first - result['binary_test'].encoding.should eql(Encoding.find('binary')) + expect(result['binary_test'].encoding).to eql(Encoding::BINARY) end - else - it "should default to utf-8 if Encoding.default_internal is nil" do - Encoding.default_internal = nil + end + else + it "should default to utf-8 if Encoding.default_internal is nil" do + with_internal_encoding nil do result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first - result[field].encoding.should eql(Encoding.find('utf-8')) + expect(result[field].encoding).to eql(Encoding::UTF_8) - client2 = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => 'ascii')) - client2.query "USE test" + client2 = new_client(encoding: 'ascii') result = client2.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first - result[field].encoding.should eql(Encoding.find('us-ascii')) + expect(result[field].encoding).to eql(Encoding::ASCII) end + end - it "should use Encoding.default_internal" do - Encoding.default_internal = Encoding.find('utf-8') + it "should use Encoding.default_internal" do + with_internal_encoding Encoding::UTF_8 do result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first - result[field].encoding.should eql(Encoding.default_internal) - Encoding.default_internal = Encoding.find('us-ascii') + expect(result[field].encoding).to eql(Encoding.default_internal) + end + + with_internal_encoding Encoding::ASCII do result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first - result[field].encoding.should eql(Encoding.default_internal) + expect(result[field].encoding).to eql(Encoding.default_internal) end end end end end end + + context "server flags" do + let(:test_result) { @client.query("SELECT * FROM mysql2_test ORDER BY null_test DESC LIMIT 1") } + + it "should set a definitive value for query_was_slow" do + expect(test_result.server_flags[:query_was_slow]).to eql(false) + end + it "should set a definitive value for no_index_used" do + expect(test_result.server_flags[:no_index_used]).to eql(true) + end + it "should set a definitive value for no_good_index_used" do + expect(test_result.server_flags[:no_good_index_used]).to eql(false) + end + end end diff --git a/spec/mysql2/statement_spec.rb b/spec/mysql2/statement_spec.rb new file mode 100644 index 000000000..8c4d97e68 --- /dev/null +++ b/spec/mysql2/statement_spec.rb @@ -0,0 +1,734 @@ +require './spec/spec_helper' + +RSpec.describe Mysql2::Statement do # rubocop:disable Metrics/BlockLength + before(:example) do + @client = new_client(encoding: "utf8") + end + + let(:performance_schema_enabled) do + performance_schema = @client.query "SHOW VARIABLES LIKE 'performance_schema'" + performance_schema.any? { |x| x['Value'] == 'ON' } + end + + def stmt_count + # Use the performance schema in MySQL 5.7 and above + if performance_schema_enabled + @client.query("SELECT COUNT(1) AS count FROM performance_schema.prepared_statements_instances").first['count'].to_i + else + # Fall back to the global prepapred statement counter + @client.query("SHOW STATUS LIKE 'Prepared_stmt_count'").first['Value'].to_i + end + end + + it "should create a statement" do + statement = nil + expect { statement = @client.prepare 'SELECT 1' }.to change(&method(:stmt_count)).by(1) + expect(statement).to be_an_instance_of(Mysql2::Statement) + end + + it "should raise an exception when server disconnects" do + @client.close + expect { @client.prepare 'SELECT 1' }.to raise_error(Mysql2::Error) + end + + it "should tell us the param count" do + statement = @client.prepare 'SELECT ?, ?' + expect(statement.param_count).to eq(2) + + statement2 = @client.prepare 'SELECT 1' + expect(statement2.param_count).to eq(0) + end + + it "should tell us the field count" do + statement = @client.prepare 'SELECT ?, ?' + expect(statement.field_count).to eq(2) + + statement2 = @client.prepare 'SELECT 1' + expect(statement2.field_count).to eq(1) + end + + it "should let us execute our statement" do + statement = @client.prepare 'SELECT 1' + expect(statement.execute).not_to eq(nil) + end + + it "should raise an exception without a block" do + statement = @client.prepare 'SELECT 1' + expect { statement.execute.each }.to raise_error(LocalJumpError) + end + + it "should tell us the result count" do + statement = @client.prepare 'SELECT 1' + result = statement.execute + expect(result.count).to eq(1) + end + + it "should let us iterate over results" do + statement = @client.prepare 'SELECT 1' + result = statement.execute + rows = [] + result.each { |r| rows << r } + expect(rows).to eq([{ "1" => 1 }]) + end + + it "should handle booleans" do + stmt = @client.prepare('SELECT ? AS `true`, ? AS `false`') + result = stmt.execute(true, false) + expect(result.to_a).to eq(['true' => 1, 'false' => 0]) + end + + it "should handle bignum but in int64_t" do + stmt = @client.prepare('SELECT ? AS max, ? AS min') + int64_max = (1 << 63) - 1 + int64_min = -(1 << 63) + result = stmt.execute(int64_max, int64_min) + expect(result.to_a).to eq(['max' => int64_max, 'min' => int64_min]) + end + + it "should handle bignum but beyond int64_t" do + stmt = @client.prepare('SELECT ? AS max1, ? AS max2, ? AS max3, ? AS min1, ? AS min2, ? AS min3') + int64_max1 = (1 << 63) + int64_max2 = (1 << 64) - 1 + int64_max3 = 1 << 64 + int64_min1 = -(1 << 63) - 1 + int64_min2 = -(1 << 64) + 1 + int64_min3 = -0xC000000000000000 + result = stmt.execute(int64_max1, int64_max2, int64_max3, int64_min1, int64_min2, int64_min3) + expect(result.to_a).to eq(['max1' => int64_max1, 'max2' => int64_max2, 'max3' => int64_max3, 'min1' => int64_min1, 'min2' => int64_min2, 'min3' => int64_min3]) + end + + it "should accept keyword arguments on statement execute" do + stmt = @client.prepare 'SELECT 1 AS a' + + expect(stmt.execute(as: :hash).first).to eq("a" => 1) + expect(stmt.execute(as: :array).first).to eq([1]) + end + + it "should accept bind arguments and keyword arguments on statement execute" do + stmt = @client.prepare 'SELECT ? AS a' + + expect(stmt.execute(1, as: :hash).first).to eq("a" => 1) + expect(stmt.execute(1, as: :array).first).to eq([1]) + end + + it "should keep its result after other query" do + @client.query 'USE test' + @client.query 'CREATE TABLE IF NOT EXISTS mysql2_stmt_q(a int)' + @client.query 'INSERT INTO mysql2_stmt_q (a) VALUES (1), (2)' + stmt = @client.prepare('SELECT a FROM mysql2_stmt_q WHERE a = ?') + result1 = stmt.execute(1) + result2 = stmt.execute(2) + expect(result2.first).to eq("a" => 2) + expect(result1.first).to eq("a" => 1) + @client.query 'DROP TABLE IF EXISTS mysql2_stmt_q' + end + + it "should be reusable 1000 times" do + statement = @client.prepare 'SELECT 1' + 1000.times do + result = statement.execute + expect(result.to_a.length).to eq(1) + end + end + + it "should be reusable 10000 times" do + statement = @client.prepare 'SELECT 1' + 10000.times do + result = statement.execute + expect(result.to_a.length).to eq(1) + end + end + + it "should handle comparisons and likes" do + @client.query 'USE test' + @client.query 'CREATE TABLE IF NOT EXISTS mysql2_stmt_q(a int, b varchar(10))' + @client.query 'INSERT INTO mysql2_stmt_q (a, b) VALUES (1, "Hello"), (2, "World")' + statement = @client.prepare 'SELECT * FROM mysql2_stmt_q WHERE a < ?' + results = statement.execute(2) + expect(results.first).to eq("a" => 1, "b" => "Hello") + + statement = @client.prepare 'SELECT * FROM mysql2_stmt_q WHERE b LIKE ?' + results = statement.execute('%orld') + expect(results.first).to eq("a" => 2, "b" => "World") + + @client.query 'DROP TABLE IF EXISTS mysql2_stmt_q' + end + + it "should select dates" do + statement = @client.prepare 'SELECT NOW()' + result = statement.execute + expect(result.first.first[1]).to be_an_instance_of(Time) + end + + it "should prepare Date values" do + now = Date.today + statement = @client.prepare('SELECT ? AS a') + result = statement.execute(now) + expect(result.first['a'].to_s).to eql(now.strftime('%F')) + end + + it "should prepare Time values with microseconds" do + now = Time.now + statement = @client.prepare('SELECT ? AS a') + result = statement.execute(now) + # microseconds is six digits after the decimal, but only test on 5 significant figures + expect(result.first['a'].strftime('%F %T.%5N %z')).to eql(now.strftime('%F %T.%5N %z')) + end + + it "should prepare DateTime values with microseconds" do + now = DateTime.now + statement = @client.prepare('SELECT ? AS a') + result = statement.execute(now) + # microseconds is six digits after the decimal, but only test on 5 significant figures + expect(result.first['a'].strftime('%F %T.%5N %z')).to eql(now.strftime('%F %T.%5N %z')) + end + + it "should tell us about the fields" do + statement = @client.prepare 'SELECT 1 as foo, 2' + statement.execute + list = statement.fields + expect(list.length).to eq(2) + expect(list.first).to eq('foo') + expect(list[1]).to eq('2') + end + + it "should handle as a decimal binding a BigDecimal" do + stmt = @client.prepare('SELECT ? AS decimal_test') + test_result = stmt.execute(BigDecimal("123.45")).first + expect(test_result['decimal_test']).to be_an_instance_of(BigDecimal) + expect(test_result['decimal_test']).to eql(123.45) + end + + it "should update a DECIMAL value passing a BigDecimal" do + @client.query 'USE test' + @client.query 'DROP TABLE IF EXISTS mysql2_stmt_decimal_test' + @client.query 'CREATE TABLE mysql2_stmt_decimal_test (decimal_test DECIMAL(10,3))' + + @client.prepare("INSERT INTO mysql2_stmt_decimal_test VALUES (?)").execute(BigDecimal("123.45")) + + test_result = @client.query("SELECT * FROM mysql2_stmt_decimal_test").first + expect(test_result['decimal_test']).to eql(123.45) + end + + it "should warn but still work if cache_rows is set to false" do + statement = @client.prepare 'SELECT 1' + result = nil + expect { result = statement.execute(cache_rows: false).to_a }.to output(/:cache_rows is forced for prepared statements/).to_stderr + expect(result.length).to eq(1) + end + + context "utf8_db" do + before(:example) do + @client.query("DROP DATABASE IF EXISTS test_mysql2_stmt_utf8") + @client.query("CREATE DATABASE test_mysql2_stmt_utf8") + @client.query("USE test_mysql2_stmt_utf8") + @client.query("CREATE TABLE テーブル (整数 int, 文字列 varchar(32)) charset=utf8") + @client.query("INSERT INTO テーブル (整数, 文字列) VALUES (1, 'イチ'), (2, '弐'), (3, 'さん')") + end + + after(:example) do + @client.query("DROP DATABASE test_mysql2_stmt_utf8") + end + + it "should be able to retrieve utf8 field names correctly" do + stmt = @client.prepare 'SELECT * FROM `テーブル`' + expect(stmt.fields).to eq(%w[整数 文字列]) + result = stmt.execute + + expect(result.to_a).to eq([{ "整数" => 1, "文字列" => "イチ" }, { "整数" => 2, "文字列" => "弐" }, { "整数" => 3, "文字列" => "さん" }]) + end + + it "should be able to retrieve utf8 param query correctly" do + stmt = @client.prepare 'SELECT 整数 FROM テーブル WHERE 文字列 = ?' + expect(stmt.param_count).to eq(1) + + result = stmt.execute 'イチ' + + expect(result.to_a).to eq([{ "整数" => 1 }]) + end + + it "should be able to retrieve query with param in different encoding correctly" do + stmt = @client.prepare 'SELECT 整数 FROM テーブル WHERE 文字列 = ?' + expect(stmt.param_count).to eq(1) + + param = 'イチ'.encode("EUC-JP") + result = stmt.execute param + + expect(result.to_a).to eq([{ "整数" => 1 }]) + end + end + + context "streaming result" do + it "should be able to stream query result" do + n = 1 + stmt = @client.prepare("SELECT 1 UNION SELECT 2") + stmt.execute(stream: true, cache_rows: false, as: :array).each do |r| + case n + when 1 + expect(r).to eq([1]) + when 2 + expect(r).to eq([2]) + else + violated "returned more than two rows" + end + n += 1 + end + end + end + + context "#each" do + # NOTE: The current impl. of prepared statement requires results to be cached on #execute except for streaming queries + # The drawback of this is that args of Result#each is ignored... + + it "should yield rows as hash's" do + @result = @client.prepare("SELECT 1").execute + @result.each do |row| + expect(row).to be_an_instance_of(Hash) + end + end + + it "should yield rows as hash's with symbol keys if :symbolize_keys was set to true" do + @result = @client.prepare("SELECT 1").execute(symbolize_keys: true) + @result.each do |row| + expect(row.keys.first).to be_an_instance_of(Symbol) + end + end + + it "should be able to return results as an array" do + @result = @client.prepare("SELECT 1").execute(as: :array) + @result.each do |row| + expect(row).to be_an_instance_of(Array) + end + end + + it "should cache previously yielded results by default" do + @result = @client.prepare("SELECT 1").execute + expect(@result.first.object_id).to eql(@result.first.object_id) + end + + it "should yield different value for #first if streaming" do + result = @client.prepare("SELECT 1 UNION SELECT 2").execute(stream: true, cache_rows: true) + expect(result.first).not_to eql(result.first) + end + + it "should yield the same value for #first if streaming is disabled" do + result = @client.prepare("SELECT 1 UNION SELECT 2").execute(stream: false) + expect(result.first).to eql(result.first) + end + + it "should throw an exception if we try to iterate twice when streaming is enabled" do + result = @client.prepare("SELECT 1 UNION SELECT 2").execute(stream: true, cache_rows: false) + expect do + result.to_a + result.to_a + end.to raise_exception(Mysql2::Error) + end + end + + context "#fields" do + it "method should exist" do + stmt = @client.prepare("SELECT 1") + expect(stmt).to respond_to(:fields) + end + + it "should return an array of field names in proper order" do + stmt = @client.prepare("SELECT 'a', 'b', 'c'") + expect(stmt.fields).to eql(%w[a b c]) + end + + it "should return nil for statement with no result fields" do + stmt = @client.prepare("INSERT INTO mysql2_test () VALUES ()") + expect(stmt.fields).to eql(nil) + end + end + + context "row data type mapping" do + let(:test_result) { @client.prepare("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").execute.first } + + it "should return nil for a NULL value" do + expect(test_result['null_test']).to be_an_instance_of(NilClass) + expect(test_result['null_test']).to eql(nil) + end + + it "should return String for a BIT(64) value" do + expect(test_result['bit_test']).to be_an_instance_of(String) + expect(test_result['bit_test']).to eql("\000\000\000\000\000\000\000\005") + end + + it "should return String for a BIT(1) value" do + expect(test_result['single_bit_test']).to be_an_instance_of(String) + expect(test_result['single_bit_test']).to eql("\001") + end + + it "should return Fixnum for a TINYINT value" do + expect(num_classes).to include(test_result['tiny_int_test'].class) + expect(test_result['tiny_int_test']).to eql(1) + end + + context "cast booleans for TINYINT if :cast_booleans is enabled" do + # rubocop:disable Style/Semicolon + let(:id1) { @client.query 'INSERT INTO mysql2_test (bool_cast_test) VALUES ( 1)'; @client.last_id } + let(:id2) { @client.query 'INSERT INTO mysql2_test (bool_cast_test) VALUES ( 0)'; @client.last_id } + let(:id3) { @client.query 'INSERT INTO mysql2_test (bool_cast_test) VALUES (-1)'; @client.last_id } + # rubocop:enable Style/Semicolon + + after do + @client.query "DELETE from mysql2_test WHERE id IN(#{id1},#{id2},#{id3})" + end + + it "should return TrueClass or FalseClass for a TINYINT value if :cast_booleans is enabled" do + query = @client.prepare 'SELECT bool_cast_test FROM mysql2_test WHERE id = ?' + result1 = query.execute id1, cast_booleans: true + result2 = query.execute id2, cast_booleans: true + result3 = query.execute id3, cast_booleans: true + expect(result1.first['bool_cast_test']).to be true + expect(result2.first['bool_cast_test']).to be false + expect(result3.first['bool_cast_test']).to be true + end + end + + context "cast booleans for BIT(1) if :cast_booleans is enabled" do + # rubocop:disable Style/Semicolon + let(:id1) { @client.query 'INSERT INTO mysql2_test (single_bit_test) VALUES (1)'; @client.last_id } + let(:id2) { @client.query 'INSERT INTO mysql2_test (single_bit_test) VALUES (0)'; @client.last_id } + # rubocop:enable Style/Semicolon + + after do + @client.query "DELETE from mysql2_test WHERE id IN(#{id1},#{id2})" + end + + it "should return TrueClass or FalseClass for a BIT(1) value if :cast_booleans is enabled" do + query = @client.prepare 'SELECT single_bit_test FROM mysql2_test WHERE id = ?' + result1 = query.execute id1, cast_booleans: true + result2 = query.execute id2, cast_booleans: true + expect(result1.first['single_bit_test']).to be true + expect(result2.first['single_bit_test']).to be false + end + end + + it "should return Fixnum for a SMALLINT value" do + expect(num_classes).to include(test_result['small_int_test'].class) + expect(test_result['small_int_test']).to eql(10) + end + + it "should return Fixnum for a MEDIUMINT value" do + expect(num_classes).to include(test_result['medium_int_test'].class) + expect(test_result['medium_int_test']).to eql(10) + end + + it "should return Fixnum for an INT value" do + expect(num_classes).to include(test_result['int_test'].class) + expect(test_result['int_test']).to eql(10) + end + + it "should return Fixnum for a BIGINT value" do + expect(num_classes).to include(test_result['big_int_test'].class) + expect(test_result['big_int_test']).to eql(10) + end + + it "should return Fixnum for a YEAR value" do + expect(num_classes).to include(test_result['year_test'].class) + expect(test_result['year_test']).to eql(2009) + end + + it "should return BigDecimal for a DECIMAL value" do + expect(test_result['decimal_test']).to be_an_instance_of(BigDecimal) + expect(test_result['decimal_test']).to eql(10.3) + end + + it "should return Float for a FLOAT value" do + expect(test_result['float_test']).to be_an_instance_of(Float) + expect(test_result['float_test']).to be_within(1e-5).of(10.3) + end + + it "should return Float for a DOUBLE value" do + expect(test_result['double_test']).to be_an_instance_of(Float) + expect(test_result['double_test']).to eql(10.3) + end + + it "should return Time for a DATETIME value when within the supported range" do + expect(test_result['date_time_test']).to be_an_instance_of(Time) + expect(test_result['date_time_test'].strftime("%Y-%m-%d %H:%M:%S")).to eql('2010-04-04 11:44:00') + end + + it "should return Time when timestamp is < 1901-12-13 20:45:52" do + r = @client.prepare("SELECT CAST('1901-12-13 20:45:51' AS DATETIME) as test").execute + expect(r.first['test']).to be_an_instance_of(Time) + end + + it "should return Time when timestamp is > 2038-01-19T03:14:07" do + r = @client.prepare("SELECT CAST('2038-01-19 03:14:08' AS DATETIME) as test").execute + expect(r.first['test']).to be_an_instance_of(Time) + end + + it "should return Time for a TIMESTAMP value when within the supported range" do + expect(test_result['timestamp_test']).to be_an_instance_of(Time) + expect(test_result['timestamp_test'].strftime("%Y-%m-%d %H:%M:%S")).to eql('2010-04-04 11:44:00') + end + + it "should return Time for a TIME value" do + expect(test_result['time_test']).to be_an_instance_of(Time) + expect(test_result['time_test'].strftime("%Y-%m-%d %H:%M:%S")).to eql('2000-01-01 11:44:00') + end + + it "should return Date for a DATE value" do + expect(test_result['date_test']).to be_an_instance_of(Date) + expect(test_result['date_test'].strftime("%Y-%m-%d")).to eql('2010-04-04') + end + + it "should return String for an ENUM value" do + expect(test_result['enum_test']).to be_an_instance_of(String) + expect(test_result['enum_test']).to eql('val1') + end + + it "should raise an error given an invalid DATETIME" do + if @client.info[:version] < "8.0" + expect { @client.query("SELECT CAST('1972-00-27 00:00:00' AS DATETIME) as bad_datetime").each }.to \ + raise_error(Mysql2::Error, "Invalid date in field 'bad_datetime': 1972-00-27 00:00:00") + else + expect(@client.query("SELECT CAST('1972-00-27 00:00:00' AS DATETIME) as bad_datetime").to_a.first).to \ + eql("bad_datetime" => nil) + end + end + + context "string encoding for ENUM values" do + it "should default to the connection's encoding if Encoding.default_internal is nil" do + with_internal_encoding nil do + result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first + expect(result['enum_test'].encoding).to eql(Encoding::UTF_8) + + client2 = new_client(encoding: 'ascii') + result = client2.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first + expect(result['enum_test'].encoding).to eql(Encoding::US_ASCII) + end + end + + it "should use Encoding.default_internal" do + with_internal_encoding Encoding::UTF_8 do + result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first + expect(result['enum_test'].encoding).to eql(Encoding.default_internal) + end + + with_internal_encoding Encoding::ASCII do + result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first + expect(result['enum_test'].encoding).to eql(Encoding.default_internal) + end + end + end + + it "should return String for a SET value" do + expect(test_result['set_test']).to be_an_instance_of(String) + expect(test_result['set_test']).to eql('val1,val2') + end + + context "string encoding for SET values" do + it "should default to the connection's encoding if Encoding.default_internal is nil" do + with_internal_encoding nil do + result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first + expect(result['set_test'].encoding).to eql(Encoding::UTF_8) + + client2 = new_client(encoding: 'ascii') + result = client2.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first + expect(result['set_test'].encoding).to eql(Encoding::US_ASCII) + end + end + + it "should use Encoding.default_internal" do + with_internal_encoding Encoding::UTF_8 do + result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first + expect(result['set_test'].encoding).to eql(Encoding.default_internal) + end + + with_internal_encoding Encoding::ASCII do + result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first + expect(result['set_test'].encoding).to eql(Encoding.default_internal) + end + end + end + + it "should return String for a BINARY value" do + expect(test_result['binary_test']).to be_an_instance_of(String) + expect(test_result['binary_test']).to eql("test#{"\000" * 6}") + end + + context "string encoding for BINARY values" do + it "should default to binary if Encoding.default_internal is nil" do + with_internal_encoding nil do + result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first + expect(result['binary_test'].encoding).to eql(Encoding::BINARY) + end + end + + it "should not use Encoding.default_internal" do + with_internal_encoding Encoding::UTF_8 do + result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first + expect(result['binary_test'].encoding).to eql(Encoding::BINARY) + end + + with_internal_encoding Encoding::ASCII do + result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first + expect(result['binary_test'].encoding).to eql(Encoding::BINARY) + end + end + end + + { + 'char_test' => 'CHAR', + 'varchar_test' => 'VARCHAR', + 'varbinary_test' => 'VARBINARY', + 'tiny_blob_test' => 'TINYBLOB', + 'tiny_text_test' => 'TINYTEXT', + 'blob_test' => 'BLOB', + 'text_test' => 'TEXT', + 'medium_blob_test' => 'MEDIUMBLOB', + 'medium_text_test' => 'MEDIUMTEXT', + 'long_blob_test' => 'LONGBLOB', + 'long_text_test' => 'LONGTEXT', + }.each do |field, type| + it "should return a String for #{type}" do + expect(test_result[field]).to be_an_instance_of(String) + expect(test_result[field]).to eql("test") + end + + context "string encoding for #{type} values" do + if %w[VARBINARY TINYBLOB BLOB MEDIUMBLOB LONGBLOB].include?(type) + it "should default to binary if Encoding.default_internal is nil" do + with_internal_encoding nil do + result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first + expect(result['binary_test'].encoding).to eql(Encoding::BINARY) + end + end + + it "should not use Encoding.default_internal" do + with_internal_encoding Encoding::UTF_8 do + result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first + expect(result['binary_test'].encoding).to eql(Encoding::BINARY) + end + + with_internal_encoding Encoding::ASCII do + result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first + expect(result['binary_test'].encoding).to eql(Encoding::BINARY) + end + end + else + it "should default to utf-8 if Encoding.default_internal is nil" do + with_internal_encoding nil do + result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first + expect(result[field].encoding).to eql(Encoding::UTF_8) + + client2 = new_client(encoding: 'ascii') + result = client2.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first + expect(result[field].encoding).to eql(Encoding::US_ASCII) + end + end + + it "should use Encoding.default_internal" do + with_internal_encoding Encoding::UTF_8 do + result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first + expect(result[field].encoding).to eql(Encoding.default_internal) + end + + with_internal_encoding Encoding::ASCII do + result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first + expect(result[field].encoding).to eql(Encoding.default_internal) + end + end + end + end + end + end + + context 'last_id' do + before(:example) do + @client.query 'USE test' + @client.query 'CREATE TABLE IF NOT EXISTS lastIdTest (`id` BIGINT NOT NULL AUTO_INCREMENT, blah INT(11), PRIMARY KEY (`id`))' + end + + after(:example) do + @client.query 'DROP TABLE lastIdTest' + end + + it 'should return last insert id' do + stmt = @client.prepare 'INSERT INTO lastIdTest (blah) VALUES (?)' + expect(stmt.last_id).to eq 0 + stmt.execute 1 + expect(stmt.last_id).to eq 1 + end + + it 'should handle bigint ids' do + stmt = @client.prepare 'INSERT INTO lastIdTest (id, blah) VALUES (?, ?)' + stmt.execute 5000000000, 5000 + expect(stmt.last_id).to eql(5000000000) + + stmt = @client.prepare 'INSERT INTO lastIdTest (blah) VALUES (?)' + stmt.execute 5001 + expect(stmt.last_id).to eql(5000000001) + end + end + + context 'affected_rows' do + before(:example) do + @client.query 'USE test' + @client.query 'CREATE TABLE IF NOT EXISTS lastIdTest (`id` BIGINT NOT NULL AUTO_INCREMENT, blah INT(11), PRIMARY KEY (`id`))' + end + + after(:example) do + @client.query 'DROP TABLE lastIdTest' + end + + it 'should return number of rows affected by an insert' do + stmt = @client.prepare 'INSERT INTO lastIdTest (blah) VALUES (?)' + stmt.execute 1 + expect(stmt.affected_rows).to eq 1 + end + + it 'should return number of rows affected by an update' do + stmt = @client.prepare 'INSERT INTO lastIdTest (blah) VALUES (?)' + stmt.execute 1 + expect(stmt.affected_rows).to eq 1 + stmt.execute 2 + expect(stmt.affected_rows).to eq 1 + + stmt = @client.prepare 'UPDATE lastIdTest SET blah=? WHERE blah=?' + stmt.execute 0, 1 + expect(stmt.affected_rows).to eq 1 + end + + it 'should return number of rows affected by a delete' do + stmt = @client.prepare 'INSERT INTO lastIdTest (blah) VALUES (?)' + stmt.execute 1 + expect(stmt.affected_rows).to eq 1 + stmt.execute 2 + expect(stmt.affected_rows).to eq 1 + + stmt = @client.prepare 'DELETE FROM lastIdTest WHERE blah=?' + stmt.execute 1 + expect(stmt.affected_rows).to eq 1 + end + end + + context 'close' do + it 'should free server resources' do + stmt = @client.prepare 'SELECT 1' + GC.disable + expect { stmt.close }.to change(&method(:stmt_count)).by(-1) + GC.enable + end + + it 'should raise an error on subsequent execution' do + stmt = @client.prepare 'SELECT 1' + stmt.close + expect { stmt.execute }.to raise_error(Mysql2::Error, /Invalid statement handle/) + end + + it 'should not raise if called multiple times' do + stmt = @client.prepare 'SELECT 1' + expect(stmt).to_not be_closed + + 3.times do + expect { stmt.close }.to_not raise_error + expect(stmt).to be_closed + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4e29fe658..f7ac9a8fe 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,69 +1,183 @@ -# encoding: UTF-8 - require 'rspec' require 'mysql2' require 'timeout' require 'yaml' +require 'fiber' + DatabaseCredentials = YAML.load_file('spec/configuration.yml') +if GC.respond_to?(:verify_compaction_references) + # This method was added in Ruby 3.0.0. Calling it this way asks the GC to + # move objects around, helping to find object movement bugs. + if RUBY_VERSION >= "3.2" + GC.verify_compaction_references(expand_heap: true, toward: :empty) + else + GC.verify_compaction_references(double_heap: true, toward: :empty) + end +end + RSpec.configure do |config| - config.before(:all) do - client = Mysql2::Client.new DatabaseCredentials['root'] - client.query %[ - CREATE TABLE IF NOT EXISTS mysql2_test ( - id MEDIUMINT NOT NULL AUTO_INCREMENT, - null_test VARCHAR(10), - bit_test BIT(64), - tiny_int_test TINYINT, - bool_cast_test TINYINT(1), - small_int_test SMALLINT, - medium_int_test MEDIUMINT, - int_test INT, - big_int_test BIGINT, - float_test FLOAT(10,3), - float_zero_test FLOAT(10,3), - double_test DOUBLE(10,3), - decimal_test DECIMAL(10,3), - decimal_zero_test DECIMAL(10,3), - date_test DATE, - date_time_test DATETIME, - timestamp_test TIMESTAMP, - time_test TIME, - year_test YEAR(4), - char_test CHAR(10), - varchar_test VARCHAR(10), - binary_test BINARY(10), - varbinary_test VARBINARY(10), - tiny_blob_test TINYBLOB, - tiny_text_test TINYTEXT, - blob_test BLOB, - text_test TEXT, - medium_blob_test MEDIUMBLOB, - medium_text_test MEDIUMTEXT, - long_blob_test LONGBLOB, - long_text_test LONGTEXT, - enum_test ENUM('val1', 'val2'), - set_test SET('val1', 'val2'), - PRIMARY KEY (id) - ) - ] - client.query "DELETE FROM mysql2_test;" - client.query %[ - INSERT INTO mysql2_test ( - null_test, bit_test, tiny_int_test, bool_cast_test, small_int_test, medium_int_test, int_test, big_int_test, - float_test, float_zero_test, double_test, decimal_test, decimal_zero_test, date_test, date_time_test, timestamp_test, time_test, - year_test, char_test, varchar_test, binary_test, varbinary_test, tiny_blob_test, - tiny_text_test, blob_test, text_test, medium_blob_test, medium_text_test, - long_blob_test, long_text_test, enum_test, set_test - ) - - VALUES ( - NULL, b'101', 1, 1, 10, 10, 10, 10, - 10.3, 0, 10.3, 10.3, 0, '2010-4-4', '2010-4-4 11:44:00', '2010-4-4 11:44:00', '11:44:00', - 2009, "test", "test", "test", "test", "test", - "test", "test", "test", "test", "test", - "test", "test", 'val1', 'val1,val2' - ) - ] + config.disable_monkey_patching! + + config.expect_with :rspec do |expectations| + expectations.max_formatted_output_length = 1200 + end + + def with_internal_encoding(encoding) + old_enc = Encoding.default_internal + old_verbose = $VERBOSE + $VERBOSE = nil + Encoding.default_internal = encoding + $VERBOSE = old_verbose + + yield + ensure + $VERBOSE = nil + Encoding.default_internal = old_enc + $VERBOSE = old_verbose + end + + def new_client(option_overrides = {}) + client = Mysql2::Client.new(DatabaseCredentials['root'].merge(option_overrides)) + @clients ||= [] + @clients << client + return client unless block_given? + + begin + yield client + ensure + client.close + @clients.delete(client) + end + end + + def num_classes + # rubocop:disable Lint/UnifiedInteger + 0.instance_of?(Integer) ? [Integer] : [Fixnum, Bignum] + # rubocop:enable Lint/UnifiedInteger + end + + # Use monotonic time if possible (ruby >= 2.1.0) + if defined?(Process::CLOCK_MONOTONIC) + def clock_time + Process.clock_gettime Process::CLOCK_MONOTONIC + end + else + def clock_time + Time.now.to_f + end + end + + # A directory where SSL certificates pem files exist. + def ssl_cert_dir + return @ssl_cert_dir if @ssl_cert_dir + + dir = ENV['TEST_RUBY_MYSQL2_SSL_CERT_DIR'] + @ssl_cert_dir = if dir && !dir.empty? + dir + else + '/etc/mysql' + end + @ssl_cert_dir + end + + # A host used to create the certificates pem files. + def ssl_cert_host + return @ssl_cert_host if @ssl_cert_host + + host = ENV['TEST_RUBY_MYSQL2_SSL_CERT_HOST'] + @ssl_cert_host = if host && !host.empty? + host + else + 'mysql2gem.example.com' + end + @ssl_cert_host + end + + config.before(:suite) do + begin + new_client + rescue Mysql2::Error => e + username = DatabaseCredentials['root']['username'] + database = DatabaseCredentials['root']['database'] + message = %( +An error occurred while connecting to the testing database server. +Make sure that the database server is running. +Make sure that `mysql -u #{username} [options] #{database}` succeeds by the root user config in spec/configuration.yml. +Make sure that the testing database '#{database}' exists. If it does not exist, create it. +) + warn message + raise e + end + end + + config.before(:context) do + new_client do |client| + client.query %[ + CREATE TABLE IF NOT EXISTS mysql2_test ( + id MEDIUMINT NOT NULL AUTO_INCREMENT, + null_test VARCHAR(10), + bit_test BIT(64), + single_bit_test BIT(1), + tiny_int_test TINYINT, + bool_cast_test TINYINT(1), + small_int_test SMALLINT, + medium_int_test MEDIUMINT, + int_test INT, + big_int_test BIGINT, + float_test FLOAT(10,3), + float_zero_test FLOAT(10,3), + double_test DOUBLE(10,3), + decimal_test DECIMAL(10,3), + decimal_zero_test DECIMAL(10,3), + date_test DATE, + date_time_test DATETIME, + timestamp_test TIMESTAMP, + time_test TIME, + year_test YEAR(4), + char_test CHAR(10), + varchar_test VARCHAR(10), + binary_test BINARY(10), + varbinary_test VARBINARY(10), + tiny_blob_test TINYBLOB, + tiny_text_test TINYTEXT, + blob_test BLOB, + text_test TEXT, + medium_blob_test MEDIUMBLOB, + medium_text_test MEDIUMTEXT, + long_blob_test LONGBLOB, + long_text_test LONGTEXT, + enum_test ENUM('val1', 'val2'), + set_test SET('val1', 'val2'), + PRIMARY KEY (id) + ) + ] + client.query "DELETE FROM mysql2_test;" + client.query %[ + INSERT INTO mysql2_test ( + null_test, bit_test, single_bit_test, tiny_int_test, bool_cast_test, small_int_test, medium_int_test, int_test, big_int_test, + float_test, float_zero_test, double_test, decimal_test, decimal_zero_test, date_test, date_time_test, timestamp_test, time_test, + year_test, char_test, varchar_test, binary_test, varbinary_test, tiny_blob_test, + tiny_text_test, blob_test, text_test, medium_blob_test, medium_text_test, + long_blob_test, long_text_test, enum_test, set_test + ) + + VALUES ( + NULL, b'101', b'1', 1, 1, 10, 10, 10, 10, + 10.3, 0, 10.3, 10.3, 0, '2010-4-4', '2010-4-4 11:44:00', '2010-4-4 11:44:00', '11:44:00', + 2009, "test", "test", "test", "test", "test", + "test", "test", "test", "test", "test", + "test", "test", 'val1', 'val1,val2' + ) + ] + end + end + + config.before(:example) do + @client = new_client + end + + config.after(:example) do + @clients.each(&:close) end end diff --git a/spec/ssl/ca-cert.pem b/spec/ssl/ca-cert.pem new file mode 100644 index 000000000..632cdeb71 --- /dev/null +++ b/spec/ssl/ca-cert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC7jCCAdagAwIBAgIUf1RggahC+P3zuvdDnArIrPylegwwDQYJKoZIhvcNAQEL +BQAwFzEVMBMGA1UEAwwMY2FfbXlzcWwyZ2VtMB4XDTI0MDIwOTE1NDkyOFoXDTMz +MTIxODE1NDkyOFowFzEVMBMGA1UEAwwMY2FfbXlzcWwyZ2VtMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuEAFH4rhTMGs1bIHJgWvbsgNZBc2TrjPX0Jf +3kdHvq1u0bHuaYFUyY/yXoOchHbDvRYx1WL2jSkJuc2JMYN1V+j4EUtG9KAt4dqx +LNTy6SxpQeMKEqtiTNc9aMR8cAxliSZSj/Qn6JpcSJPE/loIPdEC/MTo7ONcJ0xQ +5LymZqnuKZGw8L2UzZ+Zof3cYr2nPLoZDGtBsDDf5W184nl0MqTonu6/+raL/4C+ +3Smy/5IOcJzRfvw6Nc/bvi9eWkypNZzG3XaSO6K5d399KLn0mf9ZbXy5bq9klCUI +seIhmA77vzaBOwdQUJKKijKGqlTahfoAiUV3AgcoxAnytKraVwIDAQABozIwMDAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBScsUBgsDMWnhPO1njE2uD6P7gr9TAN +BgkqhkiG9w0BAQsFAAOCAQEAQbKVJ2eDt/99lnHGr3uFgVzWr26QomGfWysHhwHf +9pgS2KKbT7u/MGgWMi2jpXAJFCeRD2v/b5lRpeI03ZGuTJ0zqlItXzlBY6bND3KB +AyJ5orfJu0NVwhjFZdnGH1IQVWjMW8pt8WzopYRyyfnqpbwE2e8wJUgOo9LDgJm8 +mK4bcpRVbvS2fo+g+CZ9HXzOXpL0m4gbsnPjeulmtSTXFX1/t00Hw+Gt2POB2A0h +VNFKxS08uohPq49+MNeaTA0CjQdpG09lh7Cua/mSgzmYvWF9iYJpsBggzDUi7hab +T07GzSz0fpfb35RAtsghgCyxaW7M3fAaztVJaKPDB4SfUw== +-----END CERTIFICATE----- diff --git a/spec/ssl/ca-key.pem b/spec/ssl/ca-key.pem new file mode 100644 index 000000000..5deca5740 --- /dev/null +++ b/spec/ssl/ca-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC4QAUfiuFMwazV +sgcmBa9uyA1kFzZOuM9fQl/eR0e+rW7Rse5pgVTJj/Jeg5yEdsO9FjHVYvaNKQm5 +zYkxg3VX6PgRS0b0oC3h2rEs1PLpLGlB4woSq2JM1z1oxHxwDGWJJlKP9CfomlxI +k8T+Wgg90QL8xOjs41wnTFDkvKZmqe4pkbDwvZTNn5mh/dxivac8uhkMa0GwMN/l +bXzieXQypOie7r/6tov/gL7dKbL/kg5wnNF+/Do1z9u+L15aTKk1nMbddpI7orl3 +f30oufSZ/1ltfLlur2SUJQix4iGYDvu/NoE7B1BQkoqKMoaqVNqF+gCJRXcCByjE +CfK0qtpXAgMBAAECggEAM5T8ujFr1MzN4b+m/66MyDNqiFB1TEGyEKWo6DZFcCzm +vv8U02W5QnqxrGMlMPJ85xVtGyPLCYbpKaLQm1OFyPg4ZsMP2NF1Nus+OeJeJQhh +aWgx/DsN2JxTnV6Qxd+6l1RqvdFpUNXSKyFvf5PeBcxbjT9lRFh8hqX3aaok3c2P +Z4sROHNqgXe/mnCXv3nkUQXct7oFgdeMR6Zy8GFNxMaxvFhCmT+Q0NFf0hqp6/at +b/P0+tZYjp7PkvCkCo2Z7MCdsnI1BNn5ggtdqhLs9tu1f6iwMH0z3tKkWXkX1dJC +Ad0G30qfv6A9IOEWevwJyuVcp9onRmcUaaVNSascbQKBgQD4ITxJcIE3MAtK1W7q +vvEm64i2ay5XfBhoi2oik3+MVlzY2dRiGOEffvysK1Xls8VlLT/RSZVWLjJ+EvrH +DJZmbl972UFvYt5JGEv53YYD0kJ1fQvkLA9//Xkg6Wdac+7BzHxc09fV1k2mUvpH +jJs7JSgLDwVvvU9+vs3eFWWAiwKBgQC+GBgARwb2NKEfXRtyT+X7Z0jmQoQCJDr7 +34D7p5u6lRShlzZNFvwbc5YAIXNNuveqTsrKL57yo4vzNHpDzpXj+kdFIKGAXzXN +BoohGCtIQrrXYGONUubvk3njlOrppdD9kMN+ioHVp/Gm9Dxrn+0HKtzALn7pgZa7 +IzA/3Mpa5QKBgEOfozOMotqskFdLxdfaRBTMWk0E9vNG0cwkOr/DnR5dJx6+dyBp +EWmpDSnLAbUBgomphFwAht+e5YnwmEIJTzAJYqJ5OlkmA9i9827cjbqa4hvtAYGk +9HB4Xzu2AMHpGKfemAIghhE0P6NVt/op+uBqpvgkluG2IWU0kRy2jhwzAoGAbWl+ +vwIirqkiJ+Q2PPhh3e7X1bhpNLZXwMsm+THCf4T5J/zZw0s8dix0JMUcEZxQmpTZ +QcBhEzUxAx2sVcTdHyfZx579dd7XH5fo/x1jJCdMVVTkV95kj3ZpzKTVBQBsptWg +v//GtQwCGd8vu56EFgEEqBTa9VmiQToCtm9FhUUCgYEAwMtvoKM7N9WawAVEeUWG +oDoJhhbnCHvg7ohNEymfsfHaHNUs/WXgk9mHTqY9lkMrnEmO71z8LPw0q67UbiIx +58MWCpDcOzzNrmXKEbZQ380qYezOFffuvkDKAAl2OmxSAbKLvV/5/vAAEXyBJe2u +brUtxFuKTvZbuCZU+L04y8I= +-----END PRIVATE KEY----- diff --git a/spec/ssl/ca.cnf b/spec/ssl/ca.cnf new file mode 100644 index 000000000..6e98bd981 --- /dev/null +++ b/spec/ssl/ca.cnf @@ -0,0 +1,26 @@ + +[ ca ] +# January 1, 2015 +default_startdate = 2015010360000Z + +[ req ] +distinguished_name = req_distinguished_name + +# Root CA certificate extensions +[ v3_ca ] +basicConstraints = critical, CA:true + +[ req_distinguished_name ] +# If this isn't set, the error is error, no objects specified in config file +commonName = Common Name (hostname, IP, or your name) + +countryName_default = US +stateOrProvinceName_default = CA +localityName_default = San Francisco +0.organizationName_default = mysql2_gem +organizationalUnitName_default = Mysql2Gem +emailAddress_default = mysql2gem@example.com + + +commonName_default = ca_mysql2gem + diff --git a/spec/ssl/cert.cnf b/spec/ssl/cert.cnf new file mode 100644 index 000000000..832ec1197 --- /dev/null +++ b/spec/ssl/cert.cnf @@ -0,0 +1,26 @@ + +[ ca ] +# January 1, 2015 +default_startdate = 2015010360000Z + +[ req ] +distinguished_name = req_distinguished_name + +# Root CA certificate extensions +[ v3_ca ] +basicConstraints = critical, CA:true + +[ req_distinguished_name ] +# If this isn't set, the error is error, no objects specified in config file +commonName = Common Name (hostname, IP, or your name) + +countryName_default = US +stateOrProvinceName_default = CA +localityName_default = San Francisco +0.organizationName_default = mysql2_gem +organizationalUnitName_default = Mysql2Gem +emailAddress_default = mysql2gem@example.com + + +commonName_default = mysql2gem.example.com + diff --git a/spec/ssl/client-cert.pem b/spec/ssl/client-cert.pem new file mode 100644 index 000000000..6c46e2d7c --- /dev/null +++ b/spec/ssl/client-cert.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICqzCCAZMCAQEwDQYJKoZIhvcNAQELBQAwFzEVMBMGA1UEAwwMY2FfbXlzcWwy +Z2VtMB4XDTI0MDIwOTE1NDkyOFoXDTMzMTIxODE1NDkyOFowIDEeMBwGA1UEAwwV +bXlzcWwyZ2VtLmV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAnaBhN4wLvmzVFg91HZH2PmacZVpb3I51qoiGQS7i8oiLAyvqCIjJVoCG +CWfBk8s6WNAPiE9pqvKyUXTfpvZBlW9t4hwQImRD15Sr55/yWWnKO7SwHhY8BTeo +QVlSBrfPnKMIjByudi/G7fR9naXoeQREAP2hl8h/gh7UBAm6kNTFxxD/1Byal/iW +BwoYqQQYVUuYThdI0C17qYn7dthtjjGRbexouVRwN446qR9dC71A27VgNUmlFb/E +ygmfEFPhJRNf9fSVAABkNHPhHtg5tURjcZi9ldZMr8o6643hY1kW1xyS3VFLIcP6 +2Cing0f0/Cjh5jg4l5uEcN/AUpbw+QIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQA2 +4Hh2Qf3eDcKZCvjHmwgCXkhFpZtgpBTgEyPfl0UKwrgkdBiqGY6jywCIsDvp0DrN +OL7Ybk+tdDE95HUfC4W7MBa2wS5WCksIjq+8N4Q61np8gine01IUofjeBSlkNgkF +phtOVeadCky3UlBBGXBSwaPC+uyHlXEOlmfm1YOP0QboqzMorEl4ZECxFVtkyKbu +tud9BDitIcb7x2JjrLlmnltE3JQ+WI9iyL0EAXDloIPUkeyf123lFGNSXDCEj2Ru +8wzQjNDGYiMrwYFib+6d0UE1ED2KLn7TN5lHhgC/H3RUv3dAIgciuysqepTC25DV +soLMzebCsw4UprXFCUbz +-----END CERTIFICATE----- diff --git a/spec/ssl/client-key.pem b/spec/ssl/client-key.pem new file mode 100644 index 000000000..ad4af278c --- /dev/null +++ b/spec/ssl/client-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCdoGE3jAu+bNUW +D3UdkfY+ZpxlWlvcjnWqiIZBLuLyiIsDK+oIiMlWgIYJZ8GTyzpY0A+IT2mq8rJR +dN+m9kGVb23iHBAiZEPXlKvnn/JZaco7tLAeFjwFN6hBWVIGt8+cowiMHK52L8bt +9H2dpeh5BEQA/aGXyH+CHtQECbqQ1MXHEP/UHJqX+JYHChipBBhVS5hOF0jQLXup +ift22G2OMZFt7Gi5VHA3jjqpH10LvUDbtWA1SaUVv8TKCZ8QU+ElE1/19JUAAGQ0 +c+Ee2Dm1RGNxmL2V1kyvyjrrjeFjWRbXHJLdUUshw/rYKKeDR/T8KOHmODiXm4Rw +38BSlvD5AgMBAAECggEABwUuaexqq3uqY+CisoNJMySby5HM1z5gXuIqDzt42nIa +tYnzqEH7VvHDcSYgriXrfoAfE6RBzF4hyKn1ZLjBWVf3Tg46PmXhIE1b+KVxhD+c +xQWk9boUyJYJgDDrtnviRb4nHSJmjMH6UKfGc8qArOnJOOgS6zEqqKTEhDzbWnmQ +Q5GjZGKjTMfbC6u9r6nwcAlPax2tzRmPNtQFYMSjcHmvN0IuhwVlZlULsdPnpDpS +3LeR8b/75Og8BbqXJNdLAXQq3Athu9ADuxfS/rOBxWwEKElV80gFMo/qiWZ/DVqw +UUExYuDAx8Jnf4j8Vb2UcY2bpxvGHLrD10gRJmUxLwKBgQDLzd95e0SXxuWEHBdA +oHfermVwUzs1aa5zqs/KJEICmFE94bJbWQKbW1TDQggIr45XylQNsQuh0RS1USsd +YweA7z00HPcj5ON7O+5tKwfP+C+jl1s1yfYPvaSiRKwLde6DdVGos29C5OpoIQ+E +hjC02n2d84F/lrjfHn3MQH/2vwKBgQDF/u6jNwfHsfgOnKGJv9BKfmLg4saYUvMv +bclkQS0qrSgrloMSZs1QcpZJWneiO0vjsxoTeLcUIcsuhfxpMsN60nb/QncB749C +ts9SuV9Rdv4+7U1tqg2ZQ52zjMliZvEDriPZ40vhONBXYyKtx1HZ9OexDOLKUzzY +c1MMobj+RwKBgBFWPwdvhANBSS720MePnwLTZQ+sFOJTTiLKyghRE0hzOp4AABMj +PESI/WnqyRIsFPjE3meXwvyN86wE7pz+WpoOP++Z8zAbfXpzO7IPsgdv/mV1L64g +swzdvg6LtvL2okaOiVbHhNR08rfO8Cn+3E/WMk9ocoCvCqT4TA0/A2OzAoGBAILO +TpgzxgcPM6NrpWkc+R4N64NJLwz5WEJQVMnQKWfVaAGL+WIR2ri4S0OA6iKa7CMt +cx/EE6fQP6ynxj8102F0ZDt1jKwRuWLI5aVwZGGsrIGkQxAdVciYnDo/29gPzFCz +HmpXuQy9fR8Olp2aXiARpXQZ4EbswPj7D7X7rf0HAoGAQhhg0xfIuOrLRXrhfFEg +NzASVKvtCEMDbJD4mWCOuqe7Jlvi/qmXZ9YnuKOmGukGV0/ePtLI1+Hcstr4kexb +lUJYZQlB9e0Ft87UD16TuGHAMBKdtXQKuBjMeI0C43kSBq0XuiyY9b44ADH/up07 +vkqwfAOUVKCHp4KOOruW9cc= +-----END PRIVATE KEY----- diff --git a/spec/ssl/client-req.pem b/spec/ssl/client-req.pem new file mode 100644 index 000000000..38774a53e --- /dev/null +++ b/spec/ssl/client-req.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICZTCCAU0CAQAwIDEeMBwGA1UEAwwVbXlzcWwyZ2VtLmV4YW1wbGUuY29tMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnaBhN4wLvmzVFg91HZH2Pmac +ZVpb3I51qoiGQS7i8oiLAyvqCIjJVoCGCWfBk8s6WNAPiE9pqvKyUXTfpvZBlW9t +4hwQImRD15Sr55/yWWnKO7SwHhY8BTeoQVlSBrfPnKMIjByudi/G7fR9naXoeQRE +AP2hl8h/gh7UBAm6kNTFxxD/1Byal/iWBwoYqQQYVUuYThdI0C17qYn7dthtjjGR +bexouVRwN446qR9dC71A27VgNUmlFb/EygmfEFPhJRNf9fSVAABkNHPhHtg5tURj +cZi9ldZMr8o6643hY1kW1xyS3VFLIcP62Cing0f0/Cjh5jg4l5uEcN/AUpbw+QID +AQABoAAwDQYJKoZIhvcNAQELBQADggEBACPgLE417R3dHf9eiwVtoOSzm8ltNBbz +5dRqiHDMEXuH+aGiNtTI1BI9akXrgjyN+nXWK09jZsWJ/+8mj+NcGS8JfdQdVn0c +Ov/kmxoVwNEzj3mboL0amM7kQv6zRa1hKk7l5ZE+5G/EvWU2xF0qrHvGLvphgL0D ++/PBJomMHAMYF5MUEOLtnwdslMS0OyAQCHu/swDhIj8jSCkoz4M8gpB2cV+VOHCG +hW2sxlF67cgXLBXCgU0TP6bZbByPb8pEVax2808+wl3fCiM2IwcRtM3WTkSbESuN +CjFO7OeUq4o1Dtiw2uBfNpe+dviGvDeyIBfUmkzqJH3BBpQyGHWU9YE= +-----END CERTIFICATE REQUEST----- diff --git a/spec/ssl/gen_certs.sh b/spec/ssl/gen_certs.sh new file mode 100644 index 000000000..41818d4a2 --- /dev/null +++ b/spec/ssl/gen_certs.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +set -eux + +# TEST_RUBY_MYSQL2_SSL_CERT_HOST: custom host for the SSL certificates. +SSL_CERT_HOST=${TEST_RUBY_MYSQL2_SSL_CERT_HOST:-mysql2gem.example.com} +echo "Generating the SSL certifications from the host ${SSL_CERT_HOST}.." + +echo " +[ ca ] +# January 1, 2015 +default_startdate = 2015010360000Z + +[ req ] +distinguished_name = req_distinguished_name + +# Root CA certificate extensions +[ v3_ca ] +basicConstraints = critical, CA:true + +[ req_distinguished_name ] +# If this isn't set, the error is "error, no objects specified in config file" +commonName = Common Name (hostname, IP, or your name) + +countryName_default = US +stateOrProvinceName_default = CA +localityName_default = San Francisco +0.organizationName_default = mysql2_gem +organizationalUnitName_default = Mysql2Gem +emailAddress_default = mysql2gem@example.com +" | tee ca.cnf cert.cnf + +# The client and server certs must have a different common name than the CA +# to avoid "SSL connection error: error:00000001:lib(0):func(0):reason(1)" + +echo " +commonName_default = ca_mysql2gem +" >> ca.cnf + +echo " +commonName_default = ${SSL_CERT_HOST} +" >> cert.cnf + +# Generate a set of certificates +openssl genrsa -out ca-key.pem 2048 +openssl req -new -x509 -nodes -extensions v3_ca -days 3600 -key ca-key.pem -out ca-cert.pem -batch -config ca.cnf +openssl req -newkey rsa:2048 -days 3600 -nodes -keyout pkcs8-server-key.pem -out server-req.pem -batch -config cert.cnf +openssl x509 -req -in server-req.pem -days 3600 -CA ca-cert.pem -CAkey ca-key.pem -set_serial 01 -out server-cert.pem +openssl req -newkey rsa:2048 -days 3600 -nodes -keyout pkcs8-client-key.pem -out client-req.pem -batch -config cert.cnf +openssl x509 -req -in client-req.pem -days 3600 -CA ca-cert.pem -CAkey ca-key.pem -set_serial 01 -out client-cert.pem + +# Convert format from PKCS#8 to PKCS#1 +openssl rsa -in pkcs8-server-key.pem -out server-key.pem +openssl rsa -in pkcs8-client-key.pem -out client-key.pem + +echo "done" diff --git a/spec/ssl/pkcs8-client-key.pem b/spec/ssl/pkcs8-client-key.pem new file mode 100644 index 000000000..ad4af278c --- /dev/null +++ b/spec/ssl/pkcs8-client-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCdoGE3jAu+bNUW +D3UdkfY+ZpxlWlvcjnWqiIZBLuLyiIsDK+oIiMlWgIYJZ8GTyzpY0A+IT2mq8rJR +dN+m9kGVb23iHBAiZEPXlKvnn/JZaco7tLAeFjwFN6hBWVIGt8+cowiMHK52L8bt +9H2dpeh5BEQA/aGXyH+CHtQECbqQ1MXHEP/UHJqX+JYHChipBBhVS5hOF0jQLXup +ift22G2OMZFt7Gi5VHA3jjqpH10LvUDbtWA1SaUVv8TKCZ8QU+ElE1/19JUAAGQ0 +c+Ee2Dm1RGNxmL2V1kyvyjrrjeFjWRbXHJLdUUshw/rYKKeDR/T8KOHmODiXm4Rw +38BSlvD5AgMBAAECggEABwUuaexqq3uqY+CisoNJMySby5HM1z5gXuIqDzt42nIa +tYnzqEH7VvHDcSYgriXrfoAfE6RBzF4hyKn1ZLjBWVf3Tg46PmXhIE1b+KVxhD+c +xQWk9boUyJYJgDDrtnviRb4nHSJmjMH6UKfGc8qArOnJOOgS6zEqqKTEhDzbWnmQ +Q5GjZGKjTMfbC6u9r6nwcAlPax2tzRmPNtQFYMSjcHmvN0IuhwVlZlULsdPnpDpS +3LeR8b/75Og8BbqXJNdLAXQq3Athu9ADuxfS/rOBxWwEKElV80gFMo/qiWZ/DVqw +UUExYuDAx8Jnf4j8Vb2UcY2bpxvGHLrD10gRJmUxLwKBgQDLzd95e0SXxuWEHBdA +oHfermVwUzs1aa5zqs/KJEICmFE94bJbWQKbW1TDQggIr45XylQNsQuh0RS1USsd +YweA7z00HPcj5ON7O+5tKwfP+C+jl1s1yfYPvaSiRKwLde6DdVGos29C5OpoIQ+E +hjC02n2d84F/lrjfHn3MQH/2vwKBgQDF/u6jNwfHsfgOnKGJv9BKfmLg4saYUvMv +bclkQS0qrSgrloMSZs1QcpZJWneiO0vjsxoTeLcUIcsuhfxpMsN60nb/QncB749C +ts9SuV9Rdv4+7U1tqg2ZQ52zjMliZvEDriPZ40vhONBXYyKtx1HZ9OexDOLKUzzY +c1MMobj+RwKBgBFWPwdvhANBSS720MePnwLTZQ+sFOJTTiLKyghRE0hzOp4AABMj +PESI/WnqyRIsFPjE3meXwvyN86wE7pz+WpoOP++Z8zAbfXpzO7IPsgdv/mV1L64g +swzdvg6LtvL2okaOiVbHhNR08rfO8Cn+3E/WMk9ocoCvCqT4TA0/A2OzAoGBAILO +TpgzxgcPM6NrpWkc+R4N64NJLwz5WEJQVMnQKWfVaAGL+WIR2ri4S0OA6iKa7CMt +cx/EE6fQP6ynxj8102F0ZDt1jKwRuWLI5aVwZGGsrIGkQxAdVciYnDo/29gPzFCz +HmpXuQy9fR8Olp2aXiARpXQZ4EbswPj7D7X7rf0HAoGAQhhg0xfIuOrLRXrhfFEg +NzASVKvtCEMDbJD4mWCOuqe7Jlvi/qmXZ9YnuKOmGukGV0/ePtLI1+Hcstr4kexb +lUJYZQlB9e0Ft87UD16TuGHAMBKdtXQKuBjMeI0C43kSBq0XuiyY9b44ADH/up07 +vkqwfAOUVKCHp4KOOruW9cc= +-----END PRIVATE KEY----- diff --git a/spec/ssl/pkcs8-server-key.pem b/spec/ssl/pkcs8-server-key.pem new file mode 100644 index 000000000..95eeb77c6 --- /dev/null +++ b/spec/ssl/pkcs8-server-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCrvY7yq+BT9ZSP +9yOdAVfgx12f3lwcO/m6F+R/Z+0bVSvVtbYiO1xn+3l8rz8wT6+fBVVrIxpupFUP +Zalb0BP07jmqhC7YgBjgad0vkUph1peCJ+ntJYtixpAZOZj70aT/2CKjuGkmWFnL +ryrxBmpplHea1BwLJtrWgQEJcaFqiJ1lbGjinudo8dY5tZ7iyyZ+dk8+TKDsV/1p +yuAOemKYDY34jUf1LE525pSR/BcDGYCdzpBsOLsqRKf+5d8cZK05I6oYYKkAwoM3 +QwmwpLSm9poFUlo83uMDU5FH76hWFwkd5PE8oSbdKcp6d0AMh/L0TS6V9lGM58jW +9g71/v2LAgMBAAECggEAOnxjBox26FDNR5vb4neXJELwxOVWS/02xeOmGqdbTYAb +Xfu0a4L4rKas0EPkCoFQpyCLXuGE+mH3X7d4zf4WFcbdF49NXsh88EvNGgpqINiS +Hy6VkP/EsJ47a4O8cCGMhd5mqYe/M2JKLj3Yq11KdusrMiyC4l9Yjk0/e6ZZWKxe +/htw3fMPnHOMfoUB9jPy+SrhbFt42bZ7+JA2Aihf8RCUb/R7OjhASKeRLPkefJTA +Z6kJUoXoCBogjdkLCuVw1zjXF92R5gy+i5o9VhELHpg1D2If7CmeEM2FfVDfWjlF +iYlIR750OsKaeWB0LVKwh+07oyIlmO1TOECXEKt+gQKBgQDpdUfImWMrrkJ5AeCL +0NmO0JIZciGFBDTHRLOdzQiWKdq87i6/LWStK/gT+eb8WVLJ8vk5KrXFIy7TpYce +4jRr9u9MG14hjVemLMMoLhPkruPoulIp+Aj0mnhKephpJQ+Khd77g/GT6ZAqxkxi +drhTfKlSou0oEEd34ZuK7d6mCwKBgQC8UrbCErc+r4Ff48/0BfbJWZ7sN/HP/QtI +R2V9v3VpGqMX93v+YhQepxpd4PSkxluEmbbwYkA81un1ot7ndNkLIN0x59P9tVR0 +ghXuLmLwxExM5ekrfPt7gbkhwUCwRTogjocm6VoF541tn+ll724XtBdewhyXEUm7 +IG0/tLU2gQKBgQCx3avaNprq7bIxVW/Jtk361AdroZvOJx068KnUQSEYnzzLEsDE +4QXCNiyks5H7kuZTfG3K0zJ3xs1nbMacjgUYeKNqnbNC5tfvgE0TsL9xTJnRdxsg +ZJwWGBYr0GmMOjMz+7iecbE9WwZ+wGPz5LWcze6HSiBblMOOn3GNEJvAbwKBgHXS +3ksgAIv0rGH9Gz9Wd+fT7Y1nFyCE9gkbulDpd6DxrHazPV2TqXjgHav8sbNh8yJM +NdvB7OTjpW8snn97aMwAnMO7grOqPpPCS8xAM2Dlv8Mg2Th/Mqw8JkMLMNjYBx0V +b1OWDd/B1odu1E0VdvDXmQONOOv/Qf0UtaV0/yeBAoGAaUO9xrblsqEmP+2lqsiv +qwNLk5PICaoXwIacFdhc8YhC5OoXxRmLlyJmz72aKLTnNH+ZYsNYcJlhexhxpxq5 +vsxdk7rO741EDuFAk1Hsx1Q2G2+q6VWIWQhsn5eTGdas2qdNrKoNwN3wuALPPeeQ +l7yIJxi7Qn24xh+Bjdp1CDU= +-----END PRIVATE KEY----- diff --git a/spec/ssl/server-cert.pem b/spec/ssl/server-cert.pem new file mode 100644 index 000000000..746bd5446 --- /dev/null +++ b/spec/ssl/server-cert.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICqzCCAZMCAQEwDQYJKoZIhvcNAQELBQAwFzEVMBMGA1UEAwwMY2FfbXlzcWwy +Z2VtMB4XDTI0MDIwOTE1NDkyOFoXDTMzMTIxODE1NDkyOFowIDEeMBwGA1UEAwwV +bXlzcWwyZ2VtLmV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAq72O8qvgU/WUj/cjnQFX4Mddn95cHDv5uhfkf2ftG1Ur1bW2IjtcZ/t5 +fK8/ME+vnwVVayMabqRVD2WpW9AT9O45qoQu2IAY4GndL5FKYdaXgifp7SWLYsaQ +GTmY+9Gk/9gio7hpJlhZy68q8QZqaZR3mtQcCyba1oEBCXGhaoidZWxo4p7naPHW +ObWe4ssmfnZPPkyg7Ff9acrgDnpimA2N+I1H9SxOduaUkfwXAxmAnc6QbDi7KkSn +/uXfHGStOSOqGGCpAMKDN0MJsKS0pvaaBVJaPN7jA1ORR++oVhcJHeTxPKEm3SnK +endADIfy9E0ulfZRjOfI1vYO9f79iwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBc +sRGEk10OWCm8MlfyWLmj3dAokO/LC1Ya6wP9gCtepvkum4hISKFmJpLYokUXpyOa +GnUJ96eyHVg5OKz2r1rEra2E6oiP6FW6WCe8tVQEfsV6B7LkJ0O2X5us1kY+gmo6 +ch2/BDWROhjV5LgSPkuCNfNS2mkKo0vEg3xovYBNlqBveyrRnkPcR1qANt3RV3JR +ADfqPGNU6IFoKiFZhK5wjwjUl2p1a12aw6C3e/O2UeJDsSEucN6yjEa/KZhlBfpH +4RSSRpSuWeGu2ndSaVwFYX44//v8vnW7+pFDWB0LFbv+Jd9qji0chaFWh3jJKNas +vALqzCG44enapVb/7m4i +-----END CERTIFICATE----- diff --git a/spec/ssl/server-key.pem b/spec/ssl/server-key.pem new file mode 100644 index 000000000..95eeb77c6 --- /dev/null +++ b/spec/ssl/server-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCrvY7yq+BT9ZSP +9yOdAVfgx12f3lwcO/m6F+R/Z+0bVSvVtbYiO1xn+3l8rz8wT6+fBVVrIxpupFUP +Zalb0BP07jmqhC7YgBjgad0vkUph1peCJ+ntJYtixpAZOZj70aT/2CKjuGkmWFnL +ryrxBmpplHea1BwLJtrWgQEJcaFqiJ1lbGjinudo8dY5tZ7iyyZ+dk8+TKDsV/1p +yuAOemKYDY34jUf1LE525pSR/BcDGYCdzpBsOLsqRKf+5d8cZK05I6oYYKkAwoM3 +QwmwpLSm9poFUlo83uMDU5FH76hWFwkd5PE8oSbdKcp6d0AMh/L0TS6V9lGM58jW +9g71/v2LAgMBAAECggEAOnxjBox26FDNR5vb4neXJELwxOVWS/02xeOmGqdbTYAb +Xfu0a4L4rKas0EPkCoFQpyCLXuGE+mH3X7d4zf4WFcbdF49NXsh88EvNGgpqINiS +Hy6VkP/EsJ47a4O8cCGMhd5mqYe/M2JKLj3Yq11KdusrMiyC4l9Yjk0/e6ZZWKxe +/htw3fMPnHOMfoUB9jPy+SrhbFt42bZ7+JA2Aihf8RCUb/R7OjhASKeRLPkefJTA +Z6kJUoXoCBogjdkLCuVw1zjXF92R5gy+i5o9VhELHpg1D2If7CmeEM2FfVDfWjlF +iYlIR750OsKaeWB0LVKwh+07oyIlmO1TOECXEKt+gQKBgQDpdUfImWMrrkJ5AeCL +0NmO0JIZciGFBDTHRLOdzQiWKdq87i6/LWStK/gT+eb8WVLJ8vk5KrXFIy7TpYce +4jRr9u9MG14hjVemLMMoLhPkruPoulIp+Aj0mnhKephpJQ+Khd77g/GT6ZAqxkxi +drhTfKlSou0oEEd34ZuK7d6mCwKBgQC8UrbCErc+r4Ff48/0BfbJWZ7sN/HP/QtI +R2V9v3VpGqMX93v+YhQepxpd4PSkxluEmbbwYkA81un1ot7ndNkLIN0x59P9tVR0 +ghXuLmLwxExM5ekrfPt7gbkhwUCwRTogjocm6VoF541tn+ll724XtBdewhyXEUm7 +IG0/tLU2gQKBgQCx3avaNprq7bIxVW/Jtk361AdroZvOJx068KnUQSEYnzzLEsDE +4QXCNiyks5H7kuZTfG3K0zJ3xs1nbMacjgUYeKNqnbNC5tfvgE0TsL9xTJnRdxsg +ZJwWGBYr0GmMOjMz+7iecbE9WwZ+wGPz5LWcze6HSiBblMOOn3GNEJvAbwKBgHXS +3ksgAIv0rGH9Gz9Wd+fT7Y1nFyCE9gkbulDpd6DxrHazPV2TqXjgHav8sbNh8yJM +NdvB7OTjpW8snn97aMwAnMO7grOqPpPCS8xAM2Dlv8Mg2Th/Mqw8JkMLMNjYBx0V +b1OWDd/B1odu1E0VdvDXmQONOOv/Qf0UtaV0/yeBAoGAaUO9xrblsqEmP+2lqsiv +qwNLk5PICaoXwIacFdhc8YhC5OoXxRmLlyJmz72aKLTnNH+ZYsNYcJlhexhxpxq5 +vsxdk7rO741EDuFAk1Hsx1Q2G2+q6VWIWQhsn5eTGdas2qdNrKoNwN3wuALPPeeQ +l7yIJxi7Qn24xh+Bjdp1CDU= +-----END PRIVATE KEY----- diff --git a/spec/ssl/server-req.pem b/spec/ssl/server-req.pem new file mode 100644 index 000000000..1600bfe65 --- /dev/null +++ b/spec/ssl/server-req.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICZTCCAU0CAQAwIDEeMBwGA1UEAwwVbXlzcWwyZ2VtLmV4YW1wbGUuY29tMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq72O8qvgU/WUj/cjnQFX4Mdd +n95cHDv5uhfkf2ftG1Ur1bW2IjtcZ/t5fK8/ME+vnwVVayMabqRVD2WpW9AT9O45 +qoQu2IAY4GndL5FKYdaXgifp7SWLYsaQGTmY+9Gk/9gio7hpJlhZy68q8QZqaZR3 +mtQcCyba1oEBCXGhaoidZWxo4p7naPHWObWe4ssmfnZPPkyg7Ff9acrgDnpimA2N ++I1H9SxOduaUkfwXAxmAnc6QbDi7KkSn/uXfHGStOSOqGGCpAMKDN0MJsKS0pvaa +BVJaPN7jA1ORR++oVhcJHeTxPKEm3SnKendADIfy9E0ulfZRjOfI1vYO9f79iwID +AQABoAAwDQYJKoZIhvcNAQELBQADggEBAIAzuYloX5Pwi+5n73yhaw5V+jMABiuw +rYI2LziLBqw4vuvjEqvyr80Y9H2fLHOfVRaFnU6PaMkkH/p8d8YBD4/ZRGSd3oHX +hlNltqyTCx1LNIUCXkl18jfPK3sVwwC07cLqSxQP8diauaDE59F6lsP3L0Gbwntd +brSVJcY+5JGuWMx2BDXECxu6E7D/8drvPQa6EXDJjk1WVF/69I3TWhfX4/a5zIrc +bJDTRBl5yQA0dpPmr/d8Di4mqAqeecVPNXi/CWkDQl3PoBp7O69T6VG3R00krgQr +rXzbPJsEDLm7ynu/TWMamSCOUiMH5CBVBVXJSTVevGFK+gdjXf8LJ/0= +-----END CERTIFICATE REQUEST----- diff --git a/spec/test_data b/spec/test_data new file mode 100644 index 000000000..e5488b1b2 --- /dev/null +++ b/spec/test_data @@ -0,0 +1 @@ +\N Hello World diff --git a/support/3A79BD29.asc b/support/3A79BD29.asc new file mode 100644 index 000000000..d9e7a76f5 --- /dev/null +++ b/support/3A79BD29.asc @@ -0,0 +1,49 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: SKS 1.1.6 +Comment: Hostname: pgp.mit.edu + +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 +sTSKHe+QnnnoFmu4gnmDU31i +=Xqbo +-----END PGP PUBLIC KEY BLOCK----- diff --git a/support/5072E1F5.asc b/support/5072E1F5.asc new file mode 100644 index 000000000..281e134fb --- /dev/null +++ b/support/5072E1F5.asc @@ -0,0 +1,432 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1 + +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 +roUTAKClYAhZuX2nUNwH4vlEJQHDqYa5yQ== +=ghXk +-----END PGP PUBLIC KEY BLOCK----- diff --git a/support/B7B3B788A8D3785C.asc b/support/B7B3B788A8D3785C.asc new file mode 100644 index 000000000..9c9f79203 --- /dev/null +++ b/support/B7B3B788A8D3785C.asc @@ -0,0 +1,66 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Comment: Hostname: +Version: Hockeypuck 2.2 + +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 +73BwBlSVqPeJ3dPGq4CW53iVYPM= +=mheB +-----END PGP PUBLIC KEY BLOCK----- diff --git a/support/C74CD1D8.asc b/support/C74CD1D8.asc new file mode 100644 index 000000000..d35c5a492 --- /dev/null +++ b/support/C74CD1D8.asc @@ -0,0 +1,104 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=mCOI +-----END PGP PUBLIC KEY BLOCK----- diff --git a/support/libmysql.def b/support/libmysql.def new file mode 100644 index 000000000..614655a03 --- /dev/null +++ b/support/libmysql.def @@ -0,0 +1,219 @@ +; MySQL's Connector/C ships with a libmysql.dll main library and libmysql.lib +; interface library. However, the interface library is not linkable by MinGW. +; +; At compile time, we generate a libmysql.a interface library with dlltool.exe. +; +; This def file can be re-generated using the reimp.exe or gendef.exe tools. +; +LIBRARY libmysql.dll +EXPORTS +mysql_affected_rows +mysql_affected_rows@4 +mysql_change_user +mysql_change_user@16 +mysql_character_set_name +mysql_character_set_name@4 +mysql_close +mysql_close@4 +mysql_data_seek +mysql_data_seek@12 +mysql_debug +mysql_debug@4 +mysql_dump_debug_info +mysql_dump_debug_info@4 +mysql_eof +mysql_eof@4 +mysql_errno +mysql_errno@4 +mysql_error +mysql_error@4 +mysql_escape_string +mysql_escape_string@12 +mysql_fetch_field +mysql_fetch_field@4 +mysql_fetch_field_direct +mysql_fetch_field_direct@8 +mysql_fetch_fields +mysql_fetch_fields@4 +mysql_fetch_lengths +mysql_fetch_lengths@4 +mysql_fetch_row +mysql_fetch_row@4 +mysql_field_count +mysql_field_count@4 +mysql_field_seek +mysql_field_seek@8 +mysql_field_tell +mysql_field_tell@4 +mysql_free_result +mysql_free_result@4 +mysql_get_client_info +mysql_get_client_info@0 +mysql_get_client_version +mysql_get_client_version@0 +mysql_get_host_info +mysql_get_host_info@4 +mysql_get_option +mysql_get_option@12 +mysql_get_proto_info +mysql_get_proto_info@4 +mysql_get_server_info +mysql_get_server_info@4 +mysql_get_server_version +mysql_get_server_version@4 +mysql_get_ssl_cipher +mysql_get_ssl_cipher@4 +mysql_hex_string +mysql_hex_string@12 +mysql_info +mysql_info@4 +mysql_init +mysql_init@4 +mysql_insert_id +mysql_insert_id@4 +mysql_kill +mysql_kill@8 +mysql_library_end +mysql_library_end@0 +mysql_library_init +mysql_library_init@12 +mysql_list_dbs +mysql_list_dbs@8 +mysql_list_fields +mysql_list_fields@12 +mysql_list_processes +mysql_list_processes@4 +mysql_list_tables +mysql_list_tables@8 +mysql_more_results +mysql_more_results@4 +mysql_next_result +mysql_next_result@4 +mysql_num_fields +mysql_num_fields@4 +mysql_num_rows +mysql_num_rows@4 +mysql_options +mysql_options@12 +mysql_options4 +mysql_options4@16 +mysql_ping +mysql_ping@4 +mysql_query +mysql_query@8 +mysql_read_query_result +mysql_read_query_result@4 +mysql_real_connect +mysql_real_connect@32 +mysql_real_escape_string +mysql_real_escape_string@16 +mysql_real_query +mysql_real_query@12 +mysql_refresh +mysql_refresh@8 +mysql_reset_connection +mysql_reset_connection@4 +mysql_rollback +mysql_rollback@4 +mysql_row_seek +mysql_row_seek@8 +mysql_row_tell +mysql_row_tell@4 +mysql_select_db +mysql_select_db@8 +mysql_send_query +mysql_send_query@12 +mysql_server_end +mysql_server_end@0 +mysql_server_init +mysql_server_init@12 +mysql_session_track_get_first +mysql_session_track_get_first@16 +mysql_session_track_get_next +mysql_session_track_get_next@16 +mysql_set_character_set +mysql_set_character_set@8 +mysql_set_local_infile_default +mysql_set_local_infile_default@4 +mysql_set_local_infile_handler +mysql_set_local_infile_handler@24 +mysql_set_server_option +mysql_set_server_option@8 +mysql_shutdown +mysql_shutdown@8 +mysql_sqlstate +mysql_sqlstate@4 +mysql_ssl_set +mysql_ssl_set@24 +mysql_stat +mysql_stat@4 +mysql_stmt_affected_rows +mysql_stmt_affected_rows@4 +mysql_stmt_attr_get +mysql_stmt_attr_get@12 +mysql_stmt_attr_set +mysql_stmt_attr_set@12 +mysql_stmt_bind_param +mysql_stmt_bind_param@8 +mysql_stmt_bind_result +mysql_stmt_bind_result@8 +mysql_stmt_close +mysql_stmt_close@4 +mysql_stmt_data_seek +mysql_stmt_data_seek@12 +mysql_stmt_errno +mysql_stmt_errno@4 +mysql_stmt_error +mysql_stmt_error@4 +mysql_stmt_execute +mysql_stmt_execute@4 +mysql_stmt_fetch +mysql_stmt_fetch@4 +mysql_stmt_fetch_column +mysql_stmt_fetch_column@16 +mysql_stmt_field_count +mysql_stmt_field_count@4 +mysql_stmt_free_result +mysql_stmt_free_result@4 +mysql_stmt_init +mysql_stmt_init@4 +mysql_stmt_insert_id +mysql_stmt_insert_id@4 +mysql_stmt_next_result +mysql_stmt_next_result@4 +mysql_stmt_num_rows +mysql_stmt_num_rows@4 +mysql_stmt_param_count +mysql_stmt_param_count@4 +mysql_stmt_param_metadata +mysql_stmt_param_metadata@4 +mysql_stmt_prepare +mysql_stmt_prepare@12 +mysql_stmt_reset +mysql_stmt_reset@4 +mysql_stmt_result_metadata +mysql_stmt_result_metadata@4 +mysql_stmt_row_seek +mysql_stmt_row_seek@8 +mysql_stmt_row_tell +mysql_stmt_row_tell@4 +mysql_stmt_send_long_data +mysql_stmt_send_long_data@16 +mysql_stmt_sqlstate +mysql_stmt_sqlstate@4 +mysql_stmt_store_result +mysql_stmt_store_result@4 +mysql_store_result +mysql_store_result@4 +mysql_thread_end +mysql_thread_end@0 +mysql_thread_id +mysql_thread_id@4 +mysql_thread_init +mysql_thread_init@0 +mysql_thread_safe +mysql_thread_safe@0 +mysql_use_result +mysql_use_result@4 +mysql_warning_count +mysql_warning_count@4 diff --git a/support/mysql_enc_to_ruby.rb b/support/mysql_enc_to_ruby.rb index 4a3ef70db..7207981b9 100644 --- a/support/mysql_enc_to_ruby.rb +++ b/support/mysql_enc_to_ruby.rb @@ -33,6 +33,7 @@ "macroman" => "macRoman", "cp852" => "CP852", "latin7" => "ISO-8859-13", + "utf8mb3" => "UTF-8", "utf8mb4" => "UTF-8", "cp1251" => "Windows-1251", "utf16" => "UTF-16", @@ -42,18 +43,23 @@ "binary" => "ASCII-8BIT", "geostd8" => "NULL", "cp932" => "Windows-31J", - "eucjpms" => "eucJP-ms" + "eucjpms" => "eucJP-ms", + "utf16le" => "UTF-16LE", + "gb18030" => "GB18030", } -client = Mysql2::Client.new(:username => user, :password => pass, :host => host, :port => port.to_i) -collations = client.query "SHOW COLLATION", :as => :array +client = Mysql2::Client.new(username: user, password: pass, host: host, port: port.to_i) +collations = client.query "SHOW COLLATION", as: :array encodings = Array.new(collations.to_a.last[2].to_i) encodings_with_nil = Array.new(encodings.size) collations.each do |collation| mysql_col_idx = collation[2].to_i - rb_enc = mysql_to_rb[collation[1]] - encodings[mysql_col_idx-1] = [mysql_col_idx, rb_enc] + rb_enc = mysql_to_rb.fetch(collation[1]) do |mysql_enc| + warn "WARNING: Missing mapping for collation \"#{collation[0]}\" with encoding \"#{mysql_enc}\" and id #{mysql_col_idx}, assuming NULL" + "NULL" + end + encodings[mysql_col_idx - 1] = [mysql_col_idx, rb_enc] end encodings.each_with_index do |encoding, idx| @@ -65,10 +71,10 @@ end encodings_with_nil = encodings_with_nil.map do |encoding| - name = "NULL" - - if !encoding.nil? && encoding[1] != "NULL" - name = "\"#{encoding[1]}\"" + name = if encoding.nil? || encoding[1] == 'NULL' + 'NULL' + else + "\"#{encoding[1]}\"" end " #{name}" @@ -76,7 +82,6 @@ # start printing output -puts "const char *mysql2_mysql_enc_to_rb[] = {" +puts "static const char *mysql2_mysql_enc_to_rb[] = {" puts encodings_with_nil.join(",\n") puts "};" -puts diff --git a/support/ruby_enc_to_mysql.rb b/support/ruby_enc_to_mysql.rb index 112016c94..30dfe2651 100644 --- a/support/ruby_enc_to_mysql.rb +++ b/support/ruby_enc_to_mysql.rb @@ -28,6 +28,7 @@ "macroman" => "macRoman", "cp852" => "CP852", "latin7" => "ISO-8859-13", + "utf8mb3" => "UTF-8", "utf8mb4" => "UTF-8", "cp1251" => "Windows-1251", "utf16" => "UTF-16", @@ -37,10 +38,12 @@ "binary" => "ASCII-8BIT", "geostd8" => nil, "cp932" => "Windows-31J", - "eucjpms" => "eucJP-ms" + "eucjpms" => "eucJP-ms", + "utf16le" => "UTF-16LE", + "gb18030" => "GB18030", } -puts <<-header +puts <<-HEADER %readonly-tables %enum %define lookup-function-name mysql2_mysql_enc_name_to_rb @@ -48,13 +51,13 @@ %struct-type struct mysql2_mysql_enc_name_to_rb_map { const char *name; const char *rb_name; } %% -header +HEADER mysql_to_rb.each do |mysql, ruby| - if ruby.nil? - name = "NULL" + name = if ruby.nil? + "NULL" else - name = "\"#{ruby}\"" + "\"#{ruby}\"" end puts "#{mysql}, #{name}" diff --git a/tasks/benchmarks.rake b/tasks/benchmarks.rake index 33c85686c..b587ecdc0 100644 --- a/tasks/benchmarks.rake +++ b/tasks/benchmarks.rake @@ -1,11 +1,11 @@ BENCHMARKS = Dir["#{File.dirname(__FILE__)}/../benchmark/*.rb"].map do |path| File.basename(path, '.rb') -end.select { |x| x != 'setup_db' } +end - ['setup_db'] namespace :bench do BENCHMARKS.each do |feature| - desc "Run #{feature} benchmarks" - task(feature){ ruby "benchmark/#{feature}.rb" } + desc "Run #{feature} benchmarks" + task(feature) { ruby "benchmark/#{feature}.rb" } end task :all do @@ -17,4 +17,4 @@ namespace :bench do task :setup do ruby 'benchmark/setup_db' end -end \ No newline at end of file +end diff --git a/tasks/compile.rake b/tasks/compile.rake index e42b5f1b3..07aa1abea 100644 --- a/tasks/compile.rake +++ b/tasks/compile.rake @@ -1,33 +1,38 @@ require "rake/extensiontask" -CONNECTOR_VERSION = "6.0.2" #"mysql-connector-c-noinstall-6.0.2-win32.zip" -CONNECTOR_MIRROR = ENV['CONNECTOR_MIRROR'] || ENV['MYSQL_MIRROR'] || "http://mysql.he.net/" +load File.expand_path('../../mysql2.gemspec', __FILE__) unless defined? Mysql2::GEMSPEC -def gemspec - @clean_gemspec ||= eval(File.read(File.expand_path('../../mysql2.gemspec', __FILE__))) -end - -Rake::ExtensionTask.new("mysql2", gemspec) do |ext| - # reference where the vendored MySQL got extracted - connector_lib = File.expand_path(File.join(File.dirname(__FILE__), '..', 'vendor', "mysql-connector-c-noinstall-#{CONNECTOR_VERSION}-win32")) +Rake::ExtensionTask.new("mysql2", Mysql2::GEMSPEC) do |ext| + # put binaries into lib/mysql2/ or lib/mysql2/x.y/ + ext.lib_dir = File.join 'lib', 'mysql2' - # DRY options feed into compile or cross-compile process - windows_options = [ - "--with-mysql-include=#{connector_lib}/include", - "--with-mysql-lib=#{connector_lib}/lib" - ] + # clean compiled extension + CLEAN.include "#{ext.lib_dir}/*.#{RbConfig::CONFIG['DLEXT']}" - # automatically add build options to avoid need of manual input - if RUBY_PLATFORM =~ /mswin|mingw/ then - ext.config_options = windows_options + if RUBY_PLATFORM =~ /mswin|mingw/ && !defined?(RubyInstaller) + # Expand the path because the build dir is 3-4 levels deep in tmp/platform/version/ + connector_dir = File.expand_path("../../vendor/#{vendor_mysql_dir}", __FILE__) + ext.config_options = ["--with-mysql-dir=#{connector_dir}"] else ext.cross_compile = true - ext.cross_platform = ['x86-mingw32', 'x86-mswin32-60'] - ext.cross_config_options = windows_options + ext.cross_platform = ENV['CROSS_PLATFORMS'] ? ENV['CROSS_PLATFORMS'].split(':') : ['x86-mingw32', 'x86-mswin32-60', 'x64-mingw32'] + ext.cross_config_options << { + 'x86-mingw32' => "--with-mysql-dir=" + File.expand_path("../../vendor/#{vendor_mysql_dir('x86')}", __FILE__), + 'x86-mswin32-60' => "--with-mysql-dir=" + File.expand_path("../../vendor/#{vendor_mysql_dir('x86')}", __FILE__), + 'x64-mingw32' => "--with-mysql-dir=" + File.expand_path("../../vendor/#{vendor_mysql_dir('x64')}", __FILE__), + } - # inject 1.8/1.9 pure-ruby entry point when cross compiling only ext.cross_compiling do |spec| + Rake::Task['lib/mysql2/mysql2.rb'].invoke + # vendor/libmysql.dll is invoked from extconf.rb + Rake::Task['vendor/README'].invoke + + # only the source gem has a package dependency - the binary gem ships it's own DLL version + spec.metadata.delete('msys2_mingw_dependencies') + spec.files << 'lib/mysql2/mysql2.rb' + spec.files << 'vendor/libmysql.dll' + spec.files << 'vendor/README' spec.post_install_message = <<-POST_INSTALL_MESSAGE ====================================================================================================== @@ -36,36 +41,65 @@ Rake::ExtensionTask.new("mysql2", gemspec) do |ext| It was built using MySQL Connector/C version #{CONNECTOR_VERSION}. It's recommended to use the exact same version to avoid potential issues. - At the time of building this gem, the necessary DLL files where available - in the following download: - - http://dev.mysql.com/get/Downloads/Connector-C/mysql-connector-c-noinstall-#{CONNECTOR_VERSION}-win32.zip/from/pick + At the time of building this gem, the necessary DLL files were retrieved from: + #{vendor_mysql_url(http://github.com/spec.platform)} - And put lib\\libmysql.dll file in your Ruby bin directory, for example C:\\Ruby\\bin + This gem *includes* vendor/libmysql.dll with redistribution notice in vendor/README. ====================================================================================================== POST_INSTALL_MESSAGE end end - - ext.lib_dir = File.join 'lib', 'mysql2' - - # clean compiled extension - CLEAN.include "#{ext.lib_dir}/*.#{RbConfig::CONFIG['DLEXT']}" end Rake::Task[:spec].prerequisites << :compile +file 'vendor/README' do + connector_dir = File.expand_path("../../vendor/#{vendor_mysql_dir}", __FILE__) + when_writing 'copying Connector/C README' do + cp "#{connector_dir}/README", 'vendor/README' + end +end + file 'lib/mysql2/mysql2.rb' do |t| - name = gemspec.name + name = Mysql2::GEMSPEC.name File.open(t.name, 'wb') do |f| - f.write <<-eoruby + f.write <<-END_OF_RUBY RUBY_VERSION =~ /(\\d+.\\d+)/ require "#{name}/\#{$1}/#{name}" - eoruby + END_OF_RUBY end end -if Rake::Task.task_defined?(:cross) - Rake::Task[:cross].prerequisites << "lib/mysql2/mysql2.rb" +# DevKit task following the example of Luis Lavena's test-ruby-c-extension +task :devkit do + begin + require "devkit" + rescue LoadError + abort "Failed to activate RubyInstaller's DevKit required for compilation." + end +end + +if RUBY_PLATFORM =~ /mingw|mswin/ + Rake::Task['compile'].prerequisites.unshift 'vendor:mysql' unless defined?(RubyInstaller) + Rake::Task['compile'].prerequisites.unshift 'devkit' +elsif Rake::Task.tasks.map(&:name).include? 'cross' + Rake::Task['cross'].prerequisites.unshift 'vendor:mysql:cross' +end + +desc "Build binary gems for Windows with rake-compiler-dock" +task 'gem:windows' do + require 'rake_compiler_dock' + RakeCompilerDock.sh <<-EOT + bundle install + rake clean + rm vendor/libmysql.dll + rake cross native gem CROSS_PLATFORMS=x86-mingw32:x86-mswin32-60 + EOT + RakeCompilerDock.sh <<-EOT + bundle install + rake clean + rm vendor/libmysql.dll + rake cross native gem CROSS_PLATFORMS=x64-mingw32 + EOT end diff --git a/tasks/rspec.rake b/tasks/rspec.rake index c4173f6e8..efff7a25c 100644 --- a/tasks/rspec.rake +++ b/tasks/rspec.rake @@ -2,25 +2,61 @@ begin require 'rspec' require 'rspec/core/rake_task' + desc " Run all examples with Valgrind" + namespace :spec do + task :valgrind do + VALGRIND_OPTS = %w[ + --num-callers=50 + --error-limit=no + --partial-loads-ok=yes + --undef-value-errors=no + --trace-children=yes + ].freeze + cmdline = "valgrind #{VALGRIND_OPTS.join(' ')} bundle exec rake spec" + puts cmdline + system cmdline + end + end + desc "Run all examples with RCov" RSpec::Core::RakeTask.new('spec:rcov') do |t| t.rcov = true end + RSpec::Core::RakeTask.new('spec') do |t| t.verbose = true end - - task :default => :spec rescue LoadError puts "rspec, or one of its dependencies, is not available. Install it with: sudo gem install rspec" end +# Get the value from `id` command as the environment variable USER is +# not defined in a container. +user_name = ENV['USER'] || `id -un`.rstrip + file 'spec/configuration.yml' => 'spec/configuration.yml.example' do |task| CLEAN.exclude task.name src_path = File.expand_path("../../#{task.prerequisites.first}", __FILE__) dst_path = File.expand_path("../../#{task.name}", __FILE__) - cp src_path, dst_path - sh "sed -i 's/LOCALUSERNAME/#{ENV['USER']}/' #{dst_path}" + + File.open(dst_path, 'w') do |dst_file| + File.open(src_path).each_line do |line| + dst_file.write line.gsub(/LOCALUSERNAME/, user_name) + end + end +end + +file 'spec/my.cnf' => 'spec/my.cnf.example' do |task| + CLEAN.exclude task.name + src_path = File.expand_path("../../#{task.prerequisites.first}", __FILE__) + dst_path = File.expand_path("../../#{task.name}", __FILE__) + + File.open(dst_path, 'w') do |dst_file| + File.open(src_path).each_line do |line| + dst_file.write line.gsub(/LOCALUSERNAME/, user_name) + end + end end Rake::Task[:spec].prerequisites << :'spec/configuration.yml' +Rake::Task[:spec].prerequisites << :'spec/my.cnf' diff --git a/tasks/vendor_mysql.rake b/tasks/vendor_mysql.rake index ce7684065..85e88fe68 100644 --- a/tasks/vendor_mysql.rake +++ b/tasks/vendor_mysql.rake @@ -1,40 +1,67 @@ require 'rake/clean' require 'rake/extensioncompiler' -# download mysql library and headers -directory "vendor" - -file "vendor/mysql-connector-c-noinstall-#{CONNECTOR_VERSION}-win32.zip" => ["vendor"] do |t| - url = "http://dev.mysql.com/get/Downloads/Connector-C/mysql-connector-c-noinstall-#{CONNECTOR_VERSION}-win32.zip/from/#{CONNECTOR_MIRROR}/" - when_writing "downloading #{t.name}" do - cd File.dirname(t.name) do - sh "wget -c #{url} || curl -C - -O #{url}" - end - end +CONNECTOR_VERSION = "6.1.11".freeze # NOTE: Track the upstream version from time to time + +def vendor_mysql_platform(platform = nil) + platform ||= RUBY_PLATFORM + platform =~ /x64/ ? "winx64" : "win32" end -file "vendor/mysql-connector-c-noinstall-#{CONNECTOR_VERSION}-win32/include/mysql.h" => ["vendor/mysql-connector-c-noinstall-#{CONNECTOR_VERSION}-win32.zip"] do |t| - full_file = File.expand_path(t.prerequisites.last) - when_writing "creating #{t.name}" do - cd "vendor" do - sh "unzip #{full_file} mysql-connector-c-noinstall-#{CONNECTOR_VERSION}-win32/bin/** mysql-connector-c-noinstall-#{CONNECTOR_VERSION}-win32/include/** mysql-connector-c-noinstall-#{CONNECTOR_VERSION}-win32/lib/**" - end - # update file timestamp to avoid Rake perform this extraction again. - touch t.name - end +def vendor_mysql_dir(*args) + "mysql-connector-c-#{CONNECTOR_VERSION}-#{vendor_mysql_platform(*args)}" end -# clobber expanded packages -CLOBBER.include("vendor/mysql-connector-c-noinstall-#{CONNECTOR_VERSION}-win32") +def vendor_mysql_zip(*args) + "#{vendor_mysql_dir(*args)}.zip" +end + +def vendor_mysql_url(http://github.com/*args) + "http://cdn.mysql.com/Downloads/Connector-C/#{vendor_mysql_zip(*args)}" +end # vendor:mysql -task 'vendor:mysql' => ["vendor/mysql-connector-c-noinstall-#{CONNECTOR_VERSION}-win32/include/mysql.h"] - -# hook into cross compilation vendored mysql dependency -if RUBY_PLATFORM =~ /mingw|mswin/ then - Rake::Task['compile'].prerequisites.unshift 'vendor:mysql' -else - if Rake::Task.tasks.map {|t| t.name }.include? 'cross' - Rake::Task['cross'].prerequisites.unshift 'vendor:mysql' +task "vendor:mysql:cross" do + # When cross-compiling, grab both 32 and 64 bit connectors + Rake::Task['vendor:mysql'].invoke('x86') + Rake::Task['vendor:mysql'].invoke('x64') +end + +task "vendor:mysql", [:platform] do |_t, args| + puts "vendor:mysql for #{vendor_mysql_dir(args[:platform])}" + + # download mysql library and headers + directory "vendor" + + file "vendor/#{vendor_mysql_zip(args[:platform])}" => ["vendor"] do |t| + url = vendor_mysql_url(http://github.com/args[:platform]) + when_writing "downloading #{t.name}" do + cd "vendor" do + sh "curl", "-C", "-", "-O", url do |ok| + sh "wget", "-c", url unless ok + end + end + end end + + file "vendor/#{vendor_mysql_dir(args[:platform])}/include/mysql.h" => ["vendor/#{vendor_mysql_zip(args[:platform])}"] do |t| + full_file = File.expand_path(t.prerequisites.last) + when_writing "creating #{t.name}" do + cd "vendor" do + sh "unzip", "-uq", full_file, + "#{vendor_mysql_dir(args[:platform])}/bin/**", + "#{vendor_mysql_dir(args[:platform])}/include/**", + "#{vendor_mysql_dir(args[:platform])}/lib/**", + "#{vendor_mysql_dir(args[:platform])}/README" # contains the license info + end + # update file timestamp to avoid Rake performing this extraction again. + touch t.name + end + end + + # clobber expanded packages + CLOBBER.include("vendor/#{vendor_mysql_dir(args[:platform])}") + + Rake::Task["vendor/#{vendor_mysql_dir(args[:platform])}/include/mysql.h"].invoke + Rake::Task["vendor:mysql"].reenable # allow task to be invoked again (with another platform) end