Skip to content

Commit e98589a

Browse files
committed
Merge pull request #328 from agios/uuid
Rails 4 compatible UUID Support
2 parents adfdf7e + 6e22701 commit e98589a

File tree

7 files changed

+220
-11
lines changed

7 files changed

+220
-11
lines changed

lib/active_record/connection_adapters/sqlserver/database_statements.rb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -305,14 +305,15 @@ def select(sql, name = nil, binds = [])
305305
end
306306

307307
def sql_for_insert(sql, pk, id_value, sequence_name, binds)
308-
sql = "#{sql}; SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident"# unless binds.empty?
308+
sql =
309+
if pk
310+
sql.insert(sql.index(/ (DEFAULT )?VALUES/), " OUTPUT inserted.#{pk}")
311+
else
312+
"#{sql}; SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident"
313+
end
309314
super
310315
end
311316

312-
def last_inserted_id(result)
313-
super || select_value('SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident')
314-
end
315-
316317
# === SQLServer Specific ======================================== #
317318

318319
def valid_isolation_levels

lib/active_record/connection_adapters/sqlserver/quoting.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ def quote(value, column = nil)
1212
value.to_i.to_s
1313
elsif column && column.type == :binary
1414
column.class.string_to_binary(value)
15+
elsif column && [:uuid, :uniqueidentifier].include?(column.type)
16+
"'#{quote_string(value)}'"
1517
elsif value.is_utf8? || (column && column.type == :string)
1618
"#{quoted_string_prefix}'#{quote_string(value)}'"
1719
else
@@ -52,6 +54,15 @@ def quote_database_name(name)
5254
schema_cache.quote_name(name, false)
5355
end
5456

57+
# Does not quote function default values for UUID columns
58+
def quote_default_value(value, column)
59+
if column.type == :uuid && value =~ /\(\)/
60+
value
61+
else
62+
quote(value)
63+
end
64+
end
65+
5566
def substitute_at(column, index)
5667
if column.respond_to?(:sql_type) && column.sql_type == 'timestamp'
5768
nil
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
module ActiveRecord
2+
module ConnectionAdapters
3+
module Sqlserver
4+
class SchemaCreation < AbstractAdapter::SchemaCreation
5+
private
6+
7+
def visit_ColumnDefinition(o)
8+
sql = super
9+
if o.primary_key? && o.type == :uuid
10+
sql << " PRIMARY KEY "
11+
add_column_options!(sql, column_options(o))
12+
end
13+
sql
14+
end
15+
16+
def add_column_options!(sql, options)
17+
column = options.fetch(:column) { return super }
18+
if [:uniqueidentifier, :uuid].include?(column.type) && options[:default] =~ /\(\)/
19+
sql << " DEFAULT #{options.delete(:default)}"
20+
super
21+
else
22+
super
23+
end
24+
end
25+
end
26+
end
27+
end
28+
end
29+

lib/active_record/connection_adapters/sqlserver/schema_statements.rb

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,11 @@ def change_column(table_name, column_name, type, options = {})
7575
indexes = indexes(table_name).select { |index| index.columns.include?(column_name.to_s) }
7676
remove_indexes(table_name, column_name)
7777
end
78-
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?
78+
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?
7979
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])}"
8080
sql_commands[-1] << ' NOT NULL' if !options[:null].nil? && options[:null] == false
8181
if options_include_default?(options)
82-
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)}"
82+
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)}"
8383
end
8484

8585
# Add any removed indexes back
@@ -91,7 +91,9 @@ def change_column(table_name, column_name, type, options = {})
9191

9292
def change_column_default(table_name, column_name, default)
9393
remove_default_constraint(table_name, column_name)
94-
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)}"
94+
column_object = schema_cache.columns(table_name).find { |c| c.name.to_s == column_name.to_s }
95+
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)}"
96+
schema_cache.clear_table_cache!(table_name)
9597
end
9698

9799
def rename_column(table_name, column_name, new_column_name)
@@ -160,6 +162,7 @@ def initialize_native_database_types
160162
date: { name: native_date_database_type },
161163
binary: { name: native_binary_database_type },
162164
boolean: { name: 'bit' },
165+
uuid: { name: 'uniqueidentifier'},
163166
# These are custom types that may move somewhere else for good schema_dumper.rb hacking to output them.
164167
char: { name: 'char' },
165168
varchar_max: { name: 'varchar(max)' },
@@ -381,6 +384,12 @@ def set_identity_insert(table_name, enable = true)
381384
def identity_column(table_name)
382385
schema_cache.columns(table_name).find(&:is_identity?)
383386
end
387+
388+
private
389+
390+
def create_table_definition(name, temporary, options)
391+
TableDefinition.new native_database_types, name, temporary, options
392+
end
384393
end
385394
end
386395
end
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
module ActiveRecord
2+
module ConnectionAdapters
3+
module Sqlserver
4+
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
5+
def uuid(name, options = {})
6+
column(name, 'uniqueidentifier', options)
7+
end
8+
9+
def primary_key(name, type = :primary_key, options = {})
10+
return super unless type == :uuid
11+
options[:default] = options.fetch(:default, 'NEWID()')
12+
options[:primary_key] = true
13+
column name, type, options
14+
end
15+
16+
def column(name, type = nil, options = {})
17+
super
18+
self
19+
end
20+
end
21+
end
22+
end
23+
end
24+
25+

lib/active_record/connection_adapters/sqlserver_adapter.rb

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
require 'active_record/connection_adapters/sqlserver/database_statements'
1515
require 'active_record/connection_adapters/sqlserver/errors'
1616
require 'active_record/connection_adapters/sqlserver/schema_cache'
17+
require 'active_record/connection_adapters/sqlserver/schema_creation'
1718
require 'active_record/connection_adapters/sqlserver/schema_statements'
1819
require 'active_record/connection_adapters/sqlserver/showplan'
20+
require 'active_record/connection_adapters/sqlserver/table_definition'
1921
require 'active_record/connection_adapters/sqlserver/quoting'
2022
require 'active_record/connection_adapters/sqlserver/utils'
2123

@@ -140,7 +142,7 @@ def simplified_type(field_type)
140142
when /money/i then :decimal
141143
when /image/i then :binary
142144
when /bit/i then :boolean
143-
when /uniqueidentifier/i then :string
145+
when /uniqueidentifier/i then :uuid
144146
when /datetime/i then simplified_datetime
145147
when /varchar\(max\)/ then :text
146148
when /timestamp/ then :binary
@@ -305,14 +307,18 @@ def reset!
305307
# === Abstract Adapter (Misc Support) =========================== #
306308

307309
def pk_and_sequence_for(table_name)
308-
idcol = identity_column(table_name)
309-
idcol ? [idcol.name, nil] : nil
310+
pk = primary_key(table_name)
311+
pk ? [pk, nil] : nil
310312
end
311313

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

318+
def schema_creation
319+
Sqlserver::SchemaCreation.new self
320+
end
321+
316322
# === SQLServer Specific (DB Reflection) ======================== #
317323

318324
def sqlserver?
@@ -529,3 +535,4 @@ def auto_reconnected?
529535
end # class SQLServerAdapter < AbstractAdapter
530536
end # module ConnectionAdapters
531537
end # module ActiveRecord
538+

