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
Original file line number Diff line number Diff line change
Expand Up @@ -305,14 +305,15 @@ def select(sql, name = nil, binds = [])
end

def sql_for_insert(sql, pk, id_value, sequence_name, binds)
sql = "#{sql}; SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident"# unless binds.empty?
sql =
if pk
sql.insert(sql.index(/ (DEFAULT )?VALUES/), " OUTPUT inserted.#{pk}")
else
"#{sql}; SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident"
end
super
end

def last_inserted_id(result)
super || select_value('SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident')
end

# === SQLServer Specific ======================================== #

def valid_isolation_levels
Expand Down
11 changes: 11 additions & 0 deletions lib/active_record/connection_adapters/sqlserver/quoting.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ def quote(value, column = nil)
value.to_i.to_s
elsif column && column.type == :binary
column.class.string_to_binary(value)
elsif column && [:uuid, :uniqueidentifier].include?(column.type)
"'#{quote_string(value)}'"
elsif value.is_utf8? || (column && column.type == :string)
"#{quoted_string_prefix}'#{quote_string(value)}'"
else
Expand Down Expand Up @@ -52,6 +54,15 @@ def quote_database_name(name)
schema_cache.quote_name(name, false)
end

# Does not quote function default values for UUID columns
def quote_default_value(value, column)
if column.type == :uuid && value =~ /\(\)/
value
else
quote(value)
end
end

def substitute_at(column, index)
if column.respond_to?(:sql_type) && column.sql_type == 'timestamp'
nil
Expand Down
29 changes: 29 additions & 0 deletions lib/active_record/connection_adapters/sqlserver/schema_creation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module ActiveRecord
module ConnectionAdapters
module Sqlserver
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 add_column_options!(sql, options)
column = options.fetch(:column) { return super }
if [:uniqueidentifier, :uuid].include?(column.type) && options[:default] =~ /\(\)/
sql << " DEFAULT #{options.delete(:default)}"
super
else
super
end
end
end
end
end
end

Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,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(options[:default])} 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_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 << "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(options[:default])} 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_value(options[:default], column_object)} FOR #{quote_column_name(column_name)}"
end

# Add any removed indexes back
Expand All @@ -96,7 +96,9 @@ def change_column(table_name, column_name, type, options = {})

def change_column_default(table_name, column_name, default)
remove_default_constraint(table_name, column_name)
do_execute "ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{default_constraint_name(table_name, column_name)} DEFAULT #{quote(default)} FOR #{quote_column_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)
end

def rename_column(table_name, column_name, new_column_name)
Expand Down Expand Up @@ -165,6 +167,7 @@ def initialize_native_database_types
date: { name: native_date_database_type },
binary: { name: native_binary_database_type },
boolean: { name: 'bit' },
uuid: { name: 'uniqueidentifier'},
# These are custom types that may move somewhere else for good schema_dumper.rb hacking to output them.
char: { name: 'char' },
varchar_max: { name: 'varchar(max)' },
Expand Down Expand Up @@ -386,6 +389,12 @@ def set_identity_insert(table_name, enable = true)
def identity_column(table_name)
schema_cache.columns(table_name).find(&:is_identity?)
end

private

def create_table_definition(name, temporary, options)
TableDefinition.new native_database_types, name, temporary, options
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module ActiveRecord
module ConnectionAdapters
module Sqlserver
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
def uuid(name, options = {})
column(name, 'uniqueidentifier', options)
end

def primary_key(name, type = :primary_key, options = {})
return super unless type == :uuid
options[:default] = options.fetch(:default, 'NEWID()')
options[:primary_key] = true
column name, type, options
end

def column(name, type = nil, options = {})
super
self
end
end
end
end
end


13 changes: 10 additions & 3 deletions lib/active_record/connection_adapters/sqlserver_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
require 'active_record/connection_adapters/sqlserver/database_statements'
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/showplan'
require 'active_record/connection_adapters/sqlserver/table_definition'
require 'active_record/connection_adapters/sqlserver/quoting'
require 'active_record/connection_adapters/sqlserver/utils'

Expand Down Expand Up @@ -140,7 +142,7 @@ def simplified_type(field_type)
when /money/i then :decimal
when /image/i then :binary
when /bit/i then :boolean
when /uniqueidentifier/i then :string
when /uniqueidentifier/i then :uuid
when /datetime/i then simplified_datetime
when /varchar\(max\)/ then :text
when /timestamp/ then :binary
Expand Down Expand Up @@ -301,14 +303,18 @@ def reset!
# === Abstract Adapter (Misc Support) =========================== #

