diff --git a/.gitignore b/.gitignore index ff20d0deb..511d7d68d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,9 +9,7 @@ Gemfile.lock test/profile/output/* .rvmrc .rbenv-version -.tool-versions .idea coverage/* .flooignore -.floo -.byebug_history +.floo \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index eee1f33f7..efc76819a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,22 +4,25 @@ services: - docker env: global: - - COMPOSE_FILE: docker-compose.ci.yml + - TINYTDS_VERSION=2.1.0 + - ACTIVERECORD_UNITTEST_HOST=localhost + - ACTIVERECORD_UNITTEST_DATASERVER=localhost +rvm: + - 2.2.5 + - 2.3.1 + - 2.4.0 before_install: - - sudo rm /usr/local/bin/docker-compose - - sudo curl -L "https://github.com/docker/compose/releases/download/1.22.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose - - sudo chmod +x /usr/local/bin/docker-compose + - export PATH=/opt/local/bin:$PATH + - docker info + - sudo ./test/bin/setup.sh + - sudo ./test/bin/install-openssl.sh + - openssl version + - sudo ./test/bin/install-freetds.sh + - tsql -C install: - - docker-compose build --build-arg TARGET_VERSION=$TARGET_VERSION + - export PATH=/opt/local/bin:$PATH + - gem install bundler + - bundle --version + - bundle install script: - - docker-compose run ci -matrix: - include: - - name: 2.3.8 - env: TARGET_VERSION=2.3.8 - - name: 2.4.6 - env: TARGET_VERSION=2.4.6 - - name: 2.5.5 - env: TARGET_VERSION=2.5.5 - - name: 2.6.3 - env: TARGET_VERSION=2.6.3 + - bundle exec rake diff --git a/BACKERS.md b/BACKERS.md new file mode 100644 index 000000000..5797c4bc8 --- /dev/null +++ b/BACKERS.md @@ -0,0 +1,32 @@ +# Backers + +You can join in supporting TinyTDS and the Rails SQL Server Adapter development by [pledging on Patreon](https://www.patreon.com/metaskills)! Backers in the same pledge level appear in the order of pledge date. + +### $2000 + +[It could be you!](https://www.patreon.com/bePatron?c=765225&rid=1611218) + + +### $500 + +[It could be you!](https://www.patreon.com/bePatron?c=765225&rid=1611209) + + +### $250 + +[It could be you!](https://www.patreon.com/bePatron?c=765225&rid=1611199) + + +### $100 + +[It could be you!](https://www.patreon.com/bePatron?c=765225&rid=1611196) + + +### $50+ + +[It could be you!](https://www.patreon.com/bePatron?c=765225&rid=1611186) + + +### $10+ + +[It could be you!](https://www.patreon.com/bePatron?c=765225&rid=1611149) diff --git a/CHANGELOG.md b/CHANGELOG.md index 873505b5c..4a5eb112a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,25 +1,61 @@ -## v5.2.0 +## v5.1.6.odbc -- #686 sql_for_insert set table name in case when pk is not nil +#### Added -## v5.2.0.rc2 +* ODBC restoration. + + +## v5.1.6 + +#### Added + +* Use lock hint when joining table in query. + + +## v5.1.5 #### Fixed -- #681 change_column_null should not clear other column attributes. Fixes #582. -- #684 Fix explain with array conditions. Fixes #673. +* Memoize `@@version` queries. Fixes #632 + -## v5.2.0.rc1 +## v5.1.4 #### Fixed -- #638 Don't disable referential integrity for the same table twice. -- #646 Make String equality check work for Type::Data values. Fixes #645. -- #671 Fix tinyint columns schema migration. Fixes #670. +* Add case insensitive comparison for better performance with CI collations. Fixes #624 + + +## v5.1.3 + +#### Fixed + +* Use bigint type in sqlserver_type when needed. Fixes #616 + + +## v5.1.2 + +#### Fixed + +* The `fast_string_to_time` method when zone local. Fixes #609 #614 #620 +* Patched `Relation#build_count_subquery`. Fixes #613. +* Inserts to tables with triggers using default `OUTPUT INSERTED` style. Fixes #595. + + +## v5.1.1 + +#### Fixed + +* Use `ActiveSupport.on_load` to hook into ActiveRecord Fixes #588 #598 + + +## v5.1.0 #### Changed -- #642 Added with (nolock) hint to information_schema.views. +* The `drop_table` with force cascade option now mimics in via pure SQL for us. + +#### Added +* Support MismatchedForeignKey exception. -Please check [5-1-stable](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/blob/5-1-stable/CHANGELOG.md) for previous changes. diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 8b18d5c6a..000000000 --- a/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -ARG TARGET_VERSION=2.6.3 - -FROM wpolicarpo/activerecord-sqlserver-adapter:${TARGET_VERSION} - -ENV WORKDIR /activerecord-sqlserver-adapter - -RUN mkdir -p $WORKDIR -WORKDIR $WORKDIR - -COPY . $WORKDIR - -RUN bundle install --jobs `expr $(cat /proc/cpuinfo | grep -c "cpu cores") - 1` --retry 3 - -CMD ["sh"] diff --git a/Gemfile b/Gemfile index f27143d4d..14a0925f3 100644 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,8 @@ require 'openssl' source 'https://rubygems.org' gemspec -gem 'sqlite3', '~> 1.3.6' +gem 'sqlite3', '< 1.4' +gem 'minitest', '< 5.3.4' gem 'bcrypt' gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] @@ -50,6 +51,10 @@ group :tinytds do end end +group :odbc do + gem 'ruby-odbc', :git => 'https://github.com/cloudvolumes/ruby-odbc.git', :tag => '0.101.cv' +end + group :development do gem 'byebug' gem 'mocha' diff --git a/README.md b/README.md index f09cff6cc..a8c06b03c 100644 --- a/README.md +++ b/README.md @@ -7,15 +7,20 @@ * [![Dependency Status](https://dependencyci.com/github/rails-sqlserver/activerecord-sqlserver-adapter/badge)](https://dependencyci.com/github/rails-sqlserver/activerecord-sqlserver-adapter) - Dependency Status * [![Gitter chat](https://img.shields.io/badge/%E2%8A%AA%20GITTER%20-JOIN%20CHAT%20%E2%86%92-brightgreen.svg?style=flat)](https://gitter.im/rails-sqlserver/activerecord-sqlserver-adapter) - Community +## Supporting TinyTDS/Adapter + +Both TinyTDS and the Rails SQL Server Adapter are MIT-licensed open source projects. Its ongoing development is made possible thanks to the support by these awesome [backers](https://github.com/rails-sqlserver/tiny_tds/blob/master/BACKERS.md). If you'd like to join them, check out our [Patreon Campaign](https://www.patreon.com/metaskills). + + ## About The Adapter -The SQL Server adapter for ActiveRecord v5.2 using SQL Server 2012 or higher. +The SQL Server adapter for ActiveRecord v5.1 using SQL Server 2012 or higher. -Interested in older versions? We follow a rational versioning policy that tracks Rails. That means that our 5.1.x version of the adapter is only for the latest 5.1 version of Rails. If you need the adapter for SQL Server 2008 or 2005, you are still in the right spot. Just install the latest 3.2.x to 4.1.x version of the adapter that matches your Rails version. We also have stable branches for each major/minor release of ActiveRecord. +Interested in older versions? We follow a rational versioning policy that tracks Rails. That means that our 5.0.x version of the adapter is only for the latest 5.0 version of Rails. If you need the adapter for SQL Server 2008 or 2005, you are still in the right spot. Just install the latest 3.2.x to 4.1.x version of the adapter that matches your Rails version. We also have stable branches for each major/minor release of ActiveRecord. #### Native Data Type Support -We support every data type supported by FreeTDS. All simplified Rails types in migrations will coorespond to a matching SQL Server national (unicode) data type. Always check the `initialize_native_database_types` [(here)](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/blob/master/lib/active_record/connection_adapters/sqlserver/schema_statements.rb) for an updated list. +We support every data type supported by FreeTDS. All simplified Rails types in migrations will coorespond to a matching SQL Server national (unicode) data type. Always check the `initialize_native_database_types` [(here)](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/blob/master/lib/active_record/connection_adapters/sqlserver/schema_statements.rb#L243) for an updated list. The following types (date, datetime2, datetimeoffset, time) all require TDS version 7.3 with TinyTDS. We recommend using FreeTDS 1.0 or higher which default to using `TDSVER` to "7.3". The adapter also sets TinyTDS's `tds_version` to this as well if non is specified. @@ -131,7 +136,7 @@ gem 'activerecord-sqlserver-adapter' ## Contributing -If you would like to contribute a feature or bugfix, thanks! To make sure your fix/feature has a high chance of being added, please read the following guidelines. First, ask on the Gitter, or post a ticket on github issues. Second, make sure there are tests! We will not accept any patch that is not tested. Please read the [`RUNNING_UNIT_TESTS`](RUNNING_UNIT_TESTS.md) file for the details of how to run the unit tests. +If you would like to contribute a feature or bugfix, thanks! To make sure your fix/feature has a high chance of being added, please read the following guidelines. First, ask on the Gitter, or post a ticket on github issues. Second, make sure there are tests! We will not accept any patch that is not tested. Please read the `RUNNING_UNIT_TESTS` file for the details of how to run the unit tests. * Github: http://github.com/rails-sqlserver/activerecord-sqlserver-adapter * Gitter: https://gitter.im/rails-sqlserver/activerecord-sqlserver-adapter diff --git a/Rakefile b/Rakefile index 79a4a5885..ed71ab146 100644 --- a/Rakefile +++ b/Rakefile @@ -8,7 +8,7 @@ task default: [:test] namespace :test do - %w(dblib).each do |mode| + %w(dblib odbc).each do |mode| Rake::TestTask.new(mode) do |t| t.libs = ARTest::SQLServer.test_load_paths @@ -23,12 +23,17 @@ namespace :test do ENV['ARCONN'] = 'dblib' end + task 'odbc:env' do + ENV['ARCONN'] = 'odbc' + end + end task 'test:dblib' => 'test:dblib:env' +task 'test:odbc' => 'test:odbc:env' namespace :profile do - ['dblib'].each do |mode| + ['dblib', 'odbc'].each do |mode| namespace mode.to_sym do Dir.glob('test/profile/*_profile_case.rb').sort.each do |test_file| profile_case = File.basename(test_file).sub('_profile_case.rb', '') diff --git a/VERSION b/VERSION index 91ff57278..ce05aad52 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.2.0 +5.1.6.odbc diff --git a/activerecord-sqlserver-adapter.gemspec b/activerecord-sqlserver-adapter.gemspec index b560cf30a..0dd17033e 100644 --- a/activerecord-sqlserver-adapter.gemspec +++ b/activerecord-sqlserver-adapter.gemspec @@ -16,6 +16,6 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ['lib'] - spec.add_dependency 'activerecord', '~> 5.2.0' - spec.add_dependency 'tiny_tds' + spec.add_dependency 'activerecord', '~> 5.1.0' + spec.add_dependency 'ruby-odbc' end diff --git a/circle.yml b/circle.yml index ce0dfafc6..d13053f64 100644 --- a/circle.yml +++ b/circle.yml @@ -18,10 +18,9 @@ dependencies: - openssl version - sudo ./test/bin/install-freetds.sh - tsql -C - - rvm-exec 2.3.8 bundle install - - rvm-exec 2.4.5 bundle install - - rvm-exec 2.5.3 bundle install - - rvm-exec 2.6.0 bundle install + - rvm-exec 2.2.5 bundle install + - rvm-exec 2.3.1 bundle install + - rvm-exec 2.4.0 bundle install database: override: @@ -32,7 +31,6 @@ database: test: override: - - rvm-exec 2.3.8 bundle exec rake test - - rvm-exec 2.4.5 bundle exec rake test - - rvm-exec 2.5.3 bundle exec rake test - - rvm-exec 2.6.0 bundle exec rake test + - rvm-exec 2.2.5 bundle exec rake test + - rvm-exec 2.3.1 bundle exec rake test + - rvm-exec 2.4.0 bundle exec rake test diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml deleted file mode 100644 index a80798097..000000000 --- a/docker-compose.ci.yml +++ /dev/null @@ -1,11 +0,0 @@ -version: "2.2" -services: - database: - image: metaskills/mssql-server-linux-rails - ci: - environment: - - ACTIVERECORD_UNITTEST_HOST=database - build: . - command: wait-for database:1433 -- bundle exec rake test - depends_on: - - "database" diff --git a/lib/active_record/connection_adapters/sqlserver/core_ext/calculations.rb b/lib/active_record/connection_adapters/sqlserver/core_ext/calculations.rb index cfdb65453..e6478df3e 100644 --- a/lib/active_record/connection_adapters/sqlserver/core_ext/calculations.rb +++ b/lib/active_record/connection_adapters/sqlserver/core_ext/calculations.rb @@ -9,16 +9,19 @@ module Calculations private def build_count_subquery(relation, column_name, distinct) - super(relation.unscope(:order), column_name, distinct) - end + relation.select_values = [ + if column_name == :all + distinct ? table[Arel.star] : Arel.sql(FinderMethods::ONE_AS_ONE) + else + column_alias = Arel.sql("count_column") + aggregate_column(column_name).as(column_alias) + end + ] + + subquery = relation.arel.as(Arel.sql("subquery_for_count")) + select_value = operation_over_aggregate_column(column_alias || Arel.star, "count", false) - def type_cast_calculated_value(value, type, operation = nil) - case operation - when "count" then value.to_i - when "sum" then type.deserialize(value || 0) - when "average" then value&.respond_to?(:to_d) ? value.to_d : value - else type.deserialize(value) - end + Arel::SelectManager.new(subquery).project(select_value) end end end @@ -27,6 +30,10 @@ def type_cast_calculated_value(value, type, operation = nil) end ActiveSupport.on_load(:active_record) do - mod = ActiveRecord::ConnectionAdapters::SQLServer::CoreExt::Calculations - ActiveRecord::Relation.prepend(mod) + if ActiveRecord::VERSION::MAJOR == 5 && + ActiveRecord::VERSION::MINOR == 1 && + ActiveRecord::VERSION::TINY >= 4 + mod = ActiveRecord::ConnectionAdapters::SQLServer::CoreExt::Calculations + ActiveRecord::Relation.prepend(mod) + end end diff --git a/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb b/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb index baa8d736c..b12502197 100644 --- a/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +++ b/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb @@ -5,11 +5,12 @@ module CoreExt module Explain SQLSERVER_STATEMENT_PREFIX = 'EXEC sp_executesql '.freeze - SQLSERVER_STATEMENT_REGEXP = /N'(.+)', N'(.+)', (.+)/ + SQLSERVER_PARAM_MATCHER = /@\d+ = (.*)/ + SQLSERVER_NATIONAL_STRING_MATCHER = /N'(.*)'/m def exec_explain(queries) unprepared_queries = queries.map do |(sql, binds)| - [unprepare_sqlserver_statement(sql, binds), binds] + [unprepare_sqlserver_statement(sql), binds] end super(unprepared_queries) end @@ -18,19 +19,22 @@ def exec_explain(queries) # This is somewhat hacky, but it should reliably reformat our prepared sql statment # which uses sp_executesql to just the first argument, then unquote it. Likewise our - # `sp_executesql` method should substitude the @n args with the quoted values. - def unprepare_sqlserver_statement(sql, binds) - return sql unless sql.starts_with?(SQLSERVER_STATEMENT_PREFIX) - - executesql = sql.from(SQLSERVER_STATEMENT_PREFIX.length) - executesql = executesql.match(SQLSERVER_STATEMENT_REGEXP).to_a[1] - - binds.each_with_index do |bind, index| - value = connection.quote(bind) - executesql = executesql.sub("@#{index}", value) + # `sp_executesql` method should substitude the @n args withe the quoted values. + def unprepare_sqlserver_statement(sql) + if sql.starts_with?(SQLSERVER_STATEMENT_PREFIX) + executesql = sql.from(SQLSERVER_STATEMENT_PREFIX.length) + args = executesql.split(', ') + unprepared_sql = args.shift.strip.match(SQLSERVER_NATIONAL_STRING_MATCHER)[1] + unprepared_sql = Utils.unquote_string(unprepared_sql) + args = args.from(args.length / 2) + args.each_with_index do |arg, index| + value = arg.match(SQLSERVER_PARAM_MATCHER)[1] + unprepared_sql.sub! "@#{index}", value + end + unprepared_sql + else + sql end - - executesql end end diff --git a/lib/active_record/connection_adapters/sqlserver/core_ext/odbc.rb b/lib/active_record/connection_adapters/sqlserver/core_ext/odbc.rb new file mode 100644 index 000000000..712751a2b --- /dev/null +++ b/lib/active_record/connection_adapters/sqlserver/core_ext/odbc.rb @@ -0,0 +1,34 @@ +module ActiveRecord + module ConnectionAdapters + module SQLServer + module CoreExt + module ODBC + + module Statement + + def finished? + connected? + false + rescue ::ODBC::Error + true + end + + end + + module Database + + def run_block(*args) + yield sth = run(*args) + sth.drop + end + + end + + end + end + end + end +end + +ODBC::Statement.send :include, ActiveRecord::ConnectionAdapters::SQLServer::CoreExt::ODBC::Statement +ODBC::Database.send :include, ActiveRecord::ConnectionAdapters::SQLServer::CoreExt::ODBC::Database diff --git a/lib/active_record/connection_adapters/sqlserver/database_limits.rb b/lib/active_record/connection_adapters/sqlserver/database_limits.rb index c5f419094..e822ca96a 100644 --- a/lib/active_record/connection_adapters/sqlserver/database_limits.rb +++ b/lib/active_record/connection_adapters/sqlserver/database_limits.rb @@ -2,6 +2,7 @@ module ActiveRecord module ConnectionAdapters module SQLServer module DatabaseLimits + def table_alias_length 128 end @@ -31,7 +32,7 @@ def columns_per_multicolumn_index end def in_clause_length - 10_000 + 65_536 end def sql_query_length @@ -42,18 +43,6 @@ def joins_per_query 256 end - private - - # The max number of binds is 2100, but because sp_executesql takes - # the first 2 params as the query string and the list of types, - # we have only 2098 spaces left - def bind_params_length - 2_098 - end - - def insert_rows_length - 1_000 - end end end end diff --git a/lib/active_record/connection_adapters/sqlserver/database_statements.rb b/lib/active_record/connection_adapters/sqlserver/database_statements.rb index 6f543a59a..98aa47c7f 100644 --- a/lib/active_record/connection_adapters/sqlserver/database_statements.rb +++ b/lib/active_record/connection_adapters/sqlserver/database_statements.rb @@ -24,13 +24,15 @@ def exec_insert(sql, name = nil, binds = [], pk = nil, _sequence_name = nil) end def exec_delete(sql, name, binds) - sql = sql.dup << '; SELECT @@ROWCOUNT AS AffectedRows' - super(sql, name, binds).rows.first.first + super.rows.first.try(:first) || super("SELECT @@ROWCOUNT As AffectedRows", "", []).rows.first.try(:first) end def exec_update(sql, name, binds) - sql = sql.dup << '; SELECT @@ROWCOUNT AS AffectedRows' - super(sql, name, binds).rows.first.first + super.rows.first.try(:first) || super("SELECT @@ROWCOUNT As AffectedRows", "", []).rows.first.try(:first) + end + + def supports_statement_cache? + true end def begin_db_transaction @@ -73,55 +75,17 @@ def release_savepoint(name = current_savepoint_name) def case_sensitive_comparison(table, attribute, column, value) if column.collation && !column.case_sensitive? - table[attribute].eq(Arel::Nodes::Bin.new(value)) + table[attribute].eq(Arel::Nodes::Bin.new(Arel::Nodes::BindParam.new)) else super end end - # We should propose this change to Rails team - def insert_fixtures_set(fixture_set, tables_to_delete = []) - fixture_inserts = [] - - fixture_set.each do |table_name, fixtures| - fixtures.each_slice(insert_rows_length) do |batch| - fixture_inserts << build_fixture_sql(batch, table_name) - end - end - - table_deletes = tables_to_delete.map { |table| "DELETE FROM #{quote_table_name table}".dup } - total_sql = Array.wrap(combine_multi_statements(table_deletes + fixture_inserts)) - - disable_referential_integrity do - transaction(requires_new: true) do - total_sql.each do |sql| - execute sql, "Fixtures Load" - yield if block_given? - end - end - end - end - def can_perform_case_insensitive_comparison_for?(column) column.type == :string && (!column.collation || column.case_sensitive?) end private :can_perform_case_insensitive_comparison_for? - def combine_multi_statements(total_sql) - total_sql - end - private :combine_multi_statements - - def default_insert_value(column) - if column.is_identity? - table_name = quote(quote_table_name(column.table_name)) - Arel.sql("IDENT_CURRENT(#{table_name}) + IDENT_INCR(#{table_name})") - else - super - end - end - private :default_insert_value - # === SQLServer Specific ======================================== # def execute_procedure(proc_name, *variables) @@ -142,6 +106,18 @@ def execute_procedure(proc_name, *variables) yield(r) if block_given? end result.each.map { |row| row.is_a?(Hash) ? row.with_indifferent_access : row } + when :odbc + results = [] + raw_connection_run(sql) do |handle| + get_rows = lambda do + rows = handle_to_names_and_values handle, fetch: :all + rows.each_with_index { |r, i| rows[i] = r.with_indifferent_access } + results << rows + end + get_rows.call + get_rows.call while handle_more_results?(handle) + end + results.many? ? results : results.first end end end @@ -228,20 +204,27 @@ def sql_for_insert(sql, pk, id_value, sequence_name, binds) end sql = if pk && use_output_inserted? && !database_prefix_remote_server? quoted_pk = SQLServer::Utils.extract_identifiers(pk).quoted - table_name ||= get_table_name(sql) exclude_output_inserted = exclude_output_inserted_table_name?(table_name, sql) if exclude_output_inserted id_sql_type = exclude_output_inserted.is_a?(TrueClass) ? 'bigint' : exclude_output_inserted <<-SQL.strip_heredoc + SET NOCOUNT ON DECLARE @ssaIdInsertTable table (#{quoted_pk} #{id_sql_type}); #{sql.dup.insert sql.index(/ (DEFAULT )?VALUES/), " OUTPUT INSERTED.#{quoted_pk} INTO @ssaIdInsertTable"} - SELECT CAST(#{quoted_pk} AS #{id_sql_type}) FROM @ssaIdInsertTable + SELECT CAST(#{quoted_pk} AS #{id_sql_type}) FROM @ssaIdInsertTable; + SET NOCOUNT OFF SQL else sql.dup.insert sql.index(/ (DEFAULT )?VALUES/), " OUTPUT INSERTED.#{quoted_pk}" end else - "#{sql}; SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident" + table = get_table_name(sql) + id_column = identity_columns(table.to_s.strip).first + if !id_column.blank? + sql.sub(/\s*VALUES\s*\(/, " OUTPUT INSERTED.#{id_column.name} VALUES (") + else + sql.sub(/\s*VALUES\s*\(/, " OUTPUT CAST(SCOPE_IDENTITY() AS bigint) AS Ident VALUES (") + end end super end @@ -272,8 +255,6 @@ def sp_executesql(sql, name, binds, options = {}) def sp_executesql_types_and_parameters(binds) types, params = [], [] binds.each_with_index do |attr, index| - attr = attr.value if attr.is_a?(Arel::Nodes::BindParam) - types << "@#{index} #{sp_executesql_sql_type(attr)}" params << sp_executesql_sql_param(attr) end @@ -291,12 +272,12 @@ def sp_executesql_sql_type(attr) end def sp_executesql_sql_param(attr) - case value = attr.value_for_database + case attr.value_for_database when Type::Binary::Data, ActiveRecord::Type::SQLServer::Data - quote(value) + quote(attr.value_for_database) else - quote(type_cast(value)) + quote(type_cast(attr.value_for_database)) end end @@ -319,6 +300,8 @@ def raw_connection_do(sql) case @connection_options[:mode] when :dblib @connection.execute(sql).do + when :odbc + @connection.do(sql) end ensure @update_sql = false @@ -342,7 +325,7 @@ def exclude_output_inserted_table_name?(table_name, sql) end def exec_insert_requires_identity?(sql, pk, binds) - query_requires_identity_insert?(sql) + query_requires_identity_insert?(sql) if pk && binds.map(&:name).include?(pk) end def query_requires_identity_insert?(sql) @@ -381,12 +364,16 @@ def raw_connection_run(sql) case @connection_options[:mode] when :dblib @connection.execute(sql) + when :odbc + block_given? ? @connection.run_block(sql) { |handle| yield(handle) } : @connection.run(sql) end end def handle_more_results?(handle) case @connection_options[:mode] when :dblib + when :odbc + handle.more_results end end @@ -394,6 +381,8 @@ def handle_to_names_and_values(handle, options = {}) case @connection_options[:mode] when :dblib handle_to_names_and_values_dblib(handle, options) + when :odbc + handle_to_names_and_values_odbc(handle, options) end end @@ -407,10 +396,28 @@ def handle_to_names_and_values_dblib(handle, options = {}) options[:ar_result] ? ActiveRecord::Result.new(columns, results) : results end + def handle_to_names_and_values_odbc(handle, options = {}) + @connection.use_utc = ActiveRecord::Base.default_timezone == :utc + if options[:ar_result] + columns = lowercase_schema_reflection ? handle.columns(true).map { |c| c.name.downcase } : handle.columns(true).map { |c| c.name } + rows = handle.fetch_all || [] + ActiveRecord::Result.new(columns, rows) + else + case options[:fetch] + when :all + handle.each_hash || [] + when :rows + handle.fetch_all || [] + end + end + end + def finish_statement_handle(handle) case @connection_options[:mode] when :dblib handle.cancel if handle + when :odbc + handle.drop if handle && handle.respond_to?(:drop) && !handle.finished? end handle end diff --git a/lib/active_record/connection_adapters/sqlserver/database_tasks.rb b/lib/active_record/connection_adapters/sqlserver/database_tasks.rb index e1ab94871..8e24234a3 100644 --- a/lib/active_record/connection_adapters/sqlserver/database_tasks.rb +++ b/lib/active_record/connection_adapters/sqlserver/database_tasks.rb @@ -12,7 +12,6 @@ def create_database(database, options = {}) def drop_database(database) name = SQLServer::Utils.extract_identifiers(database) - do_execute "ALTER DATABASE #{name} SET SINGLE_USER WITH ROLLBACK IMMEDIATE" do_execute "DROP DATABASE #{name}" end diff --git a/lib/active_record/connection_adapters/sqlserver/schema_creation.rb b/lib/active_record/connection_adapters/sqlserver/schema_creation.rb index 31875a13b..382216573 100644 --- a/lib/active_record/connection_adapters/sqlserver/schema_creation.rb +++ b/lib/active_record/connection_adapters/sqlserver/schema_creation.rb @@ -8,8 +8,7 @@ class SchemaCreation < AbstractAdapter::SchemaCreation def visit_TableDefinition(o) if o.as table_name = quote_table_name(o.temporary ? "##{o.name}" : o.name) - query = o.as.respond_to?(:to_sql) ? o.as.to_sql : o.as - projections, source = query.match(%r{SELECT\s+(.*)?\s+FROM\s+(.*)?}).captures + projections, source = @conn.to_sql(o.as).match(%r{SELECT\s+(.*)?\s+FROM\s+(.*)?}).captures select_into = "SELECT #{projections} INTO #{table_name} FROM #{source}" else o.instance_variable_set :@as, nil diff --git a/lib/active_record/connection_adapters/sqlserver/schema_dumper.rb b/lib/active_record/connection_adapters/sqlserver/schema_dumper.rb index d801d1d86..708238d42 100644 --- a/lib/active_record/connection_adapters/sqlserver/schema_dumper.rb +++ b/lib/active_record/connection_adapters/sqlserver/schema_dumper.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module SQLServer - class SchemaDumper < ConnectionAdapters::SchemaDumper + module SchemaDumper SQLSEVER_NO_LIMIT_TYPES = [ 'text', @@ -24,7 +24,7 @@ def schema_limit(column) def schema_collation(column) return unless column.collation - column.collation if column.collation != @connection.collation + column.collation if column.collation != collation end def default_primary_key?(column) diff --git a/lib/active_record/connection_adapters/sqlserver/schema_statements.rb b/lib/active_record/connection_adapters/sqlserver/schema_statements.rb index 185cc2154..3800f4d22 100644 --- a/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +++ b/lib/active_record/connection_adapters/sqlserver/schema_statements.rb @@ -32,33 +32,22 @@ def drop_table(table_name, options = {}) end end - def indexes(table_name) - data = select("EXEC sp_helpindex #{quote(table_name)}", "SCHEMA") rescue [] - + def indexes(table_name, name = nil) + data = select("EXEC sp_helpindex #{quote(table_name)}", name) rescue [] data.reduce([]) do |indexes, index| index = index.with_indifferent_access - if index[:index_description] =~ /primary key/ indexes else name = index[:index_name] unique = index[:index_description] =~ /unique/ where = select_value("SELECT [filter_definition] FROM sys.indexes WHERE name = #{quote(name)}") - orders = {} - columns = [] - - index[:index_keys].split(',').each do |column| + columns = index[:index_keys].split(',').map do |column| column.strip! - - if column.ends_with?('(-)') - column.gsub! '(-)', '' - orders[column] = :desc - end - - columns << column + column.gsub! '(-)', '' if column.ends_with?('(-)') + column end - - indexes << IndexDefinition.new(table_name, name, unique, columns, where: where, orders: orders) + indexes << IndexDefinition.new(table_name, name, unique, columns, nil, nil, where) end end end @@ -141,13 +130,7 @@ def change_column(table_name, column_name, type, options = {}) sql_commands = [] indexes = [] column_object = schema_cache.columns(table_name).find { |c| c.name.to_s == column_name.to_s } - without_constraints = options.key?(:default) || options.key?(:limit) - default = if !options.key?(:default) && column_object - column_object.default - else - options[:default] - end - if without_constraints || (column_object && column_object.type != type.to_sym) + if options_include_default?(options) || (column_object && column_object.type != type.to_sym) remove_default_constraint(table_name, column_name) indexes = indexes(table_name).select { |index| index.columns.include?(column_name.to_s) } remove_indexes(table_name, column_name) @@ -155,9 +138,10 @@ def change_column(table_name, column_name, type, options = {}) sql_commands << "UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_expression(options[:default], column_object)} WHERE #{quote_column_name(column_name)} IS NULL" if !options[:null].nil? && options[:null] == false && !options[:default].nil? sql_commands << "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, limit: options[:limit], precision: options[:precision], scale: options[:scale])}" sql_commands.last << ' NOT NULL' if !options[:null].nil? && options[:null] == false - if without_constraints - default = quote_default_expression(default, column_object || column_for(table_name, column_name)) - sql_commands << "ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{default_constraint_name(table_name, column_name)} DEFAULT #{default} FOR #{quote_column_name(column_name)}" + if options.key?(:default) && default_constraint_name(table_name, column_name).present? + change_column_default(table_name, column_name, options[:default]) + elsif options_include_default?(options) + sql_commands << "ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{default_constraint_name(table_name, column_name)} DEFAULT #{quote_default_expression(options[:default], column_object)} FOR #{quote_column_name(column_name)}" end # Add any removed indexes back indexes.each do |index| @@ -225,8 +209,7 @@ def type_to_sql(type, limit: nil, precision: nil, scale: nil, **) case type.to_s when 'integer' case limit - when 1 then 'tinyint' - when 2 then 'smallint' + when 1..2 then 'smallint' when 3..4, nil then 'integer' when 5..8 then 'bigint' else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.") @@ -252,8 +235,7 @@ def columns_for_distinct(columns, orders) s.gsub(/\s+(?:ASC|DESC)\b/i, '') .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, '') }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" } - - (order_columns << super).join(", ") + [super, *order_columns].join(', ') end def update_table_definition(table_name, base) @@ -263,7 +245,7 @@ def update_table_definition(table_name, base) def change_column_null(table_name, column_name, allow_null, default = nil) table_id = SQLServer::Utils.extract_identifiers(table_name) column_id = SQLServer::Utils.extract_identifiers(column_name) - column = column_for(table_name, column_name) + column = detect_column_for! table_name, column_name if !allow_null.nil? && allow_null == false && !default.nil? do_execute("UPDATE #{table_id} SET #{column_id}=#{quote(default)} WHERE #{column_id} IS NULL") end @@ -272,17 +254,13 @@ def change_column_null(table_name, column_name, allow_null, default = nil) do_execute sql end - def create_schema_dumper(options) - SQLServer::SchemaDumper.create(self, options) - end - private def data_source_sql(name = nil, type: nil) scope = quoted_scope name, type: type table_name = lowercase_schema_reflection_sql 'TABLE_NAME' sql = "SELECT #{table_name}" - sql << ' FROM INFORMATION_SCHEMA.TABLES WITH (NOLOCK)' + sql << ' FROM INFORMATION_SCHEMA.TABLES' sql << ' WHERE TABLE_CATALOG = DB_NAME()' sql << " AND TABLE_SCHEMA = #{quote(scope[:schema])}" sql << " AND TABLE_NAME = #{quote(scope[:name])}" if scope[:name] @@ -421,13 +399,11 @@ def column_definitions(table_name) ci[:default_function] = begin default = ci[:default_value] if default.nil? && view_exists - default = select_value %{ + default = select_value " SELECT c.COLUMN_DEFAULT FROM #{database}.INFORMATION_SCHEMA.COLUMNS c - WHERE - c.TABLE_NAME = '#{view_tblnm}' - AND c.COLUMN_NAME = '#{views_real_column_name(table_name, ci[:name])}' - }.squish, 'SCHEMA' + WHERE c.TABLE_NAME = '#{view_tblnm}' + AND c.COLUMN_NAME = '#{views_real_column_name(table_name, ci[:name])}'".squish, 'SCHEMA' end case default when nil @@ -446,7 +422,7 @@ def column_definitions(table_name) else ci[:type] end value = default.match(/\A\((.*)\)\Z/m)[1] - value = select_value("SELECT CAST(#{value} AS #{type}) AS value", 'SCHEMA') + value = select_value "SELECT CAST(#{value} AS #{type}) AS value", 'SCHEMA' [value, nil] end end @@ -497,6 +473,13 @@ def default_constraint_name(table_name, column_name) "DF_#{table_name}_#{column_name}" end + def detect_column_for!(table_name, column_name) + unless column = schema_cache.columns(table_name).find { |c| c.name == column_name.to_s } + raise ActiveRecordError, "No such column: #{table_name}.#{column_name}" + end + column + end + def lowercase_schema_reflection_sql(node) lowercase_schema_reflection ? "LOWER(#{node})" : node end @@ -512,7 +495,7 @@ def view_information(table_name) @view_information ||= {} @view_information[table_name] ||= begin identifier = SQLServer::Utils.extract_identifiers(table_name) - view_info = select_one "SELECT * FROM INFORMATION_SCHEMA.VIEWS WITH (NOLOCK) WHERE TABLE_NAME = #{quote(identifier.object)}", 'SCHEMA' + view_info = select_one "SELECT * FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME = #{quote(identifier.object)}", 'SCHEMA' if view_info view_info = view_info.with_indifferent_access if view_info[:VIEW_DEFINITION].blank? || view_info[:VIEW_DEFINITION].length == 4000 diff --git a/lib/active_record/connection_adapters/sqlserver/transaction.rb b/lib/active_record/connection_adapters/sqlserver/transaction.rb index 0e9f759f6..591613f1e 100644 --- a/lib/active_record/connection_adapters/sqlserver/transaction.rb +++ b/lib/active_record/connection_adapters/sqlserver/transaction.rb @@ -26,13 +26,13 @@ def current_isolation_level end - Transaction.send :prepend, SQLServerTransaction + Transaction.send :include, SQLServerTransaction module SQLServerRealTransaction attr_reader :starting_isolation_level - def initialize(connection, options, *args) + def initialize(connection, options, run_commit_callbacks: false) @connection = connection @starting_isolation_level = current_isolation_level if options[:isolation] super @@ -58,6 +58,7 @@ def reset_starting_isolation_level end - RealTransaction.send :prepend, SQLServerRealTransaction + RealTransaction.send :include, SQLServerRealTransaction + end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/binary.rb b/lib/active_record/connection_adapters/sqlserver/type/binary.rb index 06cb42b37..807e323b7 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/binary.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/binary.rb @@ -4,6 +4,14 @@ module SQLServer module Type class Binary < ActiveRecord::Type::Binary + def cast_value(value) + if value.class.to_s == 'String' and !value.frozen? + value.force_encoding(Encoding::BINARY) =~ /[^[:xdigit:]]/ ? value : [value].pack('H*') + else + value + end + end + def type :binary_basic end diff --git a/lib/active_record/connection_adapters/sqlserver/type/json.rb b/lib/active_record/connection_adapters/sqlserver/type/json.rb index 403261b5b..3e01977bc 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/json.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/json.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module SQLServer module Type - class Json < ActiveRecord::Type::Json + class Json < ActiveRecord::Type::Internal::AbstractJson end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/string.rb b/lib/active_record/connection_adapters/sqlserver/type/string.rb index a1439f45b..e6b12b3bb 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/string.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/string.rb @@ -4,13 +4,6 @@ module SQLServer module Type class String < ActiveRecord::Type::String - def changed_in_place?(raw_old_value, new_value) - if raw_old_value.is_a?(Data) - raw_old_value.value != new_value - else - super - end - end end end diff --git a/lib/active_record/connection_adapters/sqlserver_adapter.rb b/lib/active_record/connection_adapters/sqlserver_adapter.rb index b3f745ac6..0be6af5f2 100644 --- a/lib/active_record/connection_adapters/sqlserver_adapter.rb +++ b/lib/active_record/connection_adapters/sqlserver_adapter.rb @@ -1,5 +1,6 @@ require 'base64' require 'active_record' +require 'odbc_utf8' require 'arel_sqlserver' require 'active_record/connection_adapters/abstract_adapter' require 'active_record/connection_adapters/sqlserver/core_ext/active_record' @@ -34,6 +35,7 @@ class SQLServerAdapter < AbstractAdapter SQLServer::Quoting, SQLServer::DatabaseStatements, SQLServer::Showplan, + SQLServer::SchemaDumper, SQLServer::SchemaStatements, SQLServer::DatabaseLimits, SQLServer::DatabaseTasks @@ -135,10 +137,6 @@ def supports_comments_in_create? false end - def supports_savepoints? - true - end - def supports_in_memory_oltp? @version_year >= 2014 end @@ -172,6 +170,8 @@ def disconnect! case @connection_options[:mode] when :dblib @connection.close rescue nil + when :odbc + @connection.disconnect rescue nil end @connection = nil @spid = nil @@ -192,7 +192,7 @@ def reset! def tables_with_referential_integrity schemas_and_tables = select_rows <<-SQL.strip_heredoc - SELECT DISTINCT s.name, o.name + SELECT s.name, o.name FROM sys.foreign_keys i INNER JOIN sys.objects o ON i.parent_object_id = o.OBJECT_ID INNER JOIN sys.schemas s ON o.schema_id = s.schema_id @@ -215,7 +215,7 @@ def sqlserver? end def sqlserver_azure? - !!(sqlserver_version =~ /Azure/i) + @sqlserver_azure ||= !!(select_value('SELECT @@version', 'SCHEMA') =~ /Azure/i) end def database_prefix_remote_server? @@ -256,7 +256,7 @@ def combine_bind_parameters(from_clause: [], join_clause: [], where_clause: [], # === Abstract Adapter (Misc Support) =========================== # - def initialize_type_map(m = type_map) + def initialize_type_map(m) m.register_type %r{.*}, SQLServer::Type::UnicodeString.new # Exact Numerics register_class_with_limit m, 'bigint(8)', SQLServer::Type::BigInteger @@ -359,6 +359,8 @@ def connect @connection = case config[:mode] when :dblib dblib_connect(config) + when :odbc + odbc_connect(config) end @spid = _raw_select('SELECT @@SPID', fetch: :rows).first.first @version_year = version_year @@ -368,6 +370,7 @@ def connect def connection_errors @connection_errors ||= [].tap do |errors| errors << TinyTds::Error if defined?(TinyTds::Error) + errors << ODBC::Error if defined?(ODBC::Error) end end @@ -403,6 +406,25 @@ def dblib_connect(config) end end + def odbc_connect(config) + if config[:dsn].include?(';') + driver = ODBC::Driver.new.tap do |d| + d.name = config[:dsn_name] || 'Driver1' + d.attrs = config[:dsn].split(';').map { |atr| atr.split('=') }.reject { |kv| kv.size != 2 }.reduce({}) { |a, e| k, v = e ; a[k] = v ; a } + end + ODBC::Database.new.drvconnect(driver) + else + ODBC.connect config[:dsn], config[:username], config[:password] + end.tap do |c| + begin + c.use_time = true + c.use_utc = ActiveRecord::Base.default_timezone == :utc + rescue Exception + warn 'Ruby ODBC v0.99992 or higher is required.' + end + end + end + def config_appname(config) config[:appname] || configure_application_name || Rails.application.class.name.split('::').first rescue nil end @@ -438,15 +460,16 @@ def initialize_dateformatter end def version_year - return 2016 if sqlserver_version =~ /vNext/ - /SQL Server (\d+)/.match(sqlserver_version).to_a.last.to_s.to_i - rescue StandardError => e - 2016 + return @version_year if defined?(@version_year) + @version_year = begin + vstring = _raw_select('SELECT @@version', fetch: :rows).first.first.to_s + return 2016 if vstring =~ /vNext/ + /SQL Server (\d+)/.match(vstring).to_a.last.to_s.to_i + rescue Exception => e + 2016 + end end - def sqlserver_version - @sqlserver_version ||= _raw_select('SELECT @@version', fetch: :rows).first.first.to_s - end end end end diff --git a/lib/active_record/sqlserver_base.rb b/lib/active_record/sqlserver_base.rb index 4601b41d1..95eb3fc6e 100644 --- a/lib/active_record/sqlserver_base.rb +++ b/lib/active_record/sqlserver_base.rb @@ -7,6 +7,10 @@ def sqlserver_connection(config) #:nodoc: case mode when :dblib require 'tiny_tds' + when :odbc + raise ArgumentError, 'Missing :dsn configuration.' unless config.key?(:dsn) + require 'odbc' + require 'active_record/connection_adapters/sqlserver/core_ext/odbc' else raise ArgumentError, "Unknown connection mode in #{config.inspect}." end diff --git a/lib/active_record/tasks/sqlserver_database_tasks.rb b/lib/active_record/tasks/sqlserver_database_tasks.rb index cd10b87c2..0c62d58e4 100644 --- a/lib/active_record/tasks/sqlserver_database_tasks.rb +++ b/lib/active_record/tasks/sqlserver_database_tasks.rb @@ -49,11 +49,9 @@ def purge end def structure_dump(filename, extra_flags) - server_arg = "-S #{Shellwords.escape(configuration['host'])}" - server_arg += ":#{Shellwords.escape(configuration['port'])}" if configuration['port'] command = [ "defncopy-ttds", - server_arg, + "-S #{Shellwords.escape(configuration['host'])}", "-D #{Shellwords.escape(configuration['database'])}", "-U #{Shellwords.escape(configuration['username'])}", "-P #{Shellwords.escape(configuration['password'])}", diff --git a/lib/arel/visitors/sqlserver.rb b/lib/arel/visitors/sqlserver.rb index e17726d23..6f66ef507 100644 --- a/lib/arel/visitors/sqlserver.rb +++ b/lib/arel/visitors/sqlserver.rb @@ -14,7 +14,7 @@ class SQLServer < Arel::Visitors::ToSql # SQLServer ToSql/Visitor (Overides) def visit_Arel_Nodes_BindParam o, collector - collector.add_bind(o.value) { |i| "@#{i-1}" } + collector.add_bind(o) { |i| "@#{i-1}" } end def visit_Arel_Nodes_Bin o, collector diff --git a/lib/arel_sqlserver.rb b/lib/arel_sqlserver.rb index d437be596..3fa9d949b 100644 --- a/lib/arel_sqlserver.rb +++ b/lib/arel_sqlserver.rb @@ -1,2 +1,3 @@ require 'arel' +require 'arel/visitors/bind_visitor' require 'arel/visitors/sqlserver' diff --git a/test/bin/.keep b/test/bin/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/test/bin/install-freetds.sh b/test/bin/install-freetds.sh index 755ff928a..16e9f66fe 100755 --- a/test/bin/install-freetds.sh +++ b/test/bin/install-freetds.sh @@ -5,7 +5,7 @@ set -e FREETDS_VERSION=1.00.21 -wget http://www.freetds.org/files/stable/freetds-$FREETDS_VERSION.tar.gz +wget ftp://ftp.freetds.org/pub/freetds/stable/freetds-$FREETDS_VERSION.tar.gz tar -xzf freetds-$FREETDS_VERSION.tar.gz cd freetds-$FREETDS_VERSION ./configure --prefix=/opt/local \ diff --git a/test/cases/adapter_test_sqlserver.rb b/test/cases/adapter_test_sqlserver.rb index 34d75bce7..c69c769c4 100644 --- a/test/cases/adapter_test_sqlserver.rb +++ b/test/cases/adapter_test_sqlserver.rb @@ -17,7 +17,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase string = connection.inspect string.must_match %r{ActiveRecord::ConnectionAdapters::SQLServerAdapter} string.must_match %r{version\: \d.\d} - string.must_match %r{mode: dblib} + string.must_match %r{mode: (dblib|odbc)} string.must_match %r{azure: (true|false)} string.wont_match %r{host} string.wont_match %r{password} @@ -231,11 +231,6 @@ class AdapterTestSQLServer < ActiveRecord::TestCase end end - it 'not disable referential integrity for the same table twice' do - tables = SSTestHasPk.connection.tables_with_referential_integrity - assert_equal tables.size, tables.uniq.size - end - end describe 'database statements' do @@ -273,12 +268,9 @@ class AdapterTestSQLServer < ActiveRecord::TestCase assert_equal 'integer', connection.type_to_sql(:integer, limit: 3) end - it 'create smallints when limit is 2' do + it 'create smallints when limit is less than 3' do assert_equal 'smallint', connection.type_to_sql(:integer, limit: 2) - end - - it 'create tinyints when limit is 1' do - assert_equal 'tinyint', connection.type_to_sql(:integer, limit: 1) + assert_equal 'smallint', connection.type_to_sql(:integer, limit: 1) end it 'create bigints when limit is greateer than 4' do diff --git a/test/cases/change_column_null_test_sqlserver.rb b/test/cases/change_column_null_test_sqlserver.rb deleted file mode 100644 index 60fcf1077..000000000 --- a/test/cases/change_column_null_test_sqlserver.rb +++ /dev/null @@ -1,42 +0,0 @@ -require 'cases/helper_sqlserver' -require 'migrations/create_clients_and_change_column_null' - -class ChangeColumnNullTestSqlServer < ActiveRecord::TestCase - before do - @old_verbose = ActiveRecord::Migration.verbose - ActiveRecord::Migration.verbose = false - CreateClientsAndChangeColumnNull.new.up - end - - after do - CreateClientsAndChangeColumnNull.new.down - ActiveRecord::Migration.verbose = @old_verbose - end - - def find_column(table, name) - table.find { |column| column.name == name } - end - - let(:clients_table) { connection.columns('clients') } - let(:name_column) { find_column(clients_table, 'name') } - let(:code_column) { find_column(clients_table, 'code') } - let(:value_column) { find_column(clients_table, 'value') } - - describe '#change_column_null' do - it 'does not change the column limit' do - name_column.limit.must_equal 15 - end - - it 'does not change the column default' do - code_column.default.must_equal 'n/a' - end - - it 'does not change the column precision' do - value_column.precision.must_equal 32 - end - - it 'does not change the column scale' do - value_column.scale.must_equal 8 - end - end -end diff --git a/test/cases/coerced_tests.rb b/test/cases/coerced_tests.rb index 6bcf406b6..7b7bf5074 100644 --- a/test/cases/coerced_tests.rb +++ b/test/cases/coerced_tests.rb @@ -33,7 +33,6 @@ module ActiveRecord class AdapterTest < ActiveRecord::TestCase # I really dont think we can support legacy binds. coerce_tests! :test_select_all_with_legacy_binds - coerce_tests! :test_insert_update_delete_with_legacy_binds # As far as I can tell, SQL Server does not support null bytes in strings. coerce_tests! :test_update_prepared_statement @@ -72,11 +71,7 @@ def test_typecast_attribute_from_select_to_true_coerced end -class NumericDataTest < ActiveRecord::TestCase - # We do not have do the DecimalWithoutScale type. - coerce_tests! :test_numeric_fields - coerce_tests! :test_numeric_fields_with_scale -end + class BasicsTest < ActiveRecord::TestCase coerce_tests! :test_column_names_are_escaped @@ -85,6 +80,10 @@ def test_column_names_are_escaped_coerced assert_equal '[t]]]', conn.quote_column_name('t]') end + # We do not have do the DecimalWithoutScale type. + coerce_tests! :test_numeric_fields + coerce_tests! :test_numeric_fields_with_scale + # Just like PostgreSQLAdapter does. coerce_tests! :test_respect_internal_encoding @@ -113,16 +112,6 @@ def test_update_date_time_attributes_with_default_timezone_local end end end - - # Need to escape `quoted_id` once it contains brackets - coerce_tests! %r{column names are quoted when using #from clause and model has ignored columns} - test "column names are quoted when using #from clause and model has ignored columns coerced" do - refute_empty Developer.ignored_columns - query = Developer.from("developers").to_sql - quoted_id = "#{Developer.quoted_table_name}.#{Developer.quoted_primary_key}" - - assert_match(/SELECT #{Regexp.escape(quoted_id)}.* FROM developers/, query) - end end @@ -151,25 +140,7 @@ class BindParameterTest < ActiveRecord::TestCase end -module ActiveRecord - class InstrumentationTest < ActiveRecord::TestCase - # This fails randomly due to schema cache being lost? - coerce_tests! :test_payload_name_on_load - def test_payload_name_on_load_coerced - Book.create(name: "test book") - Book.first - subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args| - event = ActiveSupport::Notifications::Event.new(*args) - if event.payload[:sql].match "SELECT" - assert_equal "Book Load", event.payload[:name] - end - end - Book.first - ensure - ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber - end - end -end + class CalculationsTest < ActiveRecord::TestCase # This fails randomly due to schema cache being lost? @@ -202,14 +173,6 @@ def test_limit_with_offset_is_kept_coerced queries.first.must_match %r{ORDER BY \[accounts\]\.\[id\] ASC OFFSET @0 ROWS FETCH NEXT @1 ROWS ONLY.*@0 = 1, @1 = 1} end - # SQL Server needs an alias for the calculated column - coerce_tests! :test_distinct_count_all_with_custom_select_and_order - def test_distinct_count_all_with_custom_select_and_order_coerced - accounts = Account.distinct.select("credit_limit % 10 AS the_limit").order(Arel.sql("credit_limit % 10")) - assert_queries(1) { assert_equal 3, accounts.count(:all) } - assert_queries(1) { assert_equal 3, accounts.load.size } - end - # Leave it up to users to format selects/functions so HAVING works correctly. coerce_tests! :test_having_with_strong_parameters end @@ -223,7 +186,6 @@ class ChangeSchemaTest < ActiveRecord::TestCase coerce_tests! :test_create_table_with_bigint, :test_create_table_with_defaults end - class ChangeSchemaWithDependentObjectsTest < ActiveRecord::TestCase # In SQL Server you have to delete the tables yourself in the right order. coerce_tests! :test_create_table_with_force_cascade_drops_dependent_objects @@ -351,7 +313,7 @@ def test_add_table_with_decimals_coerced end # For some reason our tests set Rails.@_env which breaks test env switching. - coerce_tests! :test_internal_metadata_stores_environment_when_other_data_exists + coerce_tests! :test_migration_sets_internal_metadata_even_when_fully_migrated coerce_tests! :test_internal_metadata_stores_environment end @@ -377,7 +339,6 @@ module ConnectionAdapters # a value of 'default_env' will still show tests failing. Just ignoring all # of them since we have no monkey in this circus. MergeAndResolveDefaultUrlConfigTest.coerce_all_tests! if defined?(MergeAndResolveDefaultUrlConfigTest) - ConnectionHandlerTest.coerce_all_tests! if defined?(ConnectionHandlerTest) end end @@ -552,7 +513,7 @@ def test_a_bad_type_column_coerced coerce_tests! :test_eager_load_belongs_to_primary_key_quoting def test_eager_load_belongs_to_primary_key_quoting_coerced con = Account.connection - assert_sql(/\[companies\]\.\[id\] = @0.* @0 = 1/) do + assert_sql(/\[companies\]\.\[id\] = 1/) do Account.all.merge!(:includes => :firm).find(1) end end @@ -605,7 +566,6 @@ def test_merge_options_coerced -require 'models/parrot' require 'models/topic' class PersistenceTest < ActiveRecord::TestCase # We can not UPDATE identity columns. @@ -682,8 +642,8 @@ class PrimaryKeysTest < ActiveRecord::TestCase require 'models/task' class QueryCacheTest < ActiveRecord::TestCase - coerce_tests! :test_cache_does_not_wrap_results_in_arrays - def test_cache_does_not_wrap_results_in_arrays_coerced + coerce_tests! :test_cache_does_not_wrap_string_results_in_arrays + def test_cache_does_not_wrap_string_results_in_arrays_coerced Task.cache do assert_kind_of Numeric, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") end @@ -698,16 +658,16 @@ class RelationTest < ActiveRecord::TestCase # Use LEN vs LENGTH function. coerce_tests! :test_reverse_order_with_function def test_reverse_order_with_function_coerced - topics = Topic.order(Arel.sql("LEN(title)")).reverse_order + topics = Topic.order("LEN(title)").reverse_order assert_equal topics(:second).title, topics.first.title end # Use LEN vs LENGTH function. coerce_tests! :test_reverse_order_with_function_other_predicates def test_reverse_order_with_function_other_predicates_coerced - topics = Topic.order(Arel.sql("author_name, LEN(title), id")).reverse_order + topics = Topic.order("author_name, LEN(title), id").reverse_order assert_equal topics(:second).title, topics.first.title - topics = Topic.order(Arel.sql("LEN(author_name), id, LEN(title)")).reverse_order + topics = Topic.order("LEN(author_name), id, LEN(title)").reverse_order assert_equal topics(:fifth).title, topics.first.title end @@ -737,22 +697,6 @@ def test_relations_dont_load_all_records_in_inspect_coerced # so we are skipping all together. coerce_tests! :test_empty_complex_chained_relations - # Can't apply offset withour ORDER - coerce_tests! %r{using a custom table affects the wheres} - test 'using a custom table affects the wheres coerced' do - post = posts(:welcome) - - assert_equal post, custom_post_relation.where!(title: post.title).order(:id).take - end - - # Can't apply offset withour ORDER - coerce_tests! %r{using a custom table with joins affects the joins} - test 'using a custom table with joins affects the joins coerced' do - post = posts(:welcome) - - assert_equal post, custom_post_relation.joins(:author).where!(title: post.title).order(:id).take - end - # Use LEN() vs length() function. coerce_tests! :test_reverse_arel_assoc_order_with_function def test_reverse_arel_assoc_order_with_function_coerced @@ -761,24 +705,6 @@ def test_reverse_arel_assoc_order_with_function_coerced end end -class ActiveRecord::RelationTest < ActiveRecord::TestCase - coerce_tests! :test_relation_merging_with_merged_symbol_joins_is_aliased - def test_relation_merging_with_merged_symbol_joins_is_aliased__coerced - categorizations_with_authors = Categorization.joins(:author) - queries = capture_sql { Post.joins(:author, :categorizations).merge(Author.select(:id)).merge(categorizations_with_authors).to_a } - - nb_inner_join = queries.sum { |sql| sql.scan(/INNER\s+JOIN/i).size } - assert_equal 3, nb_inner_join, "Wrong amount of INNER JOIN in query" - - # using `\W` as the column separator - query_matches = queries.any? do |sql| - %r[INNER\s+JOIN\s+#{Regexp.escape(Author.quoted_table_name)}\s+\Wauthors_categorizations\W]i.match?(sql) - end - - assert query_matches, "Should be aliasing the child INNER JOINs in query" - end -end - @@ -931,53 +857,10 @@ def test_invalid_datetime_precision_raises_error_coerced end end end - - # datetime is rounded to increments of .000, .003, or .007 seconds - coerce_tests! :test_datetime_precision_is_truncated_on_assignment - def test_datetime_precision_is_truncated_on_assignment_coerced - @connection.create_table(:foos, force: true) - @connection.add_column :foos, :created_at, :datetime, precision: 0 - @connection.add_column :foos, :updated_at, :datetime, precision: 6 - - time = ::Time.now.change(nsec: 123456789) - foo = Foo.new(created_at: time, updated_at: time) - - assert_equal 0, foo.created_at.nsec - assert_equal 123457000, foo.updated_at.nsec - - foo.save! - foo.reload - - assert_equal 0, foo.created_at.nsec - assert_equal 123457000, foo.updated_at.nsec - end end -class TimePrecisionTest < ActiveRecord::TestCase - # datetime is rounded to increments of .000, .003, or .007 seconds - coerce_tests! :test_time_precision_is_truncated_on_assignment - def test_time_precision_is_truncated_on_assignment_coerced - @connection.create_table(:foos, force: true) - @connection.add_column :foos, :start, :time, precision: 0 - @connection.add_column :foos, :finish, :time, precision: 6 - - time = ::Time.now.change(nsec: 123456789) - foo = Foo.new(start: time, finish: time) - - assert_equal 0, foo.start.nsec - assert_equal 123457000, foo.finish.nsec - - foo.save! - foo.reload - - assert_equal 0, foo.start.nsec - assert_equal 123457000, foo.finish.nsec - end -end - - class DefaultNumbersTest < ActiveRecord::TestCase # We do better with native types and do not return strings for everything. @@ -1030,135 +913,3 @@ def schema_dump_path end end -class UnsafeRawSqlTest < ActiveRecord::TestCase - coerce_tests! %r{always allows Arel} - test 'order: always allows Arel' do - ids_depr = with_unsafe_raw_sql_deprecated { Post.order(Arel.sql("len(title)")).pluck(:title) } - ids_disabled = with_unsafe_raw_sql_disabled { Post.order(Arel.sql("len(title)")).pluck(:title) } - - assert_equal ids_depr, ids_disabled - end - - test "pluck: always allows Arel" do - values_depr = with_unsafe_raw_sql_deprecated { Post.includes(:comments).pluck(:title, Arel.sql("len(title)")) } - values_disabled = with_unsafe_raw_sql_disabled { Post.includes(:comments).pluck(:title, Arel.sql("len(title)")) } - - assert_equal values_depr, values_disabled - end - - - coerce_tests! %r{order: disallows invalid Array arguments} - test "order: disallows invalid Array arguments" do - with_unsafe_raw_sql_disabled do - assert_raises(ActiveRecord::UnknownAttributeReference) do - Post.order(["author_id", "len(title)"]).pluck(:id) - end - end - end - - coerce_tests! %r{order: allows valid Array arguments} - test "order: allows valid Array arguments" do - ids_expected = Post.order(Arel.sql("author_id, len(title)")).pluck(:id) - - ids_depr = with_unsafe_raw_sql_deprecated { Post.order(["author_id", Arel.sql("len(title)")]).pluck(:id) } - ids_disabled = with_unsafe_raw_sql_disabled { Post.order(["author_id", Arel.sql("len(title)")]).pluck(:id) } - - assert_equal ids_expected, ids_depr - assert_equal ids_expected, ids_disabled - end - - coerce_tests! %r{order: logs deprecation warning for unrecognized column} - test "order: logs deprecation warning for unrecognized column" do - with_unsafe_raw_sql_deprecated do - assert_deprecated(/Dangerous query method/) do - Post.order("len(title)") - end - end - end - - coerce_tests! %r{pluck: disallows invalid column name} - test "pluck: disallows invalid column name" do - with_unsafe_raw_sql_disabled do - assert_raises(ActiveRecord::UnknownAttributeReference) do - Post.pluck("len(title)") - end - end - end - - coerce_tests! %r{pluck: disallows invalid column name amongst valid names} - test "pluck: disallows invalid column name amongst valid names" do - with_unsafe_raw_sql_disabled do - assert_raises(ActiveRecord::UnknownAttributeReference) do - Post.pluck(:title, "len(title)") - end - end - end - - coerce_tests! %r{pluck: disallows invalid column names with includes} - test "pluck: disallows invalid column names with includes" do - with_unsafe_raw_sql_disabled do - assert_raises(ActiveRecord::UnknownAttributeReference) do - Post.includes(:comments).pluck(:title, "len(title)") - end - end - end - - coerce_tests! %r{pluck: logs deprecation warning} - test "pluck: logs deprecation warning" do - with_unsafe_raw_sql_deprecated do - assert_deprecated(/Dangerous query method/) do - Post.includes(:comments).pluck(:title, "len(title)") - end - end - end -end - - -class ReservedWordTest < ActiveRecord::TestCase - coerce_tests! :test_change_columns - def test_change_columns_coerced - assert_nothing_raised { @connection.change_column_default(:group, :order, "whatever") } - assert_nothing_raised { @connection.change_column("group", "order", :text) } - assert_nothing_raised { @connection.change_column_null("group", "order", true) } - assert_nothing_raised { @connection.rename_column(:group, :order, :values) } - end -end - - - -class OptimisticLockingTest < ActiveRecord::TestCase - # We do not allow updating identities, but we can test using a non-identity key - coerce_tests! :test_update_with_dirty_primary_key - def test_update_with_dirty_primary_key_coerced - assert_raises(ActiveRecord::RecordNotUnique) do - record = StringKeyObject.find('record1') - record.id = 'record2' - record.save! - end - - record = StringKeyObject.find('record1') - record.id = 'record42' - record.save! - - assert StringKeyObject.find('record42') - assert_raises(ActiveRecord::RecordNotFound) do - StringKeyObject.find('record1') - end - end -end - - - -class RelationMergingTest < ActiveRecord::TestCase - coerce_tests! :test_merging_with_order_with_binds - def test_merging_with_order_with_binds_coerced - relation = Post.all.merge(Post.order([Arel.sql("title LIKE ?"), "%suffix"])) - assert_equal ["title LIKE N'%suffix'"], relation.order_values - end -end - - -class EagerLoadingTooManyIdsTest < ActiveRecord::TestCase - # Temporarily coerce this test due to https://github.com/rails/rails/issues/34945 - coerce_tests! :test_eager_loading_too_may_ids -end diff --git a/test/cases/column_test_sqlserver.rb b/test/cases/column_test_sqlserver.rb index e3f869135..ca52c7758 100644 --- a/test/cases/column_test_sqlserver.rb +++ b/test/cases/column_test_sqlserver.rb @@ -273,8 +273,8 @@ def assert_obj_set_and_save(attribute, value) col.sql_type.must_equal 'date' col.type.must_equal :date col.null.must_equal true - col.default.must_equal connection_dblib_73? ? Date.civil(0001, 1, 1) : '0001-01-01' - obj.date.must_equal Date.civil(0001, 1, 1) + col.default.must_equal Date.civil(1900, 1, 1) + obj.date.must_equal Date.civil(1900, 1, 1) col.default_function.must_be_nil type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::Date @@ -282,19 +282,19 @@ def assert_obj_set_and_save(attribute, value) type.precision.must_be_nil type.scale.must_be_nil # Can cast strings. SQL Server format. - obj.date = '04-01-0001' - obj.date.must_equal Date.civil(0001, 4, 1) + obj.date = '04-01-1900' + obj.date.must_equal Date.civil(1900, 4, 1) obj.save! - obj.date.must_equal Date.civil(0001, 4, 1) + obj.date.must_equal Date.civil(1900, 4, 1) obj.reload - obj.date.must_equal Date.civil(0001, 4, 1) + obj.date.must_equal Date.civil(1900, 4, 1) # Can cast strings. ISO format. - obj.date = '0001-04-01' - obj.date.must_equal Date.civil(0001, 4, 1) + obj.date = '1900-04-01' + obj.date.must_equal Date.civil(1900, 4, 1) obj.save! - obj.date.must_equal Date.civil(0001, 4, 1) + obj.date.must_equal Date.civil(1900, 4, 1) obj.reload - obj.date.must_equal Date.civil(0001, 4, 1) + obj.date.must_equal Date.civil(1900, 4, 1) # Can keep and return assigned date. assert_obj_set_and_save :date, Date.civil(1972, 04, 14) # Can accept and cast time objects. @@ -798,12 +798,6 @@ def assert_obj_set_and_save(attribute, value) obj.save! end - it 'does not mark object as changed after save' do - obj.save! - obj.attributes - obj.changed?.must_equal false - end - end end diff --git a/test/cases/connection_test_sqlserver.rb b/test/cases/connection_test_sqlserver.rb index 656c42fd4..d0ac58f89 100644 --- a/test/cases/connection_test_sqlserver.rb +++ b/test/cases/connection_test_sqlserver.rb @@ -33,6 +33,61 @@ class ConnectionTestSQLServer < ActiveRecord::TestCase end end unless connection_sqlserver_azure? + describe 'ODBC connection management' do + + it 'return finished ODBC statement handle from #execute without block' do + assert_all_odbc_statements_used_are_closed do + connection.execute('SELECT * FROM [topics]') + end + end + + it 'finish ODBC statement handle from #execute with block' do + assert_all_odbc_statements_used_are_closed do + connection.execute('SELECT * FROM [topics]') { } + end + end + + it 'finish connection from #raw_select' do + assert_all_odbc_statements_used_are_closed do + connection.send(:raw_select,'SELECT * FROM [topics]') + end + end + + it 'execute without block closes statement' do + assert_all_odbc_statements_used_are_closed do + connection.execute("SELECT 1") + end + end + + it 'execute with block closes statement' do + assert_all_odbc_statements_used_are_closed do + connection.execute("SELECT 1") do |sth| + assert !sth.finished?, "Statement should still be alive within block" + end + end + end + + it 'insert with identity closes statement' do + assert_all_odbc_statements_used_are_closed do + connection.exec_insert "INSERT INTO accounts ([id],[firm_id],[credit_limit]) VALUES (999, 1, 50)", "SQL", [] + end + end + + it 'insert without identity closes statement' do + assert_all_odbc_statements_used_are_closed do + connection.exec_insert "INSERT INTO accounts ([firm_id],[credit_limit]) VALUES (1, 50)", "SQL", [] + end + end + + it 'active closes statement' do + assert_all_odbc_statements_used_are_closed do + connection.active? + end + end + + end if connection_odbc? + + describe 'Connection management' do it 'set spid on connect' do @@ -65,7 +120,25 @@ def disconnect_raw_connection! case connection_options[:mode] when :dblib connection.raw_connection.close rescue nil + when :odbc + connection.raw_connection.disconnect rescue nil end end + def assert_all_odbc_statements_used_are_closed(&block) + odbc = connection.raw_connection.class.parent + existing_handles = [] + ObjectSpace.each_object(odbc::Statement) { |h| existing_handles << h } + existing_handle_ids = existing_handles.map(&:object_id) + assert existing_handles.all?(&:finished?), "Somewhere before the block some statements were not closed" + GC.disable + yield + used_handles = [] + ObjectSpace.each_object(odbc::Statement) { |h| used_handles << h unless existing_handle_ids.include?(h.object_id) } + assert used_handles.size > 0, "No statements were used within given block" + assert used_handles.all?(&:finished?), "Statement should have been closed within given block" + ensure + GC.enable + end + end diff --git a/test/cases/helper_sqlserver.rb b/test/cases/helper_sqlserver.rb index 677c40bf7..74e364d8f 100644 --- a/test/cases/helper_sqlserver.rb +++ b/test/cases/helper_sqlserver.rb @@ -2,7 +2,6 @@ require 'bundler/setup' Bundler.require :default, :development require 'pry' -require 'support/core_ext/query_cache' require 'support/minitest_sqlserver' require 'support/test_in_memory_oltp' require 'cases/helper' @@ -10,7 +9,7 @@ require 'support/coerceable_test_sqlserver' require 'support/sql_counter_sqlserver' require 'support/connection_reflection' -require 'mocha/minitest' +require 'mocha/mini_test' module ActiveRecord class TestCase < ActiveSupport::TestCase diff --git a/test/cases/migration_test_sqlserver.rb b/test/cases/migration_test_sqlserver.rb index fb059ddee..17ba57000 100644 --- a/test/cases/migration_test_sqlserver.rb +++ b/test/cases/migration_test_sqlserver.rb @@ -20,7 +20,7 @@ class MigrationTestSQLServer < ActiveRecord::TestCase it 'not create a tables if error in migrations' do begin migrations_dir = File.join ARTest::SQLServer.migrations_root, 'transaction_table' - quietly { ActiveRecord::MigrationContext.new(migrations_dir).up } + quietly { ActiveRecord::Migrator.up(migrations_dir) } rescue Exception => e assert_match %r|this and all later migrations canceled|, e.message end @@ -41,13 +41,11 @@ class MigrationTestSQLServer < ActiveRecord::TestCase lock_version_column = Person.columns_hash['lock_version'] assert_equal :string, lock_version_column.type assert lock_version_column.default.nil? - assert_nothing_raised { connection.change_column 'people', 'lock_version', :integer } - Person.reset_column_information end it 'not drop the default contraint if just renaming' do find_default = lambda do - connection.execute_procedure(:sp_helpconstraint, 'sst_string_defaults', 'nomsg').select do |row| + connection.execute_procedure(:sp_helpconstraint, 'sst_string_defaults', 'nomsg').flatten.select do |row| row['constraint_type'] == "DEFAULT on column string_with_pretend_paren_three" end.last end @@ -57,14 +55,6 @@ class MigrationTestSQLServer < ActiveRecord::TestCase assert default_after assert_equal default_before['constraint_keys'], default_after['constraint_keys'] end - - it 'change limit' do - assert_nothing_raised { connection.change_column :people, :lock_version, :integer, limit: 8 } - end - - it 'change null and default' do - assert_nothing_raised { connection.change_column :people, :first_name, :text, null: true, default: nil } - end end diff --git a/test/cases/order_test_sqlserver.rb b/test/cases/order_test_sqlserver.rb index d0707eede..839a4e1a9 100644 --- a/test/cases/order_test_sqlserver.rb +++ b/test/cases/order_test_sqlserver.rb @@ -44,13 +44,13 @@ class OrderTestSQLServer < ActiveRecord::TestCase it 'support quoted column' do order = "[title]" post1 = Post.create title: 'AAA Post', body: 'Test cased orders.' - assert_equal post1, Post.order(Arel.sql(order)).first + assert_equal post1, Post.order(order).first end it 'support quoted table and column' do order = "[posts].[title]" post1 = Post.create title: 'AAA Post', body: 'Test cased orders.' - assert_equal post1, Post.order(Arel.sql(order)).first + assert_equal post1, Post.order(order).first end it 'support primary: column, secondary: column' do @@ -73,74 +73,74 @@ class OrderTestSQLServer < ActiveRecord::TestCase order = "(CASE WHEN [title] LIKE N'ZZZ%' THEN title ELSE '' END) DESC, body" post1 = Post.create title: 'ZZZ Post', body: 'Test cased orders.' post2 = Post.create title: 'ZZZ Post', body: 'ZZZ Test cased orders.' - assert_equal post1, Post.order(Arel.sql(order)).first - assert_equal post2, Post.order(Arel.sql(order)).second + assert_equal post1, Post.order(order).first + assert_equal post2, Post.order(order).second end it 'support primary: quoted table and column, secondary: case expresion' do order = "[posts].[body] DESC, (CASE WHEN [title] LIKE N'ZZZ%' THEN title ELSE '' END) DESC" post1 = Post.create title: 'ZZZ Post', body: 'ZZZ Test cased orders.' post2 = Post.create title: 'ZZY Post', body: 'ZZZ Test cased orders.' - assert_equal post1, Post.order(Arel.sql(order)).first - assert_equal post2, Post.order(Arel.sql(order)).second + assert_equal post1, Post.order(order).first + assert_equal post2, Post.order(order).second end it 'support inline function' do order = "LEN(title)" post1 = Post.create title: 'A', body: 'AAA Test cased orders.' - assert_equal post1, Post.order(Arel.sql(order)).first + assert_equal post1, Post.order(order).first end it 'support inline function with parameters' do order = "SUBSTRING(title, 1, 3)" post1 = Post.create title: 'AAA Post', body: 'Test cased orders.' - assert_equal post1, Post.order(Arel.sql(order)).first + assert_equal post1, Post.order(order).first end it 'support inline function with parameters DESC' do order = "SUBSTRING(title, 1, 3) DESC" post1 = Post.create title: 'ZZZ Post', body: 'Test cased orders.' - assert_equal post1, Post.order(Arel.sql(order)).first + assert_equal post1, Post.order(order).first end it 'support primary: inline function, secondary: column' do order = "LEN(title), body" post1 = Post.create title: 'A', body: 'AAA Test cased orders.' post2 = Post.create title: 'A', body: 'Test cased orders.' - assert_equal post1, Post.order(Arel.sql(order)).first - assert_equal post2, Post.order(Arel.sql(order)).second + assert_equal post1, Post.order(order).first + assert_equal post2, Post.order(order).second end it 'support primary: inline function, secondary: column with direction' do order = "LEN(title) ASC, body DESC" post1 = Post.create title: 'A', body: 'ZZZ Test cased orders.' post2 = Post.create title: 'A', body: 'Test cased orders.' - assert_equal post1, Post.order(Arel.sql(order)).first - assert_equal post2, Post.order(Arel.sql(order)).second + assert_equal post1, Post.order(order).first + assert_equal post2, Post.order(order).second end it 'support primary: column, secondary: inline function' do order = "body DESC, LEN(title)" post1 = Post.create title: 'Post', body: 'ZZZ Test cased orders.' post2 = Post.create title: 'Longer Post', body: 'ZZZ Test cased orders.' - assert_equal post1, Post.order(Arel.sql(order)).first - assert_equal post2, Post.order(Arel.sql(order)).second + assert_equal post1, Post.order(order).first + assert_equal post2, Post.order(order).second end it 'support primary: case expression, secondary: inline function' do order = "CASE WHEN [title] LIKE N'ZZZ%' THEN title ELSE '' END DESC, LEN(body) ASC" post1 = Post.create title: 'ZZZ Post', body: 'Z' post2 = Post.create title: 'ZZZ Post', body: 'Test cased orders.' - assert_equal post1, Post.order(Arel.sql(order)).first - assert_equal post2, Post.order(Arel.sql(order)).second + assert_equal post1, Post.order(order).first + assert_equal post2, Post.order(order).second end it 'support primary: inline function, secondary: case expression' do order = "LEN(body), CASE WHEN [title] LIKE N'ZZZ%' THEN title ELSE '' END DESC" post1 = Post.create title: 'ZZZ Post', body: 'Z' post2 = Post.create title: 'Post', body: 'Z' - assert_equal post1, Post.order(Arel.sql(order)).first - assert_equal post2, Post.order(Arel.sql(order)).second + assert_equal post1, Post.order(order).first + assert_equal post2, Post.order(order).second end diff --git a/test/cases/schema_dumper_test_sqlserver.rb b/test/cases/schema_dumper_test_sqlserver.rb index 950c85b62..075138134 100644 --- a/test/cases/schema_dumper_test_sqlserver.rb +++ b/test/cases/schema_dumper_test_sqlserver.rb @@ -23,7 +23,7 @@ class SchemaDumperTestSQLServer < ActiveRecord::TestCase assert_line :float, type: 'float', limit: nil, precision: nil, scale: nil, default: 123.00000001 assert_line :real, type: 'real', limit: nil, precision: nil, scale: nil, default: 123.45 # Date and Time - assert_line :date, type: 'date', limit: nil, precision: nil, scale: nil, default: "01-01-0001" + assert_line :date, type: 'date', limit: nil, precision: nil, scale: nil, default: "01-01-1900" assert_line :datetime, type: 'datetime', limit: nil, precision: nil, scale: nil, default: "01-01-1753 00:00:00.123" if connection_dblib_73? assert_line :datetime2_7, type: 'datetime', limit: nil, precision: 7, scale: nil, default: "12-31-9999 23:59:59.9999999" diff --git a/test/cases/showplan_test_sqlserver.rb b/test/cases/showplan_test_sqlserver.rb index 9e4afb3cd..a140f8e32 100644 --- a/test/cases/showplan_test_sqlserver.rb +++ b/test/cases/showplan_test_sqlserver.rb @@ -26,18 +26,6 @@ class ShowplanTestSQLServer < ActiveRecord::TestCase plan.must_include "Clustered Index Scan", 'make sure we do not showplan the sp_executesql' end - it 'from array condition using index' do - plan = Car.where(id: [1, 2]).explain - plan.must_include " SELECT [cars].* FROM [cars] WHERE [cars].[id] IN (1, 2)" - plan.must_include "Clustered Index Seek", 'make sure we do not showplan the sp_executesql' - end - - it 'from array condition' do - plan = Car.where(name: ['honda', 'zyke']).explain - plan.must_include " SELECT [cars].* FROM [cars] WHERE [cars].[name] IN (N'honda', N'zyke')" - plan.must_include "Clustered Index Scan", 'make sure we do not showplan the sp_executesql' - end - end describe 'With SHOWPLAN_TEXT option' do diff --git a/test/config.yml b/test/config.yml index 9b30ae6be..458e71b11 100644 --- a/test/config.yml +++ b/test/config.yml @@ -29,3 +29,12 @@ connections: azure: <%= !ENV['ACTIVERECORD_UNITTEST_AZURE'].nil? %> timeout: <%= ENV['ACTIVERECORD_UNITTEST_AZURE'].present? ? 20 : 10 %> + odbc: + arunit: + <<: *default_connection_info + dsn: <%= ENV['ACTIVERECORD_UNITTEST_DSN'] || 'activerecord_unittest' %> + arunit2: + <<: *default_connection_info + database: activerecord_unittest2 + dsn: <%= ENV['ACTIVERECORD_UNITTEST2_DSN'] || 'activerecord_unittest2' %> + diff --git a/test/migrations/create_clients_and_change_column_null.rb b/test/migrations/create_clients_and_change_column_null.rb deleted file mode 100644 index 96e459455..000000000 --- a/test/migrations/create_clients_and_change_column_null.rb +++ /dev/null @@ -1,23 +0,0 @@ -class CreateClientsAndChangeColumnNull < ActiveRecord::Migration[5.2] - def up - create_table :clients do |t| - t.string :name - t.string :code - t.decimal :value - - t.timestamps - end - - change_column :clients, :name, :string, limit: 15 - change_column :clients, :code, :string, default: 'n/a' - change_column :clients, :value, :decimal, precision: 32, scale: 8 - - change_column_null :clients, :name, false - change_column_null :clients, :code, false - change_column_null :clients, :value, false - end - - def down - drop_table :clients - end -end diff --git a/test/schema/datatypes/2012.sql b/test/schema/datatypes/2012.sql index 044b78c97..369ec9b23 100644 --- a/test/schema/datatypes/2012.sql +++ b/test/schema/datatypes/2012.sql @@ -23,7 +23,7 @@ CREATE TABLE [sst_datatypes] ( [float] [float] NULL DEFAULT 123.00000001, [real] [real] NULL DEFAULT 123.45, -- Date and Time - [date] [date] NULL DEFAULT '0001-01-01', + [date] [date] NULL DEFAULT '1900-01-01', [datetime] [datetime] NULL DEFAULT '1753-01-01T00:00:00.123', [datetime2_7] [datetime2](7) NULL DEFAULT '9999-12-31 23:59:59.9999999', [datetime2_3] [datetime2](3) NULL, diff --git a/test/schema/sqlserver_specific_schema.rb b/test/schema/sqlserver_specific_schema.rb index 00fef941e..52329387f 100644 --- a/test/schema/sqlserver_specific_schema.rb +++ b/test/schema/sqlserver_specific_schema.rb @@ -144,20 +144,12 @@ # Constraints - create_table(:sst_has_fks, force: true) do |t| - t.column(:fk_id, :bigint, null: false) - t.column(:fk_id2, :bigint) - end - + create_table(:sst_has_fks, force: true) { |t| t.column(:fk_id, :bigint, null: false) } create_table(:sst_has_pks, force: true) { } execute <<-ADDFKSQL ALTER TABLE sst_has_fks ADD CONSTRAINT FK__sst_has_fks_id FOREIGN KEY ([fk_id]) - REFERENCES [sst_has_pks] ([id]), - - CONSTRAINT FK__sst_has_fks_id2 - FOREIGN KEY ([fk_id2]) REFERENCES [sst_has_pks] ([id]) ADDFKSQL diff --git a/test/support/connection_reflection.rb b/test/support/connection_reflection.rb index 20ef1fd8a..1c2e08e5c 100644 --- a/test/support/connection_reflection.rb +++ b/test/support/connection_reflection.rb @@ -24,6 +24,10 @@ def connection_dblib_73? rc.respond_to?(:tds_73?) && rc.tds_73? end + def connection_odbc? + connection_options[:mode] == :odbc + end + def connection_sqlserver_azure? connection.sqlserver_azure? end diff --git a/test/support/core_ext/query_cache.rb b/test/support/core_ext/query_cache.rb deleted file mode 100644 index 6661696e4..000000000 --- a/test/support/core_ext/query_cache.rb +++ /dev/null @@ -1,29 +0,0 @@ -require 'active_record/connection_adapters/sqlserver_adapter' - -module SqlIgnoredCache - extend ActiveSupport::Concern - - IGNORED_SQL = [ - /INFORMATION_SCHEMA\.(TABLES|VIEWS|COLUMNS|KEY_COLUMN_USAGE)/im, - /SELECT @@version/, - /SELECT @@TRANCOUNT/, - /(BEGIN|COMMIT|ROLLBACK|SAVE) TRANSACTION/, - /SELECT CAST\(.* AS .*\) AS value/, - /SELECT DATABASEPROPERTYEX/im - ] - - # We don't want to coerce every ActiveRecord test that relies on `query_cache` - # just because we do more queries than the other adapters. - # - # Removing internal queries from the cache will make AR tests pass without - # compromising cache outside tests. - def cache_sql(sql, name, binds) - result = super - @query_cache.delete_if { |k, v| k =~ Regexp.union(IGNORED_SQL) } - result - end -end - -ActiveSupport.on_load(:active_record) do - ActiveRecord::ConnectionAdapters::SQLServerAdapter.prepend(SqlIgnoredCache) -end