Skip to content

Commit e38cc37

Browse files
committed
New #lowercase_schema_reflection configuration that allows you to downcase all tables and columns. Good for legacy databases. Fixes #86. Thanks @dmajkic.
1 parent 19d2223 commit e38cc37

File tree

8 files changed

+89
-40
lines changed

8 files changed

+89
-40
lines changed

CHANGELOG

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11

22
* 3.1.0.rc *
33

4+
* New #lowercase_schema_reflection configuration that allows you to downcase all tables and columns.
5+
Good for legacy databases. Fixes #86. Thanks @dmajkic.
6+
47
* Rails 3.1 with prepared statement support. Uses "EXEC sp_executesql ..." for just about everything now.
58

69

README.rdoc

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ The SQL Server adapter for ActiveRecord.
77
== What's New
88

99
* Rails 3.1 prepared statement support leverages cached query plans.
10+
* New #lowercase_schema_reflection configuration option for legacy DBs.
1011
* New dblib connection mode using TinyTds!
1112

1213

@@ -88,7 +89,14 @@ By default the adapter will use non-unicode safe data types for :string and :tex
8889
It is important to remember that unicode types in SQL Server have approximately half the storage capacity as their counter parts. So where a normal string would max out at (8000) a unicode string will top off at (4000).
8990

9091

91-
==== Schema Information Logging
92+
==== Force Schema To Lowercase
93+
94+
Although it is not necessary, the Ruby convention is to use lowercase method names. If your database schema is in upper or mixed case, we can force all table and column names during the schema reflection process to be lowercase. Add this to your config/initializers file for the adapter.
95+
96+
ActiveRecord::ConnectionAdapters::SQLServerAdapter.lowercase_schema_reflection = true
97+
98+
99+
==== Schema Information Logging
92100

93101
By default all queries to the INFORMATION_SCHEMA table is silenced. If you think logging these queries are useful, you can enable it by adding this like to a initializer file.
94102

lib/active_record/connection_adapters/sqlserver/database_statements.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,13 +325,14 @@ def handle_to_names_and_values_dblib(handle, options={})
325325
qo[:as] = (options[:ar_result] || options[:fetch] == :rows) ? :array : :hash
326326
end
327327
results = handle.each(query_options)
328-
options[:ar_result] ? ActiveRecord::Result.new(handle.fields, results) : results
328+
columns = lowercase_schema_reflection ? handle.fields.map { |c| c.downcase } : handle.fields
329+
options[:ar_result] ? ActiveRecord::Result.new(columns, results) : results
329330
end
330331

331332
def handle_to_names_and_values_odbc(handle, options={})
332333
@connection.use_utc = ActiveRecord::Base.default_timezone == :utc
333334
if options[:ar_result]
334-
columns = handle.columns(true).map { |c| c.name }
335+
columns = lowercase_schema_reflection ? handle.columns(true).map { |c| c.name.downcase } : handle.columns(true).map { |c| c.name }
335336
rows = handle.fetch_all || []
336337
ActiveRecord::Result.new(columns, rows)
337338
else

lib/active_record/connection_adapters/sqlserver/schema_statements.rb

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def native_database_types
99

1010
def tables(name = nil)
1111
info_schema_query do
12-
select_values "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME <> 'dtproperties' AND TABLE_SCHEMA = schema_name()"
12+
select_values "SELECT #{lowercase_schema_reflection_sql('TABLE_NAME')} FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME <> 'dtproperties' AND TABLE_SCHEMA = schema_name()"
1313
end
1414
end
1515

@@ -118,7 +118,7 @@ def change_column_null(table_name, column_name, null, default = nil)
118118

119119
def views(name = nil)
120120
@sqlserver_views_cache ||=
121-
info_schema_query { select_values("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME NOT IN ('sysconstraints','syssegments')") }
121+
info_schema_query { select_values("SELECT #{lowercase_schema_reflection_sql('TABLE_NAME')} FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME NOT IN ('sysconstraints','syssegments')") }
122122
end
123123

