2

We have an app that uses the Sequel gem to connect to a data source, perform some work, and then return a result which has a number of convenience methods attached to the singleton_class of that object. In ruby 2.3, this code is working as expected:

result = EpulseDB::Employee.where(normalized_args)
result.singleton_class.include(EpulseNormalization)

And we can see using ruby 2.3.4 the singleton_class is not frozen:

[1] pry(main)> result = EpulseDB::Employee.where(employee_id: 2)
=> #<Sequel::Postgres::Dataset: "SELECT * FROM \"employee\" WHERE (\"employee_id\" = 2)">
[2] pry(main)> result.frozen?
=> true
[3] pry(main)> result.singleton_class.frozen?
=> false
[4] pry(main)> result.singleton_class.include(EpulseNormalization)
=> #<Class:#<Sequel::Postgres::Dataset:0x007feff0903660>>

But in Ruby 2.4.2 it appears the singleton_class is being returned as frozen and we can no longer extend it. Is there a new way of extending the singleton that I should be using??

[1] pry(main)> result = EpulseDB::Employee.where(employee_id: 2)
=> #<Sequel::Postgres::Dataset: "SELECT * FROM \"employee\" WHERE (\"employee_id\" = 2)">
[2] pry(main)> result.frozen?
=> true
[3] pry(main)> result.singleton_class.frozen?
=> true
[4] pry(main)> result.singleton_class.include(EpulseNormalization)
RuntimeError: can't modify frozen object
from (pry):4:in `append_features'
Joe
  • 41,484
  • 20
  • 104
  • 125
DaKaZ
  • 925
  • 1
  • 7
  • 18
  • If this is the same version of Sequel it could be a shift in how Ruby handles these things. – tadman Dec 11 '17 at 20:06
  • Exactly - nothing changes except for switching for Ruby 2.3.4 to 2.4.2. I use RVM and switch between them. – DaKaZ Dec 11 '17 at 20:44
  • 1
    It looks like a bug in Ruby 2.3 that was fixed in 2.4. – Jörg W Mittag Dec 11 '17 at 20:45
  • @JörgWMittag do you have a reference to that bug or are you speculating? – DaKaZ Dec 11 '17 at 20:53
  • 1
    Just a guess. Being able to modify the singleton class of a frozen object pretty much negates the idea of being "frozen" IMO, so, it seems clear to me that the singleton class of a frozen object must be frozen also. [It looks like that is specified behavior](https://github.com/ruby/ruby/blob/trunk/test/ruby/test_class.rb#L442-L449). – Jörg W Mittag Dec 11 '17 at 21:12

1 Answers1

2

Use Dataset#with_extend to return a modified copy of the dataset extended with a module, instead of calling Dataset#extend to modify the dataset itself. This works on all versions of ruby that Sequel supports.

Backstory: This is not related to Ruby itself, it's due to a workaround in Sequel for the lack of a feature in Ruby <2.4.

In Ruby <2.4, Object#freeze can't handle cases where Object#clone is used to create modified copies of frozen objects (including copies of the object's singleton class). Ruby 2.4 added the freeze: false option to Object#clone to allow creating modified copies of frozen objects including their singleton class (see https://bugs.ruby-lang.org/issues/12300).

Sequel::Dataset uses #clone internally to return modified datasets, and it's required the datasets include copies of any singleton classes used for proper functioning. Since I wanted Sequel::Dataset to be frozen, but still work on ruby < 2.4, it basically fakes being frozen on ruby <2.4. It's only truly frozen in ruby 2.4. See:

Jeremy Evans
  • 11,959
  • 27
  • 26