Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion lib/active_record/connection_adapters/sqlserver/quoting.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
109 changes: 74 additions & 35 deletions lib/active_record/connection_adapters/sqlserver_column.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions test/cases/adapter_test_sqlserver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
35 changes: 35 additions & 0 deletions test/cases/rake_test_sqlserver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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