124124

@@ -158,24 +158,24 @@ def column_definitions(table_name)
158158
table_name = unqualify_table_name(table_name)
159159
sql = %{
160160
SELECT
161-
columns.TABLE_NAME as table_name,
162-
columns.COLUMN_NAME as name,
163-
columns.DATA_TYPE as type,
164-
columns.COLUMN_DEFAULT as default_value,
165-
columns.NUMERIC_SCALE as numeric_scale,
166-
columns.NUMERIC_PRECISION as numeric_precision,
161+
#{lowercase_schema_reflection_sql('columns.TABLE_NAME')} AS table_name,
162+
#{lowercase_schema_reflection_sql('columns.COLUMN_NAME')} AS name,
163+
columns.DATA_TYPE AS type,
164+
columns.COLUMN_DEFAULT AS default_value,
165+
columns.NUMERIC_SCALE AS numeric_scale,
166+
columns.NUMERIC_PRECISION AS numeric_precision,
167167
CASE
168168
WHEN columns.DATA_TYPE IN ('nchar','nvarchar') THEN columns.CHARACTER_MAXIMUM_LENGTH
169169
ELSE COL_LENGTH(columns.TABLE_SCHEMA+'.'+columns.TABLE_NAME, columns.COLUMN_NAME)
170-
END as length,
170+
END AS length,
171171
CASE
172172
WHEN columns.IS_NULLABLE = 'YES' THEN 1
173173
ELSE NULL
174-
END as is_nullable,
174+
END AS is_nullable,
175175
CASE
176176
WHEN COLUMNPROPERTY(OBJECT_ID(columns.TABLE_SCHEMA+'.'+columns.TABLE_NAME), columns.COLUMN_NAME, 'IsIdentity') = 0 THEN NULL
177177
ELSE 1
178-
END as is_identity
178+
END AS is_identity
179179
FROM #{db_name_with_period}INFORMATION_SCHEMA.COLUMNS columns
180180
WHERE columns.TABLE_NAME = @0
181181
AND columns.TABLE_SCHEMA = #{table_schema.blank? ? "schema_name()" : "@1"}
@@ -279,6 +279,10 @@ def detect_column_for!(table_name, column_name)
279279
column
280280
end
281281

282+
def lowercase_schema_reflection_sql(node)
283+
lowercase_schema_reflection ? "LOWER(#{node})" : node
284+
end
285+
282286
# === SQLServer Specific (View Reflection) ====================== #
283287

284288
def view_table_name(table_name)
@@ -313,14 +317,13 @@ def table_name_or_views_table_name(table_name)
313317

314318
def views_real_column_name(table_name,column_name)
315319
view_definition = view_information(table_name)[:VIEW_DEFINITION]
316-
317320
match_data = view_definition.match(/([\w-]*)\s+as\s+#{column_name}/im)
318321
match_data ? match_data[1] : column_name
319322
end
320323

321324
# === SQLServer Specific (Column/View Caches) =================== #
322325

323-
def initialize_sqlserver_caches(reset_columns=true)
326+
def initialize_sqlserver_caches
324327
@sqlserver_views_cache = nil
325328
@sqlserver_view_information_cache = {}
326329
@sqlserver_quoted_column_and_table_names = {}

lib/active_record/connection_adapters/sqlserver_adapter.rb

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -165,12 +165,11 @@ class SQLServerAdapter < AbstractAdapter
165165
DATABASE_VERSION_REGEXP = /Microsoft SQL Server\s+"?(\d{4}|\w+)"?/
166166
SUPPORTED_VERSIONS = [2005,2008,2010,2011].freeze
167167

168-
attr_reader :database_version, :database_year,
169-
:connection_supports_native_types
168+
attr_reader :database_version, :database_year
170169

171170
cattr_accessor :native_text_database_type, :native_binary_database_type, :native_string_database_type,
172171
:log_info_schema_queries, :enable_default_unicode_types, :auto_connect,
173-
:cs_equality_operator
172+
:cs_equality_operator, :lowercase_schema_reflection
174173

175174
def initialize(logger,config)
176175
@connection_options = config
@@ -263,8 +262,7 @@ def reset!
263262
end
264263

265264
def clear_cache!
266-
# This requires db admin perms and I'm not even sure it is a good idea.
267-
# raw_connection_do "DBCC FREEPROCCACHE WITH NO_INFOMSGS" rescue nil
265+
initialize_sqlserver_caches
268266
end
269267

270268
# === Abstract Adapter (Misc Support) =========================== #

test/cases/adapter_test_sqlserver.rb

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def setup
7979
end
8080

8181
context 'for #unqualify_table_name and #unqualify_db_name' do
82-
82+
8383
setup do
8484
@expected_table_name = 'baz'
8585
@expected_db_name = 'foo'
@@ -110,7 +110,7 @@ def setup
110110
"This qualifed_table_name #{qtn} did not unqualify the db_name correctly."
111111
end
112112
end
113-
113+
114114
end
115115

116116
should 'return true to #insert_sql? for inserts only' do
@@ -121,32 +121,32 @@ def setup
121121
end
122122

123123
context 'for #get_table_name' do
124-
124+
125125
should 'return quoted table name from basic INSERT, UPDATE and SELECT statements' do
126126
assert_equal '[funny_jokes]', @connection.send(:get_table_name,@basic_insert_sql)
127127
assert_equal '[customers]', @connection.send(:get_table_name,@basic_update_sql)
128128
assert_equal '[customers]', @connection.send(:get_table_name,@basic_select_sql)
129129
end
130-
130+
131131
end
132132

133133
context 'with different language' do
134-
134+
135135
teardown do
136136
@connection.execute("SET LANGUAGE us_english") rescue nil
137137
end
138-
138+
139139
should_eventually 'do a date insertion when language is german' do
140140
@connection.execute("SET LANGUAGE deutsch")
141141
assert_nothing_raised do
142142
Task.create(:starting => Time.utc(2000, 1, 31, 5, 42, 0), :ending => Date.new(2006, 12, 31))
143143
end
144144
end
145-
145+
146146
end
147147

148148
context 'testing #enable_default_unicode_types configuration' do
149-
149+
150150
should 'use non-unicode types when set to false' do
151151
with_enable_default_unicode_types(false) do
152152
assert_equal 'varchar', @connection.native_string_database_type
@@ -160,9 +160,38 @@ def setup
160160
assert_equal 'nvarchar(max)', @connection.native_text_database_type
161161
end
162162
end
163-
163+
164164
end
165165

166+
context 'testing #lowercase_schema_reflection' do
167+
168+
setup do
169+
UpperTestDefault.delete_all
170+
UpperTestDefault.create :COLUMN1 => 'Got a minute?', :COLUMN2 => 419
171+
UpperTestDefault.create :COLUMN1 => 'Favorite number?', :COLUMN2 => 69
172+
end
173+
174+
teardown do
175+
@connection.lowercase_schema_reflection = false
176+
end
177+
178+
should 'not lowercase schema reflection by default' do
179+
assert UpperTestDefault.columns_hash['COLUMN1']
180+
assert_equal 'Got a minute?', UpperTestDefault.first.COLUMN1
181+
assert_equal 'Favorite number?', UpperTestDefault.last.COLUMN1
182+
assert UpperTestDefault.columns_hash['COLUMN2']
183+
end
184+
185+
should 'lowercase schema reflection when set' do
186+
@connection.lowercase_schema_reflection = true
187+
UpperTestLowered.reset_column_information
188+
assert UpperTestLowered.columns_hash['column1']
189+
assert_equal 'Got a minute?', UpperTestLowered.first.column1
190+
assert_equal 'Favorite number?', UpperTestLowered.last.column1
191+
assert UpperTestLowered.columns_hash['column2']
192+
end
193+
194+
end
166195

167196
end
168197

@@ -187,23 +216,23 @@ def setup
187216
end
188217

189218
context 'finding existing DB objects' do
190-
219+
191220
should 'find 003 millisecond in the DB with before and after casting' do
192221
existing_003 = SqlServerChronic.find_by_datetime!(@db_datetime_003)
193222
assert_equal @db_datetime_003, existing_003.datetime_before_type_cast if existing_003.datetime_before_type_cast.is_a?(String)
194223
assert_equal 3000, existing_003.datetime.usec, 'A 003 millisecond in SQL Server is 3000 microseconds'
195224
end
196-
225+
197226
should 'find 123 millisecond in the DB with before and after casting' do
198227
existing_123 = SqlServerChronic.find_by_datetime!(@db_datetime_123)
199228
assert_equal @db_datetime_123, existing_123.datetime_before_type_cast if existing_123.datetime_before_type_cast.is_a?(String)
200229
assert_equal 123000, existing_123.datetime.usec, 'A 123 millisecond in SQL Server is 123000 microseconds'
201230
end
202-
231+
203232
end
204233

205234
context 'saving new datetime objects' do
206-
235+
207236
should 'truncate 123456 usec to just 123 in the DB cast back to 123000' do
208237
@time.stubs(:usec).returns(123456)
209238
saved = SqlServerChronic.create!(:datetime => @time).reload
@@ -350,7 +379,7 @@ def setup
350379
end
351380

352381
end unless sqlserver_azure?
353-
382+
354383
context "altering isolation levels" do
355384

356385
should "barf if the requested isolation level is not valid" do
@@ -458,7 +487,7 @@ def setup
458487
context 'For views' do
459488

460489
context 'using @connection.views' do
461-
490+
462491
should 'return an array' do
463492
assert_instance_of Array, @connection.views
464493
end
@@ -559,7 +588,7 @@ def setup
559588
end
560589

561590
context 'doing identity inserts' do
562-
591+
563592
setup do
564593
@view_insert_sql = "INSERT INTO [customers_view] ([id],[name],[balance]) VALUES (420,'Microsoft',0)"
565594
end
@@ -572,11 +601,11 @@ def setup
572601
assert_nothing_raised { @connection.execute(@view_insert_sql) }
573602
assert CustomersView.find(420)
574603
end
575-
604+
576605
end
577606

578607
context 'that have more than 4000 chars for their defintion' do
579-
608+
580609
should 'cope with null returned for the defintion' do
581610
assert_nothing_raised() { StringDefaultsBigView.columns }
582611
end
@@ -585,7 +614,7 @@ def setup
585614
assert_equal 'null', StringDefaultsBigView.new.pretend_null,
586615
StringDefaultsBigView.columns_hash['pretend_null'].inspect
587616
end
588-
617+
589618
end
590619

591620
end

test/cases/sqlserver_helper.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626

2727
# Defining our classes in one place as well as soem core tests that need coercing date/time types.
2828

29+
class UpperTestDefault < ActiveRecord::Base ; self.table_name = 'UPPER_TESTS' ; end
30+
class UpperTestLowered < ActiveRecord::Base ; self.table_name = 'upper_tests' ; end
2931
class TableWithRealColumn < ActiveRecord::Base; end
3032
class FkTestHasFk < ActiveRecord::Base ; end
3133
class FkTestHasPk < ActiveRecord::Base ; end

test/schema/sqlserver_specific_schema.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
ActiveRecord::Schema.define do
22

3+
create_table :UPPER_TESTS, :force => true do |t|
4+
t.column :COLUMN1, :string
5+
t.column :COLUMN2, :integer
6+
end
7+
38
create_table :table_with_real_columns, :force => true do |t|
49
t.column :real_number, :real
510
end

0 commit comments

Comments
 (0)