On Thursday, February 26, 2015 at 9:03:21 AM UTC-8, Andrew Burleson wrote:
>
> I ran into something today I'm having a really hard time figuring out:
>
> I've got a model instance, `user`, and after fetching that user instance 
> I'd like to make it available in a "read only" state, which is to say I 
> don't want any 'downstream' code to be able to make changes to it. #freeze 
> accomplishes this. It also makes sense to be to pass a frozen copy of the 
> object off to another process, while retaining an unfrozen copy. Testing 
> this led to some unexpected results:
>
> The relevant portion of my tests looks like this:
> DB.transaction(:rollback => :always, :auto_savepoint=>true) do
>   user = FinishSignup.call(...)
>   assert_equal u, User.order(:id).last
>   assert_equal u.properties.first, Property.order(:id).last
> end
>
> The `FinishSignup` call creates a user and property instance. It dups the 
> user, freezes the dup, and passes this frozen dup on to some observers. The 
> function then returns the original user instance.
>
> The test fails on the last assertion, with a stack trace like this:
>
> RuntimeError: can't modify frozen Hash
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/model/associations.rb:2240:in
>>  
>> `load_associated_objects'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/plugins/association_proxies.rb:83:in
>>  
>> `method_missing'
>> ~/project/test/services/finish_signup_test.rb:18:in `block (3 levels) in 
>> <top (required)>'
>> ~/project/test/test_helper.rb:31:in `block in transaction'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/transactions.rb:119:in
>>  
>> `_transaction'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/transactions.rb:100:in
>>  
>> `block in transaction'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/connecting.rb:250:in
>>  
>> `block in synchronize'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/connection_pool/threaded.rb:98:in
>>  
>> `hold'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/connecting.rb:250:in
>>  
>> `synchronize'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/transactions.rb:89:in
>>  
>> `transaction'
>> ~/project/test/test_helper.rb:30:in `transaction'
>> ~/project/test/services/finish_signup_test.rb:9:in `block (2 levels) in 
>> <top (required)>'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/model/associations.rb:2240:in
>>  
>> `load_associated_objects'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/plugins/association_proxies.rb:83:in
>>  
>> `method_missing'
>> ~/project/test/services/finish_signup_test.rb:18:in `block (3 levels) in 
>> <top (required)>'
>> ~/project/test/test_helper.rb:31:in `block in transaction'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/transactions.rb:119:in
>>  
>> `_transaction'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/transactions.rb:100:in
>>  
>> `block in transaction'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/connecting.rb:250:in
>>  
>> `block in synchronize'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/connection_pool/threaded.rb:98:in
>>  
>> `hold'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/connecting.rb:250:in
>>  
>> `synchronize'
>> ~/.rbenv/versions/2.2.0/lib/ruby/gems/2.2.0/gems/sequel-4.19.0/lib/sequel/database/transactions.rb:89:in
>>  
>> `transaction'
>> ~/project/test/test_helper.rb:30:in `transaction'
>> ~/project/test/services/finish_signup_test.rb:9:in `block (2 levels) in 
>> <top (required)>'
>
>
>
> More background, and research I did so far:
>
> The relevant portion of the model looks like this:
> class User < Sequel::Model
>   one_to_many :properties
> end
>
> Inside the FinishSignup call looks basically like this:
>
> # Create User and Property
> user = User.create(...)
> property = Property.create(user: user, ...)
> trigger_event :user_created, user: user
>
> The trigger event method is a generic method that works like this:
> def trigger_event(name,**args)
>   frozen_args = args.each.with_object({}){|(k,v),o| o[k] = v.dup.freeze}
>   send(name,**frozen_args)
> end
>
> ^ where send actually calls the event trigger.
>
> The reason this is most confusing is I can't duplicate it in the console. 
> I've tried every variation of this procedure I can think of, creating a 
> user, creating a property, duping the user, freezing the dup, etc., and in 
> every case I'm able to call `original_user.properties.first` with no 
> runtime error. I've tried this in the console inside and outside of 
> transactions etc.
>
> So, I think something else must be going on in the Sequel Internals that 
> I'm not aware of which is the reason this specific case won't work, but I'm 
> having a terrible time figuring out what it is.
>
> Any insight you can share?
>

If you look at the line of code that raises the error:

  associations[name] = objs unless frozen?

It's already supposed to check if the object is frozen.  However, from your 
description, I think the problem is that dup doesn't duplicate the 
associations hash, when it should.  This should fix it:

class Sequel::Model
  def initialize_copy(other)
    super
    @associations = @associations.dup if @associations
    self
  end
end

I'll make sure that makes it into the next version.

Thanks,
Jeremy

-- 
You received this message because you are subscribed to the Google Groups 
"sequel-talk" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To post to this group, send email to [email protected].
Visit this group at http://groups.google.com/group/sequel-talk.
For more options, visit https://groups.google.com/d/optout.

Reply via email to