diff --git a/CHANGELOG.md b/CHANGELOG.md index c55e893e2..70db1d3d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ #### Fixed - [#933](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/933) Conditionally apply SQL Server monkey patches to ActiveRecord so that it is safe to use this gem alongside other database adapters (e.g. PostgreSQL) in a multi-database Rails app +- [#935](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/935) Fix schema cache generation + (**breaking change**) #### Changed diff --git a/lib/active_record/connection_adapters/sqlserver/quoting.rb b/lib/active_record/connection_adapters/sqlserver/quoting.rb index 09d8226bc..073e5860e 100644 --- a/lib/active_record/connection_adapters/sqlserver/quoting.rb +++ b/lib/active_record/connection_adapters/sqlserver/quoting.rb @@ -17,7 +17,8 @@ def fetch_type_metadata(sql_type, sqlserver_options = {}) precision: cast_type.precision, scale: cast_type.scale ) - SQLServer::TypeMetadata.new(simple_type, sqlserver_options: sqlserver_options) + + SQLServer::TypeMetadata.new(simple_type, **sqlserver_options) end def quote_string(s) diff --git a/lib/active_record/connection_adapters/sqlserver/schema_statements.rb b/lib/active_record/connection_adapters/sqlserver/schema_statements.rb index 641a09624..427454fde 100644 --- a/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +++ b/lib/active_record/connection_adapters/sqlserver/schema_statements.rb @@ -84,7 +84,7 @@ def columns(table_name) end def new_column(name, default, sql_type_metadata, null, default_function = nil, collation = nil, comment = nil, sqlserver_options = {}) - SQLServerColumn.new( + SQLServer::Column.new( name, default, sql_type_metadata, diff --git a/lib/active_record/connection_adapters/sqlserver/sql_type_metadata.rb b/lib/active_record/connection_adapters/sqlserver/sql_type_metadata.rb index ebcacc5cd..72abfd584 100644 --- a/lib/active_record/connection_adapters/sqlserver/sql_type_metadata.rb +++ b/lib/active_record/connection_adapters/sqlserver/sql_type_metadata.rb @@ -8,24 +8,33 @@ class TypeMetadata < DelegateClass(SqlTypeMetadata) include Deduplicable - attr_reader :sqlserver_options + attr_reader :is_identity, :is_primary, :table_name, :ordinal_position - def initialize(type_metadata, sqlserver_options: nil) + def initialize(type_metadata, is_identity: nil, is_primary: nil, table_name: nil, ordinal_position: nil) super(type_metadata) - @sqlserver_options = sqlserver_options + @is_identity = is_identity + @is_primary = is_primary + @table_name = table_name + @ordinal_position = ordinal_position end def ==(other) other.is_a?(TypeMetadata) && __getobj__ == other.__getobj__ && - sqlserver_options == other.sqlserver_options + is_identity == other.is_identity && + is_primary == other.is_primary && + table_name == other.table_name && + ordinal_position == other.ordinal_position end alias eql? == def hash TypeMetadata.hash ^ __getobj__.hash ^ - sqlserver_options.hash + is_identity.hash ^ + is_primary.hash ^ + table_name.hash ^ + ordinal_position.hash end private diff --git a/lib/active_record/connection_adapters/sqlserver_column.rb b/lib/active_record/connection_adapters/sqlserver_column.rb index 8cfe709c3..ffaf60a2a 100644 --- a/lib/active_record/connection_adapters/sqlserver_column.rb +++ b/lib/active_record/connection_adapters/sqlserver_column.rb @@ -2,48 +2,87 @@ module ActiveRecord module ConnectionAdapters - class SQLServerColumn < Column - def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation: nil, comment: nil, **sqlserver_options) - @sqlserver_options = sqlserver_options - super - end + module SQLServer + class Column < ConnectionAdapters::Column + delegate :is_identity, :is_primary, :table_name, :ordinal_position, to: :sql_type_metadata - def is_identity? - @sqlserver_options[:is_identity] - end + def initialize(*, is_identity: nil, is_primary: nil, table_name: nil, ordinal_position: nil, **) + super + @is_identity = is_identity + @is_primary = is_primary + @table_name = table_name + @ordinal_position = ordinal_position + end - def is_primary? - @sqlserver_options[:is_primary] - end + def is_identity? + is_identity + end - def table_name - @sqlserver_options[:table_name] - end + def is_primary? + is_primary + end - def is_utf8? - sql_type =~ /nvarchar|ntext|nchar/i - end + def is_utf8? + sql_type =~ /nvarchar|ntext|nchar/i + end - def case_sensitive? - collation && collation.match(/_CS/) - end + def case_sensitive? + collation && collation.match(/_CS/) + end - private - - # In the Rails version of this method there is an assumption that the `default` value will always be a - # `String` class, which must be true for the MySQL/PostgreSQL/SQLite adapters. However, in the SQL Server - # adapter the `default` value can also be Boolean/Date/Time/etc. Changed the implementation of this method - # to handle non-String `default` objects. - def deduplicated - @name = -name - @sql_type_metadata = sql_type_metadata.deduplicate if sql_type_metadata - @default = (default.is_a?(String) ? -default : default.dup.freeze) if default - @default_function = -default_function if default_function - @collation = -collation if collation - @comment = -comment if comment - - freeze + def init_with(coder) + @is_identity = coder["is_identity"] + @is_primary = coder["is_primary"] + @table_name = coder["table_name"] + @ordinal_position = coder["ordinal_position"] + super + end + + def encode_with(coder) + coder["is_identity"] = @is_identity + coder["is_primary"] = @is_primary + coder["table_name"] = @table_name + coder["ordinal_position"] = @ordinal_position + super + end + + def ==(other) + other.is_a?(Column) && + super && + is_identity? == other.is_identity? && + is_primary? == other.is_primary? && + table_name == other.table_name && + ordinal_position == other.ordinal_position + end + alias :eql? :== + + def hash + Column.hash ^ + super.hash ^ + is_identity?.hash ^ + is_primary?.hash ^ + table_name.hash ^ + ordinal_position.hash + end + + private + + # In the Rails version of this method there is an assumption that the `default` value will always be a + # `String` class, which must be true for the MySQL/PostgreSQL/SQLite adapters. However, in the SQL Server + # adapter the `default` value can also be Boolean/Date/Time/etc. Changed the implementation of this method + # to handle non-String `default` objects. + def deduplicated + @name = -name + @sql_type_metadata = sql_type_metadata.deduplicate if sql_type_metadata + @default = (default.is_a?(String) ? -default : default.dup.freeze) if default + @default_function = -default_function if default_function + @collation = -collation if collation + @comment = -comment if comment + freeze + end end + + SQLServerColumn = SQLServer::Column end end end diff --git a/test/cases/adapter_test_sqlserver.rb b/test/cases/adapter_test_sqlserver.rb index a0ea7faf7..c56954672 100644 --- a/test/cases/adapter_test_sqlserver.rb +++ b/test/cases/adapter_test_sqlserver.rb @@ -377,7 +377,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase assert !SSTestCustomersView.columns.blank? assert_equal columns.size, SSTestCustomersView.columns.size columns.each do |colname| - assert_instance_of ActiveRecord::ConnectionAdapters::SQLServerColumn, + assert_instance_of ActiveRecord::ConnectionAdapters::SQLServer::Column, SSTestCustomersView.columns_hash[colname], "Column name #{colname.inspect} was not found in these columns #{SSTestCustomersView.columns.map(&:name).inspect}" end @@ -404,7 +404,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase assert !SSTestStringDefaultsView.columns.blank? assert_equal columns.size, SSTestStringDefaultsView.columns.size columns.each do |colname| - assert_instance_of ActiveRecord::ConnectionAdapters::SQLServerColumn, + assert_instance_of ActiveRecord::ConnectionAdapters::SQLServer::Column, SSTestStringDefaultsView.columns_hash[colname], "Column name #{colname.inspect} was not found in these columns #{SSTestStringDefaultsView.columns.map(&:name).inspect}" end diff --git a/test/cases/rake_test_sqlserver.rb b/test/cases/rake_test_sqlserver.rb index 87b363a20..1195713d2 100644 --- a/test/cases/rake_test_sqlserver.rb +++ b/test/cases/rake_test_sqlserver.rb @@ -156,3 +156,38 @@ class SQLServerRakeStructureDumpLoadTest < SQLServerRakeTest _(connection.tables).must_include "users" end end + +class SQLServerRakeSchemaCacheDumpLoadTest < SQLServerRakeTest + let(:filename) { File.join ARTest::SQLServer.test_root_sqlserver, "schema_cache.yml" } + let(:filedata) { File.read(filename) } + + before do + quietly { db_tasks.create(configuration) } + + connection.create_table :users, force: true do |t| + t.string :name, null: false + end + end + + after do + FileUtils.rm_rf(filename) + end + + it "dumps schema cache with SQL Server metadata" do + quietly { db_tasks.dump_schema_cache connection, filename } + + schema_cache = YAML.load(File.read(filename)) + + col_id, col_name = connection.schema_cache.columns("users") + + assert col_id.is_identity + assert col_id.is_primary + assert_equal col_id.ordinal_position, 1 + assert_equal col_id.table_name, "users" + + assert_not col_name.is_identity + assert_not col_name.is_primary + assert_equal col_name.ordinal_position, 2 + assert_equal col_name.table_name, "users" + end +end