Search code examples
ruby-on-railsruby-on-rails-3ruby-on-rails-4activemodelactivesupport

Rails 3.2 to 4.0 Upgrade: Undefined method to_datetime for false:FalseClass


I'm upgrading a Rails application I've inherited from 3.2 to 4.0.1. I followed and finished the edge guide here:

http://edgeguides.rubyonrails.org/upgrading_ruby_on_rails.html#upgrading-from-rails-3-2-to-rails-4-0

I've gotten everything fixed except for a single error that I can't seem to find the root cause of. When I attempt to save a User model object, I'm met with the following error:

[1] pry(main)> User.create(name: "test user", email: "[email protected]", password: "testPassword123", password_confirmation: "testPassword123")                                                                                                                               

(0.6ms)  BEGIN
(0.9ms)  ROLLBACK
NoMethodError: undefined method `to_datetime' for false:FalseClass
from /home/cmhobbs/src/serve2perform/.gem/ruby/2.3.0/gems/activesupport-4.0.1/lib/active_support/core_ext/date_time/calculations.rb:161:in `<=>'

activesupport 4.0.1 and rals 4.0.1 are installed. I use chgems and I purged my .gem/ directory and Gemfile.lock before bundling again.

Here is a Gist of the User model.

And here is all of the backtrace output I could get from pry.

Here is a link to the User table schema.


Solution

  • Once you've found the offending callback to be this one:

      before_create :activate_license
    
      def activate_license
        self.active_license = true
        self.licensed_date = Time.now
      end
    

    things begin to be clearer. The activate_licence is a before callback. Before callbacks can halt the whole callbacks chain by returning false (or raising an exception).

    If we look carefully in the debug output that you provided by manually adding some puts lines into the Rails callbacks code, we can indeed find the comparison of this callback result with false (here - I removed some unimportant parts of the code):

    result = activate_license
    halted = (result == false)
    if halted
      halted_callback_hook(":activate_license")
    end 
    

    Because the support for halting before callbacks by returning false (i.e. the Rails code shown above) practically has not changed from Rails 3.2 to Rails 4.0.1, the issue must lie in the comparison itself.

    The callback returns a DateTime object (it's the last assignment in the method which is also returned). And, indeed, the comparison of DateTimes changed significantly between the two Rails versions (also note that the == operator is normally evaluated using the <=> operator):

    • in Rails 3.2 it was this:

      def <=>(other)
        if other.kind_of?(Infinity)
          super
        elsif other.respond_to? :to_datetime
         super other.to_datetime
        else
          nil
        end
      end
      

      notice especially the respond_to? check if the other object is also a date or time object while otherwise returning nil.

    • whereas in Rails 4.0.1 this changed to the bare code below:

      def <=>(other)
        super other.to_datetime
      end
      

      → all sanity checks are gone!

    Now, everything is clear: the result of the callback (a DateTime object) is compared using the <=> operator with false and under Rails 4.0, the comparison tries to convert the false object to DateTime without any sanity checks, which of course fails and throws the exception.

    To fix this issue, simply ensure that your callback returns something that Rails can compare with false without any problems, e.g. true, as your callback is never supposed to halt the chain:

      def activate_license
        self.active_license = true
        self.licensed_date = Time.now
        true
      end
    

    Now everything should work as expected again.