def pk_and_sequence_for(table_name)
idcol = identity_column(table_name)
idcol ? [idcol.name, nil] : nil
pk = primary_key(table_name)
pk ? [pk, nil] : nil
end

def primary_key(table_name)
identity_column(table_name).try(:name) || schema_cache.columns(table_name).find(&:is_primary?).try(:name)
end

def schema_creation
Sqlserver::SchemaCreation.new self
end

# === SQLServer Specific (DB Reflection) ======================== #

def sqlserver?
Expand Down Expand Up @@ -525,3 +531,4 @@ def auto_reconnected?
end # class SQLServerAdapter < AbstractAdapter
end # module ConnectionAdapters
end # module ActiveRecord

127 changes: 127 additions & 0 deletions test/cases/uuid_test_sqlserver.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
require 'cases/sqlserver_helper'

class SQLServerUUIDTest < ActiveRecord::TestCase
class UUID < ActiveRecord::Base
self.table_name = 'sql_server_uuids'
end

def setup
@connection = ActiveRecord::Base.connection

@connection.reconnect!

@connection.transaction do
@connection.create_table('sql_server_uuids', id: :uuid, default: 'NEWSEQUENTIALID()') do |t|
t.string 'name'
t.uuid 'other_uuid', default: 'NEWID()'
end
end
end

def teardown
@connection.execute "IF OBJECT_ID('sql_server_uuids', 'U') IS NOT NULL DROP TABLE sql_server_uuids"
end

def test_id_is_uuid
assert_equal :uuid, UUID.columns_hash['id'].type
assert UUID.primary_key
end

def test_id_has_a_default
u = UUID.create
assert_not_nil u.id
end

def test_auto_create_uuid
u = UUID.create
u.reload
assert_not_nil u.other_uuid
end

def test_pk_and_sequence_for_uuid_primary_key
pk, seq = @connection.pk_and_sequence_for('sql_server_uuids')
assert_equal 'id', pk
assert_equal nil, seq
end

def primary_key_for_uuid_primary_key
assert_equal 'id', @connection.primary_key('sql_server_uuids')
end

def test_change_column_default
@connection.add_column :sql_server_uuids, :thingy, :uuid, null: false, default: "NEWSEQUENTIALID()"
UUID.reset_column_information
column = UUID.columns.find { |c| c.name == 'thingy' }
assert_equal "newsequentialid()", column.default_function

@connection.change_column :sql_server_uuids, :thingy, :uuid, null: false, default: "NEWID()"

UUID.reset_column_information
column = UUID.columns.find { |c| c.name == 'thingy' }
assert_equal "newid()", column.default_function
end
end

class SQLServerUUIDTestNilDefault < ActiveRecord::TestCase
class UUID < ActiveRecord::Base
self.table_name = 'sql_server_uuids'
end

def setup
@connection = ActiveRecord::Base.connection

@connection.reconnect!

@connection.transaction do
@connection.create_table('sql_server_uuids', id: false) do |t|
t.primary_key :id, :uuid, default: nil
t.string 'name'
end
end
end

def teardown
@connection.execute "IF OBJECT_ID('sql_server_uuids', 'U') IS NOT NULL DROP TABLE sql_server_uuids"
end

end

class SQLServerUUIDTestInverseOf < ActiveRecord::TestCase
class UuidPost < ActiveRecord::Base
self.table_name = 'sql_server_uuid_posts'
has_many :uuid_comments, inverse_of: :uuid_post
end

class UuidComment < ActiveRecord::Base
self.table_name = 'sql_server_uuid_comments'
belongs_to :uuid_post
end

def setup
@connection = ActiveRecord::Base.connection
@connection.reconnect!

@connection.transaction do
@connection.create_table('sql_server_uuid_posts', id: :uuid) do |t|
t.string 'title'
end
@connection.create_table('sql_server_uuid_comments', id: :uuid) do |t|
t.uuid :uuid_post_id, default: 'NEWID()'
t.string 'content'
end
end
end

def teardown
@connection.transaction do
@connection.execute "IF OBJECT_ID('sql_server_uuid_comments', 'U') IS NOT NULL DROP TABLE sql_server_uuid_comments"
@connection.execute "IF OBJECT_ID('sql_server_uuid_posts', 'U') IS NOT NULL DROP TABLE sql_server_uuid_posts"
end
end

def test_collection_association_with_uuid
post = UuidPost.create!
comment = post.uuid_comments.create!
assert post.uuid_comments.find(comment.id)
end
end