From 682e0e257f8b4efe76d58f0119b51210d73b3c2d Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Sun, 12 Jun 2016 09:53:53 -0400 Subject: [PATCH 01/27] [Rails5] Initial gem change and DB support interfaces. --- Gemfile | 24 ++++++----- activerecord-sqlserver-adapter.gemspec | 2 +- .../connection_adapters/sqlserver_adapter.rb | 42 +++++++++++++++---- 3 files changed, 48 insertions(+), 20 deletions(-) diff --git a/Gemfile b/Gemfile index 6ae57eb90..6c5156aa4 100644 --- a/Gemfile +++ b/Gemfile @@ -16,16 +16,20 @@ else require 'net/http' require 'yaml' spec = eval(File.read('activerecord-sqlserver-adapter.gemspec')) - version = spec.dependencies.detect{ |d|d.name == 'activerecord' }.requirement.requirements.first.last.version - major, minor, tiny = version.split('.') - uri = URI.parse "https://rubygems.org/api/v1/versions/activerecord.yaml" - http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = true - http.verify_mode = OpenSSL::SSL::VERIFY_NONE - YAML.load(http.request(Net::HTTP::Get.new(uri.request_uri)).body).select do |data| - a, b, c = data['number'].split('.') - !data['prerelease'] && major == a && (minor.nil? || minor == b) - end.first['number'] + ver = spec.dependencies.detect{ |d|d.name == 'activerecord' }.requirement.requirements.first.last.version + major, minor, tiny, pre = ver.split('.') + if !pre + uri = URI.parse "https://rubygems.org/api/v1/versions/activerecord.yaml" + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + YAML.load(http.request(Net::HTTP::Get.new(uri.request_uri)).body).select do |data| + a, b, c = data['number'].split('.') + !data['prerelease'] && major == a && (minor.nil? || minor == b) + end.first['number'] + else + ver + end end gem 'rails', git: "git://github.com/rails/rails.git", tag: "v#{version}" end diff --git a/activerecord-sqlserver-adapter.gemspec b/activerecord-sqlserver-adapter.gemspec index 8d906f2b3..017fd96f9 100644 --- a/activerecord-sqlserver-adapter.gemspec +++ b/activerecord-sqlserver-adapter.gemspec @@ -16,5 +16,5 @@ 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', '~> 4.2.1' + spec.add_dependency 'activerecord', '~> 5.0.0.rc1' end diff --git a/lib/active_record/connection_adapters/sqlserver_adapter.rb b/lib/active_record/connection_adapters/sqlserver_adapter.rb index 1079f0e35..b2a5c98dd 100644 --- a/lib/active_record/connection_adapters/sqlserver_adapter.rb +++ b/lib/active_record/connection_adapters/sqlserver_adapter.rb @@ -71,10 +71,6 @@ def schema_creation SQLServer::SchemaCreation.new self end - def adapter_name - ADAPTER_NAME - end - def supports_migrations? true end @@ -83,10 +79,6 @@ def supports_primary_key? true end - def supports_count_distinct? - true - end - def supports_ddl_transactions? true end @@ -95,14 +87,26 @@ def supports_bulk_alter? false end + def supports_advisory_locks? + false + end + def supports_index_sort_order? true end + def supports_index_sort_order? + false + end + def supports_partial_index? true end + def supports_expression_index? + false + end + def supports_explain? true end @@ -111,14 +115,34 @@ def supports_transaction_isolation? true end + def supports_indexes_in_create? + false + end + + def supports_foreign_keys? + true + end + def supports_views? true end - def supports_foreign_keys? + def supports_datetime_with_precision? + true + end + + def supports_json? true end + def supports_comments? + false + end + + def supports_comments_in_create? + false + end + def disable_referential_integrity tables = tables_with_referential_integrity tables.each { |t| do_execute "ALTER TABLE #{t} NOCHECK CONSTRAINT ALL" } From 50bf4c0ac7d2cdfbfb6b0d169353da58003f5244 Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Sun, 12 Jun 2016 19:08:12 -0400 Subject: [PATCH 02/27] [Rails5] Setup new CHANGELOG file. --- CHANGELOG.md | 202 ++------------------------------------------------- 1 file changed, 6 insertions(+), 196 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index afbfb7f89..426467602 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,212 +1,22 @@ -## v4.2.15 -#### Fixed - -* Removed errand puts statment from database tasks. -* Fix quoting of non-national columns. - - -## v4.2.14 - -#### Fixed - -* Fix rescue constants for optional connection gems. Fixes #475. - - -## v4.2.13 - -#### Fixed - -* Add to_s method to SQLServer::Type::Char::Data. Thanks @marceloeloelo. - - -## v4.2.12 - -#### Fixed - -* Isolation levels not being reset on error. Fixes #469. Thanks @anthony - - -## v4.2.11 - -#### Fixed - -* Undefined method `database_prefix_remote_server?' Fixes #450. Thanks @jippeholwerda -* Document two methods for avoiding N'' quoting on char/varchar columns. -* First run failure of `change_column` while dropping constraint. Fixes #420. Thanks @GrumpyRainbow @rkr090 -* Rounding errors w/datetime2(0) types having no fractional seconds. Fixes #465. Thanks @alawton - -#### Changed - -* Supporting escape hatch for N'' quoting. Remove `#is_utf8` string check in `#_quote` method. - This duplicated strings and forced encoding which was actually wasteful. - - -## v4.2.10 - -#### Fixed - -* Ensure small datetime/datetime2 fractionals are properly quoted. Fixes #457. - - -## v4.2.9 - -#### Fixed - -* Conform to new data_sources interfaces. See: https://git.io/va4Fp -* The `primary_key` method falls back to Identity columns. Not the other way around. Fixes #454. Thanks @marceloeloelo -* Ensure that `execute_procedure` returns proper time zones. Fixes #449 - -#### Changed - -* Run tests with verbose false. - - -## v4.2.8 - -#### Fixed - -* Azure-Friendly Disable Referential Integrity. No more `sp_MSforeachtable` usage. Fixes #421 -* Azure-Friendly DB create/drop. Fixes #442 - - Create allows edition options like: MAXSIZE, EDITION, and SERVICE_OBJECTIVE. - - -## v4.2.7 - -#### Added - -* Support 2008 Datatypes Using TDSVER=7.3. Fixes #433 - -#### Changed - -* Test now use latest v0.9.5 of TinyTDS. Includes tests for `defncopy` Windows binstub. -* Make linked servers stronger. Fixes #427. Thanks @jippeholwerda -* Use proper module for the `sqlserver_connection` method. Fixes #431. Thanks @jippeholwerda -* All datetime casting using the `Time::DATE_FORMATS[:_sqlserver_*]` formats set after connection. - -#### Removed - -* The `SQLServer::Utils.with_sqlserver_db_date_formats` helper and `quoted_date` hacks. -* The `Quoter` value type which allowed column => type special case quoting. - -#### Fixed - -* Every time datatype has perfect micro/nano second handling. -* All supported datatypes dump defaults properly to schema.rb -* Partial indexes using `:where` in schema dumper. Fixes #153 - - -## v4.2.6 - -#### Fixed - -* Allow linked servers for table names. Fixes #426. Thanks @jippeholwerda - - -## v4.2.5 - -#### Removed - -* Remove Type::Castable hacks for core type objects to force trust the DB. Allows Rails 5 attributes. - -#### Fixed - -* Tests for decimal scale. See Rails commit. http://git.io/vGotB -* Improve case comparision performace per column. Fixes #414 -* DB rollback when reversable add_column has several options. Fixes #359 -* Better column definitions for default objects. Fixes #412 - - -## v4.2.4 - -#### Fixed - -* Compatible with Rails 4.2.1. -* Fix schema limit reflection for char/varchar. Fixes #394. - - -## v4.2.3 - -#### Fixed - -* Fix SET defaults when using Azure. -* Test insert 4-byte unicode chars. -* Make rollback transaction transcount aware for implicit error rollbacks. Fixes #390 - - -## v4.2.2 - -#### Added - -* DatabaseTasks support for all tasks! Uses FreeTDS `defncopy` for structure dump. Fixes #380. -* Provide class config for `use_output_inserted` (default true) for insert SQL. Fixed #381. - - -## v4.2.1 - -#### Fixed - -* Guard against empty view definitions when `sb_helptext` fails silently. Fixes #337. -* Proper table/column escaping in the `change_column_null` method. Fixes #355. -* Use `send :include` for modules for 1.9 compatibility. Fixes #383. - - -## v4.2.0 +## v5.0.0 #### Added -* New `ActiveRecord::Type` objects. See `active_record/connection_adapters/sqlserver/type` dir. -* Aliased `ActiveRecord::Type::SQLServer` to `ActiveRecord::ConnectionAdapters::SQLServer::Type` -* New `SQLServer::Utils::Name` object for decomposing and quoting SQL Server names/identifiers. -* Support for most all SQL Server types in schema statements and dumping. -* Support create table with query from relation or select statement. -* Foreign Key Support Fixes #375. +* ... #### Changed -* The `create_database` now takes an options hash. Only key/value now is `collation`. Unknown keys just use raw values for SQL. -* Complete rewrite of our Arel visitor. Focuing on 2012 and upward so we can make FETCH happen. -* Testing enhancements. - * Guard support, check our Guardfile. - * Use `ARTest` namespace with `SQLServer` module for our helpers/objects. - * Simple 2012 schmea addition and extensive column/type_cast object tests. -* Follow Rails convention and remove varying character default limits. -* The `cs_equality_operator` is now s class configuration property only. -* The `with_identity_insert_enabled(table_name)` is now public. -* Use ActiveRecord tranasaction interface vs our own `run_with_isolation_level`. +* ... #### Deprecated -* n/a +* ... #### Removed -* SQL Server versions < 2012 which do not support OFFSET and FETCH. http://bit.ly/1B5Bwsd -* The `enable_default_unicode_types` option. Default to national types all the time. -* Native type configs for older DB support. Includes the following with new default value: - * native_string_database_type => `nvarchar` - * native_text_database_type => `nvarchar(max)` - * native_binary_database_type => `varbinary(max)` -* Various version and inspection methods removed. These include: - * database_version - * database_year - * product_level - * product_version - * edition -* Removed tests for old issue #164. Handled by core types now. -* The `activity_stats` method. Please put this in a gem if needed. -* We no longer use regular expressions to fix identity inserts. Use ActiveRecord or public ID insert helper. -* All auto reconnect and SQL retry logic. Got too complicated and stood in the way of AR's pool. Speed boost too. -* The adapter will no longer try to remove duplicate order by clauses. Use relation `reorder`, `unscope`, etc. -* We no longer use regular expressions to remove identity columns from updates. Now with `attributes_for_update` AR hook. +* ... #### Fixed -* Default lock is now "WITH(UPDLOCK)". Fixes #368 -* Better bind types & params for `sp_executesql`. Fixes #239. - -#### Security - -* The connection's `inspect` method no longer returns sensitive connection info. Very basic now. - - +* ... From 5e35c779ad8daf1cd95ff41d44c704c8afde0a82 Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Sun, 12 Jun 2016 19:18:31 -0400 Subject: [PATCH 03/27] [Rails5] Remove ODBC connection mode. --- CHANGELOG.md | 2 +- Gemfile | 4 - README.md | 4 - RUNNING_UNIT_TESTS.md | 17 ----- Rakefile | 9 +-- appveyor.yml | 2 +- .../sqlserver/core_ext/odbc.rb | 34 --------- .../sqlserver/database_statements.rb | 38 ---------- .../connection_adapters/sqlserver_adapter.rb | 24 ------ lib/active_record/sqlserver_base.rb | 4 - test/cases/adapter_test_sqlserver.rb | 2 +- test/cases/connection_test_sqlserver.rb | 73 ------------------- test/config.yml | 9 --- test/support/connection_reflection.rb | 4 - 14 files changed, 5 insertions(+), 221 deletions(-) delete mode 100644 lib/active_record/connection_adapters/sqlserver/core_ext/odbc.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 426467602..033b75284 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ #### Removed -* ... +* ODBC connection mode. Not been maintained since Rails 4.0. #### Fixed diff --git a/Gemfile b/Gemfile index 6c5156aa4..d54ba5f82 100644 --- a/Gemfile +++ b/Gemfile @@ -48,10 +48,6 @@ group :tinytds do end end -group :odbc do - gem 'ruby-odbc' -end - group :development do gem 'mocha' gem 'minitest', '< 5.3.4' # PENDING: [Rails5.x] Remove test order constraint. diff --git a/README.md b/README.md index 88bdc5eff..6e8929964 100644 --- a/README.md +++ b/README.md @@ -156,10 +156,6 @@ gem 'tiny_tds' gem 'activerecord-sqlserver-adapter', '~> 4.2.0' ``` -If you want to use ruby ODBC, please use the latest least. If you have any troubles installing the lower level libraries for the adapter, please consult the wiki pages for various platform installation guides. Tons of good info can be found and we ask that you contribute too! - -http://wiki.github.com/rails-sqlserver/activerecord-sqlserver-adapter/platform-installation - ## Contributing diff --git a/RUNNING_UNIT_TESTS.md b/RUNNING_UNIT_TESTS.md index fdeed143a..59ac591eb 100644 --- a/RUNNING_UNIT_TESTS.md +++ b/RUNNING_UNIT_TESTS.md @@ -28,8 +28,6 @@ $ bundle exec rake test TEST_FILES_AR="test/cases/finder_test.rb" The default names for the test databases are `activerecord_unittest` and `activerecord_unittest2`. If you want to use another database name then be sure to update the connection file that matches your connection method in test/connections/native_sqlserver_#{connection_method}/connection.rb. Define a user named 'rails' in SQL Server with all privileges granted for the test databases. Use an empty password for said user. -The connection files make certain assumptions. For instance, the ODBC connection assumes you have a DSN setup that matches the name of the default database names. Remember too you have to set an environment variable for the DSN of the adapter, see the connection.rb file that matches your connection mode for details. - ```sql CREATE DATABASE [activerecord_unittest]; CREATE DATABASE [activerecord_unittest2]; @@ -93,18 +91,6 @@ $ bundle exec rake test ## Testing Options -The Gemfile contains groups for `:tinytds` and `:odbc`. By default it will install both gems which allows you to run the full test suite in either connection mode. If for some reason any one of these is problematic or of no concern, you could always opt out of bundling either gem with something like this. - -``` -$ bundle install --without odbc -``` - -You can run different connection modes using the following rake commands. Again, the DBLIB connection mode using TinyTDS is the default test task. - -``` -$ bundle exec rake test:dblib -$ bundle exec rake test:odbc -``` By default, Bundler will download the Rails git repo and use the git tag that matches the dependency version in our gemspec. If you want to test another version of Rails, you can either temporarily change the :tag for Rails in the Gemfile. Likewise, you can clone the Rails repo your self to another directory and use the `RAILS_SOURCE` environment variable. @@ -116,6 +102,3 @@ By default, Bundler will download the Rails git repo and use the git tag that ma * Possibly change the SQL Server TCP/IP properties in "SQL Server Configuration Manager -> SQL Server Network Configuration -> Protocols for MSSQLSERVER", and ensure that TCP/IP is enabled and the appropriate entries on the "IP Addresses" tab are enabled. -## Current Expected Failures - -* Misc Date/Time erros when using ODBC mode. diff --git a/Rakefile b/Rakefile index ed71ab146..79a4a5885 100644 --- a/Rakefile +++ b/Rakefile @@ -8,7 +8,7 @@ task default: [:test] namespace :test do - %w(dblib odbc).each do |mode| + %w(dblib).each do |mode| Rake::TestTask.new(mode) do |t| t.libs = ARTest::SQLServer.test_load_paths @@ -23,17 +23,12 @@ 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', 'odbc'].each do |mode| + ['dblib'].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/appveyor.yml b/appveyor.yml index a34c0a7de..91782c17f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -12,7 +12,7 @@ install: - ps: Update-AppveyorBuild -Version "$(Get-Content $env:appveyor_build_folder\VERSION).$env:appveyor_build_number" - ruby --version - gem --version - - bundle install --without odbc guard + - bundle install build: off test_script: - powershell -File "%APPVEYOR_BUILD_FOLDER%\test\appveyor\dbsetup.ps1" diff --git a/lib/active_record/connection_adapters/sqlserver/core_ext/odbc.rb b/lib/active_record/connection_adapters/sqlserver/core_ext/odbc.rb deleted file mode 100644 index 712751a2b..000000000 --- a/lib/active_record/connection_adapters/sqlserver/core_ext/odbc.rb +++ /dev/null @@ -1,34 +0,0 @@ -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 05ca2501f..9fc7708ab 100644 --- a/lib/active_record/connection_adapters/sqlserver/database_statements.rb +++ b/lib/active_record/connection_adapters/sqlserver/database_statements.rb @@ -122,18 +122,6 @@ 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 @@ -298,8 +286,6 @@ 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 @@ -322,16 +308,12 @@ 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 @@ -339,8 +321,6 @@ 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 @@ -354,28 +334,10 @@ 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_adapter.rb b/lib/active_record/connection_adapters/sqlserver_adapter.rb index b2a5c98dd..8df3d3a03 100644 --- a/lib/active_record/connection_adapters/sqlserver_adapter.rb +++ b/lib/active_record/connection_adapters/sqlserver_adapter.rb @@ -173,8 +173,6 @@ def disconnect! case @connection_options[:mode] when :dblib @connection.close rescue nil - when :odbc - @connection.disconnect rescue nil end @connection = nil end @@ -324,8 +322,6 @@ 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 configure_connection @@ -334,7 +330,6 @@ 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 @@ -371,25 +366,6 @@ 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 diff --git a/lib/active_record/sqlserver_base.rb b/lib/active_record/sqlserver_base.rb index 8786b5f88..e4bf3fc3d 100644 --- a/lib/active_record/sqlserver_base.rb +++ b/lib/active_record/sqlserver_base.rb @@ -7,10 +7,6 @@ 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/test/cases/adapter_test_sqlserver.rb b/test/cases/adapter_test_sqlserver.rb index e34669474..4eebad712 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|odbc)} + string.must_match %r{mode: dblib} string.must_match %r{azure: (true|false)} string.wont_match %r{host} string.wont_match %r{password} diff --git a/test/cases/connection_test_sqlserver.rb b/test/cases/connection_test_sqlserver.rb index d318ce80a..8e39b79f9 100644 --- a/test/cases/connection_test_sqlserver.rb +++ b/test/cases/connection_test_sqlserver.rb @@ -31,61 +31,6 @@ 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 @@ -118,25 +63,7 @@ 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/config.yml b/test/config.yml index 2595dcc76..fb507593a 100644 --- a/test/config.yml +++ b/test/config.yml @@ -30,12 +30,3 @@ connections: azure: <%= !ENV['ACTIVERECORD_UNITTEST_AZURE'].nil? %> timeout: <%= ENV['ACTIVERECORD_UNITTEST_AZURE'].present? ? 20 : nil %> - 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/support/connection_reflection.rb b/test/support/connection_reflection.rb index 1c2e08e5c..20ef1fd8a 100644 --- a/test/support/connection_reflection.rb +++ b/test/support/connection_reflection.rb @@ -24,10 +24,6 @@ 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 From a66b506baf02efbbd761c06bcf6ff848719fb15a Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Thu, 14 Jul 2016 16:16:23 -0400 Subject: [PATCH 04/27] Initial Rails v5 commit. --- Gemfile | 1 - VERSION | 2 +- .../sqlserver/database_statements.rb | 54 +++++------ .../connection_adapters/sqlserver/quoting.rb | 23 ++++- .../sqlserver/schema_cache.rb | 68 ++++++-------- .../sqlserver/schema_creation.rb | 18 ---- .../sqlserver/schema_statements.rb | 91 +++++++++++-------- .../sqlserver/sql_type_metadata.rb | 20 ++++ .../sqlserver/table_definition.rb | 2 +- .../sqlserver/transaction.rb | 2 +- .../connection_adapters/sqlserver/type.rb | 4 +- .../sqlserver/type/big_integer.rb | 2 + .../sqlserver/type/binary.rb | 6 ++ .../sqlserver/type/boolean.rb | 1 + .../sqlserver/type/char.rb | 8 +- .../sqlserver/type/date.rb | 6 +- .../sqlserver/type/datetime.rb | 7 +- .../sqlserver/type/datetime2.rb | 4 + .../sqlserver/type/datetimeoffset.rb | 7 +- .../sqlserver/type/decimal.rb | 5 + .../sqlserver/type/float.rb | 2 + .../sqlserver/type/integer.rb | 1 + .../sqlserver/type/money.rb | 2 + .../sqlserver/type/real.rb | 2 + .../sqlserver/type/small_integer.rb | 2 +- .../sqlserver/type/small_money.rb | 2 + .../sqlserver/type/smalldatetime.rb | 3 +- .../connection_adapters/sqlserver/type/sql.rb | 16 ++++ .../sqlserver/type/text.rb | 2 + .../sqlserver/type/time.rb | 8 +- .../sqlserver/type/timestamp.rb | 2 + .../sqlserver/type/tiny_integer.rb | 1 + .../sqlserver/type/unicode_char.rb | 6 ++ .../sqlserver/type/unicode_text.rb | 2 + .../sqlserver/type/unicode_varchar.rb | 6 ++ .../sqlserver/type/unicode_varchar_max.rb | 2 + .../sqlserver/type/uuid.rb | 3 +- .../sqlserver/type/varbinary.rb | 6 ++ .../sqlserver/type/varbinary_max.rb | 2 + .../sqlserver/type/varchar.rb | 6 ++ .../sqlserver/type/varchar_max.rb | 2 + .../connection_adapters/sqlserver_adapter.rb | 22 ++--- .../connection_adapters/sqlserver_column.rb | 33 +------ lib/active_record/sqlserver_base.rb | 2 +- lib/arel/visitors/sqlserver.rb | 11 +-- test/cases/adapter_test_sqlserver.rb | 18 ++-- test/cases/schema_test_sqlserver.rb | 4 +- 47 files changed, 285 insertions(+), 214 deletions(-) create mode 100644 lib/active_record/connection_adapters/sqlserver/sql_type_metadata.rb create mode 100644 lib/active_record/connection_adapters/sqlserver/type/sql.rb diff --git a/Gemfile b/Gemfile index d54ba5f82..40d0716b9 100644 --- a/Gemfile +++ b/Gemfile @@ -50,7 +50,6 @@ end group :development do gem 'mocha' - gem 'minitest', '< 5.3.4' # PENDING: [Rails5.x] Remove test order constraint. gem 'minitest-spec-rails' gem 'pry' end diff --git a/VERSION b/VERSION index 35a2a4450..0062ac971 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.2.15 +5.0.0 diff --git a/lib/active_record/connection_adapters/sqlserver/database_statements.rb b/lib/active_record/connection_adapters/sqlserver/database_statements.rb index 9fc7708ab..fc338adbe 100644 --- a/lib/active_record/connection_adapters/sqlserver/database_statements.rb +++ b/lib/active_record/connection_adapters/sqlserver/database_statements.rb @@ -19,11 +19,10 @@ def exec_query(sql, name = 'SQL', binds = [], sqlserver_options = {}) sp_executesql(sql, name, binds) end - def exec_insert(sql, name, binds, _pk = nil, _sequence_name = nil) - id_insert = binds_have_identity_column?(binds) - id_table = table_name_from_binds(binds) if id_insert - if id_insert && id_table - with_identity_insert_enabled(id_table) { exec_query(sql, name, binds) } + def exec_insert(sql, name, binds, pk = nil, _sequence_name = nil) + id_insert_table_name = query_requires_identity_insert?(sql) if pk + if id_insert_table_name + with_identity_insert_enabled(id_insert_table_name) { exec_query(sql, name, binds) } else exec_query(sql, name, binds) end @@ -204,31 +203,21 @@ def select(sql, name = nil, binds = []) end def sql_for_insert(sql, pk, id_value, sequence_name, binds) - sql = if pk && self.class.use_output_inserted && !database_prefix_remote_server? - quoted_pk = SQLServer::Utils.extract_identifiers(pk).quoted - sql.insert sql.index(/ (DEFAULT )?VALUES/), " OUTPUT INSERTED.#{quoted_pk}" - else - "#{sql}; SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident" + if pk.nil? + table_name = query_requires_identity_insert?(sql) + pk = primary_key(table_name) end + sql = if pk && self.class.use_output_inserted && !database_prefix_remote_server? + quoted_pk = SQLServer::Utils.extract_identifiers(pk).quoted + sql.insert sql.index(/ (DEFAULT )?VALUES/), " OUTPUT INSERTED.#{quoted_pk}" + else + "#{sql}; SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident" + end super end # === SQLServer Specific ======================================== # - def binds_have_identity_column?(binds) - binds.any? do |column_value| - column, value = column_value - SQLServerColumn === column && column.is_identity? - end - end - - def table_name_from_binds(binds) - binds.detect { |column_value| - column, value = column_value - SQLServerColumn === column - }.try(:first).try(:table_name) - end - def set_identity_insert(table_name, enable = true) do_execute "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}" rescue Exception @@ -250,20 +239,19 @@ def sp_executesql(sql, name, binds, options = {}) def sp_executesql_types_and_parameters(binds) types, params = [], [] - binds.each_with_index do |(column, value), index| - types << "@#{index} #{sp_executesql_sql_type(column, value)}" - params << quote(value, column) + binds.each_with_index do |attr, index| + types << "@#{index} #{sp_executesql_sql_type(attr)}" + params << type_cast(attr.value_for_database) end [types, params] end - def sp_executesql_sql_type(column, value) - return column.sql_type_for_statement if SQLServerColumn === column - if value.is_a?(Numeric) - 'int' - # We can do more here later. + def sp_executesql_sql_type(attr) + case value = attr.value_for_database + when Numeric + SQLServer::Type::Integer::SQLSERVER_TYPE else - 'nvarchar(max)' + attr.type.sqlserver_type end end diff --git a/lib/active_record/connection_adapters/sqlserver/quoting.rb b/lib/active_record/connection_adapters/sqlserver/quoting.rb index 724e8a312..f3aedd581 100644 --- a/lib/active_record/connection_adapters/sqlserver/quoting.rb +++ b/lib/active_record/connection_adapters/sqlserver/quoting.rb @@ -7,6 +7,18 @@ module Quoting QUOTED_FALSE = '0' QUOTED_STRING_PREFIX = 'N' + def fetch_type_metadata(sql_type, sqlserver_options = {}) + cast_type = lookup_cast_type(sql_type) + SQLServer::SqlTypeMetadata.new( + sql_type: sql_type, + type: cast_type.type, + limit: cast_type.limit, + precision: cast_type.precision, + scale: cast_type.scale, + sqlserver_options: sqlserver_options + ) + end + def quote_string(s) SQLServer::Utils.quote_string(s) end @@ -15,11 +27,12 @@ def quote_column_name(name) SQLServer::Utils.extract_identifiers(name).quoted end - def quote_default_value(value, column) - if column.type == :uuid && value =~ /\(\)/ + def quote_default_expression(value, column) + cast_type = lookup_cast_type(column.sql_type) + if cast_type.type == :uuid && value =~ /\(\)/ value else - quote(value, column) + super end end @@ -41,9 +54,9 @@ def unquoted_false def quoted_date(value) if value.acts_like?(:date) - Type::Date.new.type_cast_for_database(value) + Type::Date.new.serialize(value) else value.acts_like?(:time) - Type::DateTime.new.type_cast_for_database(value) + Type::DateTime.new.serialize(value) end end diff --git a/lib/active_record/connection_adapters/sqlserver/schema_cache.rb b/lib/active_record/connection_adapters/sqlserver/schema_cache.rb index 427794697..d1995bc7b 100644 --- a/lib/active_record/connection_adapters/sqlserver/schema_cache.rb +++ b/lib/active_record/connection_adapters/sqlserver/schema_cache.rb @@ -9,35 +9,33 @@ def initialize(conn) @view_information = {} end - # Superclass Overrides + def initialize_dup(other) + super + @views = @views.dup + @view_information = @view_information.dup + end def primary_keys(table_name) - name = key(table_name) - @primary_keys[name] ||= table_exists?(table_name) ? connection.primary_key(table_name) : nil + super tn_quoted(table_name) end - def table_exists?(table_name) - name = key(table_name) - prepare_tables_and_views - return @tables[name] if @tables.key? name - table_exists = @tables[name] = connection.table_exists?(table_name) - table_exists || view_exists?(table_name) + def data_source_exists?(table_name) + super tn_quoted(table_name) end - def tables(name) - super(key(name)) + def add(table_name) + super tn_quoted(table_name) end - def columns(table_name) - name = key(table_name) - @columns[name] ||= connection.columns(table_name) + def data_sources(name) + super tn_quoted(name) end + # No override for #columns. + # Allow `table_name` which could be fully qualified to be used with schema reflection. + def columns_hash(table_name) - name = key(table_name) - @columns_hash[name] ||= Hash[columns(table_name).map { |col| - [col.name, col] - }] + super tn_quoted(table_name) end def clear! @@ -50,12 +48,10 @@ def size super + [@views, @view_information].map{ |x| x.size }.inject(:+) end - def clear_table_cache!(table_name) - name = key(table_name) - @columns.delete name - @columns_hash.delete name - @primary_keys.delete name - @tables.delete name + def clear_data_source_cache!(table_name) + name = tn_quoted(table_name) + super(name) + @columns.delete table_name # Because... @views.delete name @view_information.delete name end @@ -72,14 +68,14 @@ def marshal_load(array) # SQL Server Specific def view_exists?(table_name) - name = key(table_name) - prepare_tables_and_views + prepare_data_sources if @views.empty? + name = tn_quoted(table_name) return @views[name] if @views.key? name - @views[name] = connection.views.include?(table_name) + @views[name] = connection.views.include?(tn_object(table_name)) end def view_information(table_name) - name = key(table_name) + name = tn_quoted(table_name) return @view_information[name] if @view_information.key? name @view_information[name] = connection.send(:view_information, table_name) end @@ -91,21 +87,17 @@ def identifier(table_name) SQLServer::Utils.extract_identifiers(table_name) end - def key(table_name) + def tn_quoted(table_name) identifier(table_name).quoted end - def prepare_tables_and_views - prepare_views if @views.empty? - prepare_tables if @tables.empty? - end - - def prepare_tables - connection.tables.each { |table| @tables[key(table)] = true } + def tn_object(table_name) + identifier(table_name).object end - def prepare_views - connection.views.each { |view| @views[key(view)] = true } + def prepare_data_sources + connection.data_sources.each { |source| @data_sources[tn_quoted(source)] = true } + connection.views.each { |source| @views[tn_quoted(source)] = true } end end diff --git a/lib/active_record/connection_adapters/sqlserver/schema_creation.rb b/lib/active_record/connection_adapters/sqlserver/schema_creation.rb index 134dd8c92..b43354c91 100644 --- a/lib/active_record/connection_adapters/sqlserver/schema_creation.rb +++ b/lib/active_record/connection_adapters/sqlserver/schema_creation.rb @@ -5,15 +5,6 @@ class SchemaCreation < AbstractAdapter::SchemaCreation private - def visit_ColumnDefinition(o) - sql = super - if o.primary_key? && o.type == :uuid - sql << ' PRIMARY KEY ' - add_column_options!(sql, column_options(o)) - end - sql - end - def visit_TableDefinition(o) if o.as table_name = quote_table_name(o.temporary ? "##{o.name}" : o.name) @@ -25,15 +16,6 @@ def visit_TableDefinition(o) end end - def add_column_options!(sql, options) - column = options.fetch(:column) { return super } - if (column.type == :uuid || column.type == :uniqueidentifier) && options[:default] =~ /\(\)/ - sql << " DEFAULT #{options.delete(:default)}" - else - super - end - end - def action_sql(action, dependency) case dependency when :restrict diff --git a/lib/active_record/connection_adapters/sqlserver/schema_statements.rb b/lib/active_record/connection_adapters/sqlserver/schema_statements.rb index 66907d6f3..562b27714 100644 --- a/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +++ b/lib/active_record/connection_adapters/sqlserver/schema_statements.rb @@ -7,23 +7,23 @@ def native_database_types @native_database_types ||= initialize_native_database_types.freeze end - def data_sources - tables + views - end - def tables(table_type = 'BASE TABLE') select_values "SELECT #{lowercase_schema_reflection_sql('TABLE_NAME')} FROM INFORMATION_SCHEMA.TABLES #{"WHERE TABLE_TYPE = '#{table_type}'" if table_type} ORDER BY TABLE_NAME", 'SCHEMA' end - def table_exists?(table_name) + def data_source_exists?(table_name) return false if table_name.blank? unquoted_table_name = SQLServer::Utils.extract_identifiers(table_name).object - super || tables.include?(unquoted_table_name) || views.include?(unquoted_table_name) + super(unquoted_table_name) end - def create_table(table_name, options = {}) + def views + tables('VIEW') + end + + def create_table(table_name, comment: nil, **options) res = super - schema_cache.clear_table_cache!(table_name) + schema_cache.clear_data_source_cache!(table_name) res end @@ -47,17 +47,40 @@ def indexes(table_name, name = nil) end end - def columns(table_name, _name = nil) + def columns(table_name) return [] if table_name.blank? column_definitions(table_name).map do |ci| - sqlserver_options = ci.slice :ordinal_position, :is_primary, :is_identity, :default_function, :table_name, :collation - cast_type = lookup_cast_type(ci[:type]) - new_column ci[:name], ci[:default_value], cast_type, ci[:type], ci[:null], sqlserver_options + sqlserver_options = ci.slice :ordinal_position, :is_primary, :is_identity + sql_type_metadata = fetch_type_metadata ci[:type], sqlserver_options + new_column( + ci[:name], + ci[:default_value], + sql_type_metadata, + ci[:null], + ci[:table_name], + ci[:default_function], + ci[:collation], + nil, + sqlserver_options + ) end end - def new_column(name, default, cast_type, sql_type = nil, null = true, sqlserver_options={}) - SQLServerColumn.new name, default, cast_type, sql_type, null, sqlserver_options + def new_column(name, default, sql_type_metadata, null, table_name, default_function = nil, collation = nil, comment = nil, sqlserver_options = {}) + SQLServerColumn.new( + name, + default, + sql_type_metadata, + null, table_name, + default_function, + collation, + comment, + sqlserver_options + ) + end + + def primary_keys(table_name) + columns(table_name).select(&:is_primary?).map(&:name) || identity_columns(table_name).map(&:name) end def rename_table(table_name, new_name) @@ -96,20 +119,20 @@ def change_column(table_name, column_name, type, options = {}) end def change_column_default(table_name, column_name, default) - schema_cache.clear_table_cache!(table_name) + schema_cache.clear_data_source_cache!(table_name) remove_default_constraint(table_name, column_name) column_object = schema_cache.columns(table_name).find { |c| c.name.to_s == column_name.to_s } do_execute "ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{default_constraint_name(table_name, column_name)} DEFAULT #{quote_default_value(default, column_object)} FOR #{quote_column_name(column_name)}" - schema_cache.clear_table_cache!(table_name) + schema_cache.clear_data_source_cache!(table_name) end def rename_column(table_name, column_name, new_column_name) - schema_cache.clear_table_cache!(table_name) + schema_cache.clear_data_source_cache!(table_name) detect_column_for! table_name, column_name identifier = SQLServer::Utils.extract_identifiers("#{table_name}.#{column_name}") execute_procedure :sp_rename, identifier.quoted, new_column_name, 'COLUMN' rename_column_indexes(table_name, column_name, new_column_name) - schema_cache.clear_table_cache!(table_name) + schema_cache.clear_data_source_cache!(table_name) end def rename_index(table_name, old_name, new_name) @@ -183,12 +206,6 @@ def change_column_null(table_name, column_name, allow_null, default = nil) do_execute sql end - # === SQLServer Specific ======================================== # - - def views - tables('VIEW') - end - protected @@ -284,9 +301,11 @@ def column_definitions(table_name) WHERE columns.TABLE_NAME = @0 AND columns.TABLE_SCHEMA = #{identifier.schema.blank? ? 'schema_name()' : '@1'} ORDER BY columns.ordinal_position - }.gsub(/[ \t\r\n]+/, ' ') - binds = [[info_schema_table_name_column, identifier.object]] - binds << [info_schema_table_schema_column, identifier.schema] unless identifier.schema.blank? + }.gsub(/[ \t\r\n]+/, ' ').strip + binds = [] + nv128 = SQLServer::Type::UnicodeVarchar.new limit: 128 + binds << Relation::QueryAttribute.new('TABLE_NAME', identifier.object, nv128) + binds << Relation::QueryAttribute.new('TABLE_SCHEMA', identifier.schema, nv128) unless identifier.schema.blank? results = sp_executesql(sql, 'SCHEMA', binds) results.map do |ci| ci = ci.symbolize_keys @@ -347,14 +366,6 @@ def column_definitions(table_name) end end - def info_schema_table_name_column - @info_schema_table_name_column ||= new_column 'table_name', nil, lookup_cast_type('nvarchar(128)'), 'nvarchar(128)', true - end - - def info_schema_table_schema_column - @info_schema_table_schema_column ||= new_column 'table_schema', nil, lookup_cast_type('nvarchar(128)'), 'nvarchar(128)', true - end - def remove_check_constraints(table_name, column_name) constraints = select_values "SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE where TABLE_NAME = '#{quote_string(table_name)}' and COLUMN_NAME = '#{quote_string(column_name)}'", 'SCHEMA' constraints.each do |constraint| @@ -445,7 +456,7 @@ def views_real_column_name(table_name, column_name) def query_requires_identity_insert?(sql) if insert_sql?(sql) table_name = get_table_name(sql) - id_column = identity_column(table_name) + id_column = identity_columns(table_name).first id_column && sql =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)[^(]+\([^)]*\b(#{id_column.name})\b,?[^)]*\)/i ? quote_table_name(table_name) : false else false @@ -456,15 +467,15 @@ def insert_sql?(sql) !(sql =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)/i).nil? end - def identity_column(table_name) - schema_cache.columns(table_name).find(&:is_identity?) + def identity_columns(table_name) + columns(table_name).select(&:is_identity?) end private - def create_table_definition(name, temporary, options, as = nil) - SQLServer::TableDefinition.new native_database_types, name, temporary, options, as + def create_table_definition(*args) + SQLServer::TableDefinition.new(*args) end end diff --git a/lib/active_record/connection_adapters/sqlserver/sql_type_metadata.rb b/lib/active_record/connection_adapters/sqlserver/sql_type_metadata.rb new file mode 100644 index 000000000..f5640ca68 --- /dev/null +++ b/lib/active_record/connection_adapters/sqlserver/sql_type_metadata.rb @@ -0,0 +1,20 @@ +module ActiveRecord + module ConnectionAdapters + module SQLServer + class SqlTypeMetadata < ActiveRecord::ConnectionAdapters::SqlTypeMetadata + + def initialize(**kwargs) + @sqlserver_options = kwargs.extract!(:sqlserver_options) + super(**kwargs) + end + + protected + + def attributes_for_hash + super + [@sqlserver_options] + end + + end + end + end +end diff --git a/lib/active_record/connection_adapters/sqlserver/table_definition.rb b/lib/active_record/connection_adapters/sqlserver/table_definition.rb index 1fcc5f757..fd2f6eb1d 100644 --- a/lib/active_record/connection_adapters/sqlserver/table_definition.rb +++ b/lib/active_record/connection_adapters/sqlserver/table_definition.rb @@ -3,7 +3,7 @@ module ConnectionAdapters module SQLServer class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition - def primary_key(name, type = :primary_key, options = {}) + def primary_key(name, type = :primary_key, **options) return super unless type == :uuid options[:default] = options.fetch(:default, 'NEWID()') options[:primary_key] = true diff --git a/lib/active_record/connection_adapters/sqlserver/transaction.rb b/lib/active_record/connection_adapters/sqlserver/transaction.rb index 5516d698b..81a58e968 100644 --- a/lib/active_record/connection_adapters/sqlserver/transaction.rb +++ b/lib/active_record/connection_adapters/sqlserver/transaction.rb @@ -25,7 +25,7 @@ module SQLServerRealTransaction attr_reader :starting_isolation_level - def initialize(connection, options) + def initialize(connection, options, run_commit_callbacks: false) @connection = connection @starting_isolation_level = current_isolation_level if options[:isolation] super diff --git a/lib/active_record/connection_adapters/sqlserver/type.rb b/lib/active_record/connection_adapters/sqlserver/type.rb index d115417c7..f6ab38ced 100644 --- a/lib/active_record/connection_adapters/sqlserver/type.rb +++ b/lib/active_record/connection_adapters/sqlserver/type.rb @@ -1,4 +1,7 @@ require 'active_record/type' +# Behaviors +require 'active_record/connection_adapters/sqlserver/type/sql.rb' +require 'active_record/connection_adapters/sqlserver/type/time_value_fractional.rb' # Exact Numerics require 'active_record/connection_adapters/sqlserver/type/integer.rb' require 'active_record/connection_adapters/sqlserver/type/big_integer.rb' @@ -12,7 +15,6 @@ require 'active_record/connection_adapters/sqlserver/type/float.rb' require 'active_record/connection_adapters/sqlserver/type/real.rb' # Date and Time -require 'active_record/connection_adapters/sqlserver/type/time_value_fractional.rb' require 'active_record/connection_adapters/sqlserver/type/date.rb' require 'active_record/connection_adapters/sqlserver/type/datetime.rb' require 'active_record/connection_adapters/sqlserver/type/datetime2.rb' diff --git a/lib/active_record/connection_adapters/sqlserver/type/big_integer.rb b/lib/active_record/connection_adapters/sqlserver/type/big_integer.rb index b95424d86..28564fdb3 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/big_integer.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/big_integer.rb @@ -4,6 +4,8 @@ module SQLServer module Type class BigInteger < Integer + SQLSERVER_TYPE = 'bigint'.freeze + def type :bigint end diff --git a/lib/active_record/connection_adapters/sqlserver/type/binary.rb b/lib/active_record/connection_adapters/sqlserver/type/binary.rb index cad49bd4f..06cb42b37 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/binary.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/binary.rb @@ -8,6 +8,12 @@ def type :binary_basic end + def sqlserver_type + 'binary'.tap do |type| + type << "(#{limit})" if limit + end + end + end end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/boolean.rb b/lib/active_record/connection_adapters/sqlserver/type/boolean.rb index 545decd17..2d2fd8b2c 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/boolean.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/boolean.rb @@ -4,6 +4,7 @@ module SQLServer module Type class Boolean < ActiveRecord::Type::Boolean + SQLSERVER_TYPE = 'bit'.freeze end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/char.rb b/lib/active_record/connection_adapters/sqlserver/type/char.rb index 8c27ba817..06ea10855 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/char.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/char.rb @@ -8,12 +8,18 @@ def type :char end - def type_cast_for_database(value) + def serialize(value) return if value.nil? return value if value.is_a?(Data) Data.new(super) end + def sqlserver_type + 'char'.tap do |type| + type << "(#{limit})" if limit + end + end + class Data def initialize(value) diff --git a/lib/active_record/connection_adapters/sqlserver/type/date.rb b/lib/active_record/connection_adapters/sqlserver/type/date.rb index ae086b1ce..60405a5c5 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/date.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/date.rb @@ -4,14 +4,16 @@ module SQLServer module Type class Date < ActiveRecord::Type::Date - def type_cast_for_database(value) + SQLSERVER_TYPE = 'date'.freeze + + def serialize(value) return unless value.present? return value if value.acts_like?(:string) value.to_s(:_sqlserver_dateformat) end def type_cast_for_schema(value) - type_cast_for_database(value).inspect + serialize(value).inspect end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/datetime.rb b/lib/active_record/connection_adapters/sqlserver/type/datetime.rb index 0ab38f03a..5d092bc9b 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/datetime.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/datetime.rb @@ -4,9 +4,11 @@ module SQLServer module Type class DateTime < ActiveRecord::Type::DateTime + SQLSERVER_TYPE = 'datetime'.freeze + include TimeValueFractional - def type_cast_for_database(value) + def serialize(value) return super unless value.acts_like?(:time) value = zone_conversion(value) datetime = value.to_s(:_sqlserver_datetime) @@ -17,10 +19,9 @@ def type_cast_for_database(value) end def type_cast_for_schema(value) - type_cast_for_database(value).inspect + serialize(value).inspect end - private def cast_value(value) diff --git a/lib/active_record/connection_adapters/sqlserver/type/datetime2.rb b/lib/active_record/connection_adapters/sqlserver/type/datetime2.rb index 02ac6010b..946418821 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/datetime2.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/datetime2.rb @@ -10,6 +10,10 @@ def type :datetime2 end + def sqlserver_type + "datetime2(#{precision.to_i})" + end + end end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/datetimeoffset.rb b/lib/active_record/connection_adapters/sqlserver/type/datetimeoffset.rb index 17b4a1a50..4798a3e94 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/datetimeoffset.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/datetimeoffset.rb @@ -8,15 +8,18 @@ def type :datetimeoffset end - def type_cast_for_database(value) + def serialize(value) return super unless value.acts_like?(:time) value.to_s :_sqlserver_datetimeoffset end def type_cast_for_schema(value) - type_cast_for_database(value).inspect + serialize(value).inspect end + def sqlserver_type + "datetimeoffset(#{precision.to_i})" + end private diff --git a/lib/active_record/connection_adapters/sqlserver/type/decimal.rb b/lib/active_record/connection_adapters/sqlserver/type/decimal.rb index 4695a585e..09bf2b50a 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/decimal.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/decimal.rb @@ -4,6 +4,11 @@ module SQLServer module Type class Decimal < ActiveRecord::Type::Decimal + def sqlserver_type + 'decimal'.tap do |type| + type << "(#{precision.to_i},#{scale.to_i})" if precision || scale + end + end end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/float.rb b/lib/active_record/connection_adapters/sqlserver/type/float.rb index f99353f65..0b4758b82 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/float.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/float.rb @@ -4,6 +4,8 @@ module SQLServer module Type class Float < ActiveRecord::Type::Float + SQLSERVER_TYPE = 'float'.freeze + def type :float end diff --git a/lib/active_record/connection_adapters/sqlserver/type/integer.rb b/lib/active_record/connection_adapters/sqlserver/type/integer.rb index edfd1d6ae..c7c42a43a 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/integer.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/integer.rb @@ -4,6 +4,7 @@ module SQLServer module Type class Integer < ActiveRecord::Type::Integer + SQLSERVER_TYPE = 'int'.freeze end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/money.rb b/lib/active_record/connection_adapters/sqlserver/type/money.rb index a03d20b72..56f8f727c 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/money.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/money.rb @@ -4,6 +4,8 @@ module SQLServer module Type class Money < Decimal + SQLSERVER_TYPE = 'money'.freeze + def initialize(options = {}) super @precision = 19 diff --git a/lib/active_record/connection_adapters/sqlserver/type/real.rb b/lib/active_record/connection_adapters/sqlserver/type/real.rb index de459e6ae..8ab4e4737 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/real.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/real.rb @@ -4,6 +4,8 @@ module SQLServer module Type class Real < Float + SQLSERVER_TYPE = 'real'.freeze + def type :real end diff --git a/lib/active_record/connection_adapters/sqlserver/type/small_integer.rb b/lib/active_record/connection_adapters/sqlserver/type/small_integer.rb index f1761e5b4..d4c09cd90 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/small_integer.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/small_integer.rb @@ -4,7 +4,7 @@ module SQLServer module Type class SmallInteger < Integer - + SQLSERVER_TYPE = 'smallint'.freeze end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/small_money.rb b/lib/active_record/connection_adapters/sqlserver/type/small_money.rb index 6f889abe7..00745233f 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/small_money.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/small_money.rb @@ -4,6 +4,8 @@ module SQLServer module Type class SmallMoney < Money + SQLSERVER_TYPE = 'smallmoney'.freeze + def initialize(options = {}) super @precision = 10 diff --git a/lib/active_record/connection_adapters/sqlserver/type/smalldatetime.rb b/lib/active_record/connection_adapters/sqlserver/type/smalldatetime.rb index 42b1cdc0d..bb41df202 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/smalldatetime.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/smalldatetime.rb @@ -4,11 +4,12 @@ module SQLServer module Type class SmallDateTime < DateTime + SQLSERVER_TYPE = 'smalldatetime'.freeze + def type :smalldatetime end - private def cast_fractional(value) diff --git a/lib/active_record/connection_adapters/sqlserver/type/sql.rb b/lib/active_record/connection_adapters/sqlserver/type/sql.rb new file mode 100644 index 000000000..16a5901f3 --- /dev/null +++ b/lib/active_record/connection_adapters/sqlserver/type/sql.rb @@ -0,0 +1,16 @@ +module ActiveRecord + module ConnectionAdapters + module SQLServer + module Type + module Sql + + def sqlserver_type + defined?(SQLSERVER_TYPE) ? SQLSERVER_TYPE : type.to_s + end + + end + ::ActiveModel::Type::Value.include SQLServer::Type::Sql + end + end + end +end diff --git a/lib/active_record/connection_adapters/sqlserver/type/text.rb b/lib/active_record/connection_adapters/sqlserver/type/text.rb index bc472046c..b47f9465e 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/text.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/text.rb @@ -4,6 +4,8 @@ module SQLServer module Type class Text < VarcharMax + SQLSERVER_TYPE = 'text'.freeze + def type :text_basic end diff --git a/lib/active_record/connection_adapters/sqlserver/type/time.rb b/lib/active_record/connection_adapters/sqlserver/type/time.rb index 36245f479..5af08949e 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/time.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/time.rb @@ -6,7 +6,7 @@ class Time < ActiveRecord::Type::Time include TimeValueFractional2 - def type_cast_for_database(value) + def serialize(value) return super unless value.acts_like?(:time) time = value.to_s(:_sqlserver_time) "#{time}".tap do |v| @@ -16,7 +16,11 @@ def type_cast_for_database(value) end def type_cast_for_schema(value) - type_cast_for_database(value).inspect + serialize(value).inspect + end + + def sqlserver_type + "time(#{precision.to_i})" end diff --git a/lib/active_record/connection_adapters/sqlserver/type/timestamp.rb b/lib/active_record/connection_adapters/sqlserver/type/timestamp.rb index 7ed713bd5..4b955b818 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/timestamp.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/timestamp.rb @@ -4,6 +4,8 @@ module SQLServer module Type class Timestamp < Binary + SQLSERVER_TYPE = 'timestamp'.freeze + def type :ss_timestamp end diff --git a/lib/active_record/connection_adapters/sqlserver/type/tiny_integer.rb b/lib/active_record/connection_adapters/sqlserver/type/tiny_integer.rb index 2f49a5313..c47ed044e 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/tiny_integer.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/tiny_integer.rb @@ -4,6 +4,7 @@ module SQLServer module Type class TinyInteger < Integer + SQLSERVER_TYPE = 'tinyint'.freeze private diff --git a/lib/active_record/connection_adapters/sqlserver/type/unicode_char.rb b/lib/active_record/connection_adapters/sqlserver/type/unicode_char.rb index 7795029a5..6934073e1 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/unicode_char.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/unicode_char.rb @@ -8,6 +8,12 @@ def type :nchar end + def sqlserver_type + 'nchar'.tap do |type| + type << "(#{limit})" if limit + end + end + end end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/unicode_text.rb b/lib/active_record/connection_adapters/sqlserver/type/unicode_text.rb index 017b6595b..1e0ef3c1b 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/unicode_text.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/unicode_text.rb @@ -4,6 +4,8 @@ module SQLServer module Type class UnicodeText < UnicodeVarcharMax + SQLSERVER_TYPE = 'ntext'.freeze + def type :ntext end diff --git a/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar.rb b/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar.rb index ed8fafc4d..008320c79 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar.rb @@ -13,6 +13,12 @@ def type :string end + def sqlserver_type + 'nvarchar'.tap do |type| + type << "(#{limit})" if limit + end + end + end end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar_max.rb b/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar_max.rb index 7ba417c9e..69ffc818c 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar_max.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar_max.rb @@ -4,6 +4,8 @@ module SQLServer module Type class UnicodeVarcharMax < UnicodeVarchar + SQLSERVER_TYPE = 'nvarchar(max)'.freeze + def initialize(options = {}) super @limit = 2_147_483_647 diff --git a/lib/active_record/connection_adapters/sqlserver/type/uuid.rb b/lib/active_record/connection_adapters/sqlserver/type/uuid.rb index 5edfb7b27..13efde800 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/uuid.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/uuid.rb @@ -4,9 +4,10 @@ module SQLServer module Type class Uuid < String + SQLSERVER_TYPE = 'uniqueidentifier'.freeze ACCEPTABLE_UUID = %r{\A\{?([a-fA-F0-9]{4}-?){8}\}?\z}x - alias_method :type_cast_for_database, :type_cast_from_database + alias_method :serialize, :deserialize def type :uuid diff --git a/lib/active_record/connection_adapters/sqlserver/type/varbinary.rb b/lib/active_record/connection_adapters/sqlserver/type/varbinary.rb index d09449f8a..265af265b 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/varbinary.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/varbinary.rb @@ -13,6 +13,12 @@ def type :varbinary end + def sqlserver_type + 'varbinary'.tap do |type| + type << "(#{limit})" if limit + end + end + end end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/varbinary_max.rb b/lib/active_record/connection_adapters/sqlserver/type/varbinary_max.rb index 65d82edcf..05a437615 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/varbinary_max.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/varbinary_max.rb @@ -4,6 +4,8 @@ module SQLServer module Type class VarbinaryMax < Varbinary + SQLSERVER_TYPE = 'varbinary(max)'.freeze + def initialize(options = {}) super @limit = 2_147_483_647 diff --git a/lib/active_record/connection_adapters/sqlserver/type/varchar.rb b/lib/active_record/connection_adapters/sqlserver/type/varchar.rb index e5a65176b..b3091718a 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/varchar.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/varchar.rb @@ -13,6 +13,12 @@ def type :varchar end + def sqlserver_type + 'varchar'.tap do |type| + type << "(#{limit})" if limit + end + end + end end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/varchar_max.rb b/lib/active_record/connection_adapters/sqlserver/type/varchar_max.rb index 601cec9f1..1f40edf46 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/varchar_max.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/varchar_max.rb @@ -4,6 +4,8 @@ module SQLServer module Type class VarcharMax < Varchar + SQLSERVER_TYPE = 'varchar(max)'.freeze + def initialize(options = {}) super @limit = 2_147_483_647 diff --git a/lib/active_record/connection_adapters/sqlserver_adapter.rb b/lib/active_record/connection_adapters/sqlserver_adapter.rb index 8df3d3a03..c796378a4 100644 --- a/lib/active_record/connection_adapters/sqlserver_adapter.rb +++ b/lib/active_record/connection_adapters/sqlserver_adapter.rb @@ -16,6 +16,7 @@ require 'active_record/connection_adapters/sqlserver/schema_cache' require 'active_record/connection_adapters/sqlserver/schema_creation' require 'active_record/connection_adapters/sqlserver/schema_statements' +require 'active_record/connection_adapters/sqlserver/sql_type_metadata' require 'active_record/connection_adapters/sqlserver/showplan' require 'active_record/connection_adapters/sqlserver/table_definition' require 'active_record/connection_adapters/sqlserver/quoting' @@ -47,22 +48,23 @@ class SQLServerAdapter < AbstractAdapter self.cs_equality_operator = 'COLLATE Latin1_General_CS_AS_WS' self.use_output_inserted = true - def initialize(connection, logger, pool, config) - super(connection, logger, pool) + def initialize(connection, logger = nil, config = {}) + super(connection, logger, config) # AbstractAdapter Responsibility - @schema_cache = SQLServer::SchemaCache.new self - @visitor = Arel::Visitors::SQLServer.new self - @prepared_statements = true + @schema_cache = SQLServer::SchemaCache.new(self) # Our Responsibility @connection_options = config connect - @sqlserver_azure = !!(select_value('SELECT @@version', 'SCHEMA') =~ /Azure/i) initialize_dateformatter use_database end # === Abstract Adapter ========================================== # + def arel_visitor + Arel::Visitors::SQLServer.new self + end + def valid_type?(type) !native_database_types[type].nil? end @@ -128,7 +130,7 @@ def supports_views? end def supports_datetime_with_precision? - true + false end def supports_json? @@ -202,10 +204,6 @@ def pk_and_sequence_for(table_name) pk ? [pk, nil] : nil end - def primary_key(table_name) - schema_cache.columns(table_name).find(&:is_primary?).try(:name) || identity_column(table_name).try(:name) - end - # === SQLServer Specific (DB Reflection) ======================== # def sqlserver? @@ -213,7 +211,7 @@ def sqlserver? end def sqlserver_azure? - @sqlserver_azure + @sqlserver_azure ||= !!(select_value('SELECT @@version', 'SCHEMA') =~ /Azure/i) end def database_prefix_remote_server? diff --git a/lib/active_record/connection_adapters/sqlserver_column.rb b/lib/active_record/connection_adapters/sqlserver_column.rb index 80ba50d5b..db683ba84 100644 --- a/lib/active_record/connection_adapters/sqlserver_column.rb +++ b/lib/active_record/connection_adapters/sqlserver_column.rb @@ -2,22 +2,9 @@ module ActiveRecord module ConnectionAdapters class SQLServerColumn < Column - def initialize(name, default, cast_type, sql_type = nil, null = true, sqlserver_options = {}) - super(name, default, cast_type, sql_type, null) - @sqlserver_options = sqlserver_options.symbolize_keys - @default_function = @sqlserver_options[:default_function] - end - - def sql_type_for_statement - if is_integer? || is_real? - sql_type.sub(/\((\d+)?\)/, '') - else - sql_type - end - end - - def table_name - @sqlserver_options[:table_name] + def initialize(name, default, sql_type_metadata = nil, null = true, table_name = nil, default_function = nil, collation = nil, comment = nil, sqlserver_options = {}) + @sqlserver_options = sqlserver_options || {} + super(name, default, sql_type_metadata, null, table_name, default_function, collation, comment: comment) end def is_identity? @@ -29,19 +16,7 @@ def is_primary? end def is_utf8? - @sql_type =~ /nvarchar|ntext|nchar/i - end - - def is_integer? - @sql_type =~ /int/i - end - - def is_real? - @sql_type =~ /real/i - end - - def collation - @sqlserver_options[:collation] + sql_type =~ /nvarchar|ntext|nchar/i end def case_sensitive? diff --git a/lib/active_record/sqlserver_base.rb b/lib/active_record/sqlserver_base.rb index e4bf3fc3d..4601b41d1 100644 --- a/lib/active_record/sqlserver_base.rb +++ b/lib/active_record/sqlserver_base.rb @@ -10,7 +10,7 @@ def sqlserver_connection(config) #:nodoc: else raise ArgumentError, "Unknown connection mode in #{config.inspect}." end - ConnectionAdapters::SQLServerAdapter.new(nil, logger, nil, config.merge(mode: mode)) + ConnectionAdapters::SQLServerAdapter.new(nil, nil, config.merge(mode: mode)) end end end diff --git a/lib/arel/visitors/sqlserver.rb b/lib/arel/visitors/sqlserver.rb index 19705d18d..b966b27ab 100644 --- a/lib/arel/visitors/sqlserver.rb +++ b/lib/arel/visitors/sqlserver.rb @@ -77,7 +77,7 @@ def visit_Arel_Table o, collector # Apparently, o.engine.connection can actually be a different adapter # than sqlserver. Can be removed if fixed in ActiveRecord. See: # github.com/rails-sqlserver/activerecord-sqlserver-adapter/issues/450 - table_name = if o.engine.connection.respond_to?(:sqlserver?) && o.engine.connection.database_prefix_remote_server? + table_name = if o.class.engine.connection.respond_to?(:sqlserver?) && o.class.engine.connection.database_prefix_remote_server? remote_server_table_name(o) else quote_table_name(o.name) @@ -190,14 +190,7 @@ def table_From_Statement o def primary_Key_From_Table t return unless t - return t.primary_key if t.primary_key - if engine_pk = t.engine.primary_key - pk = t.engine.arel_table[engine_pk] - return pk if pk - end - pk = t.engine.connection.schema_cache.primary_keys(t.engine.table_name) - return pk if pk - column_name = t.engine.columns.first.try(:name) + column_name = schema_cache.primary_keys(t.name) || column_cache(t.name).first.second.try(:name) column_name ? t[column_name] : nil end diff --git a/test/cases/adapter_test_sqlserver.rb b/test/cases/adapter_test_sqlserver.rb index 4eebad712..bac200525 100644 --- a/test/cases/adapter_test_sqlserver.rb +++ b/test/cases/adapter_test_sqlserver.rb @@ -158,14 +158,14 @@ class AdapterTestSQLServer < ActiveRecord::TestCase end end - it 'find identity column using #identity_column' do + it 'find identity column using #identity_columns' do task_id_column = Task.columns_hash['id'] - assert_equal task_id_column.name, connection.send(:identity_column, Task.table_name).name - assert_equal task_id_column.sql_type, connection.send(:identity_column, Task.table_name).sql_type + assert_equal task_id_column.name, connection.send(:identity_columns, Task.table_name).first.name + assert_equal task_id_column.sql_type, connection.send(:identity_columns, Task.table_name).first.sql_type end - it 'return nil when calling #identity_column for a table_name with no identity' do - assert_nil connection.send(:identity_column, Subscriber.table_name) + it 'return an empty array when calling #identity_columns for a table_name with no identity' do + connection.send(:identity_columns, Subscriber.table_name).must_equal [] end end @@ -364,8 +364,8 @@ class AdapterTestSQLServer < ActiveRecord::TestCase assert_equal 0, SSTestCustomersView.new.balance end - it 'respond true to table_exists?' do - assert SSTestCustomersView.table_exists? + it 'respond true to data_source_exists?' do + assert SSTestCustomersView.connection.data_source_exists? end # With aliased column names @@ -392,8 +392,8 @@ class AdapterTestSQLServer < ActiveRecord::TestCase SSTestStringDefaultsView.columns_hash['pretend_null'].inspect end - it 'respond true to table_exists?' do - assert SSTestStringDefaultsView.table_exists? + it 'respond true to data_source_exists?' do + assert SSTestStringDefaultsView.connection.data_source_exists? end # Doing identity inserts diff --git a/test/cases/schema_test_sqlserver.rb b/test/cases/schema_test_sqlserver.rb index 9f1840b0c..a2887ca2e 100644 --- a/test/cases/schema_test_sqlserver.rb +++ b/test/cases/schema_test_sqlserver.rb @@ -13,8 +13,8 @@ class SchemaTestSQLServer < ActiveRecord::TestCase describe 'When table is in non-dbo schema' do it 'work with table exists' do - assert connection.table_exists?('test.sst_schema_natural_id') - assert connection.table_exists?('[test].[sst_schema_natural_id]') + assert connection.data_source_exists?('test.sst_schema_natural_id') + assert connection.data_source_exists?('[test].[sst_schema_natural_id]') end it 'find primary key for tables with odd schema' do From ac46eeb8dc7141b4ba39f234449ac8b26de4a27e Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Mon, 1 Aug 2016 20:37:15 -0400 Subject: [PATCH 05/27] Ensure connection#primary_keys checks blanks before identity check. --- .../connection_adapters/sqlserver/schema_statements.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/active_record/connection_adapters/sqlserver/schema_statements.rb b/lib/active_record/connection_adapters/sqlserver/schema_statements.rb index 562b27714..0bb3d9489 100644 --- a/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +++ b/lib/active_record/connection_adapters/sqlserver/schema_statements.rb @@ -80,7 +80,8 @@ def new_column(name, default, sql_type_metadata, null, table_name, default_funct end def primary_keys(table_name) - columns(table_name).select(&:is_primary?).map(&:name) || identity_columns(table_name).map(&:name) + primaries = columns(table_name).select(&:is_primary?).map(&:name) + primaries.present? ? primaries : identity_columns(table_name).map(&:name) end def rename_table(table_name, new_name) From 928cd7971b229aa2194d5ec937f778178a6e7d90 Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Mon, 1 Aug 2016 21:04:06 -0400 Subject: [PATCH 06/27] Fix `data_source_exists?` tests on views. --- test/cases/adapter_test_sqlserver.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/cases/adapter_test_sqlserver.rb b/test/cases/adapter_test_sqlserver.rb index bac200525..9fe4362d9 100644 --- a/test/cases/adapter_test_sqlserver.rb +++ b/test/cases/adapter_test_sqlserver.rb @@ -365,7 +365,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase end it 'respond true to data_source_exists?' do - assert SSTestCustomersView.connection.data_source_exists? + assert SSTestCustomersView.connection.data_source_exists?(SSTestCustomersView.table_name) end # With aliased column names @@ -393,7 +393,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase end it 'respond true to data_source_exists?' do - assert SSTestStringDefaultsView.connection.data_source_exists? + assert SSTestStringDefaultsView.connection.data_source_exists?(SSTestStringDefaultsView.table_name) end # Doing identity inserts From 253fbeaddb3702dd0be55ec0781839253146ae22 Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Mon, 1 Aug 2016 21:56:50 -0400 Subject: [PATCH 07/27] All strings for us fall thru as quoted nvarchar types. --- .../connection_adapters/sqlserver/quoting.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/active_record/connection_adapters/sqlserver/quoting.rb b/lib/active_record/connection_adapters/sqlserver/quoting.rb index f3aedd581..469f37ce7 100644 --- a/lib/active_record/connection_adapters/sqlserver/quoting.rb +++ b/lib/active_record/connection_adapters/sqlserver/quoting.rb @@ -76,6 +76,16 @@ def _quote(value) end end + def _type_cast(value) + case value + when Symbol + _quote(value.to_s) + when String, ActiveSupport::Multibyte::Chars + _quote(value) + else super + end + end + end end end From f1e1138147aeac6989c135b45456d3a86e4f013a Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Tue, 2 Aug 2016 19:44:15 -0400 Subject: [PATCH 08/27] Finish abstract adapter test case by passing proper type for type match. --- test/cases/adapter_test_sqlserver.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cases/adapter_test_sqlserver.rb b/test/cases/adapter_test_sqlserver.rb index 9fe4362d9..f19dfee51 100644 --- a/test/cases/adapter_test_sqlserver.rb +++ b/test/cases/adapter_test_sqlserver.rb @@ -96,7 +96,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase connection.send :initialize_dateformatter assert_nothing_raised do starting = Time.utc(2000, 1, 31, 5, 42, 0) - ending = Date.new(2006, 12, 31) + ending = Time.new(2006, 12, 31) Task.create! starting: starting, ending: ending end end From d41ffc1cbe0fabf6ee83b01c4adac76332a0ecf6 Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Thu, 4 Aug 2016 19:23:39 -0400 Subject: [PATCH 09/27] Fix a few deprecations. --- .../connection_adapters/sqlserver_adapter.rb | 8 ++++---- test/cases/connection_test_sqlserver.rb | 2 +- test/cases/rake_test_sqlserver.rb | 2 +- test/cases/transaction_test_sqlserver.rb | 2 +- .../transaction_table/1_table_will_never_be_created.rb | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/active_record/connection_adapters/sqlserver_adapter.rb b/lib/active_record/connection_adapters/sqlserver_adapter.rb index c796378a4..2fadad14f 100644 --- a/lib/active_record/connection_adapters/sqlserver_adapter.rb +++ b/lib/active_record/connection_adapters/sqlserver_adapter.rb @@ -301,13 +301,13 @@ def initialize_type_map(m) def translate_exception(e, message) case message when /(cannot insert duplicate key .* with unique index) | (violation of unique key constraint)/i - RecordNotUnique.new(message, e) + RecordNotUnique.new(message) when /conflicted with the foreign key constraint/i - InvalidForeignKey.new(message, e) + InvalidForeignKey.new(message) when /has been chosen as the deadlock victim/i - DeadlockVictim.new(message, e) + DeadlockVictim.new(message) when /database .* does not exist/i - NoDatabaseError.new(message, e) + NoDatabaseError.new(message) else super end diff --git a/test/cases/connection_test_sqlserver.rb b/test/cases/connection_test_sqlserver.rb index 8e39b79f9..9a36120af 100644 --- a/test/cases/connection_test_sqlserver.rb +++ b/test/cases/connection_test_sqlserver.rb @@ -4,7 +4,7 @@ class ConnectionTestSQLServer < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false fixtures :topics, :accounts diff --git a/test/cases/rake_test_sqlserver.rb b/test/cases/rake_test_sqlserver.rb index 3845e7148..74ea4a996 100644 --- a/test/cases/rake_test_sqlserver.rb +++ b/test/cases/rake_test_sqlserver.rb @@ -2,7 +2,7 @@ class SQLServerRakeTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false cattr_accessor :azure_skip self.azure_skip = connection_sqlserver_azure? diff --git a/test/cases/transaction_test_sqlserver.rb b/test/cases/transaction_test_sqlserver.rb index 02e1a88b3..6db739bfb 100644 --- a/test/cases/transaction_test_sqlserver.rb +++ b/test/cases/transaction_test_sqlserver.rb @@ -5,7 +5,7 @@ class TransactionTestSQLServer < ActiveRecord::TestCase - self.use_transactional_fixtures = false + self.use_transactional_tests = false before { delete_ships } diff --git a/test/migrations/transaction_table/1_table_will_never_be_created.rb b/test/migrations/transaction_table/1_table_will_never_be_created.rb index 45d0e21fc..ffc29d634 100644 --- a/test/migrations/transaction_table/1_table_will_never_be_created.rb +++ b/test/migrations/transaction_table/1_table_will_never_be_created.rb @@ -2,7 +2,7 @@ class TableWillNeverBeCreated < ActiveRecord::Migration def self.up create_table(:sqlserver_trans_table1) { } - create_table(:sqlserver_trans_table2) { raise ActiveRecord::StatementInvalid } + create_table(:sqlserver_trans_table2) { raise('HELL') } end def self.down From 2ef2e07507f7d5069a85fc908f19438bd247c997 Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Thu, 4 Aug 2016 19:54:53 -0400 Subject: [PATCH 10/27] Fix up rake task tests with new silence/capture and deprecations. --- test/cases/helper_sqlserver.rb | 17 ++-------------- test/cases/rake_test_sqlserver.rb | 32 ++++++++++++++++++------------- 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/test/cases/helper_sqlserver.rb b/test/cases/helper_sqlserver.rb index 011abe3de..042bba2e2 100644 --- a/test/cases/helper_sqlserver.rb +++ b/test/cases/helper_sqlserver.rb @@ -15,7 +15,8 @@ class TestCase < ActiveSupport::TestCase SQLServer = ActiveRecord::ConnectionAdapters::SQLServer include ARTest::SQLServer::CoerceableTest, - ARTest::SQLServer::ConnectionReflection + ARTest::SQLServer::ConnectionReflection, + ActiveSupport::Testing::Stream let(:logger) { ActiveRecord::Base.logger } @@ -34,20 +35,6 @@ def with_use_output_inserted_disabled klass.use_output_inserted = true end - def silence_stream(stream) - old_stream = stream.dup - stream.reopen(RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ ? 'NUL:' : '/dev/null') - stream.sync = true - yield - ensure - stream.reopen(old_stream) - old_stream.close - end - - def quietly - silence_stream(STDOUT) { silence_stream(STDERR) { yield } } - end - end end diff --git a/test/cases/rake_test_sqlserver.rb b/test/cases/rake_test_sqlserver.rb index 74ea4a996..09b2bf664 100644 --- a/test/cases/rake_test_sqlserver.rb +++ b/test/cases/rake_test_sqlserver.rb @@ -42,24 +42,24 @@ class SQLServerRakeCreateTest < SQLServerRakeTest self.azure_skip = false it 'establishes connection to database after create ' do - db_tasks.create configuration + quietly { db_tasks.create configuration } connection.current_database.must_equal(new_database) end it 'creates database with default collation' do - db_tasks.create configuration + quietly { db_tasks.create configuration } connection.collation.must_equal 'SQL_Latin1_General_CP1_CI_AS' end it 'creates database with given collation' do - db_tasks.create configuration.merge('collation' => 'Latin1_General_CI_AS') + quietly { db_tasks.create configuration.merge('collation' => 'Latin1_General_CI_AS') } connection.collation.must_equal 'Latin1_General_CI_AS' end it 'prints error message when database exists' do - db_tasks.create configuration + quietly { db_tasks.create configuration } message = capture(:stderr) { db_tasks.create configuration } - message.must_match %r{activerecord_unittest_tasks already exists} + message.must_match %r{activerecord_unittest_tasks.*already exists} end end @@ -69,8 +69,10 @@ class SQLServerRakeDropTest < SQLServerRakeTest self.azure_skip = false it 'drops database and uses master' do - db_tasks.create configuration - db_tasks.drop configuration + quietly do + db_tasks.create configuration + db_tasks.drop configuration + end connection.current_database.must_equal 'master' end @@ -84,7 +86,7 @@ class SQLServerRakeDropTest < SQLServerRakeTest class SQLServerRakePurgeTest < SQLServerRakeTest before do - db_tasks.create(configuration) + quietly { db_tasks.create(configuration) } connection.create_table :users, force: true do |t| t.string :name, :email t.timestamps null: false @@ -94,7 +96,7 @@ class SQLServerRakePurgeTest < SQLServerRakeTest it 'clears active connections, drops database, and recreates with established connection' do connection.current_database.must_equal(new_database) connection.tables.must_include 'users' - db_tasks.purge(configuration) + quietly { db_tasks.purge(configuration) } connection.current_database.must_equal(new_database) connection.tables.wont_include 'users' end @@ -103,7 +105,9 @@ class SQLServerRakePurgeTest < SQLServerRakeTest class SQLServerRakeCharsetTest < SQLServerRakeTest - before { db_tasks.create(configuration) } + before do + quietly { db_tasks.create(configuration) } + end it 'retrieves charset' do db_tasks.charset(configuration).must_equal 'iso_1' @@ -113,7 +117,9 @@ class SQLServerRakeCharsetTest < SQLServerRakeTest class SQLServerRakeCollationTest < SQLServerRakeTest - before { db_tasks.create(configuration) } + before do + quietly { db_tasks.create(configuration) } + end it 'retrieves collation' do db_tasks.collation(configuration).must_equal 'SQL_Latin1_General_CP1_CI_AS' @@ -127,7 +133,7 @@ class SQLServerRakeStructureDumpLoadTest < SQLServerRakeTest let(:filedata) { File.read(filename) } before do - db_tasks.create(configuration) + quietly { db_tasks.create(configuration) } connection.create_table :users, force: true do |t| t.string :name, :email t.text :background1 @@ -156,7 +162,7 @@ class SQLServerRakeStructureDumpLoadTest < SQLServerRakeTest filedata.must_match %r{CREATE TABLE dbo\.users} db_tasks.purge(configuration) connection.tables.wont_include 'users' - db_tasks.load_schema_for configuration, :sql, filename + db_tasks.load_schema configuration, :sql, filename connection.tables.must_include 'users' end From 21d77df5e05171a55bae8fa6940bd64d3b3334d8 Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Thu, 4 Aug 2016 20:21:03 -0400 Subject: [PATCH 11/27] Fix sub class of max types. The size (2147483647) given to the type 'nvarchar' exceeds the maximum allowed for any data type (8000). --- lib/active_record/connection_adapters/sqlserver/type/text.rb | 4 ++++ .../connection_adapters/sqlserver/type/unicode_text.rb | 4 ++++ .../connection_adapters/sqlserver/type/unicode_varchar_max.rb | 4 ++++ .../connection_adapters/sqlserver/type/varbinary_max.rb | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/lib/active_record/connection_adapters/sqlserver/type/text.rb b/lib/active_record/connection_adapters/sqlserver/type/text.rb index b47f9465e..e06c2799f 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/text.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/text.rb @@ -10,6 +10,10 @@ def type :text_basic end + def sqlserver_type + SQLSERVER_TYPE + end + end end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/unicode_text.rb b/lib/active_record/connection_adapters/sqlserver/type/unicode_text.rb index 1e0ef3c1b..e6673db12 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/unicode_text.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/unicode_text.rb @@ -10,6 +10,10 @@ def type :ntext end + def sqlserver_type + SQLSERVER_TYPE + end + end end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar_max.rb b/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar_max.rb index 69ffc818c..a9b823f9f 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar_max.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar_max.rb @@ -15,6 +15,10 @@ def type :text end + def sqlserver_type + SQLSERVER_TYPE + end + end end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/varbinary_max.rb b/lib/active_record/connection_adapters/sqlserver/type/varbinary_max.rb index 05a437615..a48aeaff1 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/varbinary_max.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/varbinary_max.rb @@ -15,6 +15,10 @@ def type :binary end + def sqlserver_type + SQLSERVER_TYPE + end + end end end From e53125ce4a4663707e8f652bf63cb5bcb06685b8 Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Sat, 6 Aug 2016 18:25:10 -0400 Subject: [PATCH 12/27] Pass all SQL Server type tests. --- .../sqlserver/database_statements.rb | 6 +- .../connection_adapters/sqlserver/quoting.rb | 4 +- .../connection_adapters/sqlserver/type.rb | 1 - .../sqlserver/type/big_integer.rb | 6 +- .../sqlserver/type/boolean.rb | 4 +- .../sqlserver/type/date.rb | 4 +- .../sqlserver/type/datetime.rb | 6 +- .../sqlserver/type/float.rb | 6 +- .../sqlserver/type/integer.rb | 4 +- .../sqlserver/type/money.rb | 6 +- .../sqlserver/type/real.rb | 6 +- .../sqlserver/type/small_integer.rb | 4 +- .../sqlserver/type/small_money.rb | 6 +- .../sqlserver/type/smalldatetime.rb | 6 +- .../connection_adapters/sqlserver/type/sql.rb | 16 -- .../sqlserver/type/text.rb | 4 +- .../sqlserver/type/timestamp.rb | 6 +- .../sqlserver/type/tiny_integer.rb | 4 +- .../sqlserver/type/unicode_text.rb | 4 +- .../sqlserver/type/unicode_varchar_max.rb | 4 +- .../sqlserver/type/uuid.rb | 7 +- .../sqlserver/type/varbinary_max.rb | 4 +- .../sqlserver/type/varchar_max.rb | 6 +- test/cases/column_test_sqlserver.rb | 172 +++++++----------- 24 files changed, 137 insertions(+), 159 deletions(-) delete mode 100644 lib/active_record/connection_adapters/sqlserver/type/sql.rb diff --git a/lib/active_record/connection_adapters/sqlserver/database_statements.rb b/lib/active_record/connection_adapters/sqlserver/database_statements.rb index fc338adbe..d363c9929 100644 --- a/lib/active_record/connection_adapters/sqlserver/database_statements.rb +++ b/lib/active_record/connection_adapters/sqlserver/database_statements.rb @@ -247,11 +247,13 @@ def sp_executesql_types_and_parameters(binds) end def sp_executesql_sql_type(attr) + return attr.type.sqlserver_type if attr.type.respond_to?(:sqlserver_type) case value = attr.value_for_database when Numeric - SQLServer::Type::Integer::SQLSERVER_TYPE + 'int'.freeze else - attr.type.sqlserver_type + raise TypeError, "sp_executesql_sql_type can not find sql type for attr #{attr.inspect}" + quote(value) end end diff --git a/lib/active_record/connection_adapters/sqlserver/quoting.rb b/lib/active_record/connection_adapters/sqlserver/quoting.rb index 469f37ce7..a38785ca8 100644 --- a/lib/active_record/connection_adapters/sqlserver/quoting.rb +++ b/lib/active_record/connection_adapters/sqlserver/quoting.rb @@ -80,8 +80,10 @@ def _type_cast(value) case value when Symbol _quote(value.to_s) - when String, ActiveSupport::Multibyte::Chars + when String, ActiveSupport::Multibyte::Chars, Type::Binary::Data _quote(value) + when ActiveRecord::Type::SQLServer::Char::Data + value.quoted else super end end diff --git a/lib/active_record/connection_adapters/sqlserver/type.rb b/lib/active_record/connection_adapters/sqlserver/type.rb index f6ab38ced..47bb07be5 100644 --- a/lib/active_record/connection_adapters/sqlserver/type.rb +++ b/lib/active_record/connection_adapters/sqlserver/type.rb @@ -1,6 +1,5 @@ require 'active_record/type' # Behaviors -require 'active_record/connection_adapters/sqlserver/type/sql.rb' require 'active_record/connection_adapters/sqlserver/type/time_value_fractional.rb' # Exact Numerics require 'active_record/connection_adapters/sqlserver/type/integer.rb' diff --git a/lib/active_record/connection_adapters/sqlserver/type/big_integer.rb b/lib/active_record/connection_adapters/sqlserver/type/big_integer.rb index 28564fdb3..5b786aa24 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/big_integer.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/big_integer.rb @@ -4,12 +4,14 @@ module SQLServer module Type class BigInteger < Integer - SQLSERVER_TYPE = 'bigint'.freeze - def type :bigint end + def sqlserver_type + 'bigint'.freeze + end + end end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/boolean.rb b/lib/active_record/connection_adapters/sqlserver/type/boolean.rb index 2d2fd8b2c..e2b48e701 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/boolean.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/boolean.rb @@ -4,7 +4,9 @@ module SQLServer module Type class Boolean < ActiveRecord::Type::Boolean - SQLSERVER_TYPE = 'bit'.freeze + def sqlserver_type + 'bit'.freeze + end end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/date.rb b/lib/active_record/connection_adapters/sqlserver/type/date.rb index 60405a5c5..d283e60a8 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/date.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/date.rb @@ -4,7 +4,9 @@ module SQLServer module Type class Date < ActiveRecord::Type::Date - SQLSERVER_TYPE = 'date'.freeze + def sqlserver_type + 'date'.freeze + end def serialize(value) return unless value.present? diff --git a/lib/active_record/connection_adapters/sqlserver/type/datetime.rb b/lib/active_record/connection_adapters/sqlserver/type/datetime.rb index 5d092bc9b..4cc3485d3 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/datetime.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/datetime.rb @@ -4,10 +4,12 @@ module SQLServer module Type class DateTime < ActiveRecord::Type::DateTime - SQLSERVER_TYPE = 'datetime'.freeze - include TimeValueFractional + def sqlserver_type + 'datetime'.freeze + end + def serialize(value) return super unless value.acts_like?(:time) value = zone_conversion(value) diff --git a/lib/active_record/connection_adapters/sqlserver/type/float.rb b/lib/active_record/connection_adapters/sqlserver/type/float.rb index 0b4758b82..d26433dd3 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/float.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/float.rb @@ -4,12 +4,14 @@ module SQLServer module Type class Float < ActiveRecord::Type::Float - SQLSERVER_TYPE = 'float'.freeze - def type :float end + def sqlserver_type + 'float'.freeze + end + end end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/integer.rb b/lib/active_record/connection_adapters/sqlserver/type/integer.rb index c7c42a43a..72f93e275 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/integer.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/integer.rb @@ -4,7 +4,9 @@ module SQLServer module Type class Integer < ActiveRecord::Type::Integer - SQLSERVER_TYPE = 'int'.freeze + def sqlserver_type + 'int'.freeze + end end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/money.rb b/lib/active_record/connection_adapters/sqlserver/type/money.rb index 56f8f727c..e0c93d02a 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/money.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/money.rb @@ -4,8 +4,6 @@ module SQLServer module Type class Money < Decimal - SQLSERVER_TYPE = 'money'.freeze - def initialize(options = {}) super @precision = 19 @@ -16,6 +14,10 @@ def type :money end + def sqlserver_type + 'money'.freeze + end + end end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/real.rb b/lib/active_record/connection_adapters/sqlserver/type/real.rb index 8ab4e4737..c0c143c52 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/real.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/real.rb @@ -4,12 +4,14 @@ module SQLServer module Type class Real < Float - SQLSERVER_TYPE = 'real'.freeze - def type :real end + def sqlserver_type + 'real'.freeze + end + end end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/small_integer.rb b/lib/active_record/connection_adapters/sqlserver/type/small_integer.rb index d4c09cd90..ab2659455 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/small_integer.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/small_integer.rb @@ -4,7 +4,9 @@ module SQLServer module Type class SmallInteger < Integer - SQLSERVER_TYPE = 'smallint'.freeze + def sqlserver_type + 'smallint'.freeze + end end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/small_money.rb b/lib/active_record/connection_adapters/sqlserver/type/small_money.rb index 00745233f..0ba487895 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/small_money.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/small_money.rb @@ -4,8 +4,6 @@ module SQLServer module Type class SmallMoney < Money - SQLSERVER_TYPE = 'smallmoney'.freeze - def initialize(options = {}) super @precision = 10 @@ -16,6 +14,10 @@ def type :smallmoney end + def sqlserver_type + 'smallmoney'.freeze + end + end end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/smalldatetime.rb b/lib/active_record/connection_adapters/sqlserver/type/smalldatetime.rb index bb41df202..6bf93d3f0 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/smalldatetime.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/smalldatetime.rb @@ -4,12 +4,14 @@ module SQLServer module Type class SmallDateTime < DateTime - SQLSERVER_TYPE = 'smalldatetime'.freeze - def type :smalldatetime end + def sqlserver_type + 'smalldatetime'.freeze + end + private def cast_fractional(value) diff --git a/lib/active_record/connection_adapters/sqlserver/type/sql.rb b/lib/active_record/connection_adapters/sqlserver/type/sql.rb deleted file mode 100644 index 16a5901f3..000000000 --- a/lib/active_record/connection_adapters/sqlserver/type/sql.rb +++ /dev/null @@ -1,16 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module SQLServer - module Type - module Sql - - def sqlserver_type - defined?(SQLSERVER_TYPE) ? SQLSERVER_TYPE : type.to_s - end - - end - ::ActiveModel::Type::Value.include SQLServer::Type::Sql - end - end - end -end diff --git a/lib/active_record/connection_adapters/sqlserver/type/text.rb b/lib/active_record/connection_adapters/sqlserver/type/text.rb index e06c2799f..8d49ff731 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/text.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/text.rb @@ -4,14 +4,12 @@ module SQLServer module Type class Text < VarcharMax - SQLSERVER_TYPE = 'text'.freeze - def type :text_basic end def sqlserver_type - SQLSERVER_TYPE + 'text'.freeze end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/timestamp.rb b/lib/active_record/connection_adapters/sqlserver/type/timestamp.rb index 4b955b818..b0e38a827 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/timestamp.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/timestamp.rb @@ -4,12 +4,14 @@ module SQLServer module Type class Timestamp < Binary - SQLSERVER_TYPE = 'timestamp'.freeze - def type :ss_timestamp end + def sqlserver_type + 'timestamp'.freeze + end + end end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/tiny_integer.rb b/lib/active_record/connection_adapters/sqlserver/type/tiny_integer.rb index c47ed044e..739b51347 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/tiny_integer.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/tiny_integer.rb @@ -4,7 +4,9 @@ module SQLServer module Type class TinyInteger < Integer - SQLSERVER_TYPE = 'tinyint'.freeze + def sqlserver_type + 'tinyint'.freeze + end private diff --git a/lib/active_record/connection_adapters/sqlserver/type/unicode_text.rb b/lib/active_record/connection_adapters/sqlserver/type/unicode_text.rb index e6673db12..c39e18a11 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/unicode_text.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/unicode_text.rb @@ -4,14 +4,12 @@ module SQLServer module Type class UnicodeText < UnicodeVarcharMax - SQLSERVER_TYPE = 'ntext'.freeze - def type :ntext end def sqlserver_type - SQLSERVER_TYPE + 'ntext'.freeze end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar_max.rb b/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar_max.rb index a9b823f9f..5f6990a0b 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar_max.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar_max.rb @@ -4,8 +4,6 @@ module SQLServer module Type class UnicodeVarcharMax < UnicodeVarchar - SQLSERVER_TYPE = 'nvarchar(max)'.freeze - def initialize(options = {}) super @limit = 2_147_483_647 @@ -16,7 +14,7 @@ def type end def sqlserver_type - SQLSERVER_TYPE + 'nvarchar(max)'.freeze end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/uuid.rb b/lib/active_record/connection_adapters/sqlserver/type/uuid.rb index 13efde800..9457f44c5 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/uuid.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/uuid.rb @@ -4,7 +4,6 @@ module SQLServer module Type class Uuid < String - SQLSERVER_TYPE = 'uniqueidentifier'.freeze ACCEPTABLE_UUID = %r{\A\{?([a-fA-F0-9]{4}-?){8}\}?\z}x alias_method :serialize, :deserialize @@ -13,7 +12,11 @@ def type :uuid end - def type_cast(value) + def sqlserver_type + 'uniqueidentifier'.freeze + end + + def cast(value) value.to_s[ACCEPTABLE_UUID, 0] end diff --git a/lib/active_record/connection_adapters/sqlserver/type/varbinary_max.rb b/lib/active_record/connection_adapters/sqlserver/type/varbinary_max.rb index a48aeaff1..a251c268a 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/varbinary_max.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/varbinary_max.rb @@ -4,8 +4,6 @@ module SQLServer module Type class VarbinaryMax < Varbinary - SQLSERVER_TYPE = 'varbinary(max)'.freeze - def initialize(options = {}) super @limit = 2_147_483_647 @@ -16,7 +14,7 @@ def type end def sqlserver_type - SQLSERVER_TYPE + 'varbinary(max)'.freeze end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/varchar_max.rb b/lib/active_record/connection_adapters/sqlserver/type/varchar_max.rb index 1f40edf46..79fa56464 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/varchar_max.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/varchar_max.rb @@ -4,8 +4,6 @@ module SQLServer module Type class VarcharMax < Varchar - SQLSERVER_TYPE = 'varchar(max)'.freeze - def initialize(options = {}) super @limit = 2_147_483_647 @@ -15,6 +13,10 @@ def type :varchar_max end + def sqlserver_type + 'varchar(max)'.freeze + end + end end end diff --git a/test/cases/column_test_sqlserver.rb b/test/cases/column_test_sqlserver.rb index 21f5dd871..1856ae155 100644 --- a/test/cases/column_test_sqlserver.rb +++ b/test/cases/column_test_sqlserver.rb @@ -36,14 +36,13 @@ def assert_obj_set_and_save(attribute, value) it 'bigint(8)' do col = column('bigint') col.sql_type.must_equal 'bigint(8)' + col.type.must_equal :bigint col.null.must_equal true col.default.must_equal 42 obj.bigint.must_equal 42 col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::BigInteger - type.type.must_equal :bigint - type.must_be :number? type.limit.must_equal 8 assert_obj_set_and_save :bigint, -9_223_372_036_854_775_808 assert_obj_set_and_save :bigint, 9_223_372_036_854_775_807 @@ -52,14 +51,13 @@ def assert_obj_set_and_save(attribute, value) it 'int(4)' do col = column('int') col.sql_type.must_equal 'int(4)' + col.type.must_equal :integer col.null.must_equal true col.default.must_equal 42 obj.int.must_equal 42 col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::Integer - type.type.must_equal :integer - type.must_be :number? type.limit.must_equal 4 assert_obj_set_and_save :int, -2_147_483_648 assert_obj_set_and_save :int, 2_147_483_647 @@ -68,14 +66,13 @@ def assert_obj_set_and_save(attribute, value) it 'smallint(2)' do col = column('smallint') col.sql_type.must_equal 'smallint(2)' + col.type.must_equal :integer col.null.must_equal true col.default.must_equal 42 obj.smallint.must_equal 42 col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::SmallInteger - type.type.must_equal :integer - type.must_be :number? type.limit.must_equal 2 assert_obj_set_and_save :smallint, -32_768 assert_obj_set_and_save :smallint, 32_767 @@ -84,14 +81,13 @@ def assert_obj_set_and_save(attribute, value) it 'tinyint(1)' do col = column('tinyint') col.sql_type.must_equal 'tinyint(1)' + col.type.must_equal :integer col.null.must_equal true col.default.must_equal 42 obj.tinyint.must_equal 42 col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::TinyInteger - type.type.must_equal :integer - type.must_be :number? type.limit.must_equal 1 assert_obj_set_and_save :tinyint, 0 assert_obj_set_and_save :tinyint, 255 @@ -100,14 +96,13 @@ def assert_obj_set_and_save(attribute, value) it 'bit' do col = column('bit') col.sql_type.must_equal 'bit' + col.type.must_equal :boolean col.null.must_equal true col.default.must_equal true obj.bit.must_equal true col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::Boolean - type.type.must_equal :boolean - type.wont_be :number? type.limit.must_equal nil obj.bit = 0 obj.bit.must_equal false @@ -122,14 +117,13 @@ def assert_obj_set_and_save(attribute, value) it 'decimal(9,2)' do col = column('decimal_9_2') col.sql_type.must_equal 'decimal(9,2)' + col.type.must_equal :decimal col.null.must_equal true col.default.must_equal BigDecimal('12345.01') obj.decimal_9_2.must_equal BigDecimal('12345.01') col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::Decimal - type.type.must_equal :decimal - type.must_be :number? type.limit.must_equal nil type.precision.must_equal 9 type.scale.must_equal 2 @@ -145,7 +139,7 @@ def assert_obj_set_and_save(attribute, value) col.default.must_equal BigDecimal('1234567.89') obj.decimal_16_4.must_equal BigDecimal('1234567.89') col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.precision.must_equal 16 type.scale.must_equal 4 obj.decimal_16_4 = '1234567.8901001' @@ -157,14 +151,13 @@ def assert_obj_set_and_save(attribute, value) it 'numeric(18,0)' do col = column('numeric_18_0') col.sql_type.must_equal 'numeric(18,0)' + col.type.must_equal :decimal col.null.must_equal true col.default.must_equal BigDecimal('191') obj.numeric_18_0.must_equal BigDecimal('191') col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::Decimal - type.type.must_equal :decimal - type.must_be :number? type.limit.must_equal nil type.precision.must_equal 18 type.scale.must_equal 0 @@ -177,14 +170,13 @@ def assert_obj_set_and_save(attribute, value) it 'numeric(36,2)' do col = column('numeric_36_2') col.sql_type.must_equal 'numeric(36,2)' + col.type.must_equal :decimal col.null.must_equal true col.default.must_equal BigDecimal('12345678901234567890.01') obj.numeric_36_2.must_equal BigDecimal('12345678901234567890.01') col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::Decimal - type.type.must_equal :decimal - type.must_be :number? type.limit.must_equal nil type.precision.must_equal 36 type.scale.must_equal 2 @@ -197,14 +189,13 @@ def assert_obj_set_and_save(attribute, value) it 'money' do col = column('money') col.sql_type.must_equal 'money' + col.type.must_equal :money col.null.must_equal true col.default.must_equal BigDecimal('4.20') obj.money.must_equal BigDecimal('4.20') col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::Money - type.type.must_equal :money - type.must_be :number? type.limit.must_equal nil type.precision.must_equal 19 type.scale.must_equal 4 @@ -217,14 +208,13 @@ def assert_obj_set_and_save(attribute, value) it 'smallmoney' do col = column('smallmoney') col.sql_type.must_equal 'smallmoney' + col.type.must_equal :smallmoney col.null.must_equal true col.default.must_equal BigDecimal('4.20') obj.smallmoney.must_equal BigDecimal('4.20') col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::SmallMoney - type.type.must_equal :smallmoney - type.must_be :number? type.limit.must_equal nil type.precision.must_equal 10 type.scale.must_equal 4 @@ -241,14 +231,13 @@ def assert_obj_set_and_save(attribute, value) it 'float' do col = column('float') col.sql_type.must_equal 'float' + col.type.must_equal :float col.null.must_equal true col.default.must_equal 123.00000001 obj.float.must_equal 123.00000001 col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::Float - type.type.must_equal :float - type.must_be :number? type.limit.must_equal nil type.precision.must_equal nil type.scale.must_equal nil @@ -261,14 +250,13 @@ def assert_obj_set_and_save(attribute, value) it 'real' do col = column('real') col.sql_type.must_equal 'real' + col.type.must_equal :real col.null.must_equal true col.default.must_be_close_to 123.45, 0.01 obj.real.must_be_close_to 123.45, 0.01 col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::Real - type.type.must_equal :real - type.must_be :number? type.limit.must_equal nil type.precision.must_equal nil type.scale.must_equal nil @@ -283,14 +271,13 @@ def assert_obj_set_and_save(attribute, value) it 'date' do col = column('date') 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_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::Date - type.type.must_equal :date - type.wont_be :number? type.limit.must_equal nil type.precision.must_equal nil type.scale.must_equal nil @@ -311,14 +298,13 @@ def assert_obj_set_and_save(attribute, value) it 'datetime' do col = column('datetime') col.sql_type.must_equal 'datetime' + col.type.must_equal :datetime col.null.must_equal true col.default.must_equal Time.utc(1753, 01, 01, 00, 00, 00, 123000), "Microseconds were <#{col.default.usec}> vs <123000>" obj.datetime.must_equal Time.utc(1753, 01, 01, 00, 00, 00, 123000), "Microseconds were <#{obj.datetime.usec}> vs <123000>" col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::DateTime - type.type.must_equal :datetime - type.wont_be :number? type.limit.must_equal nil type.precision.must_equal nil type.scale.must_equal nil @@ -338,14 +324,13 @@ def assert_obj_set_and_save(attribute, value) skip 'datetime2 not supported in this protocal version' unless connection_dblib_73? col = column('datetime2_7') col.sql_type.must_equal 'datetime2(7)' + col.type.must_equal :datetime2 col.null.must_equal true col.default.must_equal Time.utc(9999, 12, 31, 23, 59, 59, Rational(999999900, 1000)), "Nanoseconds were <#{col.default.nsec}> vs <999999900>" obj.datetime2_7.must_equal Time.utc(9999, 12, 31, 23, 59, 59, Rational(999999900, 1000)), "Nanoseconds were <#{obj.datetime2_7.nsec}> vs <999999900>" col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::DateTime2 - type.type.must_equal :datetime2 - type.wont_be :number? type.limit.must_equal nil type.precision.must_equal 7 type.scale.must_equal nil @@ -362,19 +347,19 @@ def assert_obj_set_and_save(attribute, value) # With other precisions. time = Time.utc 9999, 12, 31, 23, 59, 59, Rational(123456789, 1000) col = column('datetime2_3') - col.cast_type.precision.must_equal 3 + connection.lookup_cast_type_from_column(col).precision.must_equal 3 obj.datetime2_3 = time obj.datetime2_3.must_equal time.change(nsec: 123000000), "Nanoseconds were <#{obj.datetime2_3.nsec}> vs <123000000>" obj.save! ; obj.reload obj.datetime2_3.must_equal time.change(nsec: 123000000), "Nanoseconds were <#{obj.datetime2_3.nsec}> vs <123000000>" col = column('datetime2_1') - col.cast_type.precision.must_equal 1 + connection.lookup_cast_type_from_column(col).precision.must_equal 1 obj.datetime2_1 = time obj.datetime2_1.must_equal time.change(nsec: 100000000), "Nanoseconds were <#{obj.datetime2_1.nsec}> vs <100000000>" obj.save! ; obj.reload obj.datetime2_1.must_equal time.change(nsec: 100000000), "Nanoseconds were <#{obj.datetime2_1.nsec}> vs <100000000>" col = column('datetime2_0') - col.cast_type.precision.must_equal 0 + connection.lookup_cast_type_from_column(col).precision.must_equal 0 time = Time.utc 2016, 4, 19, 16, 45, 40, 771036 obj.datetime2_0 = time obj.datetime2_0.must_equal time.change(nsec: 0), "Nanoseconds were <#{obj.datetime2_0.nsec}> vs <0>" @@ -386,14 +371,13 @@ def assert_obj_set_and_save(attribute, value) skip 'datetimeoffset not supported in this protocal version' unless connection_dblib_73? col = column('datetimeoffset_7') col.sql_type.must_equal 'datetimeoffset(7)' + col.type.must_equal :datetimeoffset col.null.must_equal true col.default.must_equal Time.new(1984, 01, 24, 04, 20, 00, -28800).change(nsec: 123456700), "Nanoseconds <#{col.default.nsec}> vs <123456700>" obj.datetimeoffset_7.must_equal Time.new(1984, 01, 24, 04, 20, 00, -28800).change(nsec: 123456700), "Nanoseconds were <#{obj.datetimeoffset_7.nsec}> vs <999999900>" col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::DateTimeOffset - type.type.must_equal :datetimeoffset - type.wont_be :number? type.limit.must_equal nil type.precision.must_equal 7 type.scale.must_equal nil @@ -405,7 +389,7 @@ def assert_obj_set_and_save(attribute, value) # With other precisions. time = ActiveSupport::TimeZone['America/Los_Angeles'].local 2010, 12, 31, 23, 59, 59, Rational(123456755, 1000) col = column('datetimeoffset_3') - col.cast_type.precision.must_equal 3 + connection.lookup_cast_type_from_column(col).precision.must_equal 3 obj.datetimeoffset_3 = time obj.datetimeoffset_3.must_equal time.change(nsec: 123000000), "Nanoseconds were <#{obj.datetimeoffset_3.nsec}> vs <123000000>" # TODO: FreeTDS date bug fixed: https://github.com/FreeTDS/freetds/issues/44 @@ -413,7 +397,7 @@ def assert_obj_set_and_save(attribute, value) obj.save! ; obj.reload obj.datetimeoffset_3.must_equal time.change(nsec: 123000000), "Nanoseconds were <#{obj.datetimeoffset_3.nsec}> vs <123000000>" col = column('datetime2_1') - col.cast_type.precision.must_equal 1 + connection.lookup_cast_type_from_column(col).precision.must_equal 1 obj.datetime2_1 = time obj.datetime2_1.must_equal time.change(nsec: 100000000), "Nanoseconds were <#{obj.datetime2_1.nsec}> vs <100000000>" obj.save! ; obj.reload @@ -423,14 +407,13 @@ def assert_obj_set_and_save(attribute, value) it 'smalldatetime' do col = column('smalldatetime') col.sql_type.must_equal 'smalldatetime' + col.type.must_equal :smalldatetime col.null.must_equal true col.default.must_equal Time.utc(1901, 01, 01, 15, 45, 00, 000) obj.smalldatetime.must_equal Time.utc(1901, 01, 01, 15, 45, 00, 000) col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::SmallDateTime - type.type.must_equal :smalldatetime - type.wont_be :number? type.limit.must_equal nil type.precision.must_equal nil type.scale.must_equal nil @@ -445,13 +428,12 @@ def assert_obj_set_and_save(attribute, value) skip 'time() not supported in this protocal version' unless connection_dblib_73? col = column('time_7') col.sql_type.must_equal 'time(7)' + col.type.must_equal :time col.null.must_equal true col.default.must_equal Time.utc(1900, 01, 01, 04, 20, 00, Rational(288321500, 1000)), "Nanoseconds were <#{col.default.nsec}> vs <288321500>" col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::Time - type.type.must_equal :time - type.wont_be :number? type.limit.must_equal nil type.precision.must_equal 7 type.scale.must_equal nil @@ -478,13 +460,12 @@ def assert_obj_set_and_save(attribute, value) skip 'time() not supported in this protocal version' unless connection_dblib_73? col = column('time_2') col.sql_type.must_equal 'time(2)' + col.type.must_equal :time col.null.must_equal true col.default.must_equal nil col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::Time - type.type.must_equal :time - type.wont_be :number? type.limit.must_equal nil type.precision.must_equal 2 type.scale.must_equal nil @@ -510,14 +491,13 @@ def assert_obj_set_and_save(attribute, value) it 'char(10)' do col = column('char_10') col.sql_type.must_equal 'char(10)' + col.type.must_equal :char col.null.must_equal true col.default.must_equal '1234567890' obj.char_10.must_equal '1234567890' col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::Char - type.type.must_equal :char - type.wont_be :number? type.limit.must_equal 10 type.precision.must_equal nil type.scale.must_equal nil @@ -531,14 +511,13 @@ def assert_obj_set_and_save(attribute, value) it 'varchar(50)' do col = column('varchar_50') col.sql_type.must_equal 'varchar(50)' + col.type.must_equal :varchar col.null.must_equal true col.default.must_equal 'test varchar_50' obj.varchar_50.must_equal 'test varchar_50' col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::Varchar - type.type.must_equal :varchar - type.wont_be :number? type.limit.must_equal 50 type.precision.must_equal nil type.scale.must_equal nil @@ -549,14 +528,13 @@ def assert_obj_set_and_save(attribute, value) it 'varchar(max)' do col = column('varchar_max') col.sql_type.must_equal 'varchar(max)' + col.type.must_equal :varchar_max col.null.must_equal true col.default.must_equal 'test varchar_max' obj.varchar_max.must_equal 'test varchar_max' col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::VarcharMax - type.type.must_equal :varchar_max - type.wont_be :number? type.limit.must_equal 2_147_483_647 type.precision.must_equal nil type.scale.must_equal nil @@ -567,14 +545,13 @@ def assert_obj_set_and_save(attribute, value) it 'text' do col = column('text') col.sql_type.must_equal 'text' + col.type.must_equal :text_basic col.null.must_equal true col.default.must_equal 'test text' obj.text.must_equal 'test text' col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::Text - type.type.must_equal :text_basic - type.wont_be :number? type.limit.must_equal 2_147_483_647 type.precision.must_equal nil type.scale.must_equal nil @@ -587,14 +564,13 @@ def assert_obj_set_and_save(attribute, value) it 'nchar(10)' do col = column('nchar_10') col.sql_type.must_equal 'nchar(10)' + col.type.must_equal :nchar col.null.must_equal true col.default.must_equal '12345678åå' obj.nchar_10.must_equal '12345678åå' col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::UnicodeChar - type.type.must_equal :nchar - type.wont_be :number? type.limit.must_equal 10 type.precision.must_equal nil type.scale.must_equal nil @@ -608,14 +584,13 @@ def assert_obj_set_and_save(attribute, value) it 'nvarchar(50)' do col = column('nvarchar_50') col.sql_type.must_equal 'nvarchar(50)' + col.type.must_equal :string col.null.must_equal true col.default.must_equal 'test nvarchar_50 åå' obj.nvarchar_50.must_equal 'test nvarchar_50 åå' col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::UnicodeVarchar - type.type.must_equal :string - type.wont_be :number? type.limit.must_equal 50 type.precision.must_equal nil type.scale.must_equal nil @@ -626,14 +601,13 @@ def assert_obj_set_and_save(attribute, value) it 'nvarchar(max)' do col = column('nvarchar_max') col.sql_type.must_equal 'nvarchar(max)' + col.type.must_equal :text col.null.must_equal true col.default.must_equal 'test nvarchar_max åå' obj.nvarchar_max.must_equal 'test nvarchar_max åå' col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::UnicodeVarcharMax - type.type.must_equal :text - type.wont_be :number? type.limit.must_equal 2_147_483_647 type.precision.must_equal nil type.scale.must_equal nil @@ -644,14 +618,13 @@ def assert_obj_set_and_save(attribute, value) it 'ntext' do col = column('ntext') col.sql_type.must_equal 'ntext' + col.type.must_equal :ntext col.null.must_equal true col.default.must_equal 'test ntext åå' obj.ntext.must_equal 'test ntext åå' col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::UnicodeText - type.type.must_equal :ntext - type.wont_be :number? type.limit.must_equal 2_147_483_647 type.precision.must_equal nil type.scale.must_equal nil @@ -667,13 +640,12 @@ def assert_obj_set_and_save(attribute, value) it 'binary(49)' do col = column('binary_49') col.sql_type.must_equal 'binary(49)' + col.type.must_equal :binary_basic col.null.must_equal true col.default.must_equal nil col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::Binary - type.type.must_equal :binary_basic - type.wont_be :number? type.limit.must_equal 49 type.precision.must_equal nil type.scale.must_equal nil @@ -689,13 +661,12 @@ def assert_obj_set_and_save(attribute, value) it 'varbinary(49)' do col = column('varbinary_49') col.sql_type.must_equal 'varbinary(49)' + col.type.must_equal :varbinary col.null.must_equal true col.default.must_equal nil col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::Varbinary - type.type.must_equal :varbinary - type.wont_be :number? type.limit.must_equal 49 type.precision.must_equal nil type.scale.must_equal nil @@ -711,13 +682,12 @@ def assert_obj_set_and_save(attribute, value) it 'varbinary(max)' do col = column('varbinary_max') col.sql_type.must_equal 'varbinary(max)' + col.type.must_equal :binary col.null.must_equal true col.default.must_equal nil col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::VarbinaryMax - type.type.must_equal :binary - type.wont_be :number? type.limit.must_equal 2_147_483_647 type.precision.must_equal nil type.scale.must_equal nil @@ -731,13 +701,12 @@ def assert_obj_set_and_save(attribute, value) it 'uniqueidentifier' do col = column('uniqueidentifier') col.sql_type.must_equal 'uniqueidentifier' + col.type.must_equal :uuid col.null.must_equal true col.default.must_equal nil col.default_function.must_equal 'newid()' - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::Uuid - type.type.must_equal :uuid - type.wont_be :number? type.limit.must_equal nil type.precision.must_equal nil type.scale.must_equal nil @@ -755,13 +724,12 @@ def assert_obj_set_and_save(attribute, value) it 'timestamp' do col = column('timestamp') col.sql_type.must_equal 'timestamp' + col.type.must_equal :ss_timestamp col.null.must_equal true col.default.must_equal nil col.default_function.must_equal nil - type = col.cast_type + type = connection.lookup_cast_type_from_column(col) type.must_be_instance_of Type::Timestamp - type.type.must_equal :ss_timestamp - type.wont_be :number? type.limit.must_equal nil type.precision.must_equal nil type.scale.must_equal nil From 470e786262c164d6ee39ba97c6419ddf805e62e6 Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Sat, 6 Aug 2016 20:16:42 -0400 Subject: [PATCH 13/27] No more SQL Server `SchemaCache` subclass games. * Dont worry about view info cache. Since product of columns is cached. * May have to not care about a few schema tests later down the road. --- .../sqlserver/schema_cache.rb | 106 ------------------ .../sqlserver/schema_statements.rb | 13 ++- .../connection_adapters/sqlserver_adapter.rb | 3 - 3 files changed, 9 insertions(+), 113 deletions(-) delete mode 100644 lib/active_record/connection_adapters/sqlserver/schema_cache.rb diff --git a/lib/active_record/connection_adapters/sqlserver/schema_cache.rb b/lib/active_record/connection_adapters/sqlserver/schema_cache.rb deleted file mode 100644 index d1995bc7b..000000000 --- a/lib/active_record/connection_adapters/sqlserver/schema_cache.rb +++ /dev/null @@ -1,106 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module SQLServer - class SchemaCache < ActiveRecord::ConnectionAdapters::SchemaCache - - def initialize(conn) - super - @views = {} - @view_information = {} - end - - def initialize_dup(other) - super - @views = @views.dup - @view_information = @view_information.dup - end - - def primary_keys(table_name) - super tn_quoted(table_name) - end - - def data_source_exists?(table_name) - super tn_quoted(table_name) - end - - def add(table_name) - super tn_quoted(table_name) - end - - def data_sources(name) - super tn_quoted(name) - end - - # No override for #columns. - # Allow `table_name` which could be fully qualified to be used with schema reflection. - - def columns_hash(table_name) - super tn_quoted(table_name) - end - - def clear! - super - @views.clear - @view_information.clear - end - - def size - super + [@views, @view_information].map{ |x| x.size }.inject(:+) - end - - def clear_data_source_cache!(table_name) - name = tn_quoted(table_name) - super(name) - @columns.delete table_name # Because... - @views.delete name - @view_information.delete name - end - - def marshal_dump - super + [@views, @view_information] - end - - def marshal_load(array) - @views, @view_information = array[-2..-1] - super(array[0..-3]) - end - - # SQL Server Specific - - def view_exists?(table_name) - prepare_data_sources if @views.empty? - name = tn_quoted(table_name) - return @views[name] if @views.key? name - @views[name] = connection.views.include?(tn_object(table_name)) - end - - def view_information(table_name) - name = tn_quoted(table_name) - return @view_information[name] if @view_information.key? name - @view_information[name] = connection.send(:view_information, table_name) - end - - - private - - def identifier(table_name) - SQLServer::Utils.extract_identifiers(table_name) - end - - def tn_quoted(table_name) - identifier(table_name).quoted - end - - def tn_object(table_name) - identifier(table_name).object - end - - def prepare_data_sources - connection.data_sources.each { |source| @data_sources[tn_quoted(source)] = true } - connection.views.each { |source| @views[tn_quoted(source)] = true } - end - - end - end - end -end diff --git a/lib/active_record/connection_adapters/sqlserver/schema_statements.rb b/lib/active_record/connection_adapters/sqlserver/schema_statements.rb index 0bb3d9489..2f4d79b29 100644 --- a/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +++ b/lib/active_record/connection_adapters/sqlserver/schema_statements.rb @@ -253,7 +253,7 @@ def column_definitions(table_name) SQLServer::Utils.extract_identifiers(table_name) end database = identifier.fully_qualified_database_quoted - view_exists = schema_cache.view_exists?(table_name) + view_exists = view_exists?(table_name) view_tblnm = table_name_or_views_table_name(table_name) if view_exists sql = %{ SELECT DISTINCT @@ -419,8 +419,13 @@ def lowercase_schema_reflection_sql(node) # === SQLServer Specific (View Reflection) ====================== # + def view_exists?(table_name) + identifier = SQLServer::Utils.extract_identifiers(table_name) + views.include? identifier.object + end + def view_table_name(table_name) - view_info = schema_cache.view_information(table_name) + view_info = view_information(table_name) view_info ? get_table_name(view_info['VIEW_DEFINITION']) : table_name end @@ -442,11 +447,11 @@ def view_information(table_name) end def table_name_or_views_table_name(table_name) - schema_cache.view_exists?(table_name) ? view_table_name(table_name) : table_name + view_exists?(table_name) ? view_table_name(table_name) : table_name end def views_real_column_name(table_name, column_name) - view_definition = schema_cache.view_information(table_name)[:VIEW_DEFINITION] + view_definition = view_information(table_name)[:VIEW_DEFINITION] return column_name unless view_definition match_data = view_definition.match(/([\w-]*)\s+as\s+#{column_name}/im) match_data ? match_data[1] : column_name diff --git a/lib/active_record/connection_adapters/sqlserver_adapter.rb b/lib/active_record/connection_adapters/sqlserver_adapter.rb index 2fadad14f..86cd074c0 100644 --- a/lib/active_record/connection_adapters/sqlserver_adapter.rb +++ b/lib/active_record/connection_adapters/sqlserver_adapter.rb @@ -13,7 +13,6 @@ require 'active_record/connection_adapters/sqlserver/database_tasks' require 'active_record/connection_adapters/sqlserver/transaction' require 'active_record/connection_adapters/sqlserver/errors' -require 'active_record/connection_adapters/sqlserver/schema_cache' require 'active_record/connection_adapters/sqlserver/schema_creation' require 'active_record/connection_adapters/sqlserver/schema_statements' require 'active_record/connection_adapters/sqlserver/sql_type_metadata' @@ -50,8 +49,6 @@ class SQLServerAdapter < AbstractAdapter def initialize(connection, logger = nil, config = {}) super(connection, logger, config) - # AbstractAdapter Responsibility - @schema_cache = SQLServer::SchemaCache.new(self) # Our Responsibility @connection_options = config connect From bef27c2b7925f84cae7ec6af8f0ebac76e1ef55a Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Sun, 7 Aug 2016 16:20:30 -0400 Subject: [PATCH 14/27] Pass new schema dumper tests. --- .../sqlserver/type/decimal.rb | 4 + test/cases/schema_dumper_test_sqlserver.rb | 144 ++++++++++-------- 2 files changed, 87 insertions(+), 61 deletions(-) diff --git a/lib/active_record/connection_adapters/sqlserver/type/decimal.rb b/lib/active_record/connection_adapters/sqlserver/type/decimal.rb index 09bf2b50a..1383de37e 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/decimal.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/decimal.rb @@ -10,6 +10,10 @@ def sqlserver_type end end + def type_cast_for_schema(value) + value.is_a?(BigDecimal) ? value.to_s : value.inspect + end + end end end diff --git a/test/cases/schema_dumper_test_sqlserver.rb b/test/cases/schema_dumper_test_sqlserver.rb index 7845915a4..d8f954036 100644 --- a/test/cases/schema_dumper_test_sqlserver.rb +++ b/test/cases/schema_dumper_test_sqlserver.rb @@ -10,48 +10,48 @@ class SchemaDumperTestSQLServer < ActiveRecord::TestCase it 'sst_datatypes' do generate_schema_for_table 'sst_datatypes' # Exact Numerics - assert_line :bigint, type: 'bigint', limit: '8', precision: nil, scale: nil, default: '42' - assert_line :int, type: 'integer', limit: '4', precision: nil, scale: nil, default: '42' - assert_line :smallint, type: 'integer', limit: '2', precision: nil, scale: nil, default: '42' - assert_line :tinyint, type: 'integer', limit: '1', precision: nil, scale: nil, default: '42' - assert_line :bit, type: 'boolean', limit: nil, precision: nil, scale: nil, default: 'true' - assert_line :decimal_9_2, type: 'decimal', limit: nil, precision: '9', scale: '2', default: '12345.01' - assert_line :numeric_18_0, type: 'decimal', limit: nil, precision: '18', scale: '0', default: '191.0' - assert_line :numeric_36_2, type: 'decimal', limit: nil, precision: '36', scale: '2', default: '12345678901234567890.01' - assert_line :money, type: 'money', limit: nil, precision: '19', scale: '4', default: '4.2' - assert_line :smallmoney, type: 'smallmoney', limit: nil, precision: '10', scale: '4', default: '4.2' + assert_line :bigint, type: 'bigint', limit: nil, precision: nil, scale: nil, default: 42 + assert_line :int, type: 'integer', limit: nil, precision: nil, scale: nil, default: 42 + assert_line :smallint, type: 'integer', limit: 2, precision: nil, scale: nil, default: 42 + assert_line :tinyint, type: 'integer', limit: 1, precision: nil, scale: nil, default: 42 + assert_line :bit, type: 'boolean', limit: nil, precision: nil, scale: nil, default: true + assert_line :decimal_9_2, type: 'decimal', limit: nil, precision: 9, scale: 2, default: 12345.01 + assert_line :numeric_18_0, type: 'decimal', limit: nil, precision: 18, scale: 0, default: 191.0 + assert_line :numeric_36_2, type: 'decimal', limit: nil, precision: 36, scale: 2, default: 12345678901234567890.01 + assert_line :money, type: 'money', limit: nil, precision: 19, scale: 4, default: 4.2 + assert_line :smallmoney, type: 'smallmoney', limit: nil, precision: 10, scale: 4, default: 4.2 # Approximate Numerics - 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: %r{123.4[45]} + 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 :datetime, type: 'datetime', limit: nil, precision: nil, scale: nil, default: "\"01-01-1753 00:00:00.123\"" + assert_line :date, type: 'date', limit: nil, precision: nil, scale: nil, default: "01-01-0001" + 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: 'datetime2', limit: nil, precision: '7', scale: nil, default: "\"12-31-9999 23:59:59.9999999\"" - assert_line :datetime2_3, type: 'datetime2', limit: nil, precision: '3', scale: nil, default: nil - assert_line :datetime2_1, type: 'datetime2', limit: nil, precision: '1', scale: nil, default: nil + assert_line :datetime2_7, type: 'datetime2', limit: nil, precision: 7, scale: nil, default: "12-31-9999 23:59:59.9999999" + assert_line :datetime2_3, type: 'datetime2', limit: nil, precision: 3, scale: nil, default: nil + assert_line :datetime2_1, type: 'datetime2', limit: nil, precision: 1, scale: nil, default: nil end - assert_line :smalldatetime, type: 'smalldatetime',limit: nil, precision: nil, scale: nil, default: "\"01-01-1901 15:45:00\"" + assert_line :smalldatetime, type: 'smalldatetime',limit: nil, precision: nil, scale: nil, default: "01-01-1901 15:45:00" if connection_dblib_73? - assert_line :time_7, type: 'time', limit: nil, precision: '7', scale: nil, default: "\"04:20:00.2883215\"" - assert_line :time_2, type: 'time', limit: nil, precision: '2', scale: nil, default: nil + assert_line :time_7, type: 'time', limit: nil, precision: 7, scale: nil, default: "04:20:00.2883215" + assert_line :time_2, type: 'time', limit: nil, precision: 2, scale: nil, default: nil end # Character Strings - assert_line :char_10, type: 'char', limit: '10', precision: nil, scale: nil, default: "\"1234567890\"" - assert_line :varchar_50, type: 'varchar', limit: '50', precision: nil, scale: nil, default: "\"test varchar_50\"" - assert_line :varchar_max, type: 'varchar_max', limit: '2147483647', precision: nil, scale: nil, default: "\"test varchar_max\"" - assert_line :text, type: 'text_basic', limit: '2147483647', precision: nil, scale: nil, default: "\"test text\"" + assert_line :char_10, type: 'char', limit: 10, precision: nil, scale: nil, default: "1234567890", collation: "SQL_Latin1_General_CP1_CI_AS" + assert_line :varchar_50, type: 'varchar', limit: 50, precision: nil, scale: nil, default: "test varchar_50", collation: "SQL_Latin1_General_CP1_CI_AS" + assert_line :varchar_max, type: 'varchar_max', limit: 2147483647, precision: nil, scale: nil, default: "test varchar_max", collation: "SQL_Latin1_General_CP1_CI_AS" + assert_line :text, type: 'text_basic', limit: 2147483647, precision: nil, scale: nil, default: "test text", collation: "SQL_Latin1_General_CP1_CI_AS" # Unicode Character Strings - assert_line :nchar_10, type: 'nchar', limit: '10', precision: nil, scale: nil, default: "\"12345678åå\"" - assert_line :nvarchar_50, type: 'string', limit: '50', precision: nil, scale: nil, default: "\"test nvarchar_50 åå\"" - assert_line :nvarchar_max, type: 'text', limit: '2147483647', precision: nil, scale: nil, default: "\"test nvarchar_max åå\"" - assert_line :ntext, type: 'ntext', limit: '2147483647', precision: nil, scale: nil, default: "\"test ntext åå\"" + assert_line :nchar_10, type: 'nchar', limit: 10, precision: nil, scale: nil, default: "12345678åå", collation: "SQL_Latin1_General_CP1_CI_AS" + assert_line :nvarchar_50, type: 'string', limit: 50, precision: nil, scale: nil, default: "test nvarchar_50 åå", collation: "SQL_Latin1_General_CP1_CI_AS" + assert_line :nvarchar_max, type: 'text', limit: 2147483647, precision: nil, scale: nil, default: "test nvarchar_max åå", collation: "SQL_Latin1_General_CP1_CI_AS" + assert_line :ntext, type: 'ntext', limit: 2147483647, precision: nil, scale: nil, default: "test ntext åå", collation: "SQL_Latin1_General_CP1_CI_AS" # Binary Strings - assert_line :binary_49, type: 'binary_basic', limit: '49', precision: nil, scale: nil, default: nil - assert_line :varbinary_49, type: 'varbinary', limit: '49', precision: nil, scale: nil, default: nil - assert_line :varbinary_max, type: 'binary', limit: '2147483647', precision: nil, scale: nil, default: nil + assert_line :binary_49, type: 'binary_basic', limit: 49, precision: nil, scale: nil, default: nil + assert_line :varbinary_49, type: 'varbinary', limit: 49, precision: nil, scale: nil, default: nil + assert_line :varbinary_max, type: 'binary', limit: 2147483647, precision: nil, scale: nil, default: nil # Other Data Types - assert_line :uniqueidentifier, type: 'uuid', limit: nil, precision: nil, scale: nil, default: nil + assert_line :uniqueidentifier, type: 'uuid', limit: nil, precision: nil, scale: nil, default: -> { "newid()" } assert_line :timestamp, type: 'ss_timestamp', limit: nil, precision: nil, scale: nil, default: nil end @@ -71,18 +71,18 @@ class SchemaDumperTestSQLServer < ActiveRecord::TestCase columns['time_col'].sql_type.must_equal 'time(7)' columns['date_col'].sql_type.must_equal 'date' columns['binary_col'].sql_type.must_equal 'varbinary(max)' - assert_line :integer_col, type: 'integer', limit: '4', precision: nil, scale: nil, default: nil - assert_line :bigint_col, type: 'bigint', limit: '8', precision: nil, scale: nil, default: nil + assert_line :integer_col, type: 'integer', limit: nil, precision: nil, scale: nil, default: nil + assert_line :bigint_col, type: 'bigint', limit: nil, precision: nil, scale: nil, default: nil assert_line :boolean_col, type: 'boolean', limit: nil, precision: nil, scale: nil, default: nil - assert_line :decimal_col, type: 'decimal', limit: nil, precision: '18', scale: '0', default: nil + assert_line :decimal_col, type: 'decimal', limit: nil, precision: 18, scale: 0, default: nil assert_line :float_col, type: 'float', limit: nil, precision: nil, scale: nil, default: nil - assert_line :string_col, type: 'string', limit: '4000', precision: nil, scale: nil, default: nil - assert_line :text_col, type: 'text', limit: '2147483647', precision: nil, scale: nil, default: nil + assert_line :string_col, type: 'string', limit: nil, precision: nil, scale: nil, default: nil + assert_line :text_col, type: 'text', limit: 2147483647, precision: nil, scale: nil, default: nil assert_line :datetime_col, type: 'datetime', limit: nil, precision: nil, scale: nil, default: nil assert_line :timestamp_col, type: 'datetime', limit: nil, precision: nil, scale: nil, default: nil - assert_line :time_col, type: 'time', limit: nil, precision: '7', scale: nil, default: nil + assert_line :time_col, type: 'time', limit: nil, precision: 7, scale: nil, default: nil assert_line :date_col, type: 'date', limit: nil, precision: nil, scale: nil, default: nil - assert_line :binary_col, type: 'binary', limit: '2147483647', precision: nil, scale: nil, default: nil + assert_line :binary_col, type: 'binary', limit: 2147483647, precision: nil, scale: nil, default: nil # Our type methods. columns['real_col'].sql_type.must_equal 'real' columns['money_col'].sql_type.must_equal 'money' @@ -99,16 +99,16 @@ class SchemaDumperTestSQLServer < ActiveRecord::TestCase columns['uuid_col'].sql_type.must_equal 'uniqueidentifier' columns['sstimestamp_col'].sql_type.must_equal 'timestamp' assert_line :real_col, type: 'real', limit: nil, precision: nil, scale: nil, default: nil - assert_line :money_col, type: 'money', limit: nil, precision: '19', scale: '4', default: nil - assert_line :datetime2_col, type: 'datetime2', limit: nil, precision: '7', scale: nil, default: nil - assert_line :smallmoney_col, type: 'smallmoney', limit: nil, precision: '10', scale: '4', default: nil - assert_line :char_col, type: 'char', limit: '1', precision: nil, scale: nil, default: nil - assert_line :varchar_col, type: 'varchar', limit: '8000', precision: nil, scale: nil, default: nil - assert_line :text_basic_col, type: 'text_basic', limit: '2147483647', precision: nil, scale: nil, default: nil - assert_line :nchar_col, type: 'nchar', limit: '1', precision: nil, scale: nil, default: nil - assert_line :ntext_col, type: 'ntext', limit: '2147483647', precision: nil, scale: nil, default: nil - assert_line :binary_basic_col, type: 'binary_basic', limit: '1', precision: nil, scale: nil, default: nil - assert_line :varbinary_col, type: 'varbinary', limit: '8000', precision: nil, scale: nil, default: nil + assert_line :money_col, type: 'money', limit: nil, precision: 19, scale: 4, default: nil + assert_line :datetime2_col, type: 'datetime2', limit: nil, precision: 7, scale: nil, default: nil + assert_line :smallmoney_col, type: 'smallmoney', limit: nil, precision: 10, scale: 4, default: nil + assert_line :char_col, type: 'char', limit: 1, precision: nil, scale: nil, default: nil + assert_line :varchar_col, type: 'varchar', limit: nil, precision: nil, scale: nil, default: nil + assert_line :text_basic_col, type: 'text_basic', limit: 2147483647, precision: nil, scale: nil, default: nil + assert_line :nchar_col, type: 'nchar', limit: 1, precision: nil, scale: nil, default: nil + assert_line :ntext_col, type: 'ntext', limit: 2147483647, precision: nil, scale: nil, default: nil + assert_line :binary_basic_col, type: 'binary_basic', limit: 1, precision: nil, scale: nil, default: nil + assert_line :varbinary_col, type: 'varbinary', limit: nil, precision: nil, scale: nil, default: nil assert_line :uuid_col, type: 'uuid', limit: nil, precision: nil, scale: nil, default: nil assert_line :sstimestamp_col, type: 'ss_timestamp', limit: nil, precision: nil, scale: nil, default: nil end @@ -126,7 +126,7 @@ class SchemaDumperTestSQLServer < ActiveRecord::TestCase it 'no id with model driven primary key' do output = generate_schema_for_table 'sst_no_pk_data' output.must_match %r{create_table "sst_no_pk_data".*id:\sfalse.*do} - assert_line :name, type: 'string', limit: '4000' + assert_line :name, type: 'string', limit: nil, default: nil, collation: "SQL_Latin1_General_CP1_CI_AS" end @@ -162,8 +162,12 @@ def assert_line(column_name, options={}) message = "#{key.to_s.titleize} of #{expected.inspect} not found in:\n#{line}" if expected.nil? actual.must_be_nil message - elsif expected.is_a?(Regexp) - actual.must_match expected, message + elsif expected.is_a?(Array) + actual.must_include expected, message + elsif expected.is_a?(Float) + actual.must_be_close_to expected, 0.001 + elsif expected.is_a?(Proc) + actual.call.must_equal(expected.call) else actual.must_equal expected, message end @@ -172,24 +176,42 @@ def assert_line(column_name, options={}) class SchemaLine - attr_reader :line + LINE_PARSER = %r{t\.(\w+)\s+"(.*?)"[,\s+](.*)}.freeze - def self.match(method_name, pattern) - define_method(method_name) { line.match(pattern).try :[], 1 } + attr_reader :line, + :type_method, + :col_name, + :options + + def self.option(method_name) + define_method(method_name) { options.present? ? options[method_name.to_sym] : nil } end def initialize(line) @line = line + @type_method, @col_name, @options = parse_line end - match :type_method, %r{\A\s+t\.(.*?)\s} - match :limit, %r{\slimit:\s(.*?)[,\s]} - match :default, %r{\sdefault:\s(.*)\n} - match :precision, %r{\sprecision:\s(.*?)[,\s]} - match :scale, %r{\sscale:\s(.*?)[,\s]} + option :limit + option :precision + option :scale + option :default + option :collation def to_s - line + line.squish + end + + def inspect + "#" + end + + private + + def parse_line + _all, type_method, col_name, options = @line.match(LINE_PARSER).to_a + options = options.present? ? eval("{#{options}}") : {} + [type_method, col_name, options] end end From 66ab865f2f9a95e149375bc7145b1cd43f721a3d Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Sun, 7 Aug 2016 18:42:06 -0400 Subject: [PATCH 15/27] Fix database_prefix tests and arel code. --- lib/arel/visitors/sqlserver.rb | 2 +- .../fully_qualified_identifier_test_sqlserver.rb | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/arel/visitors/sqlserver.rb b/lib/arel/visitors/sqlserver.rb index b966b27ab..d494d7520 100644 --- a/lib/arel/visitors/sqlserver.rb +++ b/lib/arel/visitors/sqlserver.rb @@ -196,7 +196,7 @@ def primary_Key_From_Table t def remote_server_table_name o ActiveRecord::ConnectionAdapters::SQLServer::Utils.extract_identifiers( - "#{o.engine.connection.database_prefix}#{o.name}" + "#{o.class.engine.connection.database_prefix}#{o.name}" ).quoted end diff --git a/test/cases/fully_qualified_identifier_test_sqlserver.rb b/test/cases/fully_qualified_identifier_test_sqlserver.rb index 02bcbb7d4..a454d4baf 100644 --- a/test/cases/fully_qualified_identifier_test_sqlserver.rb +++ b/test/cases/fully_qualified_identifier_test_sqlserver.rb @@ -37,7 +37,7 @@ class FullyQualifiedIdentifierTestSQLServer < ActiveRecord::TestCase it 'should not use fully qualified table name in where clause' do table = Arel::Table.new(:table) expected_sql = "SELECT * FROM [my.server].[db].[schema].[table] WHERE [table].[id] = 42" - assert_equal expected_sql, table.project(Arel.star).where(table[:id].eq(42)).to_sql + quietly { assert_equal expected_sql, table.project(Arel.star).where(table[:id].eq(42)).to_sql } end it 'should not use fully qualified table name in order clause' do @@ -47,28 +47,28 @@ class FullyQualifiedIdentifierTestSQLServer < ActiveRecord::TestCase end it 'should use fully qualified table name in insert statement' do - manager = Arel::InsertManager.new(Arel::Table.engine) + manager = Arel::InsertManager.new manager.into Arel::Table.new(:table) manager.values = manager.create_values [Arel.sql('*')], %w{ a } expected_sql = "INSERT INTO [my.server].[db].[schema].[table] VALUES (*)" - assert_equal expected_sql, manager.to_sql + quietly { assert_equal expected_sql, manager.to_sql } end it 'should use fully qualified table name in update statement' do table = Arel::Table.new(:table) - manager = Arel::UpdateManager.new(Arel::Table.engine) + manager = Arel::UpdateManager.new manager.table(table).where(table[:id].eq(42)) manager.set([[table[:name], "Bob"]]) expected_sql = "UPDATE [my.server].[db].[schema].[table] SET [name] = N'Bob' WHERE [table].[id] = 42" - assert_equal expected_sql, manager.to_sql + quietly { assert_equal expected_sql, manager.to_sql } end it 'should use fully qualified table name in delete statement' do table = Arel::Table.new(:table) - manager = Arel::DeleteManager.new(Arel::Table.engine) + manager = Arel::DeleteManager.new manager.from(table).where(table[:id].eq(42)) expected_sql = "DELETE FROM [my.server].[db].[schema].[table] WHERE [table].[id] = 42" - assert_equal expected_sql, manager.to_sql + quietly { assert_equal expected_sql, manager.to_sql } end end From 3ddb5de55320bce627a6c6d7e60e26c74be2e63c Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Sun, 7 Aug 2016 19:12:09 -0400 Subject: [PATCH 16/27] Fix column default quoting/expression and cache clear in schema statements. --- .../sqlserver/schema_statements.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/active_record/connection_adapters/sqlserver/schema_statements.rb b/lib/active_record/connection_adapters/sqlserver/schema_statements.rb index 2f4d79b29..2c17d6741 100644 --- a/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +++ b/lib/active_record/connection_adapters/sqlserver/schema_statements.rb @@ -23,7 +23,7 @@ def views def create_table(table_name, comment: nil, **options) res = super - schema_cache.clear_data_source_cache!(table_name) + clear_cache! res end @@ -106,11 +106,11 @@ def change_column(table_name, column_name, type, options = {}) indexes = indexes(table_name).select { |index| index.columns.include?(column_name.to_s) } remove_indexes(table_name, column_name) end - sql_commands << "UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_value(options[:default], column_object)} WHERE #{quote_column_name(column_name)} IS NULL" if !options[:null].nil? && options[:null] == false && !options[:default].nil? + 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, options[:limit], options[:precision], options[:scale])}" sql_commands[-1] << ' NOT NULL' if !options[:null].nil? && options[:null] == false if 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_value(options[:default], column_object)} FOR #{quote_column_name(column_name)}" + 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| @@ -120,20 +120,20 @@ def change_column(table_name, column_name, type, options = {}) end def change_column_default(table_name, column_name, default) - schema_cache.clear_data_source_cache!(table_name) + clear_cache! remove_default_constraint(table_name, column_name) column_object = schema_cache.columns(table_name).find { |c| c.name.to_s == column_name.to_s } - do_execute "ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{default_constraint_name(table_name, column_name)} DEFAULT #{quote_default_value(default, column_object)} FOR #{quote_column_name(column_name)}" - schema_cache.clear_data_source_cache!(table_name) + do_execute "ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{default_constraint_name(table_name, column_name)} DEFAULT #{quote_default_expression(default, column_object)} FOR #{quote_column_name(column_name)}" + clear_cache! end def rename_column(table_name, column_name, new_column_name) - schema_cache.clear_data_source_cache!(table_name) + clear_cache! detect_column_for! table_name, column_name identifier = SQLServer::Utils.extract_identifiers("#{table_name}.#{column_name}") execute_procedure :sp_rename, identifier.quoted, new_column_name, 'COLUMN' rename_column_indexes(table_name, column_name, new_column_name) - schema_cache.clear_data_source_cache!(table_name) + clear_cache! end def rename_index(table_name, old_name, new_name) From 45669e57129369c584f5720a0827107b2b722a42 Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Mon, 8 Aug 2016 21:39:13 -0400 Subject: [PATCH 17/27] Bundle update to v5.0.0 release. --- lib/arel/visitors/sqlserver.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/arel/visitors/sqlserver.rb b/lib/arel/visitors/sqlserver.rb index d494d7520..a88b34d40 100644 --- a/lib/arel/visitors/sqlserver.rb +++ b/lib/arel/visitors/sqlserver.rb @@ -203,5 +203,3 @@ def remote_server_table_name o end end end - -Arel::Visitors::VISITORS['sqlserver'] = Arel::Visitors::SQLServer From e358b0f814f600ebeccb2d4665da67c88a51e0fa Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Mon, 8 Aug 2016 21:43:10 -0400 Subject: [PATCH 18/27] Flip offset/limit. --- lib/active_record/connection_adapters/sqlserver_adapter.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/active_record/connection_adapters/sqlserver_adapter.rb b/lib/active_record/connection_adapters/sqlserver_adapter.rb index 86cd074c0..2873d4a0e 100644 --- a/lib/active_record/connection_adapters/sqlserver_adapter.rb +++ b/lib/active_record/connection_adapters/sqlserver_adapter.rb @@ -229,6 +229,13 @@ def inspect "#<#{self.class} version: #{version}, mode: #{@connection_options[:mode]}, azure: #{sqlserver_azure?.inspect}>" end + def combine_bind_parameters(from_clause: [], join_clause: [], where_clause: [], having_clause: [], limit: nil, offset: nil) + result = from_clause + join_clause + where_clause + having_clause + result << offset if offset + result << limit if limit + result + end + protected From 99049ebd10313415a2765b642438efb9862e1e51 Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Tue, 9 Aug 2016 18:58:26 -0400 Subject: [PATCH 19/27] Test Rails v5. --- activerecord-sqlserver-adapter.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activerecord-sqlserver-adapter.gemspec b/activerecord-sqlserver-adapter.gemspec index 017fd96f9..9001c00dc 100644 --- a/activerecord-sqlserver-adapter.gemspec +++ b/activerecord-sqlserver-adapter.gemspec @@ -16,5 +16,5 @@ 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.0.0.rc1' + spec.add_dependency 'activerecord', '~> 5.0.0' end From ce05b8280b9eb05dcbef13674d73b906a7bb9900 Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Wed, 10 Aug 2016 19:10:34 -0400 Subject: [PATCH 20/27] [Rails5] Fix SHOWPLAN/EXPLAIN support. --- .../sqlserver/core_ext/explain.rb | 24 ++++++++++++------- .../sqlserver/database_statements.rb | 2 +- .../connection_adapters/sqlserver/showplan.rb | 14 +++++------ .../connection_adapters/sqlserver_adapter.rb | 3 ++- test/cases/showplan_test_sqlserver.rb | 2 +- 5 files changed, 27 insertions(+), 18 deletions(-) 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 02721d01e..287c31e91 100644 --- a/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +++ b/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb @@ -4,11 +4,15 @@ module SQLServer module CoreExt module Explain - SQLSERVER_STATEMENT_PREFIX = 'EXEC sp_executesql ' - SQLSERVER_PARAM_MATCHER = /@\d+ =/ + SQLSERVER_STATEMENT_PREFIX = 'EXEC sp_executesql '.freeze + SQLSERVER_PARAM_MATCHER = /@\d+ = (.*)/.freeze + SQLSERVER_NATIONAL_STRING_MATCHER = /N'(.*)'/m.freeze def exec_explain(queries) - unprepared_queries = queries.map { |sql, bind| [unprepare_sqlserver_statement(sql), bind] } + unprepared_queries = queries.map do |sql_binds| + sql, binds = sql_binds + [unprepare_sqlserver_statement(sql), binds] + end super(unprepared_queries) end @@ -20,11 +24,15 @@ def exec_explain(queries) def unprepare_sqlserver_statement(sql) if sql.starts_with?(SQLSERVER_STATEMENT_PREFIX) executesql = sql.from(SQLSERVER_STATEMENT_PREFIX.length) - executesql_args = executesql.split(', ') - found_args = executesql_args.reject! { |arg| arg =~ SQLSERVER_PARAM_MATCHER } - executesql_args.pop if found_args && executesql_args.many? - executesql = executesql_args.join(', ').strip.match(/N'(.*)'/m)[1] - Utils.unquote_string(executesql) + 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 diff --git a/lib/active_record/connection_adapters/sqlserver/database_statements.rb b/lib/active_record/connection_adapters/sqlserver/database_statements.rb index d363c9929..789fdeb9b 100644 --- a/lib/active_record/connection_adapters/sqlserver/database_statements.rb +++ b/lib/active_record/connection_adapters/sqlserver/database_statements.rb @@ -261,7 +261,7 @@ def sp_executesql_sql(sql, types, params, name) if name == 'EXPLAIN' params.each.with_index do |param, index| substitute_at_finder = /(@#{index})(?=(?:[^']|'[^']*')*$)/ # Finds unquoted @n values. - sql.sub! substitute_at_finder, param + sql.sub! substitute_at_finder, param.to_s end else types = quote(types.join(', ')) diff --git a/lib/active_record/connection_adapters/sqlserver/showplan.rb b/lib/active_record/connection_adapters/sqlserver/showplan.rb index 7c89ae89a..33839ca1e 100644 --- a/lib/active_record/connection_adapters/sqlserver/showplan.rb +++ b/lib/active_record/connection_adapters/sqlserver/showplan.rb @@ -28,32 +28,32 @@ def with_showplan_on end def set_showplan_option(enable = true) - sql = "SET #{option} #{enable ? 'ON' : 'OFF'}" + sql = "SET #{showplan_option} #{enable ? 'ON' : 'OFF'}" raw_connection_do(sql) rescue Exception - raise ActiveRecordError, "#{option} could not be turned #{enable ? 'ON' : 'OFF'}, perhaps you do not have SHOWPLAN permissions?" + raise ActiveRecordError, "#{showplan_option} could not be turned #{enable ? 'ON' : 'OFF'}, perhaps you do not have SHOWPLAN permissions?" end - def option + def showplan_option (SQLServerAdapter.showplan_option || OPTION_ALL).tap do |opt| raise(ArgumentError, "Unknown SHOWPLAN option #{opt.inspect} found.") if OPTIONS.exclude?(opt) end end def showplan_all? - option == OPTION_ALL + showplan_option == OPTION_ALL end def showplan_text? - option == OPTION_TEXT + showplan_option == OPTION_TEXT end def showplan_xml? - option == OPTION_XML + showplan_option == OPTION_XML end def showplan_printer - case option + case showplan_option when OPTION_XML then PrinterXml when OPTION_ALL, OPTION_TEXT then PrinterTable else PrinterTable diff --git a/lib/active_record/connection_adapters/sqlserver_adapter.rb b/lib/active_record/connection_adapters/sqlserver_adapter.rb index 2873d4a0e..aa3171a72 100644 --- a/lib/active_record/connection_adapters/sqlserver_adapter.rb +++ b/lib/active_record/connection_adapters/sqlserver_adapter.rb @@ -42,7 +42,8 @@ class SQLServerAdapter < AbstractAdapter cattr_accessor :cs_equality_operator, instance_accessor: false cattr_accessor :use_output_inserted, instance_accessor: false - cattr_accessor :lowercase_schema_reflection, :showplan_option + cattr_accessor :showplan_option, instance_accessor: false + cattr_accessor :lowercase_schema_reflection self.cs_equality_operator = 'COLLATE Latin1_General_CS_AS_WS' self.use_output_inserted = true diff --git a/test/cases/showplan_test_sqlserver.rb b/test/cases/showplan_test_sqlserver.rb index 17ad877b3..a140f8e32 100644 --- a/test/cases/showplan_test_sqlserver.rb +++ b/test/cases/showplan_test_sqlserver.rb @@ -19,7 +19,7 @@ class ShowplanTestSQLServer < ActiveRecord::TestCase plan.must_include "Clustered Index Seek", 'make sure we do not showplan the sp_executesql' end - it 'from prepared statement ...' do + it 'from prepared statement' do plan = Car.where(name: ',').limit(1).explain plan.must_include " SELECT [cars].* FROM [cars] WHERE [cars].[name]" plan.must_include "TOP EXPRESSION", 'make sure we do not showplan the sp_executesql' From ab0b9fd681be2943e70d7a651756e2ed48fa5d31 Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Wed, 10 Aug 2016 19:54:04 -0400 Subject: [PATCH 21/27] Fix quoted id and NULL type cast. --- lib/active_record/connection_adapters/sqlserver/quoting.rb | 2 ++ lib/active_record/connection_adapters/sqlserver/type/char.rb | 5 +++-- test/cases/specific_schema_test_sqlserver.rb | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/active_record/connection_adapters/sqlserver/quoting.rb b/lib/active_record/connection_adapters/sqlserver/quoting.rb index a38785ca8..c4c4da941 100644 --- a/lib/active_record/connection_adapters/sqlserver/quoting.rb +++ b/lib/active_record/connection_adapters/sqlserver/quoting.rb @@ -78,6 +78,8 @@ def _quote(value) def _type_cast(value) case value + when nil + "NULL" when Symbol _quote(value.to_s) when String, ActiveSupport::Multibyte::Chars, Type::Binary::Data diff --git a/lib/active_record/connection_adapters/sqlserver/type/char.rb b/lib/active_record/connection_adapters/sqlserver/type/char.rb index 06ea10855..dcf72ad7a 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/char.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/char.rb @@ -23,11 +23,12 @@ def sqlserver_type class Data def initialize(value) - @value = value.to_s + @quoted_id = value.respond_to?(:quoted_id) + @value = @quoted_id ? value.quoted_id : value.to_s end def quoted - "'#{Utils.quote_string(@value)}'" + @quoted_id ? @value : "'#{Utils.quote_string(@value)}'" end def to_s diff --git a/test/cases/specific_schema_test_sqlserver.rb b/test/cases/specific_schema_test_sqlserver.rb index 1216e167d..7af232cba 100644 --- a/test/cases/specific_schema_test_sqlserver.rb +++ b/test/cases/specific_schema_test_sqlserver.rb @@ -94,7 +94,7 @@ class SpecificSchemaTestSQLServer < ActiveRecord::TestCase end it 'use primary key for row table order in pagination sql' do - sql = /ORDER BY \[sst_natural_pk_data\]\.\[legacy_id\] ASC OFFSET 5 ROWS FETCH NEXT 5 ROWS ONLY/ + sql = /ORDER BY \[sst_natural_pk_data\]\.\[legacy_id\] ASC OFFSET @0 ROWS FETCH NEXT @1 ROWS ONLY/ assert_sql(sql) { SSTestNaturalPkData.limit(5).offset(5).load } end From a463f40f1d0a6e15f903701250d53a94fbe6ec52 Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Wed, 10 Aug 2016 20:03:17 -0400 Subject: [PATCH 22/27] Final ONLY_SQLSERVER=1 test to pass. --- test/cases/pessimistic_locking_test_sqlserver.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/cases/pessimistic_locking_test_sqlserver.rb b/test/cases/pessimistic_locking_test_sqlserver.rb index 0513b7689..1a4b8940e 100644 --- a/test/cases/pessimistic_locking_test_sqlserver.rb +++ b/test/cases/pessimistic_locking_test_sqlserver.rb @@ -73,7 +73,7 @@ class PessimisticLockingTestSQLServer < ActiveRecord::TestCase end it 'copes with eager loading un-locked paginated' do - eager_ids_sql = /SELECT\s+DISTINCT \[people\].\[id\] FROM \[people\] WITH\(UPDLOCK\) LEFT OUTER JOIN \[readers\] WITH\(UPDLOCK\)\s+ON \[readers\].\[person_id\] = \[people\].\[id\]\s+ORDER BY \[people\].\[id\] ASC OFFSET 10 ROWS FETCH NEXT 5 ROWS ONLY/ + eager_ids_sql = /SELECT\s+DISTINCT \[people\].\[id\] FROM \[people\] WITH\(UPDLOCK\) LEFT OUTER JOIN \[readers\] WITH\(UPDLOCK\)\s+ON \[readers\].\[person_id\] = \[people\].\[id\]\s+ORDER BY \[people\].\[id\] ASC OFFSET @0 ROWS FETCH NEXT @1 ROWS ONLY/ loader_sql = /SELECT.*FROM \[people\] WITH\(UPDLOCK\).*WHERE \[people\]\.\[id\] IN/ assert_sql(eager_ids_sql, loader_sql) do people = Person.lock(true).limit(5).offset(10).includes(:readers).references(:readers).to_a From a841d98af2eff94857646f9e6779015bcefebc1c Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Wed, 10 Aug 2016 20:03:43 -0400 Subject: [PATCH 23/27] TODO --- TODO.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 000000000..14b0a0bfb --- /dev/null +++ b/TODO.md @@ -0,0 +1,48 @@ + +## SHORT TERM + +Misc remidners while in the heat of adapting the adpater. + +* Try removing `sp_executesql_sql_type` all together. Do we have to add more types? +* Did we get the schema cache right? + + + +## LONG TERM + +After we get some tests passing + +* Is `primary_keys(table_name)` performant? Contribute to rails for abstract adapter. +* Check `sql_for_insert` can do without the table regular expresion. +* Do we need the `query_requires_identity_insert` check in `execute`? +* Will we have to add more Data types to our dates and use them in `quoted_date` or `quoted_string` or `_type_cast`? + + +#### Use #without_prepared_statement? + +I think we always send everything thru `sp_executesql`. Consider re-evaulating if there are no `binds` that we get any benefit from this. By doing so we also give the users the ability to turn this off completly. Would be neat to see how our prepared statments actually perform again. + +```ruby +def without_prepared_statement?(binds) + !prepared_statements || binds.empty? +end +``` + +Maybe just quick bail to `do_execute`. Maybe related: + +* [Do not cache prepared statements that are unlikely to have cache hits](https://github.com/rails/rails/commit/cbcdecd2) + + + + +#### Does Find By SQL Work? + +With binds and prepareable? + +```ruby +# Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date] +# Post.find_by_sql ["SELECT body FROM comments WHERE author = :user_id OR approved_by = :user_id", { :user_id => user_id }] +# +def find_by_sql(sql, binds = [], preparable: nil) + result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds, preparable: preparable) +``` From 77e9d6bc86b7fc5b5a1653b93f72148d5b30a7ca Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Wed, 10 Aug 2016 20:39:52 -0400 Subject: [PATCH 24/27] Allow full test suite to run. Debug'able coerced_tests --- test/cases/coerced_tests.rb | 32 +---------------------- test/support/coerceable_test_sqlserver.rb | 6 ++++- 2 files changed, 6 insertions(+), 32 deletions(-) diff --git a/test/cases/coerced_tests.rb b/test/cases/coerced_tests.rb index 9c38b7896..02787e65e 100644 --- a/test/cases/coerced_tests.rb +++ b/test/cases/coerced_tests.rb @@ -103,8 +103,7 @@ module ActiveRecord class BindParameterTest < ActiveRecord::TestCase # Never finds `sql` since we use `EXEC sp_executesql` wrappers. - coerce_tests! :test_binds_are_logged, - :test_binds_are_logged_after_type_cast + coerce_tests! :test_binds_are_logged end end @@ -404,19 +403,6 @@ def test_eager_load_belongs_to_primary_key_quoting_coerced -class BigNumber < ActiveRecord::Base - attribute :value_of_e, Type::SQLServer::Integer.new - attribute :my_house_population, Type::SQLServer::Integer.new -end -class MigrationTest < ActiveRecord::TestCase - - # PENDING: [Rails5.x] Remove coerced tests and use simple symbol types. - coerce_tests! :test_add_table_with_decimals - -end - - - class NamedScopingTest < ActiveRecord::TestCase @@ -671,22 +657,6 @@ class TransactionIsolationTest < ActiveRecord::TestCase end -require 'models/post' -module ActiveRecord - class WhereChainTest < ActiveRecord::TestCase - - coerce_tests! :test_not_eq_with_array_parameter - def test_not_eq_with_array_parameter_coerced - expected = Arel::Nodes::Not.new("title = N'hello'") - relation = Post.where.not(['title = ?', 'hello']) - assert_equal([expected], relation.where_values) - end - - end -end - - - class ViewWithPrimaryKeyTest < ActiveRecord::TestCase diff --git a/test/support/coerceable_test_sqlserver.rb b/test/support/coerceable_test_sqlserver.rb index dc6171ca1..346321539 100644 --- a/test/support/coerceable_test_sqlserver.rb +++ b/test/support/coerceable_test_sqlserver.rb @@ -34,7 +34,11 @@ def coerced_test_warning(method) method = instance_methods(false).select { |m| m =~ method } if method.is_a?(Regexp) Array(method).each do |m| result = undef_method(m) if m && method_defined?(m) - STDOUT.puts "Info: Undefined coerced test: #{self.name}##{m}" unless result.blank? + if result.blank? + STDOUT.puts "Warning: Unfound coerced test: #{self.name}##{m}" + else + STDOUT.puts "Info: Undefined coerced test: #{self.name}##{m}" + end end end From 99aaa7daf5f93b3efa610085cc6eef176682c07c Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Sun, 14 Aug 2016 19:07:03 -0400 Subject: [PATCH 25/27] Misc PR feedback. Thanks @sgrif --- .../connection_adapters/sqlserver/core_ext/explain.rb | 7 +++---- .../connection_adapters/sqlserver/database_statements.rb | 3 +-- test/cases/schema_dumper_test_sqlserver.rb | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) 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 287c31e91..bdacc573e 100644 --- a/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +++ b/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb @@ -5,12 +5,11 @@ module CoreExt module Explain SQLSERVER_STATEMENT_PREFIX = 'EXEC sp_executesql '.freeze - SQLSERVER_PARAM_MATCHER = /@\d+ = (.*)/.freeze - SQLSERVER_NATIONAL_STRING_MATCHER = /N'(.*)'/m.freeze + SQLSERVER_PARAM_MATCHER = /@\d+ = (.*)/ + SQLSERVER_NATIONAL_STRING_MATCHER = /N'(.*)'/m def exec_explain(queries) - unprepared_queries = queries.map do |sql_binds| - sql, binds = sql_binds + unprepared_queries = queries.map do |(sql, binds)| [unprepare_sqlserver_statement(sql), binds] end super(unprepared_queries) diff --git a/lib/active_record/connection_adapters/sqlserver/database_statements.rb b/lib/active_record/connection_adapters/sqlserver/database_statements.rb index 789fdeb9b..9083ead03 100644 --- a/lib/active_record/connection_adapters/sqlserver/database_statements.rb +++ b/lib/active_record/connection_adapters/sqlserver/database_statements.rb @@ -20,8 +20,7 @@ def exec_query(sql, name = 'SQL', binds = [], sqlserver_options = {}) end def exec_insert(sql, name, binds, pk = nil, _sequence_name = nil) - id_insert_table_name = query_requires_identity_insert?(sql) if pk - if id_insert_table_name + if pk && id_insert_table_name = query_requires_identity_insert?(sql) with_identity_insert_enabled(id_insert_table_name) { exec_query(sql, name, binds) } else exec_query(sql, name, binds) diff --git a/test/cases/schema_dumper_test_sqlserver.rb b/test/cases/schema_dumper_test_sqlserver.rb index d8f954036..7b84d8ebb 100644 --- a/test/cases/schema_dumper_test_sqlserver.rb +++ b/test/cases/schema_dumper_test_sqlserver.rb @@ -176,7 +176,7 @@ def assert_line(column_name, options={}) class SchemaLine - LINE_PARSER = %r{t\.(\w+)\s+"(.*?)"[,\s+](.*)}.freeze + LINE_PARSER = %r{t\.(\w+)\s+"(.*?)"[,\s+](.*)} attr_reader :line, :type_method, From a20b52f3205fd416ab83e83783a404765ddb72f7 Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Sat, 20 Aug 2016 16:50:39 -0400 Subject: [PATCH 26/27] Use ByeBug vs Pry. --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 40d0716b9..02eab67df 100644 --- a/Gemfile +++ b/Gemfile @@ -49,9 +49,9 @@ group :tinytds do end group :development do + gem 'byebug' gem 'mocha' gem 'minitest-spec-rails' - gem 'pry' end group :guard do From bec007eae766cbcab10c7ae4597a8657d96290c2 Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Sat, 20 Aug 2016 18:19:05 -0400 Subject: [PATCH 27/27] Ensure object saves maintains attributes for date/time types. --- test/cases/column_test_sqlserver.rb | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/test/cases/column_test_sqlserver.rb b/test/cases/column_test_sqlserver.rb index 1856ae155..954a9707b 100644 --- a/test/cases/column_test_sqlserver.rb +++ b/test/cases/column_test_sqlserver.rb @@ -285,7 +285,9 @@ def assert_obj_set_and_save(attribute, value) obj.date = '0001-01-01' obj.date.must_equal Date.civil(0001, 1, 1) obj.save! - obj.reload.date.must_equal Date.civil(0001, 1, 1) + obj.date.must_equal Date.civil(0001, 1, 1) + obj.reload + obj.date.must_equal Date.civil(0001, 1, 1) # Can keep and return assigned date. assert_obj_set_and_save :date, Date.civil(1972, 04, 14) # Can accept and cast time objects. @@ -312,7 +314,9 @@ def assert_obj_set_and_save(attribute, value) obj.datetime = Time.utc(2010, 01, 01, 12, 34, 56, 3000) obj.datetime.must_equal Time.utc(2010, 01, 01, 12, 34, 56, 3000), "Microseconds were <#{obj.datetime.usec}> vs <3000>" obj.save! - obj.reload.datetime.must_equal Time.utc(2010, 01, 01, 12, 34, 56, 3000), "Microseconds were <#{obj.reload.datetime.usec}> vs <3000>" + obj.datetime.must_equal Time.utc(2010, 01, 01, 12, 34, 56, 3000), "Microseconds were <#{obj.reload.datetime.usec}> vs <3000>" + obj.reload + obj.datetime.must_equal Time.utc(2010, 01, 01, 12, 34, 56, 3000), "Microseconds were <#{obj.reload.datetime.usec}> vs <3000>" # Will cast to true DB value on attribute write, save and return again. obj.datetime = Time.utc(2010, 01, 01, 12, 34, 56, 234567) obj.datetime.must_equal Time.utc(2010, 01, 01, 12, 34, 56, 233000), "Microseconds were <#{obj.datetime.usec}> vs <233000>" @@ -338,7 +342,9 @@ def assert_obj_set_and_save(attribute, value) obj.datetime2_7 = Time.utc(9999, 12, 31, 23, 59, 59, Rational(123456755, 1000)) obj.datetime2_7.must_equal Time.utc(9999, 12, 31, 23, 59, 59, Rational(123456800, 1000)), "Nanoseconds were <#{obj.datetime2_7.nsec}> vs <123456800>" obj.save! - obj.reload.datetime2_7.must_equal Time.utc(9999, 12, 31, 23, 59, 59, Rational(123456800, 1000)), "Nanoseconds were <#{obj.datetime2_7.nsec}> vs <123456800>" + obj.datetime2_7.must_equal Time.utc(9999, 12, 31, 23, 59, 59, Rational(123456800, 1000)), "Nanoseconds were <#{obj.datetime2_7.nsec}> vs <123456800>" + obj.reload + obj.datetime2_7.must_equal Time.utc(9999, 12, 31, 23, 59, 59, Rational(123456800, 1000)), "Nanoseconds were <#{obj.datetime2_7.nsec}> vs <123456800>" # Can save small fraction nanosecond precisoins and return again. obj.datetime2_7 = Time.utc(2008, 6, 21, 13, 30, 0, Rational(15020, 1000)) obj.datetime2_7.must_equal Time.utc(2008, 6, 21, 13, 30, 0, Rational(15000, 1000)), "Nanoseconds were <#{obj.datetime2_7.nsec}> vs <15000>" @@ -384,7 +390,9 @@ def assert_obj_set_and_save(attribute, value) # Can save 100 nanosecond precisoins and return again. obj.datetimeoffset_7 = Time.new(2010, 01, 01, 12, 34, 56, +18000).change(nsec: 123456755) obj.datetimeoffset_7.must_equal Time.new(2010, 01, 01, 12, 34, 56, +18000).change(nsec: 123456800), "Nanoseconds were <#{obj.datetimeoffset_7.nsec}> vs <123456800>" - obj.save! ; obj.reload + obj.save! + obj.datetimeoffset_7.must_equal Time.new(2010, 01, 01, 12, 34, 56, +18000).change(nsec: 123456800), "Nanoseconds were <#{obj.datetimeoffset_7.nsec}> vs <123456800>" + obj.reload obj.datetimeoffset_7.must_equal Time.new(2010, 01, 01, 12, 34, 56, +18000).change(nsec: 123456800), "Nanoseconds were <#{obj.datetimeoffset_7.nsec}> vs <123456800>" # With other precisions. time = ActiveSupport::TimeZone['America/Los_Angeles'].local 2010, 12, 31, 23, 59, 59, Rational(123456755, 1000) @@ -419,9 +427,11 @@ def assert_obj_set_and_save(attribute, value) type.scale.must_equal nil # Will remove fractional seconds and return again. obj.smalldatetime = Time.utc(2078, 06, 05, 4, 20, 00, 3000) - obj.smalldatetime.must_equal Time.utc(2078, 06, 05, 4, 20, 00, 0), "Microseconds were <#{obj.smalldatetime.usec}> vs <0>" + obj.smalldatetime.must_equal Time.utc(2078, 06, 05, 4, 20, 00, 0), "Microseconds were <#{obj.smalldatetime.usec}> vs <0>" obj.save! - obj.reload.smalldatetime.must_equal Time.utc(2078, 06, 05, 4, 20, 00, 0), "Microseconds were <#{obj.reload.smalldatetime.usec}> vs <0>" + obj.smalldatetime.must_equal Time.utc(2078, 06, 05, 4, 20, 00, 0), "Microseconds were <#{obj.reload.smalldatetime.usec}> vs <0>" + obj.reload + obj.smalldatetime.must_equal Time.utc(2078, 06, 05, 4, 20, 00, 0), "Microseconds were <#{obj.reload.smalldatetime.usec}> vs <0>" end it 'time(7)' do