jrochkind / attr_json

Serialized json-hash-backed ActiveRecord attributes, super smooth
MIT License
545 stars 36 forks source link

ActiveRecord uniqueness validations don't work with attr_json arrays (was: Issue seeding array values) #217

Closed theonesean closed 11 months ago

theonesean commented 11 months ago

Hi! I'm using this package for a CMS-type thing -- I have a model with an array of social media entries. One issue: when I create an instance of the class for seeding, I inevitably run into TypeError: can't cast Array -- the only thing that addresses the issue is removing array: true from the attribute definition, which defeats the purpose, because I'd like the attribute to be an array of Socials.

Creating them via hash or array of Social instances both fail, despite referencing the README section here. Just not sure what I'm doing wrong! Thanks for taking a look.

Here's the relevant code and stacktrace:

Model:

class Social 
  include AttrJson::Model
  attr_json :name, :string
  attr_json :url, :string

  SOCIAL_NAMES = %i[facebook twitter instagram linkedin].freeze

  validates :name, presence: true, inclusion: { in: SOCIAL_NAMES }
  validates :url, presence: true

end

class SiteMetum < ApplicationRecord
  include AttrJson::Record

  has_many :site_posts

  enum :status, %i[draft active sunsetted deleted], default: :draft

  SOCIAL_NAMES = Social::SOCIAL_NAMES
  # SOCIAL_NAMES = %i[facebook twitter instagram linkedin].freeze

  attr_json :social_entries, Social.to_type, array: true, container_attribute: :socials
  validates :social_entries, length: { maximum: SOCIAL_NAMES.length }
  validates :social_entries, uniqueness: { scope: :name, message: "must have only one entry per account type" }

  attr_json :street, :string, container_attribute: :address
  attr_json :suite, :string, container_attribute: :address
  attr_json :city, :string, container_attribute: :address
  attr_json :state, :string, container_attribute: :address
  attr_json :zip, :string, container_attribute: :address

  validates :domain, presence: true, uniqueness: true
  validates :site_prefix, presence: true, uniqueness: true
  validates :theme, presence: true
  validates :name, presence: true
  validates :phone,
            format: {
              with: /\A\d{3}-\d{3}-\d{4}\z/,
              message: "must be in the format 123-456-7890"
            }
  validates :phone_link,
            format: {
              with: /\A\+1\d{10}\z/,
              message: "must be in the format +11234567890"
            }
  validates :email,
            format: {
              with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i,
              message: "must be a valid email address"
            }
  validates :street, presence: true
  validates :city, presence: true
  validates :state, presence: true, length: { is: 2 }
  validates :zip,
            presence: true,
            format: {
              with: /\A\d{5}\z|\A\d{5}-\d{4}\z/,
              message: "must be in the format 12345 or 12345-6789"
            }
  validates :description, presence: true
end

seeds file:

puts "Creating site metum..."
m = SiteMetum.new(
   domain: "xyz.local",
   site_prefix: "XY",  
   theme: "xy",
   name: "XYZ",
   phone: "888-888-8888",
   phone_link: "+18888888888",
   email: "xyz@gmail.com",
   street: "123 Main St",
   suite: "Suite 100",
   city: "Anytown",
   state: "CA",
   zip: "12345",
   social_entries: [{ "name": "facebook", "url": "https://www.facebook.com/xyz" }, { "name": "twitter", "url": "https://www.twitter.com/xyz" }, { "name": "instagram", "url": "https://www.instagram.com/xyz" }, { "name": "linkedin", "url": "https://www.linkedin.com/xyz" }],
   description: "Lorem ipsum dolor xyz.",
   status: :active
)
m.save!
puts m.social_entries