test/cases/uuid_test_sqlserver.rb

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
require 'cases/sqlserver_helper'
2+
3+
class SQLServerUUIDTest < ActiveRecord::TestCase
4+
class UUID < ActiveRecord::Base
5+
self.table_name = 'sql_server_uuids'
6+
end
7+
8+
def setup
9+
@connection = ActiveRecord::Base.connection
10+
11+
@connection.reconnect!
12+
13+
@connection.transaction do
14+
@connection.create_table('sql_server_uuids', id: :uuid, default: 'NEWSEQUENTIALID()') do |t|
15+
t.string 'name'
16+
t.uuid 'other_uuid', default: 'NEWID()'
17+
end
18+
end
19+
end
20+
21+
def teardown
22+
@connection.execute "IF OBJECT_ID('sql_server_uuids', 'U') IS NOT NULL DROP TABLE sql_server_uuids"
23+
end
24+
25+
def test_id_is_uuid
26+
assert_equal :uuid, UUID.columns_hash['id'].type
27+
assert UUID.primary_key
28+
end
29+
30+
def test_id_has_a_default
31+
u = UUID.create
32+
assert_not_nil u.id
33+
end
34+
35+
def test_auto_create_uuid
36+
u = UUID.create
37+
u.reload
38+
assert_not_nil u.other_uuid
39+
end
40+
41+
def test_pk_and_sequence_for_uuid_primary_key
42+
pk, seq = @connection.pk_and_sequence_for('sql_server_uuids')
43+
assert_equal 'id', pk
44+
assert_equal nil, seq
45+
end
46+
47+
def primary_key_for_uuid_primary_key
48+
assert_equal 'id', @connection.primary_key('sql_server_uuids')
49+
end
50+
51+
def test_change_column_default
52+
@connection.add_column :sql_server_uuids, :thingy, :uuid, null: false, default: "NEWSEQUENTIALID()"
53+
UUID.reset_column_information
54+
column = UUID.columns.find { |c| c.name == 'thingy' }
55+
assert_equal "newsequentialid()", column.default_function
56+
57+
@connection.change_column :sql_server_uuids, :thingy, :uuid, null: false, default: "NEWID()"
58+
59+
UUID.reset_column_information
60+
column = UUID.columns.find { |c| c.name == 'thingy' }
61+
assert_equal "newid()", column.default_function
62+
end
63+
end
64+
65+
class SQLServerUUIDTestNilDefault < ActiveRecord::TestCase
66+
class UUID < ActiveRecord::Base
67+
self.table_name = 'sql_server_uuids'
68+
end
69+
70+
def setup
71+
@connection = ActiveRecord::Base.connection
72+
73+
@connection.reconnect!
74+
75+
@connection.transaction do
76+
@connection.create_table('sql_server_uuids', id: false) do |t|
77+
t.primary_key :id, :uuid, default: nil
78+
t.string 'name'
79+
end
80+
end
81+
end
82+
83+
def teardown
84+
@connection.execute "IF OBJECT_ID('sql_server_uuids', 'U') IS NOT NULL DROP TABLE sql_server_uuids"
85+
end
86+
87+
end
88+
89+
class SQLServerUUIDTestInverseOf < ActiveRecord::TestCase
90+
class UuidPost < ActiveRecord::Base
91+
self.table_name = 'sql_server_uuid_posts'
92+
has_many :uuid_comments, inverse_of: :uuid_post
93+
end
94+
95+
class UuidComment < ActiveRecord::Base
96+
self.table_name = 'sql_server_uuid_comments'
97+
belongs_to :uuid_post
98+
end
99+
100+
def setup
101+
@connection = ActiveRecord::Base.connection
102+
@connection.reconnect!
103+
104+
@connection.transaction do
105+
@connection.create_table('sql_server_uuid_posts', id: :uuid) do |t|
106+
t.string 'title'
107+
end
108+
@connection.create_table('sql_server_uuid_comments', id: :uuid) do |t|
109+
t.uuid :uuid_post_id, default: 'NEWID()'
110+
t.string 'content'
111+
end
112+
end
113+
end
114+
115+
def teardown
116+
@connection.transaction do
117+
@connection.execute "IF OBJECT_ID('sql_server_uuid_comments', 'U') IS NOT NULL DROP TABLE sql_server_uuid_comments"
118+
@connection.execute "IF OBJECT_ID('sql_server_uuid_posts', 'U') IS NOT NULL DROP TABLE sql_server_uuid_posts"
119+
end
120+
end
121+
122+
def test_collection_association_with_uuid
123+
post = UuidPost.create!
124+
comment = post.uuid_comments.create!
125+
assert post.uuid_comments.find(comment.id)
126+
end
127+
end

0 commit comments

Comments
 (0)