Skip to content

Commit 3ed2041

Browse files
committed
Single PORO for decomposing and quoting SQL Server names/identifiers.
Please read this article for all details: http://technet.microsoft.com/en-us/library/ms187879(v=sql.105).aspx
1 parent c14d949 commit 3ed2041

File tree

9 files changed

+212
-95
lines changed

9 files changed

+212
-95
lines changed

‎lib/active_record/connection_adapters/sqlserver/database_statements.rb

+6-5
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ def execute_procedure(proc_name, *variables)
116116

117117
def use_database(database = nil)
118118
return if sqlserver_azure?
119-
database ||= @connection_options[:database]
120-
do_execute "USE #{quote_database_name(database)}" unless database.blank?
119+
name = Sqlserver::Utils.extract_identifiers(database || @connection_options[:database])
120+
do_execute "USE #{name}" unless database.blank?
121121
end
122122

123123
def user_options
@@ -258,7 +258,7 @@ def drop_database(database)
258258
retry_count = 0
259259
max_retries = 1
260260
begin
261-
do_execute "DROP DATABASE #{quote_database_name(database)}"
261+
do_execute "DROP DATABASE #{Sqlserver::Utils.extract_identifiers(database)}"
262262
rescue ActiveRecord::StatementInvalid => err
263263
if err.message =~ /because it is currently in use/i
264264
raise if retry_count >= max_retries
@@ -274,10 +274,11 @@ def drop_database(database)
274274
end
275275

276276
def create_database(database, collation = @connection_options[:collation])
277+
name = Sqlserver::Utils.extract_identifiers(database)
277278
if collation
278-
do_execute "CREATE DATABASE #{quote_database_name(database)} COLLATE #{collation}"
279+
do_execute "CREATE DATABASE #{name} COLLATE #{collation}"
279280
else
280-
do_execute "CREATE DATABASE #{quote_database_name(database)}"
281+
do_execute "CREATE DATABASE #{name}"
281282
end
282283
end
283284

‎lib/active_record/connection_adapters/sqlserver/quoting.rb

+3-7
Original file line numberDiff line numberDiff line change
@@ -38,22 +38,18 @@ def quoted_string_prefix
3838
QUOTED_STRING_PREFIX
3939
end
4040