stacktrace (I've previously dropped the database -- starting fresh here):

$ bin/rails db:setup --trace
** Invoke db:setup (first_time)
** Invoke db:create (first_time)
** Invoke db:load_config (first_time)
** Invoke environment (first_time)
** Execute environment
** Execute db:load_config
** Execute db:create
Database 'cms_dev' already exists
** Invoke environment
** Invoke db:schema:load (first_time)
** Invoke db:load_config
** Invoke db:check_protected_environments (first_time)
** Invoke db:load_config
** Execute db:check_protected_environments
** Execute db:schema:load
** Invoke db:seed (first_time)
** Invoke db:load_config
** Execute db:seed
** Invoke db:abort_if_pending_migrations (first_time)
** Invoke db:load_config
** Execute db:abort_if_pending_migrations
Creating site metum...
rails aborted!
TypeError: can't cast Array
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/connection_adapters/abstract/quoting.rb:43:in `type_cast'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/connection_adapters/postgresql/quoting.rb:142:in `type_cast'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/connection_adapters/abstract/quoting.rb:218:in `block in type_casted_binds'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/connection_adapters/abstract/quoting.rb:216:in `map'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/connection_adapters/abstract/quoting.rb:216:in `type_casted_binds'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/connection_adapters/postgresql_adapter.rb:765:in `exec_no_cache'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/connection_adapters/postgresql_adapter.rb:745:in `execute_and_clear'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/connection_adapters/postgresql/database_statements.rb:54:in `exec_query'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/connection_adapters/abstract/database_statements.rb:560:in `select'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/connection_adapters/abstract/database_statements.rb:66:in `select_all'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/connection_adapters/abstract/query_cache.rb:107:in `block in select_all'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/connection_adapters/abstract/query_cache.rb:137:in `block in cache_sql'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activesupport-7.0.6/lib/active_support/concurrency/load_interlock_aware_monitor.rb:25:in `handle_interrupt'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activesupport-7.0.6/lib/active_support/concurrency/load_interlock_aware_monitor.rb:25:in `block in synchronize'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activesupport-7.0.6/lib/active_support/concurrency/load_interlock_aware_monitor.rb:21:in `handle_interrupt'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activesupport-7.0.6/lib/active_support/concurrency/load_interlock_aware_monitor.rb:21:in `synchronize'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/connection_adapters/abstract/query_cache.rb:128:in `cache_sql'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/connection_adapters/abstract/query_cache.rb:107:in `select_all'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/connection_adapters/abstract/database_statements.rb:91:in `select_rows'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/relation/finder_methods.rb:344:in `block in exists?'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/relation.rb:962:in `skip_query_cache_if_necessary'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/relation/finder_methods.rb:344:in `exists?'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/validations/uniqueness.rb:43:in `validate_each'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activemodel-7.0.6/lib/active_model/validator.rb:153:in `block in validate'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activemodel-7.0.6/lib/active_model/validator.rb:149:in `each'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activemodel-7.0.6/lib/active_model/validator.rb:149:in `validate'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activesupport-7.0.6/lib/active_support/callbacks.rb:423:in `block in make_lambda'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activesupport-7.0.6/lib/active_support/callbacks.rb:199:in `block (2 levels) in halting'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activesupport-7.0.6/lib/active_support/callbacks.rb:687:in `block (2 levels) in default_terminator'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activesupport-7.0.6/lib/active_support/callbacks.rb:686:in `catch'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activesupport-7.0.6/lib/active_support/callbacks.rb:686:in `block in default_terminator'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activesupport-7.0.6/lib/active_support/callbacks.rb:200:in `block in halting'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activesupport-7.0.6/lib/active_support/callbacks.rb:595:in `block in invoke_before'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activesupport-7.0.6/lib/active_support/callbacks.rb:595:in `each'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activesupport-7.0.6/lib/active_support/callbacks.rb:595:in `invoke_before'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activesupport-7.0.6/lib/active_support/callbacks.rb:106:in `run_callbacks'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activesupport-7.0.6/lib/active_support/callbacks.rb:929:in `_run_validate_callbacks'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activemodel-7.0.6/lib/active_model/validations.rb:406:in `run_validations!'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activemodel-7.0.6/lib/active_model/validations/callbacks.rb:115:in `block in run_validations!'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activesupport-7.0.6/lib/active_support/callbacks.rb:107:in `run_callbacks'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activesupport-7.0.6/lib/active_support/callbacks.rb:929:in `_run_validation_callbacks'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activemodel-7.0.6/lib/active_model/validations/callbacks.rb:115:in `run_validations!'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activemodel-7.0.6/lib/active_model/validations.rb:337:in `valid?'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/validations.rb:68:in `valid?'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/validations.rb:84:in `perform_validations'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/validations.rb:53:in `save!'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/transactions.rb:302:in `block in save!'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/transactions.rb:354:in `block in with_transaction_returning_status'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/connection_adapters/abstract/transaction.rb:319:in `block in within_new_transaction'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activesupport-7.0.6/lib/active_support/concurrency/load_interlock_aware_monitor.rb:25:in `handle_interrupt'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activesupport-7.0.6/lib/active_support/concurrency/load_interlock_aware_monitor.rb:25:in `block in synchronize'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activesupport-7.0.6/lib/active_support/concurrency/load_interlock_aware_monitor.rb:21:in `handle_interrupt'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activesupport-7.0.6/lib/active_support/concurrency/load_interlock_aware_monitor.rb:21:in `synchronize'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/connection_adapters/abstract/transaction.rb:317:in `within_new_transaction'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/connection_adapters/abstract/database_statements.rb:316:in `transaction'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/transactions.rb:350:in `with_transaction_returning_status'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/transactions.rb:302:in `save!'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/suppressor.rb:54:in `save!'
/home/spb/TestProjects/multidomain_cms/db/seeds.rb:106:in `<main>'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/railties-7.0.6/lib/rails/engine.rb:557:in `load'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/railties-7.0.6/lib/rails/engine.rb:557:in `block in load_seed'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activesupport-7.0.6/lib/active_support/callbacks.rb:118:in `block in run_callbacks'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activesupport-7.0.6/lib/active_support/execution_wrapper.rb:92:in `wrap'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/railties-7.0.6/lib/rails/engine.rb:626:in `block (2 levels) in <class:Engine>'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activesupport-7.0.6/lib/active_support/callbacks.rb:127:in `instance_exec'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activesupport-7.0.6/lib/active_support/callbacks.rb:127:in `block in run_callbacks'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activesupport-7.0.6/lib/active_support/callbacks.rb:138:in `run_callbacks'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/railties-7.0.6/lib/rails/engine.rb:557:in `load_seed'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/tasks/database_tasks.rb:497:in `load_seed'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/activerecord-7.0.6/lib/active_record/railties/databases.rake:397:in `block (2 levels) in <main>'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/task.rb:281:in `block in execute'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/task.rb:281:in `each'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/task.rb:281:in `execute'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/task.rb:219:in `block in invoke_with_call_chain'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/task.rb:199:in `synchronize'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/task.rb:199:in `invoke_with_call_chain'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/task.rb:243:in `block in invoke_prerequisites'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/task.rb:241:in `each'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/task.rb:241:in `invoke_prerequisites'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/task.rb:218:in `block in invoke_with_call_chain'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/task.rb:199:in `synchronize'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/task.rb:199:in `invoke_with_call_chain'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/task.rb:188:in `invoke'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/application.rb:160:in `invoke_task'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/application.rb:116:in `block (2 levels) in top_level'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/application.rb:116:in `each'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/application.rb:116:in `block in top_level'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/application.rb:125:in `run_with_threads'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/application.rb:110:in `top_level'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/railties-7.0.6/lib/rails/commands/rake/rake_command.rb:24:in `block (2 levels) in perform'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/application.rb:186:in `standard_exception_handling'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/railties-7.0.6/lib/rails/commands/rake/rake_command.rb:24:in `block in perform'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/rake-13.0.6/lib/rake/rake_module.rb:59:in `with_application'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/railties-7.0.6/lib/rails/commands/rake/rake_command.rb:18:in `perform'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/railties-7.0.6/lib/rails/command.rb:51:in `invoke'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/railties-7.0.6/lib/rails/commands.rb:18:in `<main>'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/bootsnap-1.16.0/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:32:in `require'
/home/spb/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/bootsnap-1.16.0/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:32:in `require'
bin/rails:4:in `<main>'
Tasks: TOP => db:setup => db:seed
jrochkind commented 11 months ago

Hi, thanks.

Does the problem reproduce if you JUST run your example code on its own, outside of "seeding", or only when seeding?

If it's only when seeding, can you make a little sample Rails app that can demonstrate the problem, that I can run? If I have a reproduction I can try to underestand the problem and fix it, but if you could help create the reproduction that would be hugely helpful, to save me the time of doing so without knowing for sure how to get to what you're doing.

Especially because I don't personally use Rails "seeds" feature so am not certain how it works and would have ot research it -- if you can instead just give me and app and tell me "run db:setup on this app to see the problem", it would give me a real head start and let me spend my time focusing on understanding and hopefully solving the problem, instead of setting up the reproduction!

The smallest possible case that reproduces the problem is especially helpful. Like I only see one attr_json there with array:true -- if we remove all the other ones, does the problem still reproduce?

Also -- what version of attr_json, is it the latest?

theonesean commented 11 months ago

Hi, @jrochkind, thanks for the quick reply!

Does the problem reproduce if you JUST run your example code on its own, outside of "seeding", or only when seeding?

There’s nowhere in the app that I wholesale create! an instance of SiteMetum outside of seeding, so I’m not 100% sure.

can you make a little sample Rails app that can demonstrate the problem, that I can run?

Absolutely! I will try to put one together on Monday when I’m back at the office — travelling this weekend.

if we remove all the other ones, does the problem still reproduce?

Yes, it reproduced even with only that use of attr_json.

what version of attr_json, is it the latest?

Yep, latest version.

Thanks so much! Appreciate it.

jrochkind commented 11 months ago

Hey @theonesean , I found some time and managed to duplicate.

The problem seems to be caused by this validation:

validates :social_entries, uniqueness: { scope: :name, message: "must have only one entry per account type" }

If you remove that validation, you no longer have the problem. Can you verify this?

I think you just can't use ActiveRecord uniqueness validations with attr_json array's -- I'm not looking at the AR implementation right now, but I suspect it's written in specific ActiveRecord database calls, and tries to make sure the value of the attribute as a whole is unique across the table ... and can't "just work" for an attr_json array -- especially not the way you were hoping scope would magically transfer to an embedded attr_json array with analogous semantics!

I am inclined not to consider this a bug in attr_json; I'm not sure there's much that could be done about it. You just have to write your own custom validation to make sure the values within the attr_json array are unique. I think that should be fairly easy to do, if the attr_json model's implement == equality usefully, which I think they do? Bonus, you won't have to make a trip to the database to ensure unqiueness, like the ActiveRecord one does.

If you come up with such a custom validation, and want to package it up as easily re-usable and submit it as a PR here (with tests and docs included), I'd be happy to review!

Let me know what you think of all this, thanks.

theonesean commented 11 months ago

Ahh, cool, thank you so much for reproducing! Strange how that validation would cause an issue -- the error at hand was super super cryptic.

I might give custom validation a go. Thanks!

On Mon, 4 Dec 2023 at 10:12, Jonathan Rochkind @.***> wrote:

Hey @theonesean https://github.com/theonesean , I found some time and managed to duplicate.

The problem seems to be caused by this validation:

validates :social_entries, uniqueness: { scope: :name, message: "must have only one entry per account type" }

If you remove that validation, you no longer have the problem. Can you verify this?

I think you just can't use ActiveRecord uniqueness validations with attr_json array's -- I'm not looking at the AR implementation right now, but I suspect it's written in specific ActiveRecord database calls, and tries to make sure the value of the attribute as a whole is unique across the table ... and can't "just work" for an attr_json array.

I am inclined not to consider this a bug in attr_json; I'm not sure there's much that could be done about it. You just have to write your own custom validation to make sure the values within the attr_json array are unique. I think that should be fairly easy to do, if the attr_json model's implement == equality usefully, which I think they do? Bonus, you won't have to make a trip to the database to ensure unqiueness, like the ActiveRecord one does.

If you come up with such a custom valication, and want to package it up as easily re-usable and submit it as a PR here (with tests and docs included), I'd be happy to review!

Let me know what you think of all this, thanks.

— Reply to this email directly, view it on GitHub https://github.com/jrochkind/attr_json/issues/217#issuecomment-1838851510, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABJJT6GUNHN4D2HJO6LHA3DYHXR47AVCNFSM6AAAAABADK6ZTKVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTQMZYHA2TCNJRGA . You are receiving this because you were mentioned.Message ID: @.***>

jrochkind commented 11 months ago

The error is trying to tell you that the ActiveRecord uniqueness validation does not know how to do what it does with the AttrJson::Array type that is the value of the attribute.

If it got past that error somehow, it would still complain that it doesn't know how to do what it does with an attribute that does not map to a column... if it did know how to do it, what it owuld do is make sure no other record in the table has the exact same value as this one, since that's what the uniqueness validation does!

AttrJson is great at helping you forget this isn't actually the same as an AR to-many association for many purposes... but it really isn't!

So to me not that strange at all once you think through it, although I'd agree the error ended up cryptic!