class ArQueryMatchers::Queries::QueryCounter
The QueryCounter
instruments a ruby block and collect stats on the SQL queries performed during its execution.
It's “generic” meaning it requires an instance of a QueryFilter
to operate. The QueryFilter
is an interface that both filters SQL statements and maps them to an ActiveRecord model, sort of a Enumerator#filter_map.
This class is meant to be wrapped by another class that provides a concrete QueryFilter
implementation. For example, you could implement a SelectQueryFilter using it: class SelectQueryCounter
class SelectFilter < Queries::QueryFilter def extract(_name, sql) select_from_table = sql.match(/SELECT .* FROM [`"](?<table_name>[^`"]+)[`"]/) Queries::TableName.new(select_from_table[:table_name]) if select_from_table end end def self.instrument(&block) QueryCounter.new(SelectFilter.new).instrument(&block) end
end
stats = SelectQueryCounter.instrument do
Company.first Employee.last(100) User.find(1) User.find(2)
end
stats.query_counts == { 'Company' => 1, Employee => '1', 'User' => 2 }
Constants
- MARGINALIA_SQL_COMMENT_PATTERN
The 'marginalia' gem adds a line from the backtrace to the SQL query in the form of a comment.
Public Class Methods
# File lib/ar_query_matchers/queries/query_counter.rb, line 58 def initialize(query_filter) @query_filter = query_filter end
Public Instance Methods
@param [block] block to instrument @return [QueryStats] stats about all the SQL queries executed during the block
# File lib/ar_query_matchers/queries/query_counter.rb, line 64 def instrument(&block) queries = Hash.new { |h, k| h[k] = { count: 0, lines: [], time: BigDecimal(0) } } ActiveSupport::Notifications.subscribed(to_proc(queries), 'sql.active_record', &block) QueryStats.new(queries) end
Private Instance Methods
# File lib/ar_query_matchers/queries/query_counter.rb, line 77 def to_proc(queries) lambda do |_name, start, finish, _message_id, payload| return if payload[:cached] # Given a `sql.active_record` event, figure out which model is being # accessed. Some of the simpler queries have a :name key that makes this # really easy. Others require parsing the SQL by hand. model_name = @query_filter.filter_map(payload[:name] || '', payload[:sql] || '')&.model_name if model_name comment = payload[:sql].match(MARGINALIA_SQL_COMMENT_PATTERN) queries[model_name][:lines] << comment[:line] if comment queries[model_name][:count] += 1 queries[model_name][:time] += (finish - start).round(6) # Round to microseconds end end end