From 37a9610731787008b0507b7d0b6204fadf55dbe5 Mon Sep 17 00:00:00 2001 From: Ken Collins Date: Thu, 8 Dec 2016 21:52:42 -0500 Subject: [PATCH] [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". https://github.com/rails/rails/commit/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 --- .../connection_adapters/sqlserver/quoting.rb | 8 +++++ .../sqlserver/type/date.rb | 25 ++++++++++++-- .../sqlserver/type/datetime.rb | 34 ++++++++++++++++--- .../sqlserver/type/datetimeoffset.rb | 5 +++ .../sqlserver/type/smalldatetime.rb | 2 +- .../sqlserver/type/time.rb | 2 +- .../sqlserver/type/time_value_fractional.rb | 2 +- 7 files changed, 67 insertions(+), 11 deletions(-) diff --git a/lib/active_record/connection_adapters/sqlserver/quoting.rb b/lib/active_record/connection_adapters/sqlserver/quoting.rb index 0e8a334bb..f259736a3 100644 --- a/lib/active_record/connection_adapters/sqlserver/quoting.rb +++ b/lib/active_record/connection_adapters/sqlserver/quoting.rb @@ -77,6 +77,10 @@ def _quote(value) "0x#{value.hex}" when ActiveRecord::Type::SQLServer::Char::Data value.quoted + when ActiveRecord::Type::SQLServer::Date::Data, + ActiveRecord::Type::SQLServer::DateTime::Data, + ActiveRecord::Type::SQLServer::DateTime2::Data + value.quoted when String, ActiveSupport::Multibyte::Chars "#{QUOTED_STRING_PREFIX}#{super}" else @@ -94,6 +98,10 @@ def _type_cast(value) _quote(value) when ActiveRecord::Type::SQLServer::Char::Data value.quoted + when ActiveRecord::Type::SQLServer::Date::Data, + ActiveRecord::Type::SQLServer::DateTime::Data, + ActiveRecord::Type::SQLServer::DateTime2::Data + value.quoted else super end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/date.rb b/lib/active_record/connection_adapters/sqlserver/type/date.rb index d283e60a8..9564c7027 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/date.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/date.rb @@ -10,12 +10,31 @@ def sqlserver_type def serialize(value) return unless value.present? - return value if value.acts_like?(:string) - value.to_s(:_sqlserver_dateformat) + return value if value.is_a?(Data) + Data.new(super) + end + + def deserialize(value) + return value.value if value.is_a?(Data) + super end def type_cast_for_schema(value) - serialize(value).inspect + serialize(value).quoted + end + + class Data + + attr_reader :value + + def initialize(value) + @value = value + end + + def quoted + Utils.quote_string_single @value.to_s(:_sqlserver_dateformat) + end + end end diff --git a/lib/active_record/connection_adapters/sqlserver/type/datetime.rb b/lib/active_record/connection_adapters/sqlserver/type/datetime.rb index 4cc3485d3..f724e4832 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/datetime.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/datetime.rb @@ -12,16 +12,40 @@ def sqlserver_type def serialize(value) return super unless value.acts_like?(:time) - value = zone_conversion(value) + Data.new super, self + end + + def deserialize(value) + datetime = value.is_a?(Data) ? value.value : super + return unless datetime + zone_conversion(datetime) + end + + def type_cast_for_schema(value) + serialize(value).quoted + end + + def quoted(value) datetime = value.to_s(:_sqlserver_datetime) - "#{datetime}".tap do |v| + datetime = "#{datetime}".tap do |v| fraction = quote_fractional(value) v << ".#{fraction}" unless fraction.to_i.zero? end + Utils.quote_string_single(datetime) end - def type_cast_for_schema(value) - serialize(value).inspect + class Data + + attr_reader :value, :type + + def initialize(value, type) + @value, @type = value, type + end + + def quoted + type.quoted(@value) + end + end private @@ -29,7 +53,7 @@ def type_cast_for_schema(value) def cast_value(value) value = value.acts_like?(:time) ? value : super return unless value - cast_fractional(value) + apply_seconds_precision(value) end def zone_conversion(value) diff --git a/lib/active_record/connection_adapters/sqlserver/type/datetimeoffset.rb b/lib/active_record/connection_adapters/sqlserver/type/datetimeoffset.rb index 4798a3e94..a5fe3dd72 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/datetimeoffset.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/datetimeoffset.rb @@ -23,6 +23,11 @@ def sqlserver_type private + def fast_string_to_time(string) + dateformat = ::Time::DATE_FORMATS[:_sqlserver_dateformat] + ::Time.strptime string, "#{dateformat} %H:%M:%S.%N %:z" + end + def zone_conversion(value) value end diff --git a/lib/active_record/connection_adapters/sqlserver/type/smalldatetime.rb b/lib/active_record/connection_adapters/sqlserver/type/smalldatetime.rb index 6bf93d3f0..67070e077 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/smalldatetime.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/smalldatetime.rb @@ -14,7 +14,7 @@ def sqlserver_type private - def cast_fractional(value) + def apply_seconds_precision(value) value.change usec: 0 end diff --git a/lib/active_record/connection_adapters/sqlserver/type/time.rb b/lib/active_record/connection_adapters/sqlserver/type/time.rb index 5af08949e..21810409a 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/time.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/time.rb @@ -30,7 +30,7 @@ def cast_value(value) value = value.acts_like?(:time) ? value : super return if value.blank? value = value.change year: 2000, month: 01, day: 01 - cast_fractional(value) + apply_seconds_precision(value) end def fractional_scale diff --git a/lib/active_record/connection_adapters/sqlserver/type/time_value_fractional.rb b/lib/active_record/connection_adapters/sqlserver/type/time_value_fractional.rb index 8171835df..d8a2ff1cd 100644 --- a/lib/active_record/connection_adapters/sqlserver/type/time_value_fractional.rb +++ b/lib/active_record/connection_adapters/sqlserver/type/time_value_fractional.rb @@ -7,7 +7,7 @@ module TimeValueFractional private - def cast_fractional(value) + def apply_seconds_precision(value) return value if !value.respond_to?(fractional_property) || value.send(fractional_property).zero? frac_seconds = if fractional_scale == 0 0