module NPlusOneControl

RSpec and Minitest matchers to prevent N+1 queries problem.

Constants

EXTRACT_TABLE_RXP

Used to extract a table name from a query

FAILURE_MESSAGES
QUERY_PART_TO_TYPE

Used to convert a query part extracted by the regexp above to the corresponding human-friendly type

VERSION

Attributes

backtrace_cleaner[RW]
backtrace_length[RW]
default_matching[R]
default_scale_factors[RW]
event[RW]
ignore[RW]
show_table_stats[RW]
truncate_query_size[RW]
verbose[RW]

Public Class Methods

default_matching=(val) click to toggle source
# File lib/n_plus_one_control.rb, line 70
def default_matching=(val)
  unless val
    @default_matching = nil
    return
  end

  @default_matching =
    if val.is_a?(Regexp)
      val
    else
      Regexp.new(val, Regexp::MULTILINE | Regexp::IGNORECASE)
    end
end
failure_message(type, queries) click to toggle source
# File lib/n_plus_one_control.rb, line 31
def failure_message(type, queries) # rubocop:disable Metrics/MethodLength
  msg = ["#{FAILURE_MESSAGES[type]}, but got:\n"]
  queries.each do |(scale, data)|
    msg << "  #{data.size} for N=#{scale}\n"
  end

  msg.concat(table_usage_stats(queries.map(&:last))) if show_table_stats

  if verbose
    queries.each do |(scale, data)|
      msg << "Queries for N=#{scale}\n"
      msg << data.map { |sql| "  #{truncate_query(sql)}\n" }.join.to_s
    end
  end

  msg.join
end
table_usage_stats(runs) click to toggle source
# File lib/n_plus_one_control.rb, line 49
def table_usage_stats(runs) # rubocop:disable Metrics/MethodLength
  msg = ["Unmatched query numbers by tables:\n"]

  before, after = runs.map do |queries|
    queries.group_by do |query|
      matches = query.match(EXTRACT_TABLE_RXP)
      next unless matches

      "  #{matches[2]} (#{QUERY_PART_TO_TYPE[matches[1].downcase]})"
    end.transform_values(&:count)
  end

  before.keys.each do |k|
    next if before[k] == after[k]

    msg << "#{k}: #{before[k]} != #{after[k]}\n"
  end

  msg
end

Private Class Methods

truncate_query(sql) click to toggle source
# File lib/n_plus_one_control.rb, line 86
def truncate_query(sql)
  return sql unless truncate_query_size

  # Only truncate query, leave tracing (if any) as is
  parts = sql.split(/(\s+↳)/)

  parts[0] =
    if truncate_query_size < 4
      "..."
    else
      parts[0][0..(truncate_query_size - 4)] + "..."
    end

  parts.join
end