diff --git a/.rubocop.yml b/.rubocop.yml index 9055a997..28d2dcd7 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,7 +1,7 @@ AllCops: DisplayCopNames: true DisplayStyleGuide: true - TargetRubyVersion: 2.1 + TargetRubyVersion: 2.2 Exclude: - 'vendor/**/*' diff --git a/.travis.yml b/.travis.yml index 08e48209..c6b505a1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,29 +1,43 @@ sudo: required language: ruby cache: bundler -matrix: - include: - - rvm: 2.3.1 - env: - - DB=mysql - - CONN_STR='DRIVER=MySQL;SERVER=localhost;DATABASE=odbc_test;USER=root;PASSWORD=;' - addons: - mysql: "5.5" - apt: - packages: - - unixodbc - - unixodbc-dev - - libmyodbc - - mysql-client - - rvm: 2.3.1 - env: - - DB=postgresql - - CONN_STR='DRIVER={PostgreSQL ANSI};SERVER=localhost;PORT=5432;DATABASE=odbc_test;UID=postgres;' - addons: - postgresql: "9.1" - apt: - packages: - - unixodbc - - unixodbc-dev - - odbc-postgresql -before_script: bin/ci-setup + +services: + - postgresql + - mysql + +addons: + apt: + packages: + - unixodbc + - unixodbc-dev + # MySQL + - libmyodbc + - mysql-client-5.6 + # Postgres + - odbc-postgresql + +rvm: + - 2.3 + - 2.4 + - 2.5 + +env: + - DB=mysql CONN_STR='DRIVER=MySQL;SERVER=localhost;DATABASE=odbc_test;USER=root;PASSWORD=;' + - DB=postgresql CONN_STR='DRIVER={PostgreSQL ANSI};SERVER=localhost;PORT=5432;DATABASE=odbc_test;UID=postgres;' + +before_install: + - gem update --system + +gemfile: + - gemfiles/active_record_5_0.gemfile + - gemfiles/active_record_5_1.gemfile + - gemfiles/active_record_5_2.gemfile + +before_script: + - sh -c "if [ '$DB' = 'mysql' ]; then sudo odbcinst -i -d -f /usr/share/libmyodbc/odbcinst.ini; fi" + - sh -c "if [ '$DB' = 'mysql' ]; then mysql -e 'DROP DATABASE IF EXISTS odbc_test; CREATE DATABASE IF NOT EXISTS odbc_test;' -uroot; fi" + - sh -c "if [ '$DB' = 'mysql' ]; then mysql -e 'GRANT ALL PRIVILEGES ON *.* TO "root"@"localhost";' -uroot; fi" + + - sh -c "if [ '$DB' = 'postgresql' ]; then sudo odbcinst -i -d -f /usr/share/psqlodbc/odbcinst.ini.template; fi" + - sh -c "if [ '$DB' = 'postgresql' ]; then psql -c 'CREATE DATABASE odbc_test;' -U postgres; fi" diff --git a/Gemfile b/Gemfile index c0cf8f42..fa75df15 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,3 @@ source 'https://rubygems.org' gemspec - -gem 'activerecord', '5.0.1' -gem 'pry', '~> 0.11.1' diff --git a/README.md b/README.md index 27f46dcc..753d87b0 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,23 @@ -# ODBCAdapter +# ODBCAdapter [![License][license-badge]][license-link] -[![Build Status](https://travis-ci.org/localytics/odbc_adapter.svg?branch=master)](https://travis-ci.org/localytics/odbc_adapter) -[![Gem](https://img.shields.io/gem/v/odbc_adapter.svg)](https://rubygems.org/gems/odbc_adapter) +| ActiveRecord | Gem Version | Branch | Status | +|--------------|-------------|--------|--------| +| `5.x` | `~> '5.0'` | [`master`][5.x-branch] | [![Build Status][5.x-build-badge]][build-link] | +| `4.x` | `~> '4.0'` | [`4.2.x`][4.x-branch] | [![Build Status][4.x-build-badge]][build-link] | -An ActiveRecord ODBC adapter. Master branch is working off of Rails 5.0.1. Previous work has been done to make it compatible with Rails 3.2 and 4.2; for those versions use the 3.2.x or 4.2.x gem releases. +## Supported Databases -This adapter will work for basic queries for most DBMSs out of the box, without support for migrations. Full support is built-in for MySQL 5 and PostgreSQL 9 databases. You can register your own adapter to get more support for your DBMS using the `ODBCAdapter.register` function. +- PostgreSQL 9 +- MySQL 5 +- Snowflake -A lot of this work is based on [OpenLink's ActiveRecord adapter](http://odbc-rails.rubyforge.org/) which works for earlier versions of Rails. +You can also register your own adapter to get more support for your DBMS +`ODBCAdapter.register`. ## Installation -Ensure you have the ODBC driver installed on your machine. You will also need the driver for whichever database to which you want ODBC to connect. +Ensure you have the ODBC driver installed on your machine. You will also need +the driver for whichever database to which you want ODBC to connect. Add this line to your application's Gemfile: @@ -29,9 +35,10 @@ Or install it yourself as: ## Usage -Configure your `database.yml` by either using the `dsn` option to point to a DSN that corresponds to a valid entry in your `~/.odbc.ini` file: +Configure your `database.yml` by either using the `dsn` option to point to a DSN +that corresponds to a valid entry in your `~/.odbc.ini` file: -``` +```yml development: adapter: odbc dsn: MyDatabaseDSN @@ -39,13 +46,32 @@ development: or by using the `conn_str` option and specifying the entire connection string: -``` +```yml development: adapter: odbc conn_str: "DRIVER={PostgreSQL ANSI};SERVER=localhost;PORT=5432;DATABASE=my_database;UID=postgres;" ``` -ActiveRecord models that use this connection will now be connecting to the configured database using the ODBC driver. +ActiveRecord models that use this connection will now be connecting to the +configured database using the ODBC driver. + +### Extending + +Configure your own adapter by registering it in your application's bootstrap +process. For example, you could add the following to a Rails application via +`config/initializers/custom_database_adapter.rb` + +```ruby +ODBCAdapter.register(/custom/, ActiveRecord::ConnectionAdapters::ODBCAdapter) do + # Overrides +end +``` + +```yml +development: + adapter: odbc + dsn: CustomDB +``` ## Testing @@ -53,8 +79,20 @@ To run the tests, you'll need the ODBC driver as well as the connection adapter ## Contributing -Bug reports and pull requests are welcome on GitHub at https://github.com/localytics/odbc_adapter. +Bug reports and pull requests are welcome on [GitHub][github-repo]. + +## Prior Work -## License +A lot of this work is based on [OpenLink's ActiveRecord adapter][openlink-activerecord-adapter] which works for earlier versions of Rails. 5.0.x compatability work was completed by the [Localytics][localytics-github] team. -The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). +[4.x-branch]: https://github.com/localytics/odbc_adapter/tree/v4.2.x +[4.x-build-badge]: https://travis-ci.org/localytics/odbc_adapter.svg?branch=4.2.x +[5.x-branch]: https://github.com/localytics/odbc_adapter/tree/master +[5.x-build-badge]: https://travis-ci.org/localytics/odbc_adapter.svg?branch=master +[build-link]: https://travis-ci.org/localytics/odbc_adapter/branches +[github-repo]: https://github.com/localytics/odbc_adapter +[license-badge]: https://img.shields.io/github/license/localytics/odbc_adapter.svg +[license-link]: https://github.com/localytics/odbc_adapter/blob/master/LICENSE +[localytics-github]: https://github.com/localytics +[openlink-activerecord-adapter]: https://github.com/dosire/activerecord-odbc-adapter +[supported-versions-badge]: https://img.shields.io/badge/active__record-4.x--5.x-green.svg diff --git a/Rakefile b/Rakefile index 6af9c2b8..b9dcde93 100644 --- a/Rakefile +++ b/Rakefile @@ -1,14 +1,24 @@ require 'bundler/gem_tasks' -require 'rake/testtask' -require 'rubocop/rake_task' -Rake::TestTask.new(:test) do |t| - t.libs << 'test' - t.libs << 'lib' - t.test_files = FileList['test/**/*_test.rb'] +task default: %i[rubocop test] + +desc 'Run rubocop' +task :rubocop do + require 'rubocop/rake_task' + + RuboCop::RakeTask.new do |task| + task.patterns = ['lib/**/*.rb'] + task.formatters = ['simple'] + end end -RuboCop::RakeTask.new(:rubocop) -Rake::Task[:test].prerequisites << :rubocop +desc 'Run tests' +task :test do + require 'rake/testtask' -task default: :test + Rake::TestTask.new do |task| + task.libs << 'test' + task.libs << 'lib' + task.test_files = FileList['test/**/*_test.rb'] + end +end diff --git a/bin/ci-setup b/bin/ci-setup deleted file mode 100755 index e7220716..00000000 --- a/bin/ci-setup +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -case "$DB" in -mysql) - sudo odbcinst -i -d -f /usr/share/libmyodbc/odbcinst.ini - mysql -e "DROP DATABASE IF EXISTS odbc_test; CREATE DATABASE IF NOT EXISTS odbc_test;" -uroot - mysql -e "GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost';" -uroot - ;; -postgresql) - sudo odbcinst -i -d -f /usr/share/psqlodbc/odbcinst.ini.template - psql -c "CREATE DATABASE odbc_test;" -U postgres - ;; -esac diff --git a/gemfiles/active_record_5_0.gemfile b/gemfiles/active_record_5_0.gemfile new file mode 100644 index 00000000..158e972a --- /dev/null +++ b/gemfiles/active_record_5_0.gemfile @@ -0,0 +1,5 @@ +source 'https://rubygems.org' + +gemspec(path: '..') + +gem 'activerecord', '~> 5.0.1' diff --git a/gemfiles/active_record_5_1.gemfile b/gemfiles/active_record_5_1.gemfile new file mode 100644 index 00000000..2c33e3ac --- /dev/null +++ b/gemfiles/active_record_5_1.gemfile @@ -0,0 +1,5 @@ +source 'https://rubygems.org' + +gemspec(path: '..') + +gem 'activerecord', '~> 5.1.0' diff --git a/gemfiles/active_record_5_2.gemfile b/gemfiles/active_record_5_2.gemfile new file mode 100644 index 00000000..4f1c5e9c --- /dev/null +++ b/gemfiles/active_record_5_2.gemfile @@ -0,0 +1,5 @@ +source 'https://rubygems.org' + +gemspec(path: '..') + +gem 'activerecord', '~> 5.2.0' diff --git a/lib/active_record/connection_adapters/odbc_adapter.rb b/lib/active_record/connection_adapters/odbc_adapter.rb index 1917e2d3..6ac7f82a 100644 --- a/lib/active_record/connection_adapters/odbc_adapter.rb +++ b/lib/active_record/connection_adapters/odbc_adapter.rb @@ -130,6 +130,7 @@ def disconnect! def new_column(name, default, sql_type_metadata, null, table_name, default_function = nil, collation = nil, native_type = nil) ::ODBCAdapter::Column.new(name, default, sql_type_metadata, null, table_name, default_function, collation, native_type) end + # rubocop:enable Metrics/ParameterLists protected diff --git a/lib/odbc_adapter/adapters/mysql_odbc_adapter.rb b/lib/odbc_adapter/adapters/mysql_odbc_adapter.rb index eaa690ef..f60d505d 100644 --- a/lib/odbc_adapter/adapters/mysql_odbc_adapter.rb +++ b/lib/odbc_adapter/adapters/mysql_odbc_adapter.rb @@ -143,7 +143,7 @@ def options_include_default?(options) protected - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) + def insert_sql(sql, name = nil, pri_key = nil, id_value = nil, sequence_name = nil) super id_value || last_inserted_id(nil) end diff --git a/lib/odbc_adapter/adapters/postgresql_odbc_adapter.rb b/lib/odbc_adapter/adapters/postgresql_odbc_adapter.rb index 28a28f7c..096cea91 100644 --- a/lib/odbc_adapter/adapters/postgresql_odbc_adapter.rb +++ b/lib/odbc_adapter/adapters/postgresql_odbc_adapter.rb @@ -17,6 +17,13 @@ def arel_visitor Arel::Visitors::PostgreSQL.new(self) end + # Explicitly disable prepared statements for now, as it's always erroring + # out with: + # ODBC::Error: INTERN (0) [RubyODBC]Too much parameters + def prepared_statements + false + end + # Filter for ODBCAdapter#tables # Omits table from #tables if table_filter returns true def table_filtered?(schema_name, table_type) @@ -29,19 +36,19 @@ def truncate(table_name, name = nil) # Returns the sequence name for a table's primary key or some other # specified key. - def default_sequence_name(table_name, pk = nil) - serial_sequence(table_name, pk || 'id').split('.').last + def default_sequence_name(table_name, pri_key = nil) + serial_sequence(table_name, pri_key || 'id').split('.').last rescue ActiveRecord::StatementInvalid - "#{table_name}_#{pk || 'id'}_seq" + "#{table_name}_#{pri_key || 'id'}_seq" end - def sql_for_insert(sql, pk, _id_value, _sequence_name, binds) - unless pk + def sql_for_insert(sql, pri_key, _id_value, _sequence_name, binds) + unless pri_key table_ref = extract_table_ref_from_insert_sql(sql) - pk = primary_key(table_ref) if table_ref + pri_key = primary_key(table_ref) if table_ref end - sql = "#{sql} RETURNING #{quote_column_name(pk)}" if pk + sql = "#{sql} RETURNING #{quote_column_name(pri_key)}" if pri_key [sql, binds] end @@ -50,7 +57,7 @@ def type_cast(value, column) case value when String - return super unless 'bytea' == column.native_type + return super unless column.native_type == 'bytea' { value: value, format: 1 } else super @@ -158,14 +165,14 @@ def distinct(columns, orders) protected # Executes an INSERT query and returns the new record's ID - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) - unless pk + def insert_sql(sql, name = nil, pri_key = nil, id_value = nil, sequence_name = nil) + unless pri_key table_ref = extract_table_ref_from_insert_sql(sql) - pk = primary_key(table_ref) if table_ref + pri_key = primary_key(table_ref) if table_ref end - if pk - select_value("#{sql} RETURNING #{quote_column_name(pk)}") + if pri_key + select_value("#{sql} RETURNING #{quote_column_name(pri_key)}") else super end @@ -180,9 +187,9 @@ def last_insert_id(sequence_name) private def serial_sequence(table, column) - result = exec_query(<<-eosql, 'SCHEMA') + result = exec_query(<<-EOSQL, 'SCHEMA') SELECT pg_get_serial_sequence('#{table}', '#{column}') - eosql + EOSQL result.rows.first.first end end diff --git a/lib/odbc_adapter/column.rb b/lib/odbc_adapter/column.rb index 36492a82..c74f242c 100644 --- a/lib/odbc_adapter/column.rb +++ b/lib/odbc_adapter/column.rb @@ -9,5 +9,6 @@ def initialize(name, default, sql_type_metadata = nil, null = true, table_name = super(name, default, sql_type_metadata, null, table_name, default_function, collation) @native_type = native_type end + # rubocop:enable Metrics/ParameterLists end end diff --git a/odbc_adapter.gemspec b/odbc_adapter.gemspec index ae02a406..4ed16569 100644 --- a/odbc_adapter.gemspec +++ b/odbc_adapter.gemspec @@ -1,6 +1,4 @@ -# coding: utf-8 - -lib = File.expand_path('../lib', __FILE__) +lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'odbc_adapter/version' @@ -21,11 +19,13 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ['lib'] + spec.add_dependency 'activerecord', '~> 5.0' spec.add_dependency 'ruby-odbc', '~> 0.9' - spec.add_development_dependency 'bundler', '~> 1.14' - spec.add_development_dependency 'minitest', '~> 5.10' - spec.add_development_dependency 'rake', '~> 12.0' - spec.add_development_dependency 'rubocop', '~> 0.48' + spec.add_development_dependency 'bundler', '>= 1.14' + spec.add_development_dependency 'minitest', '~> 5.10' + spec.add_development_dependency 'pry', '~> 0.11' + spec.add_development_dependency 'rake', '~> 12.0' + spec.add_development_dependency 'rubocop', '<= 0.58' spec.add_development_dependency 'simplecov', '~> 0.14' end diff --git a/test/connection_string_test.rb b/test/connection_string_test.rb new file mode 100644 index 00000000..53c4867b --- /dev/null +++ b/test/connection_string_test.rb @@ -0,0 +1,68 @@ +require 'test_helper' + +class ConnectionStringTest < Minitest::Test + def setup; end + + def teardown; end + + # Make sure that the connection string is parsed properly when it has an equals sign + def test_odbc_conn_str_connection_with_equals + conn_str = 'Foo=Bar;Foo2=Something=with=equals' + + odbc_driver_instance_mock = Minitest::Mock.new + odbc_database_instance_mock = Minitest::Mock.new + odbc_connection_instance_mock = Minitest::Mock.new + + # Setup ODBC::Driver instance mocks + odbc_driver_instance_mock.expect(:name=, nil, ['odbc']) + odbc_driver_instance_mock.expect(:attrs=, nil, [{ 'Foo' => 'Bar', 'Foo2' => 'Something=with=equals' }]) + + # Setup ODBC::Database instance mocks + odbc_database_instance_mock.expect(:drvconnect, odbc_connection_instance_mock, [odbc_driver_instance_mock]) + + # Stub ODBC::Driver.new + ODBC::Driver.stub :new, odbc_driver_instance_mock do + # Stub ODBC::Database.new + ODBC::Database.stub :new, odbc_database_instance_mock do + # Run under our stubs/mocks + ActiveRecord::Base.__send__(:odbc_conn_str_connection, conn_str: conn_str) + end + end + + # make sure we called the methods we expected + odbc_driver_instance_mock.verify + odbc_database_instance_mock.verify + odbc_connection_instance_mock.verify + end + + # Make sure that the connection string is parsed properly when it doesn't have an + # equals sign + def test_odbc_conn_str_connection_without_equals + conn_str = 'Foo=Bar;Foo2=Something without equals' + + odbc_driver_instance_mock = Minitest::Mock.new + odbc_database_instance_mock = Minitest::Mock.new + odbc_connection_instance_mock = Minitest::Mock.new + + # Setup ODBC::Driver instance mocks + odbc_driver_instance_mock.expect(:name=, nil, ['odbc']) + odbc_driver_instance_mock.expect(:attrs=, nil, [{ 'Foo' => 'Bar', 'Foo2' => 'Something without equals' }]) + + # Setup ODBC::Database instance mocks + odbc_database_instance_mock.expect(:drvconnect, odbc_connection_instance_mock, [odbc_driver_instance_mock]) + + # Stub ODBC::Driver.new + ODBC::Driver.stub :new, odbc_driver_instance_mock do + # Stub ODBC::Database.new + ODBC::Database.stub :new, odbc_database_instance_mock do + # Run under our stubs/mocks + ActiveRecord::Base.__send__(:odbc_conn_str_connection, conn_str: conn_str) + end + end + + # make sure we called the methods we expected + odbc_driver_instance_mock.verify + odbc_database_instance_mock.verify + odbc_connection_instance_mock.verify + end +end diff --git a/test/connections_test.rb b/test/connections_test.rb new file mode 100644 index 00000000..2ea158a5 --- /dev/null +++ b/test/connections_test.rb @@ -0,0 +1,44 @@ +require 'test_helper' + +# Dummy class for this test +class ConnectionsTestDummyActiveRecordModel < ActiveRecord::Base + self.abstract_class = true +end + +# This test makes sure that all of the connection methods work properly +class ConnectionsTest < Minitest::Test + def setup + @options = { adapter: 'odbc' } + @options[:conn_str] = ENV['CONN_STR'] if ENV['CONN_STR'] + @options[:dsn] = ENV['DSN'] if ENV['DSN'] + @options[:dsn] = 'ODBCAdapterPostgreSQLTest' if @options.values_at(:conn_str, :dsn).compact.empty? + + ConnectionsTestDummyActiveRecordModel.establish_connection @options + + @connection = ConnectionsTestDummyActiveRecordModel.connection + end + + def teardown + @connection.disconnect! + end + + def test_active? + assert_equal @connection.raw_connection.connected?, @connection.active? + end + + def test_disconnect! + @raw_connection = @connection.raw_connection + + assert_equal true, @raw_connection.connected? + @connection.disconnect! + assert_equal false, @raw_connection.connected? + end + + def test_reconnect! + @old_raw_connection = @connection.raw_connection + assert_equal true, @connection.active? + @connection.reconnect! + refute_equal @old_raw_connection, @connection.raw_connection + assert_equal true, @connection.active? + end +end diff --git a/test/registry_test.rb b/test/registry_test.rb index eb1afe2c..36b3f79e 100644 --- a/test/registry_test.rb +++ b/test/registry_test.rb @@ -24,4 +24,5 @@ def quoted_true end end end + # rubocop:enable Lint/NestedMethodDefinition end diff --git a/test/test_helper.rb b/test/test_helper.rb index 65cc6d52..c8317250 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,10 +1,11 @@ require 'simplecov' SimpleCov.start -$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) +$LOAD_PATH.unshift File.expand_path('../lib', __dir__) require 'odbc_adapter' require 'minitest/autorun' +require 'minitest/mock' require 'pry' options = { adapter: 'odbc' }