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..34a04ca4d --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,71 @@ +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 22.04 LTS (latest at this time) + - {os: ubuntu-22.04, ruby: 'head', 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} + + # 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-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} + - {os: ubuntu-20.04, ruby: '2.7', db: mysql80} + - {os: ubuntu-18.04, ruby: '2.7', db: mysql57} + + # 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: '2.6', db: mariadb, ssl: openssl@1.1, allow-failure: true} + - {os: macos-latest, ruby: '2.6', db: mysql, 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..af57ef136 --- /dev/null +++ b/.github/workflows/container.yml @@ -0,0 +1,36 @@ +# 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'} + # 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=mysql2gem.example.com:127.0.0.1 \ + -t \ + -e TEST_RUBY_MYSQL2_SSL_CERT_DIR="${{ matrix.ssl_cert_dir || '' }}" \ + --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..d66266dd6 --- /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@v3 + - name: Set up Ruby 2.6 + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.6 + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + - name: Run RuboCop + run: bundle exec rubocop diff --git a/.rubocop.yml b/.rubocop.yml index d41d8d923..856e99da9 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -2,6 +2,8 @@ inherit_from: .rubocop_todo.yml AllCops: TargetRubyVersion: 2.0 + SuggestExtensions: false + NewCops: disable DisplayCopNames: true Exclude: @@ -12,16 +14,22 @@ AllCops: Layout/CaseIndentation: EnforcedStyle: end -Layout/IndentHash: +Layout/FirstHashElementIndentation: EnforcedStyle: consistent -Lint/EndAlignment: +Layout/EndAlignment: EnforcedStyleAlignWith: variable +Layout/HashAlignment: + EnforcedHashRocketStyle: table + Style/TrailingCommaInArguments: EnforcedStyleForMultiline: consistent_comma -Style/TrailingCommaInLiteral: +Style/TrailingCommaInArrayLiteral: + EnforcedStyleForMultiline: consistent_comma + +Style/TrailingCommaInHashLiteral: EnforcedStyleForMultiline: consistent_comma Style/TrivialAccessors: diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 3da4399a2..eba2091dc 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,28 +1,41 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2017-11-25 19:54:28 -0500 using RuboCop version 0.50.0. +# on 2022-05-30 13:48:55 UTC using RuboCop version 1.30.0. # 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: 3 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles. -# SupportedStyles: auto_detection, squiggly, active_support, powerpack, unindent -Layout/IndentHeredoc: +# This cop supports safe autocorrection (--autocorrect). +Layout/HeredocIndentation: Exclude: - 'support/ruby_enc_to_mysql.rb' - 'tasks/compile.rake' # Offense count: 2 +# Configuration parameters: AllowedMethods. +# AllowedMethods: enums +Lint/ConstantDefinitionInBlock: + Exclude: + - 'spec/mysql2/client_spec.rb' + - 'tasks/rspec.rake' + +# Offense count: 1 +Lint/MissingSuper: + Exclude: + - 'lib/mysql2/em.rb' + +# Offense count: 2 +# Configuration parameters: IgnoredMethods, CountRepeatedAttributes. Metrics/AbcSize: - Max: 90 + Max: 94 -# Offense count: 31 -# Configuration parameters: CountComments, ExcludedMethods. +# Offense count: 34 +# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. +# IgnoredMethods: refine Metrics/BlockLength: - Max: 850 + Max: 592 # Offense count: 1 # Configuration parameters: CountBlocks. @@ -30,44 +43,46 @@ Metrics/BlockNesting: Max: 5 # Offense count: 1 -# Configuration parameters: CountComments. +# Configuration parameters: CountComments, CountAsOne. Metrics/ClassLength: - Max: 125 + Max: 135 # Offense count: 3 +# Configuration parameters: IgnoredMethods. Metrics/CyclomaticComplexity: - Max: 30 - -# Offense count: 313 -# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. -# URISchemes: http, https -Metrics/LineLength: - Max: 232 + Max: 34 # Offense count: 6 -# Configuration parameters: CountComments. +# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. Metrics/MethodLength: Max: 57 # Offense count: 2 +# Configuration parameters: IgnoredMethods. Metrics/PerceivedComplexity: - Max: 27 + Max: 32 -# Offense count: 3 -# Configuration parameters: Blacklist. -# Blacklist: END, (?-mix:EO[A-Z]{1}) +# Offense count: 2 +# Configuration parameters: ForbiddenDelimiters. +# ForbiddenDelimiters: (?-mix:(^|\s)(EO[A-Z]{1}|END)(\s|$)) Naming/HeredocDelimiterNaming: Exclude: - 'tasks/compile.rake' -# Offense count: 10 +# Offense count: 1 +# This cop supports unsafe autocorrection (--autocorrect-all). +Style/CaseLikeIf: + Exclude: + - 'ext/mysql2/extconf.rb' + +# Offense count: 8 +# Configuration parameters: AllowedConstants. Style/Documentation: Exclude: - 'spec/**/*' - 'test/**/*' - 'benchmark/active_record.rb' - 'benchmark/allocations.rb' - - 'benchmark/query_with_mysql_casting.rb' - 'lib/mysql2.rb' - 'lib/mysql2/client.rb' - 'lib/mysql2/em.rb' @@ -75,21 +90,78 @@ Style/Documentation: - 'lib/mysql2/result.rb' - 'lib/mysql2/statement.rb' -# Offense count: 14 +# 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: 15 # Configuration parameters: AllowedVariables. Style/GlobalVars: Exclude: - 'ext/mysql2/extconf.rb' -# Offense count: 17 -# Cop supports --auto-correct. -# Configuration parameters: Strict. +# Offense count: 8 +# 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. +Style/MultipleComparison: + Exclude: + - 'lib/mysql2/client.rb' + +# Offense count: 18 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: Strict, AllowedNumbers. Style/NumericLiterals: MinDigits: 20 -# Offense count: 726 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle, SupportedStyles, ConsistentQuotesInMultiline. +# 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: 782 +# 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: 32 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, IgnoredPatterns. +# URISchemes: http, https +Layout/LineLength: + Max: 232 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a8661f785..000000000 --- a/.travis.yml +++ /dev/null @@ -1,81 +0,0 @@ -sudo: required -dist: trusty -language: ruby -bundler_args: --without benchmarks development -# Pin Rubygems to a working version. Sometimes it breaks upstream. Update now and then. -before_install: - - gem --version - - gem update --system 2.7.6 --quiet - - gem update bundler - - gem --version - - bash .travis_setup.sh -addons: - hosts: - - mysql2gem.example.com - apt: - packages: - - mysql-server-5.6 - - mysql-client-core-5.6 - - mysql-client-5.6 -rvm: - - 2.6 - - 2.5 - - 2.4 - - 2.3 - - 2.2 - - 2.1 - - 2.0.0 - - ruby-head -matrix: - include: - - rvm: 2.4 - env: DB=mariadb10.0 - addons: - mariadb: 10.0 - hosts: - - mysql2gem.example.com - - rvm: 2.4 - env: DB=mariadb10.1 - addons: - mariadb: 10.1 - hosts: - - mysql2gem.example.com - - rvm: 2.4 - env: DB=mariadb10.2 - addons: - mariadb: 10.2 - hosts: - - mysql2gem.example.com - - rvm: 2.4 - env: DB=mariadb10.3 - addons: - mariadb: 10.3 - hosts: - - mysql2gem.example.com - - rvm: 2.4 - env: DB=mysql55 - addons: - hosts: - - mysql2gem.example.com - - rvm: 2.4 - env: DB=mysql57 - addons: - hosts: - - mysql2gem.example.com - - rvm: 2.4 - env: DB=mysql80 - addons: - hosts: - - mysql2gem.example.com - - os: osx - rvm: 2.4 - env: DB=mysql56 - addons: - hosts: - - mysql2gem.example.com - fast_finish: true - allow_failures: - - rvm: ruby-head - - os: osx - rvm: 2.4 - env: DB=mysql56 diff --git a/.travis_mysql57.sh b/.travis_mysql57.sh deleted file mode 100644 index ab0df12c4..000000000 --- a/.travis_mysql57.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/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 -add-apt-repository 'deb http://repo.mysql.com/apt/ubuntu/ trusty mysql-5.7' -apt-get update -qq -apt-get install -qq mysql-server libmysqlclient-dev - -# https://www.percona.com/blog/2016/03/16/change-user-password-in-mysql-5-7-with-plugin-auth_socket/ -mysql -u root -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY ''" diff --git a/.travis_mysql80.sh b/.travis_mysql80.sh deleted file mode 100644 index 70b6c8f86..000000000 --- a/.travis_mysql80.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/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 -add-apt-repository 'deb http://repo.mysql.com/apt/ubuntu/ trusty mysql-8.0' -apt-get update -qq -apt-get install -qq mysql-server libmysqlclient-dev - -# https://www.percona.com/blog/2016/03/16/change-user-password-in-mysql-5-7-with-plugin-auth_socket/ -mysql -u root -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY ''" diff --git a/.travis_setup.sh b/.travis_setup.sh deleted file mode 100644 index 6b68bdacd..000000000 --- a/.travis_setup.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash - -set -eux - -# Install MySQL 5.5 if DB=mysql55 -if [[ -n ${DB-} && x$DB =~ ^xmysql55 ]]; then - sudo bash .travis_mysql55.sh -fi - -# Install MySQL 5.7 if DB=mysql57 -if [[ -n ${DB-} && x$DB =~ ^xmysql57 ]]; then - sudo bash .travis_mysql57.sh -fi - -# Install MySQL 8.0 if DB=mysql80 -if [[ -n ${DB-} && x$DB =~ ^xmysql80 ]]; then - sudo bash .travis_mysql80.sh -fi - -# Install MariaDB client headers after Travis CI fix for MariaDB 10.2 broke earlier 10.x -if [[ -n ${DB-} && x$DB =~ ^xmariadb10.0 ]]; then - sudo apt-get install -y -o Dpkg::Options::='--force-confnew' libmariadbclient-dev -fi - -# Install MariaDB client headers after Travis CI fix for MariaDB 10.2 broke earlier 10.x -if [[ -n ${DB-} && x$DB =~ ^xmariadb10.1 ]]; then - sudo apt-get install -y -o Dpkg::Options::='--force-confnew' libmariadbclient-dev -fi - -# Install MariaDB 10.2 if DB=mariadb10.2 -# NOTE this is a workaround until Travis CI merges a fix to its mariadb addon. -if [[ -n ${DB-} && x$DB =~ ^xmariadb10.2 ]]; then - sudo apt-get install -y -o Dpkg::Options::='--force-confnew' mariadb-server mariadb-server-10.2 libmariadbclient18 -fi - -# Install MySQL if OS=darwin -if [[ x$OSTYPE =~ ^xdarwin ]]; then - brew update - brew install "$DB" mariadb-connector-c - $(brew --prefix "$DB")/bin/mysql.server start -fi - -# TODO: get SSL working on OS X in Travis -if ! [[ x$OSTYPE =~ ^xdarwin ]]; then - sudo bash .travis_ssl.sh - sudo service mysql restart -fi - -# Print the MySQL version and create the test DB -if [[ x$OSTYPE =~ ^xdarwin ]]; then - $(brew --prefix "$DB")/bin/mysqld --version - $(brew --prefix "$DB")/bin/mysql -u root -e 'CREATE DATABASE IF NOT EXISTS test' -else - mysqld --version - # IF NOT EXISTS is mariadb-10+ only - https://mariadb.com/kb/en/mariadb/comment-syntax/ - mysql -u root -e 'CREATE DATABASE /*M!50701 IF NOT EXISTS */ test' -fi diff --git a/Gemfile b/Gemfile index 5d9c98491..7b4e1b8a7 100644 --- a/Gemfile +++ b/Gemfile @@ -2,32 +2,42 @@ source 'https://rubygems.org' gemspec -gem 'rake', '~> 10.4.2' -gem 'rake-compiler', '~> 1.0' +if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.2") + gem 'rake', '~> 13.0.1' +else + gem 'rake', '< 13' +end +gem 'rake-compiler', '~> 1.1.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' + # https://github.com/bbatsov/rubocop/pull/4789 - gem 'rubocop', '~> 0.50.0' + gem 'rubocop', '~> 1.30', '>= 1.30.1' if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.6') end -group :benchmarks do +group :benchmarks, optional: true do gem 'activerecord', '>= 3.0' gem 'benchmark-ips' gem 'do_mysql' gem 'faker' - gem 'mysql' + # 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.6.0' + gem 'rake-compiler-dock', '~> 0.7.0' end -platforms :rbx do - gem 'rubysl-bigdecimal' - gem 'rubysl-drb' - gem 'rubysl-rake' -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/README.md b/README.md index bd7b5dc64..cf35222d5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,12 @@ # Mysql2 - A modern, simple and very fast MySQL library for Ruby - binding to libmysql -Travis CI [![Travis CI Status](https://travis-ci.org/brianmario/mysql2.png)](https://travis-ci.org/brianmario/mysql2) -Appveyor CI [![Appveyor CI Status](https://ci.appveyor.com/api/projects/status/github/sodabrew/mysql2)](https://ci.appveyor.com/project/sodabrew/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) +Travis CI +[![Travis CI Status](https://travis-ci.org/brianmario/mysql2.png)](https://travis-ci.org/brianmario/mysql2) +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 APIs available. @@ -18,16 +23,18 @@ The API consists of three classes: `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` library or `Connector/C` library, and compatible alternatives such as MariaDB. -You may need to install a package such as `libmysqlclient-dev`, `mysql-devel`, -or other appropriate package for your system. See below for system-specific -instructions. +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: @@ -58,6 +65,11 @@ 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 @@ -74,22 +86,58 @@ To see line numbers in backtraces, declare these environment variables ### Linux and other Unixes -You may need to install a package such as `libmysqlclient-dev`, `mysql-devel`, -or `default-libmysqlclient-dev`; refer to your distribution's package guide to +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. ### Mac OS X -You may use MacPorts, Homebrew, or a native MySQL installer package. The most +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, 1.0 or 1.1 or 3.0, since only one can be loaded at runtime. + +``` sh +$ brew install openssl@1.1 +$ gem install mysql2 -- --with-openssl-dir=$(brew --prefix openssl@1.1) + +or + +$ sudo port install openssl11 +``` + +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@1.1) +``` + +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 addiitonal arguments that are passed +to the mysql2 build process. + ### Windows + Make sure that you have Ruby and the DevKit compilers installed. We recommend the [Ruby Installer](http://rubyinstaller.org) distribution. @@ -140,7 +188,7 @@ results.each do |row| # the keys are the fields, as you'd expect # the values are pre-built ruby primitives mapped from their corresponding field types in MySQL puts row["id"] # row["id"].is_a? Integer - if row["dne"] # non-existant hash entry is nil + if row["dne"] # non-existent hash entry is nil puts row["dne"] end end @@ -157,16 +205,17 @@ end How about with symbolized keys? ``` ruby -client.query("SELECT * FROM users WHERE group='githubbers'", :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" @@ -180,7 +229,7 @@ question marks in the statement. Query options can be passed as keyword argument 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](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 = ?") @@ -194,6 +243,23 @@ statement = @client.prepare("SELECT * FROM users WHERE last_login >= ? AND locat 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(...): @@ -215,9 +281,9 @@ Mysql2::Client.new( :reconnect = true/false, :local_infile = true/false, :secure_auth = true/false, - :ssl_mode = :disabled / :preferred / :required / :verify_ca / :verify_identity, :default_file = '/path/to/my.cfg', :default_group = 'my.cfg section', + :default_auth = 'authentication_windows_client' :init_command => sql ) ``` @@ -235,14 +301,13 @@ type of connection to make, with special interpretation you should be aware of: * 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 options +### SSL/TLS options -Setting any of the following options will enable an SSL 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. Set `:sslverify => true` to require that the -server presents a valid certificate. +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( @@ -252,13 +317,32 @@ Mysql2::Client.new( :sslca => '/path/to/ca-cert.pem', :sslcapath => '/path/to/cacerts', :sslcipher => 'DHE-RSA-AES256-SHA', - :sslverify => true, + :sslverify => true, # Removed in MySQL 8.0 + :ssl_mode => :disabled / :preferred / :required / :verify_ca / :verify_identity, ) ``` +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 wih MySQL 5.6.5, secure_auth is enabled by default on servers (it was disabled by default prior to this). +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(). @@ -274,8 +358,10 @@ 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. -This allows easier use with ActiveRecord's database.yml, avoiding the need for magic flag numbers. -For example, to disable protocol compression, and enable multiple statements and result sets: +### 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: @@ -293,6 +379,17 @@ development: 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 @@ -347,7 +444,8 @@ end ``` Yields: -``` + +``` ruby {"1"=>1} {"2"=>2} next_result: Unknown column 'A' in 'field list' (Mysql2::Error) @@ -407,7 +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 +The default result type is set to `:hash`, but you can override a previous setting to something else with `:as => :hash` ### Timezones @@ -506,7 +604,7 @@ 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 @@ -527,21 +625,23 @@ As for field values themselves, I'm workin on it - but expect that soon. This gem is tested with the following Ruby versions on Linux and Mac OS X: - * Ruby MRI 2.0.0, 2.1.x, 2.2.x, 2.3.x, 2.4.x, 2.5.x, 2.6.x - * Rubinius 2.x and 3.x do work but may fail under some workloads +* 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 This gem is tested with the following MySQL and MariaDB versions: - * MySQL 5.5, 5.6, 5.7, 8.0 - * MySQL Connector/C 6.0 and 6.1 (primarily on Windows) - * MariaDB 5.5, 10.0, 10.1, 10.2, 10.3 +* 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 ### Ruby on Rails / Active Record - * mysql2 0.5.x works with Rails / Active Record 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. +* 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 @@ -624,11 +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 -* 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 +* [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/appveyor.yml b/appveyor.yml index 274bc6d80..cde312693 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,7 +7,7 @@ install: - ruby --version - gem --version - bundler --version - - bundle install --without benchmarks --path vendor/bundle + - bundle install --path vendor/bundle - IF DEFINED MINGW_PACKAGE_PREFIX (ridk exec pacman -S --noconfirm --needed %MINGW_PACKAGE_PREFIX%-libmariadbclient) build_script: - bundle exec rake compile @@ -25,6 +25,10 @@ on_failure: - find tmp -name "*.log" | xargs cat environment: matrix: + - ruby_version: "26-x64" + MINGW_PACKAGE_PREFIX: "mingw-w64-x86_64" + - ruby_version: "25-x64" + MINGW_PACKAGE_PREFIX: "mingw-w64-x86_64" - ruby_version: "24-x64" MINGW_PACKAGE_PREFIX: "mingw-w64-x86_64" - ruby_version: "24" diff --git a/benchmark/allocations.rb b/benchmark/allocations.rb index 7926a837d..cc6df4d18 100644 --- a/benchmark/allocations.rb +++ b/benchmark/allocations.rb @@ -16,7 +16,7 @@ def bench_allocations(feature, iterations = 10, batch_size = 1000) GC::Profiler.clear GC::Profiler.enable iterations.times { yield batch_size } - GC::Profiler.report(STDOUT) + GC::Profiler.report($stdout) GC::Profiler.disable end diff --git a/ci/Dockerfile_centos b/ci/Dockerfile_centos new file mode 100644 index 000000000..2e07e31ea --- /dev/null +++ b/ci/Dockerfile_centos @@ -0,0 +1,25 @@ +ARG IMAGE=centos:7 +FROM ${IMAGE} + +WORKDIR /build +COPY . . + +RUN cat /etc/redhat-release +RUN yum -yq update +RUN yum -yq install epel-release +# The options are to install faster. +RUN yum -yq 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 +RUN 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..5d595f847 --- /dev/null +++ b/ci/Dockerfile_fedora @@ -0,0 +1,27 @@ +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 \ + 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..90552a919 --- /dev/null +++ b/ci/container.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -eux + +ruby -v +bundle install --path vendor/bundle --without development + +# 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..3122bb279 --- /dev/null +++ b/ci/mariadb1011.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -eux + +apt purge -qq '^mysql*' '^libmysql*' +rm -fr /etc/mysql +rm -fr /var/lib/mysql + +apt-key add support/C74CD1D8.asc +add-apt-repository "deb https://deb.mariadb.org/10.11/ubuntu $(lsb_release -cs) main" +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..b6c7153bc --- /dev/null +++ b/ci/mariadb106.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -eux + +apt purge -qq '^mysql*' '^libmysql*' +rm -fr /etc/mysql +rm -fr /var/lib/mysql + +apt-key add support/C74CD1D8.asc +add-apt-repository "deb https://deb.mariadb.org/10.6/ubuntu $(lsb_release -cs) main" +apt install -y -o Dpkg::Options::='--force-confnew' mariadb-server libmariadb-dev diff --git a/.travis_mysql55.sh b/ci/mysql55.sh similarity index 100% rename from .travis_mysql55.sh rename to ci/mysql55.sh diff --git a/ci/mysql57.sh b/ci/mysql57.sh new file mode 100644 index 000000000..8c8928315 --- /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 adv --keyserver keyserver.ubuntu.com --recv-keys B7B3B788A8D3785C +# 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 mysql-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..ae7e88313 --- /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 adv --keyserver keyserver.ubuntu.com --recv-keys B7B3B788A8D3785C +# 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 mysql-8.0' +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..956608c69 --- /dev/null +++ b/ci/setup.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash + +set -eux + +# Change the password to be empty. +CHANGED_PASSWORD=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=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 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" + 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_BY_RECREATE}" = true ]; then + sudo mysql ${MYSQL_OPTS} -u "${DB_SYS_USER}" </dev/null | xargs dirname) @@ -14,7 +17,7 @@ cd $(my_print_defaults --help | grep my.cnf | xargs find 2>/dev/null | xargs dir # Put the configs into the server echo " [mysqld] -ssl-ca=/etc/mysql/ca-cert.pem -ssl-cert=/etc/mysql/server-cert.pem -ssl-key=/etc/mysql/server-key.pem +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/ext/mysql2/client.c b/ext/mysql2/client.c index d12f2c180..5a2fcc1bd 100644 --- a/ext/mysql2/client.c +++ b/ext/mysql2/client.c @@ -18,7 +18,8 @@ VALUE cMysql2Client; 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; +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) { \ @@ -44,7 +45,7 @@ static ID intern_brackets, intern_merge, intern_merge_bang, intern_new_with_args } /* - * compatability with mysql-connector-c, where LIBMYSQL_VERSION is the correct + * 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 */ @@ -57,8 +58,23 @@ static ID intern_brackets, intern_merge, intern_merge_bang, intern_new_with_args #endif /* - * compatibility with mysql-connector-c 6.1.x, and with MySQL 5.7.3 - 5.7.10. + * 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 @@ -104,40 +120,80 @@ struct nogvl_select_db_args { 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(); - if (version < 50703) { - rb_warn( "Your mysql client library does not support setting ssl_mode; full support comes with 5.7.11." ); + /* 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; } -#ifdef HAVE_CONST_MYSQL_OPT_SSL_ENFORCE + + /* 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 ); - if (version >= 50703 && version < 50711) { + 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 ); + my_bool b = (val == SSL_MODE_REQUIRED); + int result = mysql_options(wrapper->client, MYSQL_OPT_SSL_ENFORCE, &b); return INT2NUM(result); - } else { - rb_warn( "MySQL client libraries between 5.7.3 and 5.7.10 only support SSL_MODE_DISABLED and SSL_MODE_REQUIRED" ); - return Qnil; } +#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 ); + 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 ); + 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 @@ -163,14 +219,50 @@ static VALUE rb_set_ssl_mode_option(VALUE self, VALUE setting) { 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); + } +} + +/* this is called during GC */ +static void rb_mysql_client_free(void *ptr) { + mysql_client_wrapper *wrapper = ptr; + decr_mysql2_client(wrapper); +} + +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_tainted_str_new2(mysql_sqlstate(wrapper->client)); + VALUE rb_sql_state = rb_str_new2(mysql_sqlstate(wrapper->client)); VALUE e; rb_enc_associate(rb_error_msg, rb_utf8_encoding()); @@ -264,22 +356,16 @@ static VALUE invalidate_fd(int clientfd) static void *nogvl_close(void *ptr) { mysql_client_wrapper *wrapper = ptr; - if (!wrapper->closed) { + if (wrapper->initialized && !wrapper->closed) { mysql_close(wrapper->client); wrapper->closed = 1; wrapper->reconnect_enabled = 0; - wrapper->active_thread = Qnil; + wrapper->active_fiber = Qnil; } return NULL; } -/* this is called during GC */ -static void rb_mysql_client_free(void *ptr) { - mysql_client_wrapper *wrapper = ptr; - decr_mysql2_client(wrapper); -} - void decr_mysql2_client(mysql_client_wrapper *wrapper) { wrapper->refcount--; @@ -311,16 +397,20 @@ void decr_mysql2_client(mysql_client_wrapper *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->connect_timeout = 0; - wrapper->initialized = 0; /* means that that the wrapper is initialized */ + 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->closed = 0; wrapper->client = (MYSQL*)xmalloc(sizeof(MYSQL)); return obj; @@ -462,6 +552,7 @@ static VALUE rb_mysql_connect(VALUE self, VALUE user, VALUE pass, VALUE host, VA rb_raise_mysql2_error(wrapper); } + wrapper->closed = 0; wrapper->server_version = mysql_get_server_version(wrapper->client); return self; } @@ -508,12 +599,12 @@ static void *nogvl_send_query(void *ptr) { 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 ((VALUE)rb_thread_call_without_gvl(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 */ - wrapper->active_thread = Qnil; + wrapper->active_fiber = Qnil; rb_raise_mysql2_error(wrapper); } return Qnil; @@ -543,7 +634,7 @@ static void *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 result; } @@ -569,17 +660,17 @@ static VALUE rb_mysql_client_async_result(VALUE self) { 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 ((VALUE)rb_thread_call_without_gvl(nogvl_read_query_result, wrapper->client, RUBY_UBF_IO, 0) == Qfalse) { /* an error occurred, mark this connection inactive */ - wrapper->active_thread = Qnil; + wrapper->active_fiber = Qnil; rb_raise_mysql2_error(wrapper); } - is_streaming = rb_hash_aref(rb_iv_get(self, "@current_query_options"), sym_stream); + 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 { @@ -588,7 +679,7 @@ static VALUE rb_mysql_client_async_result(VALUE self) { if (result == NULL) { if (mysql_errno(wrapper->client) != 0) { - wrapper->active_thread = Qnil; + wrapper->active_fiber = Qnil; rb_raise_mysql2_error(wrapper); } /* no data and no error, so query was not a SELECT */ @@ -596,7 +687,7 @@ static VALUE rb_mysql_client_async_result(VALUE self) { } // Duplicate the options hash and put the copy in the Result object - current = rb_hash_dup(rb_iv_get(self, "@current_query_options")); + 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); @@ -615,7 +706,7 @@ struct async_query_args { static VALUE disconnect_and_raise(VALUE self, VALUE error) { GET_CLIENT(self); - wrapper->active_thread = Qnil; + wrapper->active_fiber = Qnil; /* Invalidate the MySQL socket to prevent further communication. * The GC will come along later and call mysql_close to free it. @@ -631,15 +722,15 @@ static VALUE disconnect_and_raise(VALUE self, VALUE error) { rb_exc_raise(error); } -static VALUE do_query(void *args) { - struct async_query_args *async_args = args; +static VALUE do_query(VALUE args) { + struct async_query_args *async_args = (void *)args; struct timeval tv; struct timeval *tvp; long int sec; int retval; VALUE read_timeout; - 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)) { @@ -680,7 +771,7 @@ 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_thread)) { + if (!NIL_P(wrapper->active_fiber)) { if (CONNECTED(wrapper)) { /* Invalidate the MySQL socket to prevent further communication. */ #ifndef _WIN32 @@ -695,24 +786,24 @@ static VALUE disconnect_and_mark_inactive(VALUE self) { } /* Skip mysql client check performed before command execution. */ wrapper->client->status = MYSQL_STATUS_READY; - wrapper->active_thread = Qnil; + wrapper->active_fiber = Qnil; } return Qnil; } -void rb_mysql_client_set_active_thread(VALUE self) { - VALUE thread_current = rb_thread_current(); +static void rb_mysql_client_set_active_fiber(VALUE self) { + VALUE fiber_current = rb_fiber_current(); GET_CLIENT(self); // see if this connection is still waiting on a result from a previous query - if (NIL_P(wrapper->active_thread)) { + if (NIL_P(wrapper->active_fiber)) { // mark this connection active - wrapper->active_thread = thread_current; - } else if (wrapper->active_thread == thread_current) { + 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_thread); + VALUE inspect = rb_inspect(wrapper->active_fiber); const char *thr = StringValueCStr(inspect); rb_raise(cMysql2Error, "This connection is in use by: %s", thr); @@ -767,7 +858,7 @@ static VALUE rb_mysql_query(VALUE self, VALUE sql, VALUE current) { (void)RB_GC_GUARD(current); Check_Type(current, T_HASH); - rb_iv_set(self, "@current_query_options", current); + rb_ivar_set(self, intern_current_query_options, current); Check_Type(sql, T_STRING); /* ensure the string is in the encoding the connection is expecting */ @@ -776,10 +867,11 @@ static VALUE rb_mysql_query(VALUE self, VALUE sql, VALUE current) { args.sql_len = RSTRING_LEN(args.sql); args.wrapper = wrapper; - rb_mysql_client_set_active_thread(self); + 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 (rb_hash_aref(current, sym_async) == Qtrue) { return Qnil; @@ -792,7 +884,8 @@ static VALUE rb_mysql_query(VALUE self, VALUE sql, VALUE current) { 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, disconnect_and_mark_inactive, self); @@ -903,6 +996,13 @@ static VALUE _mysql_client_options(VALUE self, int opt, VALUE value) { retval = charval; break; +#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); @@ -1011,6 +1111,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 * @@ -1174,7 +1304,7 @@ static VALUE rb_mysql_client_store_result(VALUE self) } // Duplicate the options hash and put the copy in the Result object - current = rb_hash_dup(rb_iv_get(self, "@current_query_options")); + 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); @@ -1260,7 +1390,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); } @@ -1305,12 +1435,31 @@ 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); +#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; } @@ -1336,6 +1485,14 @@ static VALUE set_init_command(VALUE self, VALUE value) { return _mysql_client_options(self, MYSQL_INIT_COMMAND, value); } +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); @@ -1397,6 +1554,7 @@ void init_mysql2_client() { 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); @@ -1427,6 +1585,7 @@ void init_mysql2_client() { 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); + rb_define_method(cMysql2Client, "session_track", rb_mysql_client_session_track, 1); rb_define_private_method(cMysql2Client, "connect_timeout=", set_connect_timeout, 1); rb_define_private_method(cMysql2Client, "read_timeout=", set_read_timeout, 1); @@ -1437,6 +1596,7 @@ void init_mysql2_client() { 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, "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); @@ -1461,6 +1621,8 @@ void init_mysql2_client() { intern_merge = rb_intern("merge"); intern_merge_bang = rb_intern("merge!"); 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"), @@ -1596,16 +1758,32 @@ void init_mysql2_client() { INT2NUM(0)); #endif -#if defined(FULL_SSL_MODE_SUPPORT) // MySQL 5.7.11 and above +#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)); -#elif defined(HAVE_CONST_MYSQL_OPT_SSL_ENFORCE) // MySQL 5.7.3 - 5.7.10 +#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)); diff --git a/ext/mysql2/client.h b/ext/mysql2/client.h index 5e0ebe3f0..ad6ce8aa9 100644 --- a/ext/mysql2/client.h +++ b/ext/mysql2/client.h @@ -3,7 +3,7 @@ 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; @@ -15,12 +15,19 @@ typedef struct { MYSQL *client; } mysql_client_wrapper; -void rb_mysql_client_set_active_thread(VALUE self); 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); diff --git a/ext/mysql2/extconf.rb b/ext/mysql2/extconf.rb index 190e930e1..bee775854 100644 --- a/ext/mysql2/extconf.rb +++ b/ext/mysql2/extconf.rb @@ -1,13 +1,15 @@ require 'mkmf' require 'English' +### 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 'apt-get install libmysqlclient-dev' or 'yum install mysql-devel', and try again.\n-----" + 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 @@ -15,19 +17,57 @@ 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 - $CFLAGS << ' -DFULL_SSL_MODE_SUPPORT' if all_modes_found - # if we only have ssl toggle (--ssl,--disable-ssl) from 5.7.3 to 5.7.10 - has_no_support = all_modes_found ? false : !have_const('MYSQL_OPT_SSL_ENFORCE', header) - $CFLAGS << ' -DNO_SSL_MODE_SUPPORT' if has_no_support + 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 extention 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') +# 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 + $LDFLAGS << " -L#{openssl_location}/lib" if openssl_location +end + +### Find MySQL client library + # borrowed from mysqlplus # http://github.com/oldmoe/mysqlplus/blob/master/ext/extconf.rb dirs = ENV.fetch('PATH').split(File::PATH_SEPARATOR) + %w[ @@ -35,6 +75,7 @@ def add_ssl_defines(header) /opt/local /opt/local/mysql /opt/local/lib/mysql5* + /opt/homebrew/opt/mysql* /usr /usr/mysql /usr/local @@ -42,6 +83,9 @@ def add_ssl_defines(header) /usr/local/mysql-* /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 @@ -108,6 +152,7 @@ def add_ssl_defines(header) 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) @@ -119,10 +164,16 @@ def add_ssl_defines(header) # 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 + # 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) @@ -147,8 +198,10 @@ def add_ssl_defines(header) $CFLAGS << ' ' << usable_flags.join(' ') +### Sanitizers to help with debugging -- many are available on both Clang/LLVM and GCC + enabled_sanitizers = disabled_sanitizers = [] -# Specify a commna-separated list of sanitizers, or try them all by default +# Specify a comma-separated list of sanitizers, or try them all by default sanitizers = with_config('sanitize') case sanitizers when true @@ -164,7 +217,7 @@ def add_ssl_defines(header) end end -unless disabled_sanitizers.empty? +unless disabled_sanitizers.empty? # rubocop:disable Style/IfUnlessModifier abort "-----\nCould not enable requested sanitizers: #{disabled_sanitizers.join(',')}\n-----" end @@ -181,6 +234,8 @@ def add_ssl_defines(header) $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' diff --git a/ext/mysql2/mysql2_ext.c b/ext/mysql2/mysql2_ext.c index 2eb8b6d94..8c887a867 100644 --- a/ext/mysql2/mysql2_ext.c +++ b/ext/mysql2/mysql2_ext.c @@ -4,9 +4,14 @@ 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(); diff --git a/ext/mysql2/mysql2_ext.h b/ext/mysql2/mysql2_ext.h index e1ce0e943..f82c47e5e 100644 --- a/ext/mysql2/mysql2_ext.h +++ b/ext/mysql2/mysql2_ext.h @@ -36,6 +36,19 @@ void Init_mysql2(void); 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 diff --git a/ext/mysql2/mysql_enc_name_to_ruby.h b/ext/mysql2/mysql_enc_name_to_ruby.h index 0e2b08b99..9356f2ad0 100644 --- a/ext/mysql2/mysql_enc_name_to_ruby.h +++ b/ext/mysql2/mysql_enc_name_to_ruby.h @@ -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,32 +46,32 @@ 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]]; } @@ -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", "UTF-8"}, - {"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 b32b6925f..d3d09e2b9 100644 --- a/ext/mysql2/mysql_enc_to_ruby.h +++ b/ext/mysql2/mysql_enc_to_ruby.h @@ -54,13 +54,13 @@ static const char *mysql2_mysql_enc_to_rb[] = { "macRoman", "UTF-16", "UTF-16", - "", + "UTF-16LE", "Windows-1256", "Windows-1257", "Windows-1257", "UTF-32", "UTF-32", - "", + "UTF-16LE", "ASCII-8BIT", NULL, "US-ASCII", @@ -74,7 +74,7 @@ static const char *mysql2_mysql_enc_to_rb[] = { NULL, "KOI8-R", "KOI8-R", - NULL, + "UTF-8", "ISO-8859-2", "ISO-8859-9", "ISO-8859-13", @@ -246,14 +246,80 @@ static const char *mysql2_mysql_enc_to_rb[] = { "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, + "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" }; - -#define CHARSETNR_SIZE (sizeof(mysql2_mysql_enc_to_rb)/sizeof(mysql2_mysql_enc_to_rb[0])) diff --git a/ext/mysql2/result.c b/ext/mysql2/result.c index 0ab38aabe..23ec611ee 100644 --- a/ext/mysql2/result.c +++ b/ext/mysql2/result.c @@ -1,6 +1,7 @@ #include #include "mysql_enc_to_ruby.h" +#define MYSQL2_CHARSETNR_SIZE (sizeof(mysql2_mysql_enc_to_rb)/sizeof(mysql2_mysql_enc_to_rb[0])) static rb_encoding *binaryEncoding; @@ -16,9 +17,27 @@ static rb_encoding *binaryEncoding; */ #define MYSQL2_MIN_TIME 2678400ULL +#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_BINARY_CHARSET 63 + +#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; \ - Data_Get_Struct(self, mysql2_result_wrapper, wrapper); + TypedData_Get_Struct(self, mysql2_result_wrapper, &rb_mysql_result_type, wrapper); typedef struct { int symbolizeKeys; @@ -29,14 +48,15 @@ typedef struct { int streaming; ID db_timezone; ID app_timezone; - VALUE block_given; + 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_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; @@ -45,11 +65,11 @@ static VALUE sym_symbolize_keys, sym_as, sym_array, sym_database_timezone, 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(w->client); - rb_gc_mark(w->statement); + 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); } } @@ -111,6 +131,48 @@ static void rb_mysql_result_free(void *ptr) { 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); @@ -155,11 +217,18 @@ static VALUE rb_mysql_result_fetch_field(VALUE self, unsigned int idx, int symbo 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); - 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); } @@ -167,9 +236,171 @@ static VALUE rb_mysql_result_fetch_field(VALUE self, unsigned int idx, int symbo return rb_field; } +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; + 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 its wishes */ - if (field.flags & BINARY_FLAG && field.charsetnr == 63) { + 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 */ @@ -179,8 +410,8 @@ static VALUE mysql2_set_field_string_encoding(VALUE val, MYSQL_FIELD field, rb_e const char *enc_name; int enc_index; - enc_name = (field.charsetnr-1 < CHARSETNR_SIZE) ? mysql2_mysql_enc_to_rb[field.charsetnr-1] : NULL; - + 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); @@ -694,7 +925,7 @@ static VALUE rb_mysql_result_fetch_fields(VALUE self) { 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; @@ -714,6 +945,25 @@ static VALUE rb_mysql_result_fetch_fields(VALUE self) { return wrapper->fields; } +static VALUE rb_mysql_result_fetch_field_types(VALUE self) { + unsigned int i = 0; + + GET_RESULT(self); + + if (wrapper->fieldTypes == Qnil) { + wrapper->numberOfFields = mysql_num_fields(wrapper->result); + wrapper->fieldTypes = rb_ary_new2(wrapper->numberOfFields); + } + + if ((my_ulonglong)RARRAY_LEN(wrapper->fieldTypes) != wrapper->numberOfFields) { + for (i=0; inumberOfFields; i++) { + rb_mysql_result_fetch_field_type(self, i); + } + } + + return wrapper->fieldTypes; +} + 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) @@ -739,7 +989,7 @@ static VALUE rb_mysql_result_each_(VALUE self, row = fetch_row_func(self, fields, args); if (row != Qnil) { wrapper->numberOfRows++; - if (args->block_given != Qnil) { + if (args->block_given) { rb_yield(row); } } @@ -789,7 +1039,7 @@ static VALUE rb_mysql_result_each_(VALUE self, return Qnil; } - if (args->block_given != Qnil) { + if (args->block_given) { rb_yield(row); } } @@ -807,7 +1057,7 @@ static VALUE rb_mysql_result_each_(VALUE self, static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) { result_each_args args; - VALUE defaults, opts, block, (*fetch_row_func)(VALUE, MYSQL_FIELD *fields, const 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; @@ -817,9 +1067,12 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) { rb_raise(cMysql2Error, "Statement handle already closed"); } - defaults = rb_iv_get(self, "@query_options"); + defaults = rb_ivar_get(self, intern_query_options); Check_Type(defaults, T_HASH); - if (rb_scan_args(argc, argv, "01&", &opts, &block) == 1) { + + // 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; @@ -885,7 +1138,7 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) { args.cast = cast; args.db_timezone = db_timezone; args.app_timezone = app_timezone; - args.block_given = block; + args.block_given = rb_block_given_p(); if (wrapper->stmt_wrapper) { fetch_row_func = rb_mysql_result_fetch_row_stmt; @@ -922,13 +1175,18 @@ VALUE rb_mysql_result_to_obj(VALUE client, VALUE encoding, VALUE options, MYSQL_ 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 = encoding; wrapper->streamingComplete = 0; @@ -950,7 +1208,7 @@ VALUE rb_mysql_result_to_obj(VALUE client, VALUE encoding, VALUE options, MYSQL_ } rb_obj_call_init(obj, 0, NULL); - rb_iv_set(obj, "@query_options", options); + rb_ivar_set(obj, intern_query_options, options); /* Options that cannot be changed in results.each(...) { |row| } * should be processed here. */ @@ -961,11 +1219,17 @@ VALUE rb_mysql_result_to_obj(VALUE client, VALUE encoding, VALUE options, MYSQL_ void init_mysql2_result() { 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"); @@ -979,6 +1243,7 @@ void init_mysql2_result() { 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")); diff --git a/ext/mysql2/result.h b/ext/mysql2/result.h index 0c25b24b6..3f58b1005 100644 --- a/ext/mysql2/result.h +++ b/ext/mysql2/result.h @@ -6,6 +6,7 @@ VALUE rb_mysql_result_to_obj(VALUE client, VALUE encoding, VALUE options, MYSQL_ typedef struct { VALUE fields; + VALUE fieldTypes; VALUE rows; VALUE client; VALUE encoding; diff --git a/ext/mysql2/statement.c b/ext/mysql2/statement.c index 22e22ecfd..5506526ee 100644 --- a/ext/mysql2/statement.c +++ b/ext/mysql2/statement.c @@ -3,11 +3,16 @@ 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; +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 GET_STATEMENT(self) \ mysql_stmt_wrapper *stmt_wrapper; \ - Data_Get_Struct(self, mysql_stmt_wrapper, stmt_wrapper); \ + TypedData_Get_Struct(self, mysql_stmt_wrapper, &rb_mysql_statement_type, stmt_wrapper); \ if (!stmt_wrapper->stmt) { rb_raise(cMysql2Error, "Invalid statement handle"); } \ if (stmt_wrapper->closed) { rb_raise(cMysql2Error, "Statement handle already closed"); } @@ -15,9 +20,45 @@ static void rb_mysql_stmt_mark(void * ptr) { mysql_stmt_wrapper *stmt_wrapper = ptr; if (!stmt_wrapper) return; - rb_gc_mark(stmt_wrapper->client); + 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) { @@ -27,11 +68,6 @@ static void *nogvl_stmt_close(void *ptr) { return NULL; } -static void rb_mysql_stmt_free(void *ptr) { - mysql_stmt_wrapper *stmt_wrapper = ptr; - decr_mysql2_stmt(stmt_wrapper); -} - void decr_mysql2_stmt(mysql_stmt_wrapper *stmt_wrapper) { stmt_wrapper->refcount--; @@ -45,7 +81,7 @@ 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_tainted_str_new2(mysql_stmt_sqlstate(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); @@ -95,7 +131,11 @@ VALUE rb_mysql_stmt_new(VALUE rb_client, VALUE sql) { 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; @@ -404,7 +444,7 @@ static VALUE rb_mysql_stmt_execute(int argc, VALUE *argv, VALUE self) { } // Duplicate the options hash, merge! extra opts, put the copy into the Result object - current = rb_hash_dup(rb_iv_get(stmt_wrapper->client, "@query_options")); + current = rb_hash_dup(rb_ivar_get(stmt_wrapper->client, intern_query_options)); (void)RB_GC_GUARD(current); Check_Type(current, T_HASH); @@ -447,7 +487,7 @@ static VALUE rb_mysql_stmt_execute(int argc, VALUE *argv, VALUE self) { if (metadata == NULL) { if (mysql_stmt_errno(stmt) != 0) { // either CR_OUT_OF_MEMORY or CR_UNKNOWN_ERROR. both fatal. - wrapper->active_thread = Qnil; + wrapper->active_fiber = Qnil; rb_raise_mysql2_stmt_error(stmt_wrapper); } // no data and no error, so query was not a SELECT @@ -455,12 +495,12 @@ static VALUE rb_mysql_stmt_execute(int argc, VALUE *argv, VALUE self) { } if (!is_streaming) { - // recieve the whole result set from the server + // 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_thread = Qnil; + wrapper->active_fiber = Qnil; } resultObj = rb_mysql_result_to_obj(stmt_wrapper->client, wrapper->encoding, current, metadata, self); @@ -501,7 +541,7 @@ static VALUE rb_mysql_stmt_fields(VALUE self) { if (metadata == NULL) { if (mysql_stmt_errno(stmt) != 0) { // either CR_OUT_OF_MEMORY or CR_UNKNOWN_ERROR. both fatal. - wrapper->active_thread = Qnil; + wrapper->active_fiber = Qnil; rb_raise_mysql2_stmt_error(stmt_wrapper); } // no data and no error, so query was not a SELECT @@ -571,10 +611,18 @@ static VALUE rb_mysql_stmt_close(VALUE self) { 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); @@ -599,4 +647,5 @@ void init_mysql2_statement() { intern_to_s = rb_intern("to_s"); intern_merge_bang = rb_intern("merge!"); + intern_query_options = rb_intern("@query_options"); } diff --git a/lib/mysql2.rb b/lib/mysql2.rb index 4bf75364a..9461846e9 100644 --- a/lib/mysql2.rb +++ b/lib/mysql2.rb @@ -20,9 +20,12 @@ end if dll_path - require 'Win32API' - LoadLibrary = Win32API.new('Kernel32', 'LoadLibrary', ['P'], 'I') - if LoadLibrary.call(dll_path).zero? + 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 @@ -62,6 +65,7 @@ module Util # def self.key_hash_as_symbols(hash) return nil unless hash + Hash[hash.map { |k, v| [k.to_sym, v] }] end @@ -79,5 +83,6 @@ def self.key_hash_as_symbols(hash) 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 7e28768d5..582b6e305 100644 --- a/lib/mysql2/client.rb +++ b/lib/mysql2/client.rb @@ -20,6 +20,7 @@ def self.default_query_options def initialize(opts = {}) 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 @@ -31,8 +32,9 @@ def initialize(opts = {}) 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].each do |key| + %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].each do |key| next unless opts.key?(key) + case key when :reconnect, :local_infile, :secure_auth, :automatic_close, :enable_cleartext_plugin send(:"#{key}=", !!opts[key]) # rubocop:disable Style/DoubleNegation @@ -46,9 +48,14 @@ def initialize(opts = {}) # force the encoding to utf8 self.charset_name = opts[:encoding] || 'utf8' + 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 + ssl_options = opts.values_at(:sslkey, :sslcert, :sslca, :sslcapath, :sslcipher) ssl_set(*ssl_options) if ssl_options.any? || opts.key?(:sslverify) - self.ssl_mode = parse_ssl_mode(opts[:ssl_mode]) if opts[:ssl_mode] + self.ssl_mode = mode if mode flags = case opts[:flags] when Array @@ -115,10 +122,23 @@ def parse_flags_array(flags, initial = 0) 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| @@ -127,7 +147,7 @@ def parse_connect_attrs(conn_attrs) end def query(sql, options = {}) - Thread.handle_interrupt(::Mysql2::Util::TIMEOUT_ERROR_CLASS => :never) do + Thread.handle_interrupt(::Mysql2::Util::TIMEOUT_ERROR_NEVER) do _query(sql, @query_options.merge(options)) end end @@ -135,6 +155,7 @@ def query(sql, options = {}) 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 diff --git a/lib/mysql2/error.rb b/lib/mysql2/error.rb index 758f01a98..8cfa23ffb 100644 --- a/lib/mysql2/error.rb +++ b/lib/mysql2/error.rb @@ -24,6 +24,7 @@ class Error < StandardError 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 @@ -52,7 +53,7 @@ class Error < StandardError 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 + @sql_state = sql_state ? sql_state.encode(**ENCODE_OPTS) : nil super(clean_message(msg)) end @@ -91,9 +92,9 @@ def self.new_with_args(msg, server_version, error_number, sql_state) # Returns a valid UTF-8 string. def clean_message(message) if @server_version && @server_version > 50500 - message.encode(ENCODE_OPTS) + message.encode(**ENCODE_OPTS) else - message.encode(Encoding::UTF_8, ENCODE_OPTS) + message.encode(Encoding::UTF_8, **ENCODE_OPTS) end end end diff --git a/lib/mysql2/statement.rb b/lib/mysql2/statement.rb index 2f9f09a05..c50ed1f96 100644 --- a/lib/mysql2/statement.rb +++ b/lib/mysql2/statement.rb @@ -1,9 +1,7 @@ module Mysql2 class Statement - include Enumerable - def execute(*args, **kwargs) - Thread.handle_interrupt(::Mysql2::Util::TIMEOUT_ERROR_CLASS => :never) do + Thread.handle_interrupt(::Mysql2::Util::TIMEOUT_ERROR_NEVER) do _execute(*args, **kwargs) end end diff --git a/lib/mysql2/version.rb b/lib/mysql2/version.rb index 474616c3f..d84abfc72 100644 --- a/lib/mysql2/version.rb +++ b/lib/mysql2/version.rb @@ -1,3 +1,3 @@ module Mysql2 - VERSION = "0.5.1".freeze + VERSION = "0.5.6".freeze end diff --git a/mysql2.gemspec b/mysql2.gemspec index 98a89b95a..93e790ae0 100644 --- a/mysql2.gemspec +++ b/mysql2.gemspec @@ -10,10 +10,18 @@ Mysql2::GEMSPEC = Gem::Specification.new do |s| s.homepage = 'https://github.com/brianmario/mysql2' s.rdoc_options = ["--charset=UTF-8"] 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 LICENSE ext lib support`.split - s.test_files = `git ls-files spec examples`.split s.metadata['msys2_mingw_dependencies'] = 'libmariadbclient' + + s.add_runtime_dependency 'bigdecimal' end diff --git a/spec/mysql2/client_spec.rb b/spec/mysql2/client_spec.rb index 00d9c17db..14f446df9 100644 --- a/spec/mysql2/client_spec.rb +++ b/spec/mysql2/client_spec.rb @@ -1,6 +1,11 @@ require 'spec_helper' -RSpec.describe Mysql2::Client do +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 + context "using defaults file" do let(:cnf_file) { File.expand_path('../../my.cnf', __FILE__) } @@ -49,6 +54,7 @@ Klient = Class.new(Mysql2::Client) do attr_reader :connect_args + def connect(*args) @connect_args ||= [] @connect_args << args @@ -126,32 +132,73 @@ def connect(*args) expect(Mysql2::Client).to respond_to(:default_query_options) end - it "should be able to connect via SSL options" do - ssl = @client.query "SHOW VARIABLES LIKE 'have_ssl'" - ssl_uncompiled = ssl.any? { |x| x['Value'] == 'OFF' } - pending("DON'T WORRY, THIS TEST PASSES - but SSL is not compiled into your MySQL daemon.") if ssl_uncompiled - ssl_disabled = ssl.any? { |x| x['Value'] == 'DISABLED' } - pending("DON'T WORRY, THIS TEST PASSES - but SSL is not enabled in your MySQL daemon.") if ssl_disabled + 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 - # You may need to adjust the lines below to match your SSL certificate paths - ssl_client = nil - expect do - ssl_client = new_client( + let(:option_overrides) do + { 'host' => 'mysql2gem.example.com', # must match the certificates - :sslkey => '/etc/mysql/client-key.pem', - :sslcert => '/etc/mysql/client-cert.pem', - :sslca => '/etc/mysql/ca-cert.pem', + :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.not_to raise_error + } + end - 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 + let(:ssl_client) do + new_client(option_overrides) + end - expect(ssl_client.ssl_cipher).not_to be_empty - expect(results['Ssl_cipher']).to eql(ssl_client.ssl_cipher) + # '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 def run_gc @@ -176,6 +223,7 @@ def run_gc 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) @@ -400,7 +448,7 @@ def run_gc end context ":local_infile" do - before(:all) 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' } @@ -416,7 +464,7 @@ def run_gc end end - after(:all) do + after(:context) do new_client do |client| client.query "DROP TABLE IF EXISTS infileTest" end @@ -472,6 +520,7 @@ def run_gc 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.') @@ -481,6 +530,7 @@ def run_gc 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.') @@ -566,7 +616,7 @@ def run_gc end expect do @client.query("SELECT SLEEP(1)") - end.to raise_error(Mysql2::Error, /Lost connection to MySQL server/) + end.to raise_error(Mysql2::Error, /Lost connection/) if RUBY_PLATFORM !~ /mingw|mswin/ expect do @@ -584,10 +634,13 @@ def run_gc end it "should describe the thread holding the active query" do - thr = Thread.new { @client.query("SELECT 1", async: true) } + thr = Thread.new do + @client.query("SELECT 1", async: true) + Fiber.current + end - thr.join - expect { @client.query('SELECT 1') }.to raise_error(Mysql2::Error, Regexp.new(Regexp.escape(thr.inspect))) + 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 @@ -600,21 +653,21 @@ def run_gc # 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.1 - query_time = 2 * kill_time + kill_time = 0.25 + query_time = 4 * kill_time mark = {} begin - trap(:USR1) { mark.store(:USR1, Time.now) } + trap(:USR1) { mark.store(:USR1, clock_time) } pid = fork do sleep kill_time # wait for client query to start Process.kill(:USR1, Process.ppid) sleep # wait for explicit kill to prevent GC disconnect end - mark.store(:QUERY_START, Time.now) + mark.store(:QUERY_START, clock_time) @client.query("SELECT SLEEP(#{query_time})") - mark.store(:QUERY_END, Time.now) + mark.store(:QUERY_END, clock_time) ensure Process.kill(:TERM, pid) Process.waitpid2(pid) @@ -622,7 +675,7 @@ def run_gc end # the query ran uninterrupted - expect(mark.fetch(:QUERY_END) - mark.fetch(:QUERY_START)).to be_within(0.02).of(query_time) + 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 @@ -687,6 +740,7 @@ def run_gc 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| @@ -695,10 +749,12 @@ def run_gc Thread.current.object_id end end + values = threads.map(&:value) + stop = clock_time - # This timeout demonstrates that the threads are sleeping concurrently: - # In the serial case, the timeout would fire and the test would fail - values = Timeout.timeout(sleep_time * 1.1) { threads.map(&:value) } + # 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 @@ -720,7 +776,7 @@ def run_gc end context "Multiple results sets" do - before(:each) do + before(:example) do @multi_client = new_client(flags: Mysql2::Client::MULTI_STATEMENTS) end @@ -964,12 +1020,12 @@ def run_gc 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` BIGINT NOT NULL AUTO_INCREMENT, blah INT(11), PRIMARY KEY (`id`))" end - after(:each) do + after(:example) do @client.query "DROP TABLE lastIdTest" end @@ -1016,8 +1072,56 @@ def run_gc 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}") @@ -1025,7 +1129,7 @@ def run_gc end end - after(:each) do + after(:example) do 2.times do |i| @client.query("DROP DATABASE test_selectdb_#{i}") end diff --git a/spec/mysql2/error_spec.rb b/spec/mysql2/error_spec.rb index c94a31961..efa3437f8 100644 --- a/spec/mysql2/error_spec.rb +++ b/spec/mysql2/error_spec.rb @@ -39,6 +39,10 @@ end end + let(:server_info) do + @client.server_info + end + before do # sanity check expect(valid_utf8.encoding).to eql(Encoding::UTF_8) @@ -56,7 +60,15 @@ expect(bad_err.message.encoding).to eql(Encoding::UTF_8) expect(bad_err.message).to be_valid_encoding - expect(bad_err.message).to include("??}\u001F") + # 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 diff --git a/spec/mysql2/result_spec.rb b/spec/mysql2/result_spec.rb index 89744fc2f..ed3e9d262 100644 --- a/spec/mysql2/result_spec.rb +++ b/spec/mysql2/result_spec.rb @@ -1,16 +1,13 @@ require 'spec_helper' RSpec.describe Mysql2::Result do - before(:each) do + before(:example) do @result = @client.query "SELECT 1" end it "should raise a TypeError exception when it doesn't wrap a result set" do - r = Mysql2::Result.new - expect { r.count }.to raise_error(TypeError) - expect { r.fields }.to raise_error(TypeError) - expect { r.size }.to raise_error(TypeError) - expect { r.each }.to raise_error(TypeError) + expect { Mysql2::Result.new }.to raise_error(TypeError) + expect { Mysql2::Result.allocate }.to raise_error(TypeError) end it "should have included Enumerable" do @@ -117,6 +114,97 @@ result = @client.query "SELECT '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 "#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(10) + 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(10) + varchar(10) + binary(10) + varbinary(10) + tinyblob + tinytext + blob + text + mediumblob + mediumtext + 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 @@ -164,7 +252,7 @@ expect do res.each_with_index do |_, i| # Exhaust the first result packet then trigger a timeout - sleep 2 if i > 0 && i % 1000 == 0 + sleep 4 if i > 0 && i % 1000 == 0 end end.to raise_error(Mysql2::Error, /Lost connection/) end @@ -318,8 +406,13 @@ end it "should raise an error given an invalid DATETIME" do - 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") + 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 @@ -404,17 +497,17 @@ 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 expect(test_result[field]).to be_an_instance_of(String) diff --git a/spec/mysql2/statement_spec.rb b/spec/mysql2/statement_spec.rb index dbc185e6b..57e590804 100644 --- a/spec/mysql2/statement_spec.rb +++ b/spec/mysql2/statement_spec.rb @@ -1,16 +1,23 @@ -require './spec/spec_helper.rb' +require './spec/spec_helper' RSpec.describe Mysql2::Statement do - before :each do + 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 - @client.query("SELECT COUNT(1) AS count FROM performance_schema.prepared_statements_instances").first['count'].to_i - rescue Mysql2::Error - # Fall back to the global prepapred statement counter - @client.query("SHOW STATUS LIKE 'Prepared_stmt_count'").first['Value'].to_i + 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 @@ -211,7 +218,7 @@ def stmt_count end context "utf8_db" do - before(:each) 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") @@ -219,7 +226,7 @@ def stmt_count @client.query("INSERT INTO テーブル (整数, 文字列) VALUES (1, 'イチ'), (2, '弐'), (3, 'さん')") end - after(:each) do + after(:example) do @client.query("DROP DATABASE test_mysql2_stmt_utf8") end @@ -270,7 +277,7 @@ def stmt_count end context "#each" do - # note: The current impl. of prepared statement requires results to be cached on #execute except for streaming queries + # 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 @@ -313,7 +320,7 @@ def stmt_count result = @client.prepare("SELECT 1 UNION SELECT 2").execute(stream: true, cache_rows: false) expect do result.each {} - result.each {} + result.each {} # rubocop:disable Style/CombinableLoops end.to raise_exception(Mysql2::Error) end end @@ -475,8 +482,13 @@ def stmt_count end it "should raise an error given an invalid DATETIME" do - 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") + 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 @@ -561,17 +573,17 @@ def stmt_count 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 expect(test_result[field]).to be_an_instance_of(String) @@ -627,12 +639,12 @@ def stmt_count end context 'last_id' do - before(:each) 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(:each) do + after(:example) do @client.query 'DROP TABLE lastIdTest' end @@ -655,12 +667,12 @@ def stmt_count end context 'affected_rows' do - before :each 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 :each do + after(:example) do @client.query 'DROP TABLE lastIdTest' end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 12b48df67..677c34829 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,8 +2,20 @@ 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.disable_monkey_patching! @@ -26,6 +38,7 @@ def new_client(option_overrides = {}) @clients ||= [] @clients << client return client unless block_given? + begin yield client ensure @@ -36,19 +49,52 @@ def new_client(option_overrides = {}) def num_classes # rubocop:disable Lint/UnifiedInteger - 0.class == Integer ? [Integer] : [Fixnum, Bignum] + 0.instance_of?(Integer) ? [Integer] : [Fixnum, Bignum] # rubocop:enable Lint/UnifiedInteger end - config.before :each do - @client = new_client + # 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 - config.after :each do - @clients.each(&:close) + # 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 - config.before(:all) do + 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 ( @@ -109,4 +155,12 @@ def num_classes ] end end + + config.before(:example) do + @client = new_client + end + + config.after(:example) do + @clients.each(&:close) + end end diff --git a/spec/ssl/gen_certs.sh b/spec/ssl/gen_certs.sh index 728748126..3d48da014 100644 --- a/spec/ssl/gen_certs.sh +++ b/spec/ssl/gen_certs.sh @@ -22,7 +22,7 @@ organizationalUnitName_default = Mysql2Gem emailAddress_default = mysql2gem@example.com " | tee ca.cnf cert.cnf -# The client and server certs must have a diferent common name than the CA +# 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 " 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 index 6c9fd8dec..281e134fb 100644 --- a/support/5072E1F5.asc +++ b/support/5072E1F5.asc @@ -1,5 +1,5 @@ -----BEGIN PGP PUBLIC KEY BLOCK----- -Version: GnuPG v1.4.5 (GNU/Linux) +Version: GnuPG v1 mQGiBD4+owwRBAC14GIfUfCyEDSIePvEW3SAFUdJBtoQHH/nJKZyQT7h9bPlUWC3 RODjQReyCITRrdwyrKUGku2FmeVGwn2u2WmDMNABLnpprWPkBdCk96+OmSLN9brZ @@ -11,9 +11,9 @@ kYpXBACmWpP8NJTkamEnPCia2ZoOHODANwpUkP43I7jsDmgtobZX9qnrAXw+uNDI QJEXM6FSbi0LLtZciNlYsafwAPEOMDKpMqAK6IyisNtPvaLd8lH0bPAnWqcyefep rv0sxxqUEMcM3o7wwgfN83POkDasDbs3pjwPhxvhz6//62zQJ7Q2TXlTUUwgUmVs ZWFzZSBFbmdpbmVlcmluZyA8bXlzcWwtYnVpbGRAb3NzLm9yYWNsZS5jb20+iGwE -ExECACwCGyMCHgECF4ACGQEGCwkIBwMCBhUKCQgCAwUWAgMBAAUCWKcFIAUJHirJ -FAAKCRCMcY07UHLh9VcFAJ46pUyVd8BZ2r5CppMC1tmyQ3ceRgCfVPwuVsiS0VER -5WUqtAQDt+DoetCIaQQTEQIAKQIbIwYLCQgHAwIEFQIIAwQWAgMBAh4BAheAAhkB +ExECACwCGyMCHgECF4ACGQEGCwkIBwMCBhUKCQgCAwUWAgMBAAUCXEBY+wUJI87e +5AAKCRCMcY07UHLh9RZPAJ9uvm0zlzfCN+DHxHVaoFLFjdVYTQCfborsC9tmEZYa +whhogjeBkZkorbyIaQQTEQIAKQIbIwYLCQgHAwIEFQIIAwQWAgMBAh4BAheAAhkB BQJTAdRmBQkaZsvLAAoJEIxxjTtQcuH1X4MAoKNLWAbCBUj96637kv6Xa/fJuX5m AJwPtmgDfjUe2iuhXdTrFEPT19SB6ohmBBMRAgAmAhsjBgsJCAcDAgQVAggDBBYC AwECHgECF4AFAk53PioFCRP7AhUACgkQjHGNO1By4fUmzACeJdfqgc9gWTUhgmcM @@ -428,5 +428,5 @@ GoaU9u41oyZTIiXPiFidJoIZCh7fdurP8pn3X+R5HUNXMr7M+ba8lSNxce/F3kmH 0L7rsKqdh9d/aVxhJINJ+inVDnrXWVoXu9GBjT8Nco1iU9SIVAQYEQIADAUCTnc9 7QUJE/sBuAASB2VHUEcAAQEJEIxxjTtQcuH1FJsAmwWK9vmwRJ/y9gTnJ8PWf0BV roUTAKClYAhZuX2nUNwH4vlEJQHDqYa5yQ== -=HfUN +=ghXk -----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/mysql_enc_to_ruby.rb b/support/mysql_enc_to_ruby.rb index 0ca73dad8..d292e1444 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", @@ -43,6 +44,8 @@ "geostd8" => "NULL", "cp932" => "Windows-31J", "eucjpms" => "eucJP-ms", + "utf16le" => "UTF-16LE", + "gb18030" => "GB18030", } client = Mysql2::Client.new(username: user, password: pass, host: host, port: port.to_i) @@ -52,7 +55,10 @@ collations.each do |collation| mysql_col_idx = collation[2].to_i - rb_enc = mysql_to_rb[collation[1]] + 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 diff --git a/support/ruby_enc_to_mysql.rb b/support/ruby_enc_to_mysql.rb index 96f6f056c..4106604c4 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", @@ -38,6 +39,8 @@ "geostd8" => nil, "cp932" => "Windows-31J", "eucjpms" => "eucJP-ms", + "utf16le" => "UTF-16LE", + "gb18030" => "GB18030", } puts <<-HEADER diff --git a/tasks/rspec.rake b/tasks/rspec.rake index ea8dc2258..efff7a25c 100644 --- a/tasks/rspec.rake +++ b/tasks/rspec.rake @@ -30,6 +30,10 @@ 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__) @@ -37,7 +41,7 @@ file 'spec/configuration.yml' => 'spec/configuration.yml.example' do |task| File.open(dst_path, 'w') do |dst_file| File.open(src_path).each_line do |line| - dst_file.write line.gsub(/LOCALUSERNAME/, ENV['USER']) + dst_file.write line.gsub(/LOCALUSERNAME/, user_name) end end end @@ -49,7 +53,7 @@ file 'spec/my.cnf' => 'spec/my.cnf.example' do |task| File.open(dst_path, 'w') do |dst_file| File.open(src_path).each_line do |line| - dst_file.write line.gsub(/LOCALUSERNAME/, ENV['USER']) + dst_file.write line.gsub(/LOCALUSERNAME/, user_name) end end end