Skip to content

Commit b56f09c

Browse files
committed
Fix millisecond support in datetime columns. ODBC::Timestamp incorrectly takes SQL Server milliseconds and applies them as nanoseconds. We cope with this at the DBI layer by using SQLServerDBI::Type::SqlserverTimestamp class to parse the before type cast value appropriately. Also update the adapters #quoted_date method to work more simply by converting ruby's #usec milliseconds to SQL Server microseconds.
1 parent ef97131 commit b56f09c

File tree

4 files changed

+61
-19
lines changed

4 files changed

+61
-19
lines changed

CHANGELOG

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

22
MASTER
33

4+
* Fix millisecond support in datetime columns. ODBC::Timestamp incorrectly takes SQL Server milliseconds
5+
and applies them as nanoseconds. We cope with this at the DBI layer by using SQLServerDBI::Type::SqlserverTimestamp
6+
class to parse the before type cast value appropriately. Also update the adapters #quoted_date method
7+
to work more simply by converting ruby's #usec milliseconds to SQL Server microseconds. [Ken Collins]
8+
49
* Core extensions for ActiveRecord now reflect on the connection before doing SQL Server things. Now
510
this adapter is compatible for using with other adapters. [Ken Collins]
611

lib/active_record/connection_adapters/sqlserver_adapter.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def self.sqlserver_connection(config) #:nodoc:
2727
conn["AutoCommit"] = true
2828
ConnectionAdapters::SQLServerAdapter.new(conn, logger, [driver_url, username, password])
2929
end
30-
30+
3131
end
3232

3333
module ConnectionAdapters
@@ -264,7 +264,7 @@ def quoted_false
264264

265265
def quoted_date(value)
266266
if value.acts_like?(:time) && value.respond_to?(:usec)
267-
"#{super}.#{sprintf("%06d",value.usec)[0..2]}"
267+
"#{super}.#{sprintf("%03d",value.usec/1000)}"
268268
else
269269
super
270270
end

lib/core_ext/dbi.rb

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,24 @@ def to_sqlserver_string
1313
end
1414
end
1515

16-
1716
module Type
1817

1918
# 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:
19+
# that represents what is in the DB before type casting while letting core
20+
# ActiveRecord do the reset. It is assumed that DBI is using ODBC connections
21+
# and that ODBC::Timestamp is taking the native milliseconds that SQL Server
22+
# stores and returning them incorrect using ODBC::Timestamp#fraction which is
23+
# nanoseconds. Below shows the incorrect ODBC::Timestamp represented by DBI
24+
# and the conversion we expect to have in the DB before type casting.
25+
#
2226
# "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"
27+
# "2008-11-08 10:24:36 300000000" # => "2008-11-08 10:24:36.003"
28+
# "2008-11-08 10:24:36 123000000" # => "2008-11-08 10:24:36.123"
2529
class SqlserverTimestamp
2630
def self.parse(obj)
2731
return nil if ::DBI::Type::Null.parse(obj).nil?
28-
date, time, fraction = obj.split(' ')
29-
"#{date} #{time}.#{sprintf("%03d",fraction)}"
32+
date, time, nanoseconds = obj.split(' ')
33+
"#{date} #{time}.#{sprintf("%03d",nanoseconds.to_i/1000000)}"
3034
end
3135
end
3236

test/cases/adapter_test_sqlserver.rb

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -199,22 +199,55 @@ def setup
199199
context 'For chronic data types' do
200200

201201
context 'with a usec' do
202-
202+
203203
setup do
204204
@time = Time.now
205+
@db_datetime_003 = '2012-11-08 10:24:36.003'
206+
@db_datetime_123 = '2012-11-08 10:24:36.123'
207+
@all_datetimes = [@db_datetime_003, @db_datetime_123]
208+
@all_datetimes.each do |datetime|
209+
@connection.execute("INSERT INTO [sql_server_chronics] ([datetime]) VALUES('#{datetime}')")
210+
end
205211
end
206212

207-
should 'truncate 123456 usec to just 123' do
208-
@time.stubs(:usec).returns(123456)
209-
saved = SqlServerChronic.create!(:datetime => @time).reload
210-
assert_equal 123000, saved.datetime.usec
213+
teardown do
214+
@all_datetimes.each do |datetime|
215+
@connection.execute("DELETE FROM [sql_server_chronics] WHERE [datetime] = '#{datetime}'")
216+
end
211217
end
212218

213-
should 'drop 123 to 0' do
214-
@time.stubs(:usec).returns(123)
215-
saved = SqlServerChronic.create!(:datetime => @time).reload
216-
assert_equal 0, saved.datetime.usec
217-
assert_equal '000', saved.datetime_before_type_cast.split('.').last
219+
context 'finding existing DB objects' do
220+
221+
should 'find 003 millisecond in the DB with before and after casting' do
222+
existing_003 = SqlServerChronic.find_by_datetime!(@db_datetime_003)
223+
assert_equal @db_datetime_003, existing_003.datetime_before_type_cast
224+
assert_equal 3000, existing_003.datetime.usec, 'A 003 millisecond in SQL Server is 3000 milliseconds'
225+
end
226+
227+
should 'find 123 millisecond in the DB with before and after casting' do
228+
existing_123 = SqlServerChronic.find_by_datetime!(@db_datetime_123)
229+
assert_equal @db_datetime_123, existing_123.datetime_before_type_cast
230+
assert_equal 123000, existing_123.datetime.usec, 'A 123 millisecond in SQL Server is 123000 milliseconds'
231+
end
232+
233+
end
234+
235+
context 'saving new datetime objects' do
236+
237+
should 'truncate 123456 usec to just 123 in the DB cast back to 123000' do
238+
@time.stubs(:usec).returns(123456)
239+
saved = SqlServerChronic.create!(:datetime => @time).reload
240+
assert_equal '123', saved.datetime_before_type_cast.split('.')[1]
241+
assert_equal 123000, saved.datetime.usec
242+
end
243+
244+
should 'truncate 3001 usec to just 003 in the DB cast back to 3000' do
245+
@time.stubs(:usec).returns(3001)
246+
saved = SqlServerChronic.create!(:datetime => @time).reload
247+
assert_equal '003', saved.datetime_before_type_cast.split('.')[1]
248+
assert_equal 3000, saved.datetime.usec
249+
end
250+
218251
end
219252

220253
end

0 commit comments

Comments
 (0)