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'
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: