diff --git a/CHANGELOG.md b/CHANGELOG.md index fd61922ce..1cfafa003 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ +## v7.2.4.odbc + +#### Added + +- ODBC restoration. + ## v7.2.4 +#### Changed + +- [#1073](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1073) Improve performance of view default function lookup + #### Fixed - [#1270](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1270) Fix parsing of raw table name from SQL with extra parentheses diff --git a/Gemfile b/Gemfile index 0f435985e..e0b38a91e 100644 --- a/Gemfile +++ b/Gemfile @@ -57,6 +57,10 @@ group :tinytds do end # rubocop:enable Bundler/DuplicatedGem +group :odbc do + gem 'ruby-odbc', git: 'https://github.com/cloudvolumes/ruby-odbc.git', tag: '0.103.cv' +end + group :development do gem "minitest-spec-rails" gem "mocha" diff --git a/Rakefile b/Rakefile index 3c6f58630..d2546b1cb 100644 --- a/Rakefile +++ b/Rakefile @@ -5,13 +5,13 @@ require "rake/testtask" require_relative "test/support/paths_sqlserver" require_relative "test/support/rake_helpers" -task test: ["test:dblib"] +task test: ["test:dblib", "test:odbc"] task default: [:test] namespace :test do ENV["ARCONN"] = "sqlserver" - %w(dblib).each do |mode| + %w(dblib odbc).each do |mode| Rake::TestTask.new(mode) do |t| t.libs = ARTest::SQLServer.test_load_paths t.test_files = test_files @@ -22,7 +22,7 @@ namespace :test do end 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", "") @@ -35,3 +35,12 @@ namespace :profile do end end end + +task "test:odbc" => "test:odbc:env" +task "test:dblib" => "test:dblib:env" + +namespace :test do + task "odbc:env" do + ENV['ARCONN'] = 'odbc' + end +end diff --git a/VERSION b/VERSION index 2bbaead44..0e87797f8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.2.4 +7.2.4.odbc diff --git a/activerecord-sqlserver-adapter.gemspec b/activerecord-sqlserver-adapter.gemspec index 47d6f0f13..63529106a 100644 --- a/activerecord-sqlserver-adapter.gemspec +++ b/activerecord-sqlserver-adapter.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |spec| spec.platform = Gem::Platform::RUBY spec.version = version - spec.required_ruby_version = ">= 3.1.0" + spec.required_ruby_version = ">= 3.2.0" spec.license = "MIT" spec.authors = ["Ken Collins", "Anna Carey", "Will Bond", "Murray Steele", "Shawn Balestracci", "Joe Rafaniello", "Tom Ward", "Aidan Haran"] @@ -28,5 +28,5 @@ Gem::Specification.new do |spec| spec.require_paths = ["lib"] spec.add_dependency "activerecord", "~> 7.2.0" - spec.add_dependency "tiny_tds" + spec.add_dependency "ruby-odbc" 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_statements.rb b/lib/active_record/connection_adapters/sqlserver/database_statements.rb index 64a3ed22b..4df88f063 100644 --- a/lib/active_record/connection_adapters/sqlserver/database_statements.rb +++ b/lib/active_record/connection_adapters/sqlserver/database_statements.rb @@ -16,7 +16,7 @@ def write_query?(sql) # :nodoc: def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true) log(sql, name, async: async) do |notification_payload| with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn| - result = if id_insert_table_name = query_requires_identity_insert?(sql) + result = if (id_insert_table_name = query_requires_identity_insert?(sql)) with_identity_insert_enabled(id_insert_table_name, conn) { internal_raw_execute(sql, conn, perform_do: true) } else internal_raw_execute(sql, conn, perform_do: true) @@ -67,13 +67,11 @@ def internal_exec_sql_query(sql, conn) 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 begin_db_transaction @@ -173,11 +171,17 @@ def execute_procedure(proc_name, *variables) else variables.map { |v| quote(v) } end.join(", ") + sql = "EXEC #{proc_name} #{vars}".strip log(sql, "Execute Procedure") do |notification_payload| with_raw_connection do |conn| - result = internal_raw_execute(sql, conn) + if odbc_connection?(conn) + result = execute_odbc_procedure(sql, conn) + else + result = internal_raw_execute(sql, conn) + end + verified! options = { as: :hash, cache_rows: true, timezone: ActiveRecord.default_timezone || :utc } @@ -288,9 +292,11 @@ def sql_for_insert(sql, pk, binds, returning) end <<~SQL.squish + SET NOCOUNT ON DECLARE @ssaIdInsertTable table (#{pk_and_types.map { |pk_and_type| "#{pk_and_type[:quoted]} #{pk_and_type[:id_sql_type]}"}.join(", ") }); #{sql.dup.insert sql.index(/ (DEFAULT )?VALUES/i), " OUTPUT #{ pk_and_types.map { |pk_and_type| "INSERTED.#{pk_and_type[:quoted]}" }.join(", ") } INTO @ssaIdInsertTable"} SELECT #{pk_and_types.map {|pk_and_type| "CAST(#{pk_and_type[:quoted]} AS #{pk_and_type[:id_sql_type]}) #{pk_and_type[:quoted]}"}.join(", ")} FROM @ssaIdInsertTable + SET NOCOUNT OFF SQL else returning_columns = returning || Array(pk) @@ -303,7 +309,13 @@ def sql_for_insert(sql, pk, binds, returning) end 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 [sql, binds] @@ -429,7 +441,45 @@ def _raw_select(sql, conn) finish_statement_handle(handle) end + def execute_odbc_procedure(sql, conn) + results = [] + raw_connection_run(sql, conn) 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 + + # Wrapper for raw_connection's run method + def raw_connection_run(sql, conn) + conn.raw_connection.run(sql) do |handle| + yield(handle) + end + end + + def handle_more_results?(handle) + case @config[:mode].to_sym + when :dblib + when :odbc + handle.more_results + end + end + def handle_to_names_and_values(handle, options = {}) + case @config[:mode].to_sym + when :dblib + handle_to_names_and_values_dblib(handle, options) + when :odbc + handle_to_names_and_values_odbc(handle, options) + end + end + + def handle_to_names_and_values_dblib(handle, options = {}) query_options = {}.tap do |qo| qo[:timezone] = ActiveRecord.default_timezone || :utc qo[:as] = (options[:ar_result] || options[:fetch] == :rows) ? :array : :hash @@ -444,8 +494,30 @@ def handle_to_names_and_values(handle, options = {}) options[:ar_result] ? ActiveRecord::Result.new(columns, results) : results end + def handle_to_names_and_values_odbc(handle, options = {}) + @raw_connection.use_utc = ActiveRecord.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) - handle.cancel if handle + case @config[:mode].to_sym + when :dblib + handle.cancel if handle + when :odbc + handle.drop if handle && handle.respond_to?(:drop) && !handle.finished? + end handle end @@ -453,11 +525,19 @@ def finish_statement_handle(handle) # Getting around this by raising an exception ourselves while PR # https://github.com/rails-sqlserver/tiny_tds/pull/469 is not released. def internal_raw_execute(sql, conn, perform_do: false) - result = conn.execute(sql).tap do |_result| - raise TinyTds::Error, "failed to execute statement" if _result.is_a?(FalseClass) + if odbc_connection?(conn) + conn.do(sql) + else + result = conn.execute(sql).tap do |_result| + raise TinyTds::Error, "failed to execute statement" if _result.is_a?(FalseClass) + end + + perform_do ? result.do : result end + end - perform_do ? result.do : result + def odbc_connection?(connection) + connection.is_a?(ODBC::Database) end 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 1f14a4078..37506e7b9 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/binary.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/binary.rb @@ -5,6 +5,15 @@ module ConnectionAdapters 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_adapter.rb b/lib/active_record/connection_adapters/sqlserver_adapter.rb index 3f3e72808..58dc44e3e 100644 --- a/lib/active_record/connection_adapters/sqlserver_adapter.rb +++ b/lib/active_record/connection_adapters/sqlserver_adapter.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require "tiny_tds" 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" @@ -82,6 +82,17 @@ def dbconsole(config, options = {}) end def new_client(config) + case config[:mode].to_sym + when :dblib + dblib_connect(config) + when :odbc + odbc_connect(config) + else + raise ArgumentError, "Unknown connection mode in #{config.inspect}." + end + end + + def dblib_connect(config) TinyTds::Client.new(config) rescue TinyTds::Error => error if error.message.match(/database .* does not exist/i) @@ -91,6 +102,26 @@ def new_client(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 + puts config + ODBC.connect config[:dsn], config[:username], config[:password] + end.tap do |c| + begin + c.use_time = true + c.use_utc = ActiveRecord.default_timezone == :utc + rescue Exception + warn 'Ruby ODBC v0.99992 or higher is required.' + end + end + end + def rails_application_name Rails.application.class.name.split("::").first rescue @@ -245,7 +276,13 @@ def active? end def reconnect - @raw_connection&.close rescue nil + case @config[:mode].to_sym + when :dblib + @raw_connection&.close rescue nil + when :odbc + @raw_connection.disconnect rescue nil + end + @raw_connection = nil @spid = nil @collation = nil @@ -256,7 +293,13 @@ def reconnect def disconnect! super - @raw_connection&.close rescue nil + case @config[:mode].to_sym + when :dblib + @raw_connection&.close rescue nil + when :odbc + @raw_connection.disconnect rescue nil + end + @raw_connection = nil @spid = nil @collation = nil @@ -468,12 +511,63 @@ def translate_exception(e, message:, sql:, binds:) # === SQLServer Specific (Connection Management) ================ # + # def connect + # config = @connection_options + # @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 + # configure_connection + # end + def connection_errors @raw_connection_errors ||= [].tap do |errors| errors << TinyTds::Error if defined?(TinyTds::Error) + errors << ODBC::Error if defined?(ODBC::Error) 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 + + def config_login_timeout(config) + config[:login_timeout].present? ? config[:login_timeout].to_i : nil + end + + def config_timeout(config) + config[:timeout].present? ? config[:timeout].to_i / 1000 : nil + end + + def config_encoding(config) + config[:encoding].present? ? config[:encoding] : nil + end + + def configure_application_name; end + def initialize_dateformatter @database_dateformat = user_options_dateformat a, b, c = @database_dateformat.each_char.to_a @@ -512,20 +606,22 @@ def connect end def configure_connection - if @config[:azure] - @raw_connection.execute("SET ANSI_NULLS ON").do - @raw_connection.execute("SET ANSI_NULL_DFLT_ON ON").do - @raw_connection.execute("SET ANSI_PADDING ON").do - @raw_connection.execute("SET ANSI_WARNINGS ON").do - else - @raw_connection.execute("SET ANSI_DEFAULTS ON").do - end + unless @config[:mode] == 'odbc' + if @config[:azure] + @raw_connection.execute("SET ANSI_NULLS ON").do + @raw_connection.execute("SET ANSI_NULL_DFLT_ON ON").do + @raw_connection.execute("SET ANSI_PADDING ON").do + @raw_connection.execute("SET ANSI_WARNINGS ON").do + else + @raw_connection.execute("SET ANSI_DEFAULTS ON").do + end - @raw_connection.execute("SET QUOTED_IDENTIFIER ON").do - @raw_connection.execute("SET CURSOR_CLOSE_ON_COMMIT OFF").do - @raw_connection.execute("SET IMPLICIT_TRANSACTIONS OFF").do - @raw_connection.execute("SET TEXTSIZE 2147483647").do - @raw_connection.execute("SET CONCAT_NULL_YIELDS_NULL ON").do + @raw_connection.execute("SET QUOTED_IDENTIFIER ON").do + @raw_connection.execute("SET CURSOR_CLOSE_ON_COMMIT OFF").do + @raw_connection.execute("SET IMPLICIT_TRANSACTIONS OFF").do + @raw_connection.execute("SET TEXTSIZE 2147483647").do + @raw_connection.execute("SET CONCAT_NULL_YIELDS_NULL ON").do + end @spid = _raw_select("SELECT @@SPID", @raw_connection).first.first diff --git a/lib/active_record/sqlserver_base.rb b/lib/active_record/sqlserver_base.rb new file mode 100644 index 000000000..ea3dd53a0 --- /dev/null +++ b/lib/active_record/sqlserver_base.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module ActiveRecord + module ConnectionHandling + def sqlserver_adapter_class + ConnectionAdapters::SQLServerAdapter + end + + def sqlserver_connection(config) #:nodoc: + config = config.symbolize_keys + config.reverse_merge! mode: :dblib + mode = config[:mode].to_s.downcase.underscore.to_sym + 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 + sqlserver_adapter_class.new( + sqlserver_adapter_class.new_client(config), + logger, + nil, + config + ) + rescue ODBC::Error => e + if e.message.match(/database .* does not exist/i) + raise ActiveRecord::NoDatabaseError + else + raise + end + end + end +end \ No newline at end of file diff --git a/test/cases/adapter_test_sqlserver.rb b/test/cases/adapter_test_sqlserver.rb index 7b9f0eca5..20f28e7dc 100644 --- a/test/cases/adapter_test_sqlserver.rb +++ b/test/cases/adapter_test_sqlserver.rb @@ -18,6 +18,9 @@ class AdapterTestSQLServer < ActiveRecord::TestCase it "has basic and non-sensitive information in the adapters inspect method" do string = connection.inspect _(string).must_match %r{ActiveRecord::ConnectionAdapters::SQLServerAdapter} + _(string).must_match %r{version\: \d.\d} + _(string).must_match %r{mode: (dblib|odbc)} + _(string).must_match %r{azure: (true|false)} _(string).wont_match %r{host} _(string).wont_match %r{password} _(string).wont_match %r{username} diff --git a/test/cases/column_test_sqlserver.rb b/test/cases/column_test_sqlserver.rb index dfbd42e47..aead2aac8 100644 --- a/test/cases/column_test_sqlserver.rb +++ b/test/cases/column_test_sqlserver.rb @@ -277,8 +277,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_tds_73 ? Date.civil(1, 1, 1) : "0001-01-01" - _(obj.date).must_equal Date.civil(1, 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 @@ -286,21 +286,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(1, 4, 1) + obj.date = "04-01-1900" + obj.date.must_equal Date.civil(1900, 4, 1) obj.save! - _(obj.date).must_equal Date.civil(1, 4, 1) + obj.date.must_equal Date.civil(1900, 4, 1) obj.reload - _(obj.date).must_equal Date.civil(1, 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(1, 4, 1) + obj.date = "1900-04-01" + obj.date.must_equal Date.civil(1900, 4, 1) obj.save! - _(obj.date).must_equal Date.civil(1, 4, 1) + obj.date.must_equal Date.civil(1900, 4, 1) obj.reload - _(obj.date).must_equal Date.civil(1, 4, 1) - # Can filter by date range - _(obj).must_equal obj.class.where(date: obj.date..Date::Infinity.new).first + obj.date.must_equal Date.civil(1900, 4, 1) # Can keep and return assigned date. assert_obj_set_and_save :date, Date.civil(1972, 4, 14) # Can accept and cast time objects. diff --git a/test/cases/connection_test_sqlserver.rb b/test/cases/connection_test_sqlserver.rb index 1cb05cc3e..7e40f6ffb 100644 --- a/test/cases/connection_test_sqlserver.rb +++ b/test/cases/connection_test_sqlserver.rb @@ -37,6 +37,65 @@ class ConnectionTestSQLServer < ActiveRecord::TestCase describe "Connection management" do it "set spid on connect" do _(["Fixnum", "Integer"]).must_include connection.spid.class.name + 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 + ['Fixnum', 'Integer'].must_include connection.spid.class.name end it "reset spid on disconnect!" do @@ -60,6 +119,28 @@ class ConnectionTestSQLServer < ActiveRecord::TestCase private def disconnect_raw_connection! - connection.raw_connection.close rescue nil + 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/migration_test_sqlserver.rb b/test/cases/migration_test_sqlserver.rb index 0ffe20861..b2ce45599 100644 --- a/test/cases/migration_test_sqlserver.rb +++ b/test/cases/migration_test_sqlserver.rb @@ -45,7 +45,7 @@ class MigrationTestSQLServer < ActiveRecord::TestCase it "not drop the default constraint 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 @@ -60,6 +60,14 @@ class MigrationTestSQLServer < ActiveRecord::TestCase assert_nothing_raised { connection.change_column :people, :lock_version, :integer, limit: 8 } 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 + it "change null and default" do assert_nothing_raised { connection.change_column :people, :first_name, :text, null: true, default: nil } end diff --git a/test/cases/schema_dumper_test_sqlserver.rb b/test/cases/schema_dumper_test_sqlserver.rb index 6e7140ddf..6d1d99736 100644 --- a/test/cases/schema_dumper_test_sqlserver.rb +++ b/test/cases/schema_dumper_test_sqlserver.rb @@ -26,9 +26,9 @@ class SchemaDumperTestSQLServer < ActiveRecord::TestCase assert_line :float, type: "float", default: 123.00000001 assert_line :real, type: "real", default: 123.45 # Date and Time - assert_line :date, type: "date", default: "01-01-0001" - assert_line :datetime, type: "datetime", precision: nil, default: "01-01-1753 00:00:00.123" - if connection_tds_73 + 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", precision: 7, default: "12-31-9999 23:59:59.9999999" assert_line :datetime2_3, type: "datetime", precision: 3 assert_line :datetime2_1, type: "datetime", precision: 1 diff --git a/test/config.yml b/test/config.yml index ef095168c..c01ad3925 100644 --- a/test/config.yml +++ b/test/config.yml @@ -28,3 +28,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/schema/datatypes/2012.sql b/test/schema/datatypes/2012.sql index 77b14807d..2dd3f6487 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/support/connection_reflection.rb b/test/support/connection_reflection.rb index b7fe15ce2..b952d99bb 100644 --- a/test/support/connection_reflection.rb +++ b/test/support/connection_reflection.rb @@ -20,6 +20,10 @@ def connection_tds_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