Skip to content

Commit 0a3231e

Browse files
committed
Support newer version of DBI/ODBC. Tested with DBI 0.4.0 and DBD::ODBC 0.2.4 via gem install. Biggest bugs were to fix the #column_definitions method to not have DBI cast values for column_info which was very buggy. Also added a few DBI::Type classes for custom parsing like datetime.
1 parent 144b89c commit 0a3231e

File tree

4 files changed

+107
-9
lines changed

4 files changed

+107
-9
lines changed

lib/active_record/connection_adapters/sqlserver_adapter.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,13 @@ def raw_execute(sql, name = nil, &block)
610610
end
611611
end
612612

613+
def without_type_conversion
614+
raw_connection.convert_types = false if raw_connection.respond_to?(:convert_types=)
615+
yield
616+
ensure
617+
raw_connection.convert_types = true if raw_connection.respond_to?(:convert_types=)
618+
end
619+
613620
def do_execute(sql,name=nil)
614621
log(sql, name || 'EXECUTE') do
615622
raw_connection.do(sql)
@@ -622,6 +629,7 @@ def raw_select(sql, name = nil)
622629
results = handle_as_array(handle)
623630
rows = results.inject([]) do |rows,row|
624631
row.each_with_index do |value, i|
632+
# DEPRECATED in DBI 0.4.0 and above. Remove when 0.2.2 and lower is no longer supported.
625633
if value.is_a? DBI::Timestamp
626634
row[i] = value.to_sqlserver_string
627635
end
@@ -802,7 +810,7 @@ def column_definitions(table_name)
802810
WHERE columns.TABLE_NAME = '#{table_name}'
803811
ORDER BY columns.ordinal_position
804812
}.gsub(/[ \t\r\n]+/,' ')
805-
results = select(sql,nil,true)
813+
results = without_type_conversion { select(sql,nil,true) }
806814
results.collect do |ci|
807815
ci.symbolize_keys!
808816
ci[:type] = if ci[:type] =~ /numeric|decimal/i
@@ -811,7 +819,7 @@ def column_definitions(table_name)
811819
"#{ci[:type]}(#{ci[:length]})"
812820
end
813821
ci[:default_value] = ci[:default_value].match(/\A\(+N?'?(.*?)'?\)+\Z/)[1] if ci[:default_value]
814-
ci[:null] = ci[:is_nullable] == 1 ; ci.delete(:is_nullable)
822+
ci[:null] = ci[:is_nullable].to_i == 1 ; ci.delete(:is_nullable)
815823
ci
816824
end
817825
end

lib/core_ext/dbi.rb

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,83 @@
1+
12
module SQLServerDBI
3+
24
module Timestamp
3-
4-
# Will further change DBI's #to_s return value by limiting the usec of the time
5-
# to 3 digits and in some cases adding zeros if needed. For example:
5+
# Will further change DBI::Timestamp #to_s return value by limiting the usec of
6+
# the time to 3 digits and in some cases adding zeros if needed. For example:
67
# "1985-04-15 00:00:00.0" # => "1985-04-15 00:00:00.000"
78
# "2008-11-08 10:24:36.547000" # => "2008-11-08 10:24:36.547"
89
# "2008-11-08 10:24:36.123" # => "2008-11-08 10:24:36.000"
910
def to_sqlserver_string
1011
datetime, usec = to_s[0..22].split('.')
1112
"#{datetime}.#{sprintf("%03d",usec)}"
1213
end
14+
end
15+
16+
17+
module Type
18+
19+
# Make sure we get DBI::Type::Timestamp returning a string NOT a time object
20+
# that represents what is in the DB before type casting and let the adapter
21+
# do the reset. DBI::DBD::ODBC will typically return a string like:
22+
# "1985-04-15 00:00:00 0" # => "1985-04-15 00:00:00.000"
23+
# "2008-11-08 10:24:36 547000000" # => "2008-11-08 10:24:36.547"
24+
# "2008-11-08 10:24:36 123000000" # => "2008-11-08 10:24:36.000"
25+
class SqlserverTimestamp
26+
def self.parse(obj)
27+
return nil if ::DBI::Type::Null.parse(obj).nil?
28+
date, time, fraction = obj.split(' ')
29+
"#{date} #{time}.#{sprintf("%03d",fraction)}"
30+
end
31+
end
32+
33+
# The adapter and rails will parse our floats, decimals, and money field correctly
34+
# from a string. Do not let the DBI::Type classes create Float/BigDecimal objects
35+
# for us. Trust rails .type_cast to do what it is built to do.
36+
class SqlserverForcedString
37+
def self.parse(obj)
38+
return nil if ::DBI::Type::Null.parse(obj).nil?
39+
obj.to_s
40+
end
41+
end
42+
43+
end
44+
45+
module TypeUtil
46+
47+
def self.included(klass)
48+
klass.extend ClassMethods
49+
class << klass
50+
alias_method_chain :type_name_to_module, :sqlserver_types
51+
end
52+
end
53+
54+
module ClassMethods
55+
56+
# Capture all types classes that we need to handle directly for SQL Server
57+
# and allow normal processing for those that we do not.
58+
def type_name_to_module_with_sqlserver_types(type_name)
59+
case type_name
60+
when /^timestamp$/i
61+
DBI::Type::SqlserverTimestamp
62+
when /^float|decimal|money$/i
63+
DBI::Type::SqlserverForcedString
64+
else
65+
type_name_to_module_without_sqlserver_types(type_name)
66+
end
67+
end
68+
69+
end
1370