41-
def quote_string(string)
42-
string.to_s.gsub(/\'/, "''")
41+
def quote_string(s)
42+
Sqlserver::Utils.quote_string(s)
4343
end
4444

4545
def quote_column_name(name)
46-
schema_cache.quote_name(name)
46+
Sqlserver::Utils.extract_identifiers(name).object_quoted
4747
end
4848

4949
def quote_table_name(name)
5050
quote_column_name(name)
5151
end
5252

53-
def quote_database_name(name)
54-
schema_cache.quote_name(name, false)
55-
end
56-
5753
# Does not quote function default values for UUID columns
5854
def quote_default_value(value, column)
5955
if column.type == :uuid && value =~ /\(\)/

‎lib/active_record/connection_adapters/sqlserver/schema_cache.rb

+3-16
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ module ActiveRecord
22
module ConnectionAdapters
33
module Sqlserver
44
class SchemaCache < ActiveRecord::ConnectionAdapters::SchemaCache
5+
56
attr_reader :view_information
67

78
def initialize(conn)
89
super
910
@table_names = nil
1011
@view_names = nil
1112
@view_information = {}
12-
@quoted_names = {}
1313
end
1414

1515
# Superclass Overrides
@@ -26,7 +26,6 @@ def clear!
2626
@table_names = nil
2727
@view_names = nil
2828
@view_information.clear
29-
@quoted_names.clear
3029
end
3130

3231
def clear_table_cache!(table_name)
@@ -65,25 +64,13 @@ def view_information(table_name)
6564
@view_information[key] = connection.send(:view_information, table_name)
6665
end
6766

68-
def quote_name(name, split_on_dots = true)
69-
return @quoted_names[name] if @quoted_names.key? name
70-
71-
@quoted_names[name] = if split_on_dots
72-
name.to_s.split('.').map { |n| quote_name_part(n) }.join('.')
73-
else
74-
quote_name_part(name.to_s)
75-
end
76-
end
7767

7868
private
7969

80-
def quote_name_part(part)
81-
part =~ /^\[.*\]$/ ? part : "[#{part.to_s.gsub(']', ']]')}]"
82-
end
83-
8470
def table_name_key(table_name)
85-
Utils.unqualify_table_name(table_name)
71+
Sqlserver::Utils.extract_identifiers(table_name).object
8672
end
73+
8774
end
8875
end
8976
end

‎lib/active_record/connection_adapters/sqlserver/schema_statements.rb

+6-6
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def tables(table_type = 'BASE TABLE')
1212

1313
def table_exists?(table_name)
1414
return false if table_name.blank?
15-
unquoted_table_name = Utils.unqualify_table_name(table_name)
15+
unquoted_table_name = Sqlserver::Utils.extract_identifiers(table_name).object
1616
super || tables.include?(unquoted_table_name) || views.include?(unquoted_table_name)
1717
end
1818

@@ -181,10 +181,10 @@ def initialize_native_database_types
181181
end
182182

183183
def column_definitions(table_name)
184-
db_name = Utils.unqualify_db_name(table_name)
184+
db_name = Sqlserver::Utils.extract_identifiers(table_name).database
185185
db_name_with_period = "#{db_name}." if db_name
186-
table_schema = Utils.unqualify_table_schema(table_name)
187-
table_name = Utils.unqualify_table_name(table_name)
186+
table_schema = Sqlserver::Utils.extract_identifiers(table_name).schema
187+
table_name = Sqlserver::Utils.extract_identifiers(table_name).object
188188
sql = %{
189189
SELECT DISTINCT
190190
#{lowercase_schema_reflection_sql('columns.TABLE_NAME')} AS table_name,
@@ -329,7 +329,7 @@ def view_table_name(table_name)
329329
end
330330

331331
def view_information(table_name)
332-
table_name = Utils.unqualify_table_name(table_name)
332+
table_name = Sqlserver::Utils.extract_identifiers(table_name).object
333333
view_info = select_one "SELECT * FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME = '#{table_name}'", 'SCHEMA'
334334
if view_info
335335
view_info = view_info.with_indifferent_access
@@ -346,7 +346,7 @@ def view_information(table_name)
346346
end
347347

348348
def table_name_or_views_table_name(table_name)
349-
unquoted_table_name = Utils.unqualify_table_name(table_name)
349+
unquoted_table_name = Sqlserver::Utils.extract_identifiers(table_name).object
350350
schema_cache.view_names.include?(unquoted_table_name) ? view_table_name(unquoted_table_name) : unquoted_table_name
351351
end
352352

‎lib/active_record/connection_adapters/sqlserver/utils.rb

+103-11
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,117 @@
1+
require 'strscan'
2+
13
module ActiveRecord
24
module ConnectionAdapters
35
module Sqlserver
4-
class Utils
5-
class << self
6-
def unquote_string(string)
7-
string.to_s.gsub(/\'\'/, "'")
6+
module Utils
7+
8+
# Value object to return identifiers from SQL Server names http://bit.ly/1CZ3EiL
9+
# Inspiried from Rails PostgreSQL::Name adapter object in their own Utils.
10+
#
11+
class Name
12+
13+
SEPARATOR = "."
14+
SCANNER = /\]?\./
15+
16+
attr_reader :server, :database, :schema, :object
17+
attr_reader :raw_name
18+
19+
def initialize(name)
20+
@raw_name = name.to_s
21+
parse_raw_name
22+
end
23+
24+
def object_quoted
25+
quote object
26+
end
27+
28+
def schema_quoted
29+
schema ? quote(schema) : schema
830
end
931

10-
def unqualify_table_name(table_name)
11-
table_name.to_s.split('.').last.tr('[]', '')
32+
def database_quoted
33+
database ? quote(database) : database
1234
end
1335

14-
def unqualify_table_schema(table_name)
15-
table_name.to_s.split('.')[-2].gsub(/[\[\]]/, '') rescue nil
36+
def server_quoted
37+
server ? quote(server) : server
1638
end
1739

18-
def unqualify_db_name(table_name)
19-
table_names = table_name.to_s.split('.')
20-
table_names.length == 3 ? table_names.first.tr('[]', '') : nil
40+
def to_s
41+
quoted
2142
end
43+
44+
def quoted
45+
parts.map{ |p| quote(p) if p }.join SEPARATOR
46+
end
47+
48+
def ==(o)
49+
o.class == self.class && o.parts == parts
50+
end
51+
alias_method :eql?, :==
52+
53+
def hash
54+
parts.hash
55+
end
56+
57+
protected
58+
59+
def parse_raw_name
60+
@parts = []
61+
return if raw_name.blank?
62+
scanner = StringScanner.new(raw_name)
63+
matched = scanner.scan_until(SCANNER)
64+
while matched
65+
part = matched[0..-2]
66+
@parts << (part.blank? ? nil : unquote(part))
67+
matched = scanner.scan_until(SCANNER)
68+
end
69+
case @parts.length
70+
when 3
71+
@server, @database, @schema = @parts
72+
when 2
73+
@database, @schema = @parts
74+
when 1
75+
@schema = @parts.first
76+
end
77+
rest = scanner.rest
78+
rest = rest.starts_with?('.') ? rest[1..-1] : rest[0..-1]
79+
@object = unquote(rest)
80+
@parts << @object
81+
end
82+
83+
def quote(part)
84+
part =~ /\A\[.*\]\z/ ? part : "[#{part.to_s.gsub(']', ']]')}]"
85+
end
86+
87+
def unquote(part)
88+
if part && part.start_with?('[')
89+
part[1..-2]
90+
else
91+
part
92+
end
93+
end
94+
95+
def parts
96+
@parts
97+
end
98+
2299
end
100+
101+
extend self
102+
103+
def quote_string(s)
104+
s.gsub /\'/, "''"
105+
end
106+
107+
def unquote_string(s)
108+
s.to_s.gsub(/\'\'/, "'")
109+
end
110+
111+
def extract_identifiers(name)
112+
Sqlserver::Utils::Name.new(name)
113+
end
114+
23115
end
24116
end
25117
end

‎lib/active_record/connection_adapters/sqlserver_adapter.rb

+3-3
Original file line numberDiff line numberDiff line change
@@ -373,12 +373,12 @@ def initialize_dateformatter
373373
end
374374

375375
def remove_database_connections_and_rollback(database = nil)
376-
database ||= current_database
377-
do_execute "ALTER DATABASE #{quote_database_name(database)} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"
376+
name = Sqlserver::Utils.extract_identifiers(database || current_database)
377+
do_execute "ALTER DATABASE #{name} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"
378378
begin
379379
yield
380380
ensure
381-
do_execute "ALTER DATABASE #{quote_database_name(database)} SET MULTI_USER"
381+
do_execute "ALTER DATABASE #{name} SET MULTI_USER"
382382
end if block_given?
383383
end
384384

‎test/cases/adapter_test_sqlserver.rb

-35
Original file line numberDiff line numberDiff line change
@@ -112,41 +112,6 @@ class AdapterTestSqlserver < ActiveRecord::TestCase
112112

113113
end
114114

115-
context 'for Utils.unqualify_table_name and Utils.unqualify_db_name' do
116-
117-
setup do
118-
@expected_table_name = 'baz'
119-
@expected_db_name = 'foo'
120-
@first_second_table_names = ['[baz]','baz','[bar].[baz]','bar.baz']
121-
@third_table_names = ['[foo].[bar].[baz]','foo.bar.baz']
122-
@qualifed_table_names = @first_second_table_names + @third_table_names
123-
end
124-
125-
should 'return clean table_name from Utils.unqualify_table_name' do
126-
@qualifed_table_names.each do |qtn|
127-
assert_equal @expected_table_name,
128-
ActiveRecord::ConnectionAdapters::Sqlserver::Utils.unqualify_table_name(qtn),
129-
"This qualifed_table_name #{qtn} did not unqualify correctly."
130-
end
131-
end
132-
133-
should 'return nil from Utils.unqualify_db_name when table_name is less than 2 qualified' do
134-
@first_second_table_names.each do |qtn|
135-
assert_equal nil, ActiveRecord::ConnectionAdapters::Sqlserver::Utils.unqualify_db_name(qtn),
136-
"This qualifed_table_name #{qtn} did not return nil."
137-
end
138-
end
139-
140-
should 'return clean db_name from Utils.unqualify_db_name when table is thrid level qualified' do
141-
@third_table_names.each do |qtn|
142-
assert_equal @expected_db_name,
143-
ActiveRecord::ConnectionAdapters::Sqlserver::Utils.unqualify_db_name(qtn),
144-
"This qualifed_table_name #{qtn} did not unqualify the db_name correctly."
145-
end
146-
end
147-
148-
end
149-
150115
should 'return true to #insert_sql? for inserts only' do
151116
assert @connection.send(:insert_sql?,'INSERT...')
152117
assert @connection.send(:insert_sql?, "EXEC sp_executesql N'INSERT INTO [fk_test_has_fks] ([fk_id]) VALUES (@0); SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident', N'@0 int', @0 = 0")

‎test/cases/schema_test_sqlserver.rb

-12
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,6 @@ class SchemaTestSqlserver < ActiveRecord::TestCase
88
@connection = ActiveRecord::Base.connection
99
end
1010

11-
def read_schema_name(table_name)
12-
ActiveRecord::ConnectionAdapters::Sqlserver::Utils.unqualify_table_schema(table_name)
13-
end
14-
1511
context 'When table is dbo schema' do
1612

1713
should 'find primary key for tables with odd schema' do
@@ -50,14 +46,6 @@ def read_schema_name(table_name)
5046
assert_equal 1, columns.select{ |c| c.primary }.size
5147
end
5248

53-
should "return schema name in all cases" do
54-
assert_nil read_schema_name("table")
55-
assert_equal "schema1", read_schema_name("schema1.table")
56-
assert_equal "schema2", read_schema_name("database.schema2.table")
57-
assert_equal "schema3", read_schema_name("server.database.schema3.table")
58-
assert_equal "schema3", read_schema_name("[server].[database].[schema3].[table]")
59-
end
60-
6149
should "return correct varchar and nvarchar column limit (length) when table is in non dbo schema" do
6250
columns = @connection.columns("test.sql_server_schema_columns")
6351
assert_equal 255, columns.find {|c| c.name == 'name'}.limit

0 commit comments

Comments
 (0)