Skip to content

Commit

Permalink
Add temporary names for internally defined anonymous classes and modu…
Browse files Browse the repository at this point in the history
…les on Ruby 3.3+

These should hopefully aid debugging and ease understanding.

I don't believe any of these calls are in performance-sensitive
code.  Most are called only during application startup.  In terms
of runtime calls, Dataset#with_extend may be the most common, but
in that case, you are allocating a new class, so setting the temporary
name shouldn't be that significant in comparison.  The temporary names
are provided inside a block, so the dynamically created strings are
not allocated on Ruby < 3.3.
  • Loading branch information
jeremyevans committed Jan 16, 2025
1 parent 0ad0bb3 commit 066e256
Show file tree
Hide file tree
Showing 38 changed files with 147 additions and 22 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
=== master

* Add temporary names for internally defined anonymous classes and modules on Ruby 3.3+ (jeremyevans)

=== 5.88.0 (2025-01-01)

* Add subset_static_cache plugin for statically caching subsets of a model class (jeremyevans)
Expand Down
1 change: 1 addition & 0 deletions lib/sequel/adapters/ibmdb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module Sequel

module IBMDB
tt = Class.new do
Sequel.set_temp_name(self){"Sequel::IBMDB::_TypeTranslator"}
def boolean(s) !s.to_i.zero? end
def int(s) s.to_i end
end.new
Expand Down
1 change: 1 addition & 0 deletions lib/sequel/adapters/shared/access.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def type_literal_generic_file(column)

module DatasetMethods
include(Module.new do
Sequel.set_temp_name(self){"Sequel::Access::DatasetMethods::_SQLMethods"}
Dataset.def_sql_method(self, :select, %w'select distinct limit columns into from join where group order having compounds')
end)
include EmulateOffsetWithReverseAndCount
Expand Down
1 change: 1 addition & 0 deletions lib/sequel/adapters/shared/mssql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,7 @@ def view_with_check_option_support

module DatasetMethods
include(Module.new do
Sequel.set_temp_name(self){"Sequel::MSSQL::DatasetMethods::_SQLMethods"}
Dataset.def_sql_method(self, :select, %w'with select distinct limit columns into from lock join where group having compounds order')
end)
include EmulateOffsetWithRowNumber
Expand Down
1 change: 1 addition & 0 deletions lib/sequel/adapters/shared/oracle.rb
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ module DatasetMethods
BITAND_PROC = lambda{|a, b| Sequel.lit(["CAST(BITAND(", ", ", ") AS INTEGER)"], a, b)}

include(Module.new do
Sequel.set_temp_name(self){"Sequel::Oracle::DatasetMethods::_SQLMethods"}
Dataset.def_sql_method(self, :select, %w'with select distinct columns from join where group having compounds order limit lock')
end)

Expand Down
15 changes: 15 additions & 0 deletions lib/sequel/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,21 @@ def json_parser_error_class
JSON::ParserError
end

if RUBY_VERSION >= '3.3'
# Create a new module using the block, and set the temporary name
# on it using the given a containing module and name.
def set_temp_name(mod)
mod.set_temporary_name(yield)
mod
end
# :nocov:
else
def set_temp_name(mod)
mod
end
end
# :nocov:

# Convert given object to json and return the result.
# This can be overridden to use an alternative json implementation.
def object_to_json(obj, *args, &block)
Expand Down
6 changes: 3 additions & 3 deletions lib/sequel/database/dataset_defaults.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class Database
# as the dataset class.
def dataset_class=(c)
unless @dataset_modules.empty?
c = Class.new(c)
c = Sequel.set_temp_name(Class.new(c)){"Sequel::Dataset::_Subclass"}
@dataset_modules.each{|m| c.send(:include, m)}
end
@dataset_class = c
Expand Down Expand Up @@ -61,10 +61,10 @@ def dataset_class=(c)
# # SELECT id, name FROM table WHERE active ORDER BY id
def extend_datasets(mod=nil, &block)
raise(Error, "must provide either mod or block, not both") if mod && block
mod = Dataset::DatasetModule.new(&block) if block
mod = Sequel.set_temp_name(Dataset::DatasetModule.new(&block)){"Sequel::Dataset::_DatasetModule(#{block.source_location.join(':')})"} if block
if @dataset_modules.empty?
@dataset_modules = [mod]
@dataset_class = Class.new(@dataset_class)
@dataset_class = Sequel.set_temp_name(Class.new(@dataset_class)){"Sequel::Dataset::_Subclass"}
else
@dataset_modules << mod
end
Expand Down
2 changes: 1 addition & 1 deletion lib/sequel/dataset/deprecated_singleton_class_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def extension(*a)
def with_extend(*mods, &block)
c = _clone(:freeze=>false)
c.extend(*mods) unless mods.empty?
c.extend(DatasetModule.new(&block)) if block
c.extend(Sequel.set_temp_name(DatasetModule.new(&block)){"Sequel::Dataset::_DatasetModule(#{block.source_location.join(':')})"}) if block
c.freeze
end

