adzap / validates_timeliness

Date and time validation plugin for ActiveModel and Rails. Supports multiple ORMs and allows custom date/time formats.
MIT License
1.59k stars 227 forks source link

NoMethodError: undefined method `deduplicate' with ActiveRecord 6.1 #192

Closed mintyfresh closed 3 years ago

mintyfresh commented 3 years ago

Date and time validations raise NoMethodError when used with transient attributes in Rails 6.1.

Executable Test Case:

# frozen_string_literal: true

require 'bundler/inline'

gemfile(true) do
  source 'https://rubygems.org'

  git_source(:github) { |repo| "https://github.com/#{repo}.git" }

  # Activate the gem you are reporting the issue against.
  gem 'activerecord', '6.1.0'
  gem 'sqlite3'
  gem 'validates_timeliness'
end

require 'active_record'
require 'minitest/autorun'
require 'logger'

# This connection will do for database-independent bug reports.
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
ActiveRecord::Base.logger = Logger.new(STDOUT)

ValidatesTimeliness.setup do |config|
  # Extend ORM/ODMs for full support (:active_record included).
  config.extend_orms = [:active_record]
end

ActiveRecord::Schema.define do
  create_table :events, force: true do |t|
  end
end

class Event < ActiveRecord::Base
  attribute :date, :date
  attribute :time, :string

  validates :date, presence: true, timeliness: { date: true }
  validates :time, presence: true, timeliness: { time: true }
end

class BugTest < Minitest::Test
  def test_timeliness_validation
    event = Event.new(
      date: '2020-12-31',
      time: '12:00'
    )

    assert_equal true, event.valid?
  end
end

Expected Result:

Validations are performed:

# Running:

.

Finished in 0.003041s, 328.8284 runs/s, 328.8284 assertions/s.

1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

Actual Result:

A NoMethodError is raised from inside ActiveRecord:

# Running:

E

Finished in 0.004236s, 236.0718 runs/s, 0.0000 assertions/s.

  1) Error:
BugTest#test_association_stuff:
NoMethodError: undefined method `deduplicate' for #<ActiveRecord::Type::DateTime:0x00005635a512e180>
Did you mean?  duplicable?
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.1.0/lib/active_record/connection_adapters/column.rb:93:in `deduplicated'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.1.0/lib/active_record/connection_adapters/deduplicable.rb:19:in `deduplicate'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.1.0/lib/active_record/connection_adapters/deduplicable.rb:14:in `new'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/validates_timeliness-4.1.1/lib/validates_timeliness/orm/active_record.rb:21:in `block in timeliness_column_for_attribute'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/validates_timeliness-4.1.1/lib/validates_timeliness/orm/active_record.rb:19:in `fetch'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/validates_timeliness-4.1.1/lib/validates_timeliness/orm/active_record.rb:19:in `timeliness_column_for_attribute'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/validates_timeliness-4.1.1/lib/validates_timeliness/orm/active_record.rb:10:in `timeliness_attribute_timezone_aware?'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/validates_timeliness-4.1.1/lib/validates_timeliness/validator.rb:110:in `timezone_aware?'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/validates_timeliness-4.1.1/lib/validates_timeliness/validator.rb:67:in `validate_each'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activemodel-6.1.0/lib/active_model/validator.rb:153:in `block in validate'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activemodel-6.1.0/lib/active_model/validator.rb:149:in `each'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activemodel-6.1.0/lib/active_model/validator.rb:149:in `validate'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.1.0/lib/active_support/callbacks.rb:427:in `block in make_lambda'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.1.0/lib/active_support/callbacks.rb:198:in `block (2 levels) in halting'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.1.0/lib/active_support/callbacks.rb:604:in `block (2 levels) in default_terminator'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.1.0/lib/active_support/callbacks.rb:603:in `catch'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.1.0/lib/active_support/callbacks.rb:603:in `block in default_terminator'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.1.0/lib/active_support/callbacks.rb:199:in `block in halting'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.1.0/lib/active_support/callbacks.rb:512:in `block in invoke_before'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.1.0/lib/active_support/callbacks.rb:512:in `each'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.1.0/lib/active_support/callbacks.rb:512:in `invoke_before'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.1.0/lib/active_support/callbacks.rb:105:in `run_callbacks'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.1.0/lib/active_support/callbacks.rb:824:in `_run_validate_callbacks'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activemodel-6.1.0/lib/active_model/validations.rb:406:in `run_validations!'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activemodel-6.1.0/lib/active_model/validations/callbacks.rb:117:in `block in run_validations!'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.1.0/lib/active_support/callbacks.rb:98:in `run_callbacks'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.1.0/lib/active_support/callbacks.rb:824:in `_run_validation_callbacks'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activemodel-6.1.0/lib/active_model/validations/callbacks.rb:117:in `run_validations!'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activemodel-6.1.0/lib/active_model/validations.rb:337:in `valid?'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.1.0/lib/active_record/validations.rb:68:in `valid?'
    timeliness.rb:49:in `test_association_stuff'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

This happens with ActiveRecord 6.1.0 but not with 6.0.3.4.

Curiously, this issue only occurs with transient attributes (those declared using attribute), and stops happening if the config.extend_orms = [:active_record] configuration is removed. My guess is that the ActiveRecord extensions relied on internal functionality that was changed in between 6.0 and 6.1.

Environment:

Rails Version: 6.1.0 Ruby Version: 2.6.5 Validates Timeliness Version: 4.1.1

mintyfresh commented 3 years ago

Update: I tried upgrading to 5.0.0.beta2, but this still fails. However, a different error is produced:

# Running:

E

Finished in 0.004895s, 204.2817 runs/s, 0.0000 assertions/s.

  1) Error:
BugTest#test_timeliness_validation:
NoMethodError: undefined method `parse' for nil:NilClass
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/validates_timeliness-5.0.0.beta2/lib/validates_timeliness/converter.rb:74:in `parse'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/validates_timeliness-5.0.0.beta2/lib/validates_timeliness/validator.rb:62:in `validate_each'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activemodel-6.0.3.4/lib/active_model/validator.rb:152:in `block in validate'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activemodel-6.0.3.4/lib/active_model/validator.rb:149:in `each'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activemodel-6.0.3.4/lib/active_model/validator.rb:149:in `validate'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.0.3.4/lib/active_support/callbacks.rb:428:in `block in make_lambda'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.0.3.4/lib/active_support/callbacks.rb:200:in `block (2 levels) in halting'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.0.3.4/lib/active_support/callbacks.rb:605:in `block (2 levels) in default_terminator'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.0.3.4/lib/active_support/callbacks.rb:604:in `catch'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.0.3.4/lib/active_support/callbacks.rb:604:in `block in default_terminator'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.0.3.4/lib/active_support/callbacks.rb:201:in `block in halting'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.0.3.4/lib/active_support/callbacks.rb:513:in `block in invoke_before'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.0.3.4/lib/active_support/callbacks.rb:513:in `each'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.0.3.4/lib/active_support/callbacks.rb:513:in `invoke_before'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.0.3.4/lib/active_support/callbacks.rb:134:in `run_callbacks'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.0.3.4/lib/active_support/callbacks.rb:825:in `_run_validate_callbacks'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activemodel-6.0.3.4/lib/active_model/validations.rb:406:in `run_validations!'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activemodel-6.0.3.4/lib/active_model/validations/callbacks.rb:117:in `block in run_validations!'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.0.3.4/lib/active_support/callbacks.rb:101:in `run_callbacks'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activesupport-6.0.3.4/lib/active_support/callbacks.rb:825:in `_run_validation_callbacks'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activemodel-6.0.3.4/lib/active_model/validations/callbacks.rb:117:in `run_validations!'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activemodel-6.0.3.4/lib/active_model/validations.rb:337:in `valid?'
    /home/mintyfresh/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/activerecord-6.0.3.4/lib/active_record/validations.rb:68:in `valid?'
    timeliness.rb:49:in `test_timeliness_validation'

1 runs, 0 assertions, 0 failures, 1 errors, 0 skips

However, unlike the previous error, this one occurs in both ActiveRecord 6.0.3.4 and 6.1.0. Looks like the beta version for v5 really doesn't like transient attributes.

mintyfresh commented 3 years ago

Found the issue. A timezone wasn't set for my testcase with v5.0.0.beta2.

Upgrading to 5.0.0.beta2 and setting a timezone seems to have resolved the issue.

adzap commented 3 years ago

Haven't looked Rails v6 support yet. So it's interesting that it even works, but be careful as there might be hidden issues.