1471
end
72+
73+
74+
end
75+
76+
77+
if defined?(DBI::TypeUtil)
78+
DBI::Type.send :include, SQLServerDBI::Type
79+
DBI::TypeUtil.send :include, SQLServerDBI::TypeUtil
80+
elsif defined?(DBI::Timestamp) # DEPRECATED in DBI 0.4.0 and above. Remove when 0.2.2 and lower is no longer supported.
81+
DBI::Timestamp.send :include, SQLServerDBI::Timestamp
1582
end
1683

17-
DBI::Timestamp.send :include, SQLServerDBI::Timestamp

test/cases/column_test_sqlserver.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require 'cases/sqlserver_helper'
22
require 'models/binary'
3+
require 'models/topic'
34

45
class ColumnTestSqlserver < ActiveRecord::TestCase
56

@@ -30,6 +31,30 @@ def setup
3031

3132
end
3233

34+
context 'For .columns method' do
35+
36+
should 'return correct scales and precisions for NumericData' do
37+
bank_balance = NumericData.columns_hash['bank_balance']
38+
big_bank_balance = NumericData.columns_hash['big_bank_balance']
39+
world_population = NumericData.columns_hash['world_population']
40+
my_house_population = NumericData.columns_hash['my_house_population']
41+
assert_equal [2,10], [bank_balance.scale, bank_balance.precision]
42+
assert_equal [2,15], [big_bank_balance.scale, big_bank_balance.precision]
43+
assert_equal [0,10], [world_population.scale, world_population.precision]
44+
assert_equal [0,2], [my_house_population.scale, my_house_population.precision]
45+
end
46+
47+
should 'return correct null, limit, and default for Topic' do
48+
tch = Topic.columns_hash
49+
assert_equal false, tch['id'].null
50+
assert_equal true, tch['title'].null
51+
assert_equal 255, tch['author_name'].limit
52+
assert_equal true, tch['approved'].default
53+
assert_equal 0, tch['replies_count'].default
54+
end
55+
56+
end
57+
3358

3459

3560
end

test/cases/sqlserver_helper.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@
1515
class TableWithRealColumn < ActiveRecord::Base; end
1616
class FkTestHasFk < ActiveRecord::Base ; end
1717
class FkTestHasPk < ActiveRecord::Base ; end
18-
class SqlServerChronic < ActiveRecord::Base
19-
default_timezone = :utc
20-
end
18+
class NumericData < ActiveRecord::Base ; self.table_name = 'numeric_data' ; end
19+
class SqlServerChronic < ActiveRecord::Base ; default_timezone = :utc ; end
2120

2221
# A module that we can include in classes where we want to override an active record test.
2322

0 commit comments

Comments
 (0)