Expand Down
3 changes: 2 additions & 1 deletion lib/sequel/dataset/prepared_statements.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ class Dataset
def self.prepared_statements_module(code, mods, meths=DEFAULT_PREPARED_STATEMENT_MODULE_METHODS, &block)
code = PREPARED_STATEMENT_MODULE_CODE[code] || code

Module.new do
Module.new do
Sequel.set_temp_name(self){"Sequel::Dataset::_PreparedStatementsModule(#{block.source_location.join(':') if block})"}
Array(mods).each do |mod|
include mod
end
Expand Down
4 changes: 2 additions & 2 deletions lib/sequel/dataset/query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1238,9 +1238,9 @@ def with_recursive(name, nonrecursive, recursive, opts=OPTS)
# Note that like Object#extend, when multiple modules are provided
# as arguments the subclass includes the modules in reverse order.
def with_extend(*mods, &block)
c = Class.new(self.class)
c = Sequel.set_temp_name(Class.new(self.class)){"Sequel::Dataset::_Subclass"}
c.include(*mods) unless mods.empty?
c.include(DatasetModule.new(&block)) if block
c.include(Sequel.set_temp_name(DatasetModule.new(&block)){"Sequel::Dataset::_DatasetModule(#{block.source_location.join(':')})"}) if block
o = c.freeze.allocate
o.instance_variable_set(:@db, @db)
o.instance_variable_set(:@opts, @opts)
Expand Down
4 changes: 3 additions & 1 deletion lib/sequel/extensions/pg_row.rb
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ class << self
# automatically casted to the database type when literalizing.
def self.subclass(db_type)
Class.new(self) do
Sequel.set_temp_name(self){"Sequel::Postgres::PGRow::ArrayRow::_Subclass(#{db_type})"}
@db_type = db_type
end
end
Expand Down Expand Up @@ -170,6 +171,7 @@ class << self
# type and columns.
def self.subclass(db_type, columns)
Class.new(self) do
Sequel.set_temp_name(self){"Sequel::Postgres::PGRow::HashRow::_Subclass(#{db_type})"}
@db_type = db_type
@columns = columns
end
Expand Down Expand Up @@ -391,7 +393,7 @@ def self.extended(db)
db.instance_exec do
@row_types = {}
@row_schema_types = {}
extend(@row_type_method_module = Module.new)
extend(@row_type_method_module = Sequel.set_temp_name(Module.new){"Sequel::Postgres::PGRow::DatabaseMethods::_RowTypeMethodModule"})
add_conversion_proc(2249, PGRow::Parser.new(:converter=>PGRow::ArrayRow))
if respond_to?(:register_array_type)
register_array_type('record', :oid=>2287, :scalar_oid=>2249)
Expand Down
1 change: 1 addition & 0 deletions lib/sequel/extensions/virtual_row_method_block.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module Sequel
module SQL
class VirtualRow < BasicObject
include(Module.new do
Sequel.set_temp_name(self){"Sequel::SQL:VirtualRow::_MethodBlockMethodMissing"}
# Handle blocks passed to methods and change the behavior.
def method_missing(m, *args, &block)
if block
Expand Down
6 changes: 3 additions & 3 deletions lib/sequel/model/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ def Model(source)
end
end

klass = Class.new(self)
klass = Sequel.set_temp_name(Class.new(self)){"Sequel::_Model(#{source.inspect})"}

if source.is_a?(::Sequel::Database)
klass.db = source
Expand Down Expand Up @@ -768,7 +768,7 @@ def def_column_accessor(*columns)
# default behavior.
def dataset_methods_module
return @dataset_methods_module if defined?(@dataset_methods_module)
Sequel.synchronize{@dataset_methods_module ||= Module.new}
Sequel.synchronize{@dataset_methods_module ||= Sequel.set_temp_name(Module.new){"#{name}::@dataset_methods_module"}}
extend(@dataset_methods_module)
@dataset_methods_module
end
Expand Down Expand Up @@ -956,7 +956,7 @@ def method_added(meth)
# Module that the class includes that holds methods the class adds for column accessors and
# associations so that the methods can be overridden with +super+.
def overridable_methods_module
include(@overridable_methods_module = Module.new) unless @overridable_methods_module
include(@overridable_methods_module = Sequel.set_temp_name(Module.new){"#{name}::@overridable_methods_module"}) unless @overridable_methods_module
@overridable_methods_module
end

Expand Down
2 changes: 1 addition & 1 deletion lib/sequel/plugins/composition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ module Composition
def self.apply(model)
model.instance_exec do
@compositions = {}
include(@composition_module ||= Module.new)
include(@composition_module ||= Sequel.set_temp_name(Module.new){"#{name}::@composition_module"})
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/sequel/plugins/enum.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def enum(column, values, opts=OPTS)
inverted = values.invert.freeze

unless @enum_methods
@enum_methods = Module.new
@enum_methods = Sequel.set_temp_name(Module.new){"#{name}::@enum_methods"}
include @enum_methods
end

Expand Down
1 change: 1 addition & 0 deletions lib/sequel/plugins/inverted_subsets.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ module InvertedSubsets
def self.apply(model, &block)
model.instance_exec do
@dataset_module_class = Class.new(@dataset_module_class) do
Sequel.set_temp_name(self){"#{model.name}::@dataset_module_class(InvertedSubsets)"}
include DatasetModuleMethods
if block
define_method(:inverted_subset_name, &block)
Expand Down
2 changes: 1 addition & 1 deletion lib/sequel/plugins/lazy_attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def lazy_attributes(*attrs)
# :dataset :: The base dataset to use for the lazy attribute lookup
# :table :: The table name to use to qualify the attribute and primary key columns.
def define_lazy_attribute_getter(a, opts=OPTS)
include(@lazy_attributes_module ||= Module.new) unless @lazy_attributes_module
include(@lazy_attributes_module ||= Sequel.set_temp_name(Module.new){"#{name}::@lazy_attributes_module"}) unless @lazy_attributes_module
@lazy_attributes_module.class_eval do
define_method(a) do
if !values.has_key?(a) && !new?
Expand Down
2 changes: 1 addition & 1 deletion lib/sequel/plugins/nested_attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def freeze
#
# If a block is provided, it is used to set the :reject_if option.
def nested_attributes(*associations, &block)
include(@nested_attributes_module ||= Module.new) unless @nested_attributes_module
include(@nested_attributes_module ||= Sequel.set_temp_name(Module.new){"#{name}::@nested_attributes_module"}) unless @nested_attributes_module
opts = associations.last.is_a?(Hash) ? associations.pop : OPTS
reflections = associations.map{|a| association_reflection(a) || raise(Error, "no association named #{a} for #{self}")}
reflections.each do |r|
Expand Down
2 changes: 1 addition & 1 deletion lib/sequel/plugins/rcte_tree.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def self.apply(model, opts=OPTS)

opts = opts.dup
opts[:class] = model
opts[:methods_module] = Module.new
opts[:methods_module] = Sequel.set_temp_name(Module.new){"#{model.name}::_rcte_tree[:methods_module]"}
opts[:union_all] = opts[:union_all].nil? ? true : opts[:union_all]
model.send(:include, opts[:methods_module])

Expand Down
2 changes: 1 addition & 1 deletion lib/sequel/plugins/serialization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ def serialize_attributes(format, *columns)
# Add serializated attribute acessor methods to the serialization_module
def define_serialized_attribute_accessor(serializer, deserializer, *columns)
m = self
include(@serialization_module ||= Module.new) unless @serialization_module
include(@serialization_module ||= Sequel.set_temp_name(Module.new){"#{name}::@serialization_module"}) unless @serialization_module
@serialization_module.class_eval do
columns.each do |column|
m.serialization_map[column] = serializer
Expand Down
2 changes: 1 addition & 1 deletion lib/sequel/plugins/sql_comments.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def sql_comments_instance_methods(*meths)
# Use automatic SQL comments for the given dataset methods.
def sql_comments_dataset_methods(*meths)
unless @_sql_comments_dataset_module
dataset_module(@_sql_comments_dataset_module = Module.new)
dataset_module(@_sql_comments_dataset_module = Sequel.set_temp_name(Module.new){"#{name}::@_sql_comments_dataset_module"})
end
_sql_comments_methods(@_sql_comments_dataset_module, :dataset, meths)
end
Expand Down
1 change: 1 addition & 0 deletions lib/sequel/plugins/subset_conditions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ module SubsetConditions
def self.apply(model, &block)
model.instance_exec do
@dataset_module_class = Class.new(@dataset_module_class) do
Sequel.set_temp_name(self){"#{model.name}::@dataset_module_class(SubsetConditions)"}
include DatasetModuleMethods
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/sequel/plugins/subset_static_cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def subset_static_cache_module
# it before calling creating this module.
dataset_methods_module

Sequel.synchronize{@subset_static_cache_module ||= Module.new}
Sequel.synchronize{@subset_static_cache_module ||= Sequel.set_temp_name(Module.new){"#{name}::@subset_static_cache_module"}}
extend(@subset_static_cache_module)
@subset_static_cache_module
end
Expand Down
1 change: 1 addition & 0 deletions lib/sequel/sql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1915,6 +1915,7 @@ def initialize
end

m = Module.new do
Sequel.set_temp_name(Module.new){"Sequel::SQL::VirtualRow::_BaseMethodMissing"}
# Return an +Identifier+, +QualifiedIdentifier+, or +Function+, depending
# on arguments and whether a block is provided. Does not currently call the block.
# See the class level documentation.
Expand Down
11 changes: 11 additions & 0 deletions spec/core/database_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,11 @@ def dataset_class_default; Sequel::Dataset end
ds.db.must_be_same_as(@db)
end

it "should give temporary name to implicitly created subclass" do
@db.extend_datasets{}
@db.dataset_class.name.must_equal "Sequel::Dataset::_Subclass"
end if RUBY_VERSION >= '3.3'

it "should have getter return the class to use to create datasets" do
[@db.dataset_class, @db.dataset_class.superclass].must_include(Sequel::Dataset)
@db.dataset_class = @dsc
Expand Down Expand Up @@ -561,6 +566,12 @@ def dataset_class_default; Sequel::Dataset end
@db.dataset.foo.must_equal [5, 3]
end

it "should give temporary name to class and module" do
@db.extend_datasets{def foo() [5] + super end}
@db.dataset_class.ancestors[1].name.must_equal "Sequel::Dataset::_DatasetModule(#{__FILE__}:#{__LINE__-1})"
@db.dataset_class.name.must_equal "Sequel::Dataset::_Subclass"
end if RUBY_VERSION >= '3.3'

it "should raise an error if both a module and a block are provided" do
proc{@db.extend_datasets(@m2){def foo() [5] + super end}}.must_raise(Sequel::Error)
end
Expand Down
12 changes: 9 additions & 3 deletions spec/core/dataset_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1907,6 +1907,12 @@ def supports_cte_in_subselect?; false end
it "should work with just a block" do
Sequel.mock.dataset.with_extend{def a; 1 end}.a.must_equal 1
end

it "should give temporary name to class and module" do
ds = Sequel.mock.dataset.with_extend{def a; 1 end}
ds.class.ancestors[1].name.must_equal "Sequel::Dataset::_DatasetModule(#{__FILE__}:#{__LINE__-1})"
ds.class.name.must_equal "Sequel::Dataset::_Subclass"
end if RUBY_VERSION >= '3.3'
end

describe "Dataset#with_extend custom methods" do
Expand Down Expand Up @@ -4333,8 +4339,8 @@ def delete_sql; super << " RETURNING *" end
end

it "#inspect should indicate it is a prepared statement with the prepared SQL" do
@ds.filter(:num=>:$n).prepare(:select, :sn).inspect.must_equal \
'<Sequel::Mock::Dataset/PreparedStatement "SELECT * FROM items WHERE (num = $n)">'
@ds.filter(:num=>:$n).prepare(:select, :sn).inspect.must_match \
%r'\A<Sequel::.*/PreparedStatement "SELECT \* FROM items WHERE \(num = \$n\)">\z'
end

it "should handle literal strings" do
Expand Down Expand Up @@ -4423,7 +4429,7 @@ def prepared_statement_modules
end

it "#inspect should show the actual SQL submitted to the database" do
@ps.first.inspect.must_equal '<Sequel::Mock::Dataset/PreparedStatement "SELECT * FROM items WHERE (num = ?)">'
@ps.first.inspect.must_match %r'\A<Sequel::.*/PreparedStatement "SELECT \* FROM items WHERE \(num = \?\)">\z'
end

it "should submit the SQL to the database with placeholders and bind variables" do
Expand Down
6 changes: 6 additions & 0 deletions spec/extensions/composition_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
DB.reset
end

it "should give temporary name to name model-specific module" do
c = Sequel::Model(:items)
c.plugin :composition
c.ancestors[2].name.must_equal "Sequel::_Model(:items)::@composition_module"
end if RUBY_VERSION >= '3.3'

it ".composition should add compositions" do
@o.wont_respond_to(:date)
@c.composition :date, :mapping=>[:year, :month, :day]
Expand Down
7 changes: 7 additions & 0 deletions spec/extensions/enum_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
@album = @Album.load(:status_id=>3)
end

it "should give temporary name to name model-specific module" do
c = Sequel::Model(:items)
c.plugin :enum
c.enum :status_id, :good=>3, :bad=>5
c.ancestors[1].name.must_equal "Sequel::_Model(:items)::@enum_methods"
end if RUBY_VERSION >= '3.3'

it "should add enum_value! and enum_value? methods for setting/checking the enum values" do
@album.good?.must_equal true
@album.bad?.must_equal false
Expand Down
6 changes: 6 additions & 0 deletions spec/extensions/inverted_subsets_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
require_relative "spec_helper"

describe "Sequel::Plugins::InvertedSubsets" do
it "should name generated dataset module class" do
c = Sequel::Model(:items)
c.plugin :inverted_subsets
c.dataset_module_class.name.must_equal "Sequel::_Model(:items)::@dataset_module_class(InvertedSubsets)"
end if RUBY_VERSION >= '3.3'

it "should add an inverted subset method which inverts the condition" do
c = Class.new(Sequel::Model(:a))
c.plugin :inverted_subsets
Expand Down
4 changes: 4 additions & 0 deletions spec/extensions/lazy_attributes_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ def self.columns; [:id] end
Object.send(:remove_const, :LazyAttributesModel)
end

it "should give temporary name to name model-specific module" do
LazyAttributesModel.ancestors[1].name.must_equal "LazyAttributesModel::@lazy_attributes_module"
end if RUBY_VERSION >= '3.3'

it "should allowing adding additional lazy attributes via plugin :lazy_attributes" do
@c.set_dataset(@ds.select(:id, :blah))
@c.dataset.sql.must_equal 'SELECT id, blah FROM la'
Expand Down
8 changes: 8 additions & 0 deletions spec/extensions/nested_attributes_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ def check_sql_array(*shoulds)
@db.sqls
end

it "should give temporary name to name model-specific module" do
c = Sequel::Model(:items)
c.plugin :nested_attributes
c.many_to_one :c, :class=>c
c.nested_attributes :c
c.ancestors[1].name.must_equal "Sequel::_Model(:items)::@nested_attributes_module"
end if RUBY_VERSION >= '3.3'

it "should not modify options hash when loading plugin" do
h = {}
@Concert.nested_attributes :albums, h
Expand Down
Loading

0 comments on commit 066e256

Please sign in to comment.