Skip to content

Commit 000b9c7

Browse files
committed
[Rails5] Ensure date/time columns are not nil on create/update. Take 2.
Creating or updating records resets the attributes by mapping the hash using ActiveRecord's `Attribute#forgetting_assignment` method. This method essentiall mimics the reload by setting the attributes to FromDatabase using the FromUser value "for the database". rails/rails@07723c23 This is a problem for us because the `value_for_database` for date time objects are strings that can not be re-parsed due to SQL Server's time formats. For example, a date of August 18th 2016 would be formatted as '08-19-2016' and fail deserialize: ``` ruby ActiveModel::Type::Date.new.deserialize('08-19-2016') # => nil ``` Both ActiveModel's date and time types rescue `Date.parse` methods of `new_date` and `new_time` with nil. It was suggested that we use Data classes for our time types and we may explore that after this commit which essentially prepends a new `forgetting_assignment` method that checks the type. If it is a SQL Server date/time, it will just convert that value object to a FromDatabase using the existing value. cc @sgrif
1 parent 86a29bf commit 000b9c7

File tree

7 files changed

+67
-11
lines changed

7 files changed

+67
-11
lines changed

lib/active_record/connection_adapters/sqlserver/quoting.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ def _quote(value)
7777
"0x#{value.hex}"
7878
when ActiveRecord::Type::SQLServer::Char::Data
7979
value.quoted
80+
when ActiveRecord::Type::SQLServer::Date::Data,
81+
ActiveRecord::Type::SQLServer::DateTime::Data,
82+
ActiveRecord::Type::SQLServer::DateTime2::Data
83+
value.quoted
8084
when String, ActiveSupport::Multibyte::Chars
8185
"#{QUOTED_STRING_PREFIX}#{super}"
8286
else
@@ -94,6 +98,10 @@ def _type_cast(value)
9498
_quote(value)
9599
when ActiveRecord::Type::SQLServer::Char::Data
96100
value.quoted
101+
when ActiveRecord::Type::SQLServer::Date::Data,
102+
ActiveRecord::Type::SQLServer::DateTime::Data,
103+
ActiveRecord::Type::SQLServer::DateTime2::Data
104+
value.quoted
97105
else super
98106
end
99107
end

lib/active_record/connection_adapters/sqlserver/type/date.rb

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,31 @@ def sqlserver_type
1010

1111
def serialize(value)
1212
return unless value.present?
13-
return value if value.acts_like?(:string)
14-
value.to_s(:_sqlserver_dateformat)
13+
return value if value.is_a?(Data)
14+
Data.new(super)
15+
end
16+
17+
def deserialize(value)
18+
return value.value if value.is_a?(Data)
19+
super
1520
end
1621

1722
def type_cast_for_schema(value)
18-
serialize(value).inspect
23+
serialize(value).quoted
24+
end
25+
26+
class Data
27+
28+
attr_reader :value
29+
30+
def initialize(value)
31+
@value = value
32+
end
33+
34+
def quoted
35+
Utils.quote_string_single @value.to_s(:_sqlserver_dateformat)
36+
end
37+
1938
end
2039

2140
end

lib/active_record/connection_adapters/sqlserver/type/datetime.rb

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,48 @@ def sqlserver_type
1212

1313
def serialize(value)
1414
return super unless value.acts_like?(:time)
15-
value = zone_conversion(value)
15+
Data.new super, self
16+
end
17+
18+
def deserialize(value)
19+
datetime = value.is_a?(Data) ? value.value : super
20+
return unless datetime
21+
zone_conversion(datetime)
22+
end
23+
24+
def type_cast_for_schema(value)
25+
serialize(value).quoted
26+
end
27+
28+
def quoted(value)
1629
datetime = value.to_s(:_sqlserver_datetime)
17-
"#{datetime}".tap do |v|
30+
datetime = "#{datetime}".tap do |v|
1831
fraction = quote_fractional(value)
1932
v << ".#{fraction}" unless fraction.to_i.zero?
2033
end
34+
Utils.quote_string_single(datetime)
2135
end
2236

23-
def type_cast_for_schema(value)
24-
serialize(value).inspect
37+
class Data
38+
39+
attr_reader :value, :type
40+
41+
def initialize(value, type)
42+
@value, @type = value, type
43+
end
44+
45+
def quoted
46+
type.quoted(@value)
47+
end
48+
2549
end
2650

2751
private
2852

2953
def cast_value(value)
3054
value = value.acts_like?(:time) ? value : super
3155
return unless value
32-
cast_fractional(value)
56+
apply_seconds_precision(value)
3357
end
3458

3559
def zone_conversion(value)

lib/active_record/connection_adapters/sqlserver/type/datetimeoffset.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ def sqlserver_type
2323

2424
private
2525

26+
def fast_string_to_time(string)
27+
dateformat = ::Time::DATE_FORMATS[:_sqlserver_dateformat]
28+
::Time.strptime string, "#{dateformat} %H:%M:%S.%N %:z"
29+
end
30+
2631
def zone_conversion(value)
2732
value
2833
end

lib/active_record/connection_adapters/sqlserver/type/smalldatetime.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def sqlserver_type
1414

1515
private
1616

17-
def cast_fractional(value)
17+
def apply_seconds_precision(value)
1818
value.change usec: 0
1919
end
2020

lib/active_record/connection_adapters/sqlserver/type/time.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def cast_value(value)
3030
value = value.acts_like?(:time) ? value : super
3131
return if value.blank?
3232
value = value.change year: 2000, month: 01, day: 01
33-
cast_fractional(value)
33+
apply_seconds_precision(value)
3434
end
3535

3636
def fractional_scale

lib/active_record/connection_adapters/sqlserver/type/time_value_fractional.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ module TimeValueFractional
77

88
private
99

10-
def cast_fractional(value)
10+
def apply_seconds_precision(value)
1111
return value if !value.respond_to?(fractional_property) || value.send(fractional_property).zero?
1212
frac_seconds = if fractional_scale == 0
1313
0

0 commit comments

Comments
 (0)