Skip to content

Instantly share code, notes, and snippets.

@alpaca-tc
Last active June 29, 2023 13:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alpaca-tc/cb1976d433066ca069b6193b7e2fa2f5 to your computer and use it in GitHub Desktop.
Save alpaca-tc/cb1976d433066ca069b6193b7e2fa2f5 to your computer and use it in GitHub Desktop.
実際のスクリプトから少し削ってる。そのままだと動かないかも :gomenne:
# ./bin/rails r script/query_methods_dumper.rb
dumper = MethodDumper.new do
run_rspec
end
%i(where joins includes preload eager_load references left_joins left_outer_joins find_by find_or_create_by find_or_initialize_by create_or_find_by).each do |method_name|
dumper.add_method_trace("ActiveRecord::QueryMethods##{method_name}")
dumper.add_method_trace("ActiveRecord::QueryMethods##{method_name}!")
end
%i(exists? find_by).each do |method_name|
dumper.add_method_trace("ActiveRecord::FinderMethods##{method_name}")
end
%i(new create_or_find_by find_or_create_by find_or_initialize_by).each do |method_name|
dumper.add_method_trace("ActiveRecord::Relation##{method_name}")
end
%i(build create).each do |method_name|
dumper.add_method_trace("ActiveRecord::Associations::CollectionProxy##{method_name}")
dumper.add_method_trace("ActiveRecord::Associations::CollectionProxy##{method_name}!")
end
%i(build create attributes_for build_stubbed null).each do |method_name|
dumper.add_method_trace("FactoryBot::Syntax::Methods##{method_name}")
dumper.add_method_trace("FactoryBot::Syntax::Methods##{method_name}_list")
dumper.add_method_trace("FactoryBot::Syntax::Methods##{method_name}_pair")
end
dumper.add_method_trace("ActiveRecord::QueryMethods::WhereChain#not")
dumper.add_method_trace("ActiveRecord::QueryMethods::WhereChain#associated")
dumper.add_method_trace("ActiveRecord::QueryMethods::WhereChain#missing")
dumper.add_method_trace("ActiveRecord::Inheritance::ClassMethods.new")
dumper.add_method_trace("ActiveRecord::Persistence::ClassMethods.create")
ActiveRecord::TableMetadata.class_attribute(:enabled_reflections_collector, default: false)
ActiveRecord::TableMetadata.class_attribute(:reflections, default: [])
ActiveRecord::TableMetadata.singleton_class.prepend(Module.new do
def collect_reflections
self.enabled_reflections_collector = true
self.reflections = []
yield
reflections
ensure
self.enabled_reflections_collector = false
end
end)
ActiveRecord::TableMetadata.prepend(Module.new do
def associated_table(table_name)
if self.class.enabled_reflections_collector
reflection = klass._reflect_on_association(table_name) || klass._reflect_on_association(table_name.singularize)
self.class.reflections << [klass.name, table_name.to_sym] if reflection
end
super
end
end)
class QueryMethodAnalyzer
BACKTRACE_CLEANER = ActiveSupport::BacktraceCleaner.new.tap do |cleaner|
cleaner.add_silencer { !(_1.start_with?(Rails.root.join("app").to_s) || _1.start_with?(Rails.root.join("spec").to_s)) }
cleaner.add_silencer { _1.include?("spec/rails_helper.rb") }
end
# User.where.not のwhere句や、User.none を無視する
IGNORED_METHODS = %i(where none).freeze
ALIAS_METHODS = {
"ActiveRecord::QueryMethods#left_joins" => "ActiveRecord::QueryMethods#left_outer_joins",
"ActiveRecord::Relation#new" => "ActiveRecord::Relation#build",
}.tap { _1.merge!(_1.invert) }.freeze
attr_reader :trace_point
attr_reader :klass
attr_reader :receiver_and_method_name
def initialize(trace_point, klass, receiver_and_method_name)
@trace_point = trace_point
@klass = klass
@receiver_and_method_name = receiver_and_method_name
end
def parameters_code_location
case node.type
when :CALL
_nd_recv, _mid, nd_args = node.children
{
path: caller_location.path,
first_lineno: nd_args.first_lineno,
first_column: nd_args.first_column - 1,
last_lineno: node.last_lineno,
last_column: node.last_column,
}
when :FCALL
_method_name, nd_args = node.children
{
path: caller_location.path,
first_lineno: nd_args.first_lineno,
first_column: nd_args.first_column - 1,
last_lineno: node.last_lineno,
last_column: node.last_column,
}
else
raise NotImplementedError, "not implemented yet #{node.type}"
end
end
def model
if @klass < ActiveRecord::Associations::CollectionProxy
@klass.to_s.remove("::ActiveRecord_Associations_CollectionProxy").safe_constantize
elsif @klass < ActiveRecord::AssociationRelation
@klass.to_s.remove("::ActiveRecord_AssociationRelation").safe_constantize
elsif @klass < ActiveRecord::Relation
@klass.to_s.remove("::ActiveRecord_Relation").safe_constantize
elsif @klass < ActiveRecord::Base
@klass
elsif @trace_point.self.kind_of?(ActiveRecord::QueryMethods::WhereChain)
@trace_point.self.instance_variable_get(:@scope).klass
elsif @receiver_and_method_name.start_with?("FactoryBot::Syntax::Methods#")
name, * = extract_attributes_for_factory_bot
klass = FactoryBot.factories[name].build_class
klass if klass < ActiveRecord::Base
else
raise "not supported #{@klass}"
end
end
def extract_reflections
case @receiver_and_method_name.remove(/!$/)
when "ActiveRecord::FinderMethods#exists?"
conditions = extract_variables_from_trace_point(:conditions)[0]
if conditions == :none
[]
else
collect_reflections_from_where_clause(trace_point.self.unscoped, conditions)
end
when "ActiveRecord::Persistence::ClassMethods.create",
"ActiveRecord::Relation#new",
"ActiveRecord::Inheritance::ClassMethods.new",
"ActiveRecord::Associations::CollectionProxy#build",
"ActiveRecord::Associations::CollectionProxy#create"
attributes = extract_variables_from_trace_point(:attributes)[0]
collect_reflections_from_where_clause(trace_point.self.unscoped, attributes)
when "ActiveRecord::QueryMethods#find_or_create_by",
"ActiveRecord::QueryMethods#find_or_initialize_by",
"ActiveRecord::QueryMethods#create_or_find_by",
"ActiveRecord::Relation#find_or_initialize_by",
"ActiveRecord::Relation#find_or_create_by",
"ActiveRecord::Relation#create_or_find_by"
attributes = extract_variables_from_trace_point(:attributes)[0]
collect_reflections_from_where_clause(trace_point.self, attributes)
when "ActiveRecord::QueryMethods#find_by",
"ActiveRecord::FinderMethods#find_by"
arg, args = extract_variables_from_trace_point(:arg, :args)
collect_reflections_from_where_clause(trace_point.self, arg, *args)
when "ActiveRecord::QueryMethods#where"
args = extract_variables_from_trace_point(:args)[0]
collect_reflections_from_where_clause(trace_point.self, *args)
when "ActiveRecord::QueryMethods::WhereChain#not"
opts, rest, receiver = extract_variables_from_trace_point(:opts, :rest, :@scope)
collect_reflections_from_where_clause(receiver, opts, *rest)
when "ActiveRecord::QueryMethods#references"
table_names = extract_variables_from_trace_point(:table_names)[0]
collect_reflections_from_joins_clause(trace_point.self, *table_names)
when "ActiveRecord::QueryMethods#joins",
"ActiveRecord::QueryMethods#includes",
"ActiveRecord::QueryMethods#preload",
"ActiveRecord::QueryMethods#eager_load",
"ActiveRecord::QueryMethods#left_outer_joins"
args = extract_variables_from_trace_point(:args)[0]
collect_reflections_from_joins_clause(trace_point.self, *args)
when "ActiveRecord::QueryMethods::WhereChain#associated",
"ActiveRecord::QueryMethods::WhereChain#missing"
args, receiver = extract_variables_from_trace_point(:associations, :@scope)
collect_reflections_from_joins_clause(receiver, *args)
when "FactoryBot::Syntax::Methods#build",
"FactoryBot::Syntax::Methods#create",
"FactoryBot::Syntax::Methods#build_list",
"FactoryBot::Syntax::Methods#create_list",
"FactoryBot::Syntax::Methods#attributes_for"
name, *traits = extract_attributes_for_factory_bot
attributes = traits.extract_options!
klass = FactoryBot.factories[name].build_class
collect_reflections_from_where_clause(klass.unscoped, attributes)
else
raise NotImplementedError, "not implemented yet #{@receiver_and_method_name}"
end
end
private
def node
@node ||= RubyVM::AbstractSyntaxTree.of(caller_location, keep_script_lines: true)
end
def extract_attributes_for_factory_bot
case @receiver_and_method_name
when "FactoryBot::Syntax::Methods#build",
"FactoryBot::Syntax::Methods#create",
"FactoryBot::Syntax::Methods#build_list",
"FactoryBot::Syntax::Methods#create_list",
"FactoryBot::Syntax::Methods#attributes_for"
name, traits_and_overrides = extract_variables_from_trace_point(:name, :traits_and_overrides)
[name, *traits_and_overrides]
else
raise NotImplementedError, "not implemented yet factory attributes #{@receiver_and_method_name}"
end
end
def extract_variables_from_trace_point(*names)
names.map do |name|
if name.to_s.start_with?("@")
trace_point.self.instance_variable_get(name)
else
trace_point.binding.local_variable_get(name)
end
end
end
def flat_arguments(args, collector = [])
case args
when Array
args.each { flat_arguments(_1, collector) }
when Hash
args.each do |key, value|
collector << key
flat_arguments(value, collector)
end
when Symbol, String
collector << args
else
raise NotImplementedError, "not implemented yet #{args.class}"
end
collector
end
def collect_reflections_from_joins_clause(receiver, *args)
associations = args
candidates = flat_arguments(args)
join_dependencies = []
join_dependencies.unshift(
receiver.construct_join_dependency(
receiver.send(:select_association_list, associations, join_dependencies), nil
),
)
join_dependencies.flat_map(&:reflections).uniq.filter_map do |reflection|
[reflection.active_record.name, reflection.name] if candidates.include?(reflection.name)
end
end
def collect_reflections_from_where_clause(receiver, *args)
if args.empty? || (args.length == 1 && args.first.blank?)
[]
else
opts, *rest = *args
ActiveRecord::TableMetadata.collect_reflections do
receiver.send(:build_where_clause, opts, rest)
end
end
end
def caller_location
@caller_location ||= begin
backtrace = BACKTRACE_CLEANER.clean(@trace_point.binding.send(:caller))
called_from = backtrace[0].to_s
@trace_point.send(:caller_locations).find { _1.to_s == called_from }
end
end
end
CSV.open("tmp/dumps/query_methods.csv", "w") do |csv|
headers = %i(model klass method_name path first_lineno first_column last_lineno last_column reflections)
csv << headers
dumper.run do |row|
trace_point = row[:trace_point]
analyzer = QueryMethodAnalyzer.new(trace_point, row[:klass], row[:method_name])
next if analyzer.model.nil?
reflections = analyzer.extract_reflections
next if reflections.empty?
code_location = analyzer.parameters_code_location
row = {
model: analyzer.model,
klass: analyzer.klass,
method_name: row[:method_name],
reflections: reflections.to_json,
**code_location.to_h,
}
pp(row)
csv << headers.map { row.fetch(_1) }
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment