Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

undefined method `accessor' for #<ActiveModel::Type::Value:0x0000563c30ebdd50> #51699

Open
joesiewert opened this issue May 1, 2024 · 6 comments

Comments

@joesiewert
Copy link

I'm struggling to sort out an issue with store_accessor. Beginning in Rails 6.1, the store_accessor_for method now raises NoMethodError.

I originally raised this over at hashicorp/vault-rails#138. While an update to that gem might be required, I think there was some change between Rails 6.0 and 6.1 that is causing this.

This issue also seems very similar to #43012.

Steps to reproduce

I have a branch up over at https://github.com/codeship/vault-rails/tree/rails-6.1 that reproduces the issue.

Expected behavior

In our currently working code on Rails 6.0.6.1 type_for_attribute(store_attribute).accessor returns ActiveRecord::Store::IndifferentHashAccessor and all is well.

https://github.com/rails/rails/blob/v6.1.7.7/activerecord/lib/active_record/store.rb#L216-L218

def store_accessor_for(store_attribute)
  type_for_attribute(store_attribute).accessor
end

Actual behavior

When trying to upgrade to Rails 6.1 type_for_attribute(store_attribute).accessor instead raises:

NoMethodError:
  undefined method `accessor' for #<ActiveModel::Type::Value:0x0000563c30ebdd50>
# /home/cs/cache/bundler/ruby/2.7.0/gems/activerecord-6.1.7.7/lib/active_record/store.rb:217:in `store_accessor_for'
# /home/cs/cache/bundler/ruby/2.7.0/gems/activerecord-6.1.7.7/lib/active_record/store.rb:212:in `write_store_attribute'
# /home/cs/cache/bundler/ruby/2.7.0/gems/activerecord-6.1.7.7/lib/active_record/store.rb:136:in `block (3 levels) in store_accessor'
# /home/cs/cache/bundler/ruby/2.7.0/gems/activemodel-6.1.7.7/lib/active_model/attribute_assignment.rb:49:in `public_send'
# /home/cs/cache/bundler/ruby/2.7.0/gems/activemodel-6.1.7.7/lib/active_model/attribute_assignment.rb:49:in `_assign_attribute'
# /home/cs/cache/bundler/ruby/2.7.0/gems/activerecord-6.1.7.7/lib/active_record/attribute_assignment.rb:21:in `block in _assign_attributes'
# /home/cs/cache/bundler/ruby/2.7.0/gems/activerecord-6.1.7.7/lib/active_record/attribute_assignment.rb:13:in `each'
# /home/cs/cache/bundler/ruby/2.7.0/gems/activerecord-6.1.7.7/lib/active_record/attribute_assignment.rb:13:in `_assign_attributes'
# /home/cs/cache/bundler/ruby/2.7.0/gems/activemodel-6.1.7.7/lib/active_model/attribute_assignment.rb:34:in `assign_attributes'
# /home/cs/cache/bundler/ruby/2.7.0/gems/activerecord-6.1.7.7/lib/active_record/core.rb:525:in `initialize'
# /home/cs/cache/bundler/ruby/2.7.0/gems/activerecord-6.1.7.7/lib/active_record/inheritance.rb:72:in `new'
# /home/cs/cache/bundler/ruby/2.7.0/gems/activerecord-6.1.7.7/lib/active_record/inheritance.rb:72:in `new'
# /home/cs/cache/bundler/ruby/2.7.0/gems/activerecord-6.1.7.7/lib/active_record/persistence.rb:54:in `create!'
# ./spec/integration/rails_spec.rb:19:in `block (3 levels) in <top (required)>'

System configuration

Rails version: 6.1.7.7 (Observed the same issue on 7.0 and 7.1)
Ruby version: 2.7.8 (Observed the same issue using 3.3.0)

@rafaelfranca
Copy link
Member

Now that you know how to reproduce, you can use git bisect to find which change makes the exception to happen. Can you try that?

@maniSHarma7575
Copy link
Contributor

maniSHarma7575 commented May 5, 2024

@joesiewert, I've drafted the script to replicate the behavior.

Rails version: 6.0.0
Ruby version: ruby 2.7.8p225 (2023-03-30 revision 1f4d455848) [arm64-darwin22]

Even with Rails 6.0.0, this is breaking. I believe there's a valid reason: in the migration, we haven't included the creation of a metadata column.

Adding the following line to the migration script should resolve the issue.

    t.json :metadata

I was looking at hashicorp/vault-rails#138 and haven't found any migration with adding the column features into the database.

# frozen_string_literal: true

require "bundler/inline"

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

  gem "rails", "=6.0.0"
  # If you want to test against edge Rails replace the previous line with this:
  # gem "rails", github: "rails/rails", branch: "main"

  gem "sqlite3", "~> 1.4"
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)

ActiveRecord::Schema.define do
  create_table :posts do |t|
    t.string :title
    t.text :content
    t.timestamps
  end
end

class Post < ActiveRecord::Base
  store_accessor :metadata, [:likes_count, :comments_count, :tags]
end

class BugTest < Minitest::Test
  def test_store_accessors
    post = Post.new(title: "Example Post", content: "This is an example post")
    post.likes_count = 10
    post.comments_count = 5
    post.tags = ["ruby", "rails", "example"]
    post.save

    post = Post.last
    assert_equal 10, post.likes_count
    assert_equal 5, post.comments_count
    assert_equal %w[ruby rails example], post.tags
  end
end

Output:

E

Error:
BugTest#test_store_accessors:
NoMethodError: undefined method `accessor' for #<ActiveModel::Type::Value:0x000000011dd9e4a0>
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/activerecord-6.0.0/lib/active_record/store.rb:217:in `store_accessor_for'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/activerecord-6.0.0/lib/active_record/store.rb:212:in `write_store_attribute'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/activerecord-6.0.0/lib/active_record/store.rb:136:in `block (3 levels) in store_accessor'
    51699.rb:38:in `test_store_accessors'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest/test.rb:95:in `block (3 levels) in run'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest/test.rb:192:in `capture_exceptions'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest/test.rb:90:in `block (2 levels) in run'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:324:in `time_it'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest/test.rb:89:in `block in run'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:423:in `on_signal'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest/test.rb:240:in `with_info_handler'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest/test.rb:88:in `run'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:1138:in `run_one_method'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:389:in `run_one_method'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:376:in `block (2 levels) in run'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:375:in `each'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:375:in `block in run'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:423:in `on_signal'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:410:in `with_info_handler'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:374:in `run'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:206:in `block in __run'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:206:in `map'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:206:in `__run'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:162:in `run'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:86:in `block in autorun'


rails test 51699.rb:36

@rafaelfranca when we run this script with creating the column metadata as an text instead of the json the script throws an error detailed below. Is this desired behaviour?

    t.text :metadata
# Running:

/Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/activemodel-6.0.0/lib/active_model/type/integer.rb:13: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
/Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/activemodel-6.0.0/lib/active_model/type/value.rb:8: warning: The called method `initialize' is defined here
E

Error:
BugTest#test_store_accessors:
NoMethodError: undefined method `accessor' for #<ActiveRecord::Type::Text:0x0000000134d43660>
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/activerecord-6.0.0/lib/active_record/store.rb:217:in `store_accessor_for'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/activerecord-6.0.0/lib/active_record/store.rb:212:in `write_store_attribute'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/activerecord-6.0.0/lib/active_record/store.rb:136:in `block (3 levels) in store_accessor'
    51699.rb:39:in `test_store_accessors'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest/test.rb:95:in `block (3 levels) in run'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest/test.rb:192:in `capture_exceptions'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest/test.rb:90:in `block (2 levels) in run'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:324:in `time_it'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest/test.rb:89:in `block in run'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:423:in `on_signal'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest/test.rb:240:in `with_info_handler'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest/test.rb:88:in `run'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:1138:in `run_one_method'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:389:in `run_one_method'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:376:in `block (2 levels) in run'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:375:in `each'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:375:in `block in run'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:423:in `on_signal'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:410:in `with_info_handler'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:374:in `run'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:206:in `block in __run'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:206:in `map'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:206:in `__run'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:162:in `run'
    /Users/manishsharma/.rvm/gems/ruby-2.7.8/gems/minitest-5.22.3/lib/minitest.rb:86:in `block in autorun'

@flavorjones
Copy link
Member

I think the issue here is that this code:

  store_accessor :metadata, [:likes_count, :comments_count, :tags]

should be

  store :metadata, accessors: [:likes_count, :comments_count, :tags]

I wonder if store_accessor should raise an exception if it's not going to do what the user expects? I would need to dig deeper to understand ActiveRecord::Store before speculating more.

@maniSHarma7575
Copy link
Contributor

ActiveRecord

@flavorjones I don't think we should have any issues with the code regarding how we use it, as mentioned here:

# class User < ActiveRecord::Base
# store :settings, accessors: [ :color, :homepage ], coder: JSON
# store :parent, accessors: [ :name ], coder: JSON, prefix: true
# store :spouse, accessors: [ :name ], coder: JSON, prefix: :partner
# store :settings, accessors: [ :two_factor_auth ], suffix: true
# store :settings, accessors: [ :login_retry ], suffix: :config
# end
#
# u = User.new(color: 'black', homepage: '37signals.com', parent_name: 'Mary', partner_name: 'Lily')
# u.color # Accessor stored attribute
# u.parent_name # Accessor stored attribute with prefix
# u.partner_name # Accessor stored attribute with custom prefix
# u.two_factor_auth_settings # Accessor stored attribute with suffix
# u.login_retry_config # Accessor stored attribute with custom suffix
# u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor
#
# # There is no difference between strings and symbols for accessing custom attributes
# u.settings[:country] # => 'Denmark'
# u.settings['country'] # => 'Denmark'
#
# # Dirty tracking
# u.color = 'green'
# u.color_changed? # => true
# u.color_was # => 'black'
# u.color_change # => ['black', 'green']
#
# # Add additional accessors to an existing store through store_accessor
# class SuperUser < User
# store_accessor :settings, :privileges, :servants
# store_accessor :parent, :birthday, prefix: true
# store_accessor :settings, :secret_question, suffix: :config
# end
#

store_accessor provides an functionality to Add additional accessors to an existing store (From Parent Class) through store_accessor.

I tried the code below as well, and we're encountering the same error.

  store :metadata, accessors: [:likes_count, :comments_count, :tags]
Error:
BugTest#test_store_accessors:
NoMethodError: undefined method `accessor' for #<ActiveModel::Type::Value:0x000000011ad3c988>

