diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bcf18176..97b627bd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## v8.0.3.odbc + +#### Added + +- ODBC restoration. + ## v8.0.3 #### Fixed diff --git a/Gemfile b/Gemfile index 341d6786b..504ebe05b 100644 --- a/Gemfile +++ b/Gemfile @@ -59,6 +59,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..0446fa179 100644 --- a/Rakefile +++ b/Rakefile @@ -9,9 +9,7 @@ task test: ["test:dblib"] 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 @@ -19,10 +17,21 @@ namespace :test do t.verbose = false end end + + task "dblib:env" 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 215aacb45..bd9480183 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.0.3 +8.0.3.odbc diff --git a/activerecord-sqlserver-adapter.gemspec b/activerecord-sqlserver-adapter.gemspec index d9fdebdf1..454567e96 100644 --- a/activerecord-sqlserver-adapter.gemspec +++ b/activerecord-sqlserver-adapter.gemspec @@ -29,4 +29,5 @@ Gem::Specification.new do |spec| spec.add_dependency "activerecord", "~> 8.0.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..3a6921870 --- /dev/null +++ b/lib/active_record/connection_adapters/sqlserver/core_ext/odbc.rb @@ -0,0 +1,29 @@ +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 41fbd12aa..f83a9a318 100644 --- a/lib/active_record/connection_adapters/sqlserver/database_statements.rb +++ b/lib/active_record/connection_adapters/sqlserver/database_statements.rb @@ -39,6 +39,8 @@ def cast_result(raw_result) end def affected_rows(raw_result) + return if raw_result.blank? + column_name = lowercase_schema_reflection ? 'affectedrows' : 'AffectedRows' raw_result.first[column_name] end @@ -53,20 +55,18 @@ def raw_execute(sql, name = nil, binds = [], prepare: false, async: false, allow end def internal_exec_sql_query(sql, conn) - handle = internal_raw_execute(sql, conn) + handle = raw_connection_run(sql, conn) handle_to_names_and_values(handle, ar_result: true) ensure finish_statement_handle(handle) end def exec_delete(sql, name = nil, binds = []) - sql = sql.dup << "; SELECT @@ROWCOUNT AS AffectedRows" - super(sql, name, binds) + super || super("SELECT @@ROWCOUNT As AffectedRows", "", []) end def exec_update(sql, name = nil, binds = []) - sql = sql.dup << "; SELECT @@ROWCOUNT AS AffectedRows" - super(sql, name, binds) + super || super("SELECT @@ROWCOUNT As AffectedRows", "", []) end def begin_db_transaction @@ -170,17 +170,8 @@ def execute_procedure(proc_name, *variables) log(sql, "Execute Procedure") do |notification_payload| with_raw_connection do |conn| - result = internal_raw_execute(sql, conn) - verified! - options = { as: :hash, cache_rows: true, timezone: ActiveRecord.default_timezone || :utc } - - result.each(options) do |row| - r = row.with_indifferent_access - yield(r) if block_given? - end - - result = result.each.map { |row| row.is_a?(Hash) ? row.with_indifferent_access : row } - notification_payload[:row_count] = result.count + result = send("execute_#{@config[:mode]}_procedure", sql, conn) + notification_payload[:row_count] = result&.count result end end @@ -280,10 +271,12 @@ def sql_for_insert(sql, pk, binds, returning) } end - <<~SQL.squish + <<-SQL.strip_heredoc + 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 + #{sql.dup.insert sql.index(/ (DEFAULT )?VALUES/), " 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) @@ -296,7 +289,14 @@ 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.present? + 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] @@ -305,7 +305,11 @@ def sql_for_insert(sql, pk, binds, returning) # === SQLServer Specific ======================================== # def set_identity_insert(table_name, conn, enable) - internal_raw_execute("SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}", conn , perform_do: true) + if @config[:mode].to_sym == :dblib + internal_raw_execute("SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}", conn , perform_do: true) + else + internal_raw_execute_odbc("SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}", conn , perform_do: true) + end rescue Exception raise ActiveRecordError, "IDENTITY_INSERT could not be turned #{enable ? 'ON' : 'OFF'} for table #{table_name}" end @@ -338,7 +342,12 @@ def sp_executesql_sql_type(attr) value = active_model_attribute?(attr) ? attr.value_for_database : attr if value.is_a?(Numeric) - value > 2_147_483_647 ? "bigint".freeze : "int".freeze + if value.is_a?(Integer) + value > 2_147_483_647 ? "bigint".freeze : "int".freeze + else + # For Float, BigDecimal, Rational etc. + value.is_a?(BigDecimal) ? "decimal(18,6)".freeze : "float".freeze + end else "nvarchar(max)".freeze end @@ -420,13 +429,26 @@ def identity_columns(table_name) # === SQLServer Specific (Selecting) ============================ # def _raw_select(sql, conn) - handle = internal_raw_execute(sql, conn) + handle = raw_connection_run(sql, conn) handle_to_names_and_values(handle, fetch: :rows) ensure finish_statement_handle(handle) end + def raw_connection_run(sql, conn, perform_do: false) + case @config[:mode].to_sym + when :dblib + internal_raw_execute(sql, conn, perform_do: perform_do) + when :odbc + internal_raw_execute_odbc(sql, conn, perform_do: perform_do) + end + end + def handle_to_names_and_values(handle, options = {}) + send("handle_to_names_and_values_#{@config[:mode]}", handle, options) + 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 @@ -441,8 +463,33 @@ 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 + return unless handle + + case @config[:mode].to_sym + when :dblib + handle.cancel + when :odbc + handle.drop if handle.respond_to?(:drop) && !handle.finished? + end + handle end @@ -455,6 +502,54 @@ def internal_raw_execute(sql, raw_connection, perform_do: false) perform_do ? result.do : result end + + # Executing SQL for ODBC mode + def internal_raw_execute_odbc(sql, raw_connection, perform_do: false) + return raw_connection.do(sql) if perform_do + + block_given? ? raw_connection.run_block(sql) { |handle| yield(handle) } : raw_connection.run(sql) + end + + private + + def execute_dblib_procedure(sql, conn) + result = internal_raw_execute(sql, conn) + verified! + options = { as: :hash, cache_rows: true, timezone: ActiveRecord.default_timezone || :utc } + + raw_rows = result.each(options).map do |row| + row = row.with_indifferent_access + yield(row) if block_given? + row + end + + raw_rows.map { |row| row.is_a?(Hash) ? row.with_indifferent_access : row } + end + + def execute_odbc_procedure(sql, conn) + results = [] + + internal_raw_execute_odbc(sql, conn) do |handle| + get_rows = lambda do + rows = handle_to_names_and_values handle, fetch: :all + results << rows.map!(&:with_indifferent_access) + end + + get_rows.call + get_rows.call while handle_more_results?(handle) + end + + results.many? ? results : results.first + end + + + def handle_more_results?(handle) + case @config[:mode].to_sym + when :dblib + when :odbc + handle.more_results + end + end 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 304501834..3f05e3388 100644 --- a/lib/active_record/connection_adapters/sqlserver_adapter.rb +++ b/lib/active_record/connection_adapters/sqlserver_adapter.rb @@ -4,6 +4,7 @@ require "base64" require "active_record" require "active_record/connection_adapters/statement_pool" +require "odbc_utf8" require "arel_sqlserver" require "active_record/connection_adapters/sqlserver/core_ext/active_record" require "active_record/connection_adapters/sqlserver/core_ext/explain" @@ -81,6 +82,19 @@ 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) + require "tiny_tds" + TinyTds::Client.new(config) rescue TinyTds::Error => error if error.message.match(/database .* does not exist/i) @@ -90,6 +104,36 @@ def new_client(config) end end + def odbc_connect(config) + raise ArgumentError, "Missing :dsn configuration." unless config.key?(:dsn) + require "odbc" + require "active_record/connection_adapters/sqlserver/core_ext/odbc" + + 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.default_timezone || :utc + rescue Exception + warn 'Ruby ODBC v0.99992 or higher is required.' + end + end + rescue ODBC::Error => e + if e.message.match(/database .* does not exist/i) + raise ActiveRecord::NoDatabaseError + else + raise + end + end + def rails_application_name Rails.application.class.name.split("::").first rescue @@ -100,7 +144,7 @@ def rails_application_name def initialize(...) super - @config[:tds_version] = "7.3" unless @config[:tds_version] + @config[:tds_version] ||= "7.3" if @config[:mode].to_sym == :dblib @config[:appname] = self.class.rails_application_name unless @config[:appname] @config[:login_timeout] = @config[:login_timeout].present? ? @config[:login_timeout].to_i : nil @config[:timeout] = @config[:timeout].present? ? @config[:timeout].to_i / 1000 : nil @@ -238,13 +282,28 @@ def disable_referential_integrity # === Abstract Adapter (Connection Management) ================== # def active? - @raw_connection&.active? + return false unless @raw_connection + + @connection_parameters[:mode].to_sym == :dblib ? @raw_connection.active? : odbc_connection_active? + rescue *connection_errors + false + end + + def odbc_connection_active? + @raw_connection.do("SELECT 1") + true rescue *connection_errors false end def reconnect - @raw_connection&.close rescue nil + case @connection_parameters[: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 @@ -255,7 +314,13 @@ def reconnect def disconnect! super - @raw_connection&.close rescue nil + case @connection_parameters[: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 @@ -462,6 +527,7 @@ def translate_exception(exception, message:, sql:, binds:) 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 @@ -503,6 +569,15 @@ def connect end def configure_connection + send("configure_#{@config[:mode]}_connection") + + @spid = _raw_select("SELECT @@SPID", @raw_connection).first.first + + initialize_dateformatter + use_database + end + + def configure_dblib_connection if @config[:azure] @raw_connection.execute("SET ANSI_NULLS ON").do @raw_connection.execute("SET ANSI_NULL_DFLT_ON ON").do @@ -517,11 +592,29 @@ def configure_connection @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 + def configure_odbc_connection + if @config[:azure] + @raw_connection.do("SET ANSI_NULLS ON") + @raw_connection.do("SET ANSI_NULL_DFLT_ON ON") + @raw_connection.do("SET ANSI_PADDING ON") + @raw_connection.do("SET ANSI_WARNINGS ON") + else + @raw_connection.do("SET ANSI_DEFAULTS ON") + end - initialize_dateformatter - use_database + @raw_connection.do("SET QUOTED_IDENTIFIER ON") + @raw_connection.do("SET CURSOR_CLOSE_ON_COMMIT OFF") + @raw_connection.do("SET IMPLICIT_TRANSACTIONS OFF") + @raw_connection.do("SET TEXTSIZE 2147483647") + @raw_connection.do("SET CONCAT_NULL_YIELDS_NULL ON") + + # Moved from AVM initializer to this location to avoid conflicts. + # Prior to Rails 7.2.0, `configure_connection` was an empty method. + # Keeping this method in the AVM initializer now leads to confusion. + # To avoid such ambiguity, the AVM-specific change is added here directly. + @raw_connection.do("SET LOCK_TIMEOUT 45000") end end end diff --git a/test/cases/adapter_test_sqlserver.rb b/test/cases/adapter_test_sqlserver.rb index 5f6c64784..97bf38ae6 100644 --- a/test/cases/adapter_test_sqlserver.rb +++ b/test/cases/adapter_test_sqlserver.rb @@ -19,6 +19,7 @@ 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{mode: (dblib|odbc)} _(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..b32d20e22 100644 --- a/test/cases/connection_test_sqlserver.rb +++ b/test/cases/connection_test_sqlserver.rb @@ -34,6 +34,58 @@ 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 _(["Fixnum", "Integer"]).must_include connection.spid.class.name @@ -60,6 +112,27 @@ class ConnectionTestSQLServer < ActiveRecord::TestCase private def disconnect_raw_connection! - connection.raw_connection.close rescue nil + 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..8faee17d2 100644 --- a/test/config.yml +++ b/test/config.yml @@ -11,7 +11,7 @@ default_connection_info: &default_connection_info connections: - sqlserver: + dblib: arunit: <<: *default_connection_info appname: SQLServerAdptrUnit @@ -28,3 +28,11 @@ 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