If we add the column metadata to the migration and we also need to add the coder: JSON, the problem is solved.

ActiveRecord::Schema.define do
  create_table :posts do |t|
    t.string :title
    t.text :content
    t.text :metadata #Adding the column metadata solve the problem
    t.timestamps
  end
end
  store :metadata, accessors: [:likes_count, :comments_count, :tags], coder: JSON

@flavorjones
Copy link
Member

flavorjones commented May 5, 2024

@maniSHarma7575 I'm not sure about that. Looking at the code, the original store call is needed to set up the serializer on an existing column, and I think this is the underlying issue. (I'm using Rails edge in my analysis.) Regardless I think store_accessor should detect when there's no serializer associated and raise an error.

@maniSHarma7575
Copy link
Contributor

maniSHarma7575 commented May 6, 2024

Yeah, @flavorjones you are correct.

Here is the observered behaviour with

Ruby: ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23]
Rails: Rails Edge


1. Not creating the metadata column in migration and initializing store as following:

  store :metadata, accessors: [:likes_count, :comments_count, :tags]

Output:

Finished in 0.011299s, 88.5034 runs/s, 88.5034 assertions/s.

  1) Failure:
BugTest#test_store_accessors [51699.rb:45]:
Expected: 10
  Actual: nil

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

2. Creating metadata column in migration and initializing store as following:

  t.text :metadata
  store :metadata, accessors: [:likes_count, :comments_count, :tags]

Output:

Run options: --seed 8267

# Running:

D, [2024-05-06T09:35:58.280339 #2983] DEBUG -- :   TRANSACTION (0.0ms)  begin transaction
D, [2024-05-06T09:35:58.280448 #2983] DEBUG -- :   Post Create (0.1ms)  INSERT INTO "posts" ("title", "content", "metadata", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) RETURNING "id"  [["title", "Example Post"], ["content", "This is an example post"], ["metadata", "---\nlikes_count: 10\ncomments_count: 5\ntags:\n- ruby\n- rails\n- example\n"], ["created_at", "2024-05-06 04:05:58.279740"], ["updated_at", "2024-05-06 04:05:58.279740"]]
D, [2024-05-06T09:35:58.280578 #2983] DEBUG -- :   TRANSACTION (0.0ms)  commit transaction
D, [2024-05-06T09:35:58.284738 #2983] DEBUG -- :   Post Load (0.0ms)  SELECT "posts".* FROM "posts" ORDER BY "posts"."id" DESC LIMIT ?  [["LIMIT", 1]]
.

Finished in 0.011449s, 87.3439 runs/s, 262.0316 assertions/s.

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

3. Not creating column and store initialization as follows:

  store_accessor :metadata, [:likes_count, :comments_count, :tags]

Output:

Run options: --seed 4507

# Running:

E

Finished in 0.004929s, 202.8809 runs/s, 0.0000 assertions/s.

  1) Error:
BugTest#test_store_accessors:
NoMethodError: undefined method `accessor' for an instance of ActiveModel::Type::Value
    rails/activerecord/lib/active_record/store.rb:220:in `store_accessor_for'
    rails/activerecord/lib/active_record/store.rb:215:in `write_store_attribute'
    rails/activerecord/lib/active_record/store.rb:139:in `block (3 levels) in store_accessor'
    51699.rb:40:in `test_store_accessors'

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

4. Creating column and store initialization as follows:

    t.text :metadata
  store_accessor :metadata, [:likes_count, :comments_count, :tags]

Output:

Run options: --seed 31357

# Running:

E

Finished in 0.005465s, 182.9826 runs/s, 0.0000 assertions/s.

  1) Error:
BugTest#test_store_accessors:
NoMethodError: undefined method `accessor' for an instance of ActiveRecord::Type::Text
    rails/activerecord/lib/active_record/store.rb:220:in `store_accessor_for'
    rails/activerecord/lib/active_record/store.rb:215:in `write_store_attribute'
    rails/activerecord/lib/active_record/store.rb:139:in `block (3 levels) in store_accessor'
    51699.rb:40:in `test_store_accessors'

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants