Module: Testoscope

Defined in:
lib/testoscope.rb,
lib/testoscope/version.rb

Defined Under Namespace

Modules: AdapterUpgrade Classes: Config

Constant Summary collapse

VERSION =
"0.2.0"

Class Method Summary collapse

Class Method Details

.add_index_used(sql, explain, index_used) ⇒ Object



47
48
49
50
# File 'lib/testoscope.rb', line 47

def self.add_index_used( sql, explain, index_used )
  results[:indexes][index_used] ||= []
  results[:indexes][index_used] << { explain: explain, sql: sql }
end

.add_unintended_behaviour(sql, explain, backtrace) ⇒ Object



39
40
41
42
43
44
# File 'lib/testoscope.rb', line 39

def self.add_unintended_behaviour( sql, explain, backtrace )
  results[:unintended_behaviour][sql] ||= {}
  results[:unintended_behaviour][sql][:explain] = explain
  results[:unintended_behaviour][sql][:backtrace] ||= []
  results[:unintended_behaviour][sql][:backtrace] << backtrace.to_a unless results[:unintended_behaviour][sql][:backtrace].include?(backtrace)
end

.analyze(sql) ⇒ Object



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/testoscope.rb', line 96

def self.analyze( sql )
  if config.analyze && sql_has_analyzing_tables?(sql)

    explain = yield

    explain = config.pp_class.method(:pp).arity.abs == 1 ? config.pp_class.new.pp( explain ) : config.pp_class.new.pp( explain, 0 )

    app_trace = caller_locations( 2 ).map(&:to_s).select { |st|
      self.config.back_trace_paths.any?{|pth| st[pth]} && !self.config.back_trace_exclude_paths.any?{|epth| st[epth]}
    }
    # this is the case when we for example making query from test files,
    # we may omit some params for where clause and so.
    return if app_trace.length == 0

    unintended_found = self.config.unintened_key_words.select{|ukw| explain[ukw] }

    if unintended_found.length > 0
      raise StandardError.new("#{unintended_found.join(', ')} found!\n #{explain}") if config.raise_when_unintended
      self.add_unintended_behaviour( sql, explain, app_trace )
    end

    explain.scan(/Index Scan using (\w+)|Index Scan on (\w+)|Index Scan Backward using (\w+)/)
      .each{|found| add_index_used(sql, explain, found.compact.first ) }
  end
end

.configObject



18
# File 'lib/testoscope.rb', line 18

def self.config; @@config ||= Config.new end

.configure {|config| ... } ⇒ Object

Yields:



20
21
22
23
24
25
26
27
28
29
30
# File 'lib/testoscope.rb', line 20

def self.configure
  yield(config) if block_given?

  ::ActiveRecord::Base.connection.class.include(AdapterUpgrade)

  # since test table is small planner may want to just deal with Seq scans and skip all the index fuss
  ::ActiveRecord::Base.connection.execute( 'SET enable_seqscan=off;' ) if ::ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'


  config.tables.map!{|table| /.*[ "]#{table}[ "].*/ } if config.tables != :all
end

.get_all_indexesObject

Alternative way to get index names “without” ActiveRecord “SELECT i.relname as indname FROM pg_index as idx JOIN pg_class as i ON i.oid = idx.indexrelid WHERE idx.indrelid::regclass = ANY( ARRAY::regclass[] )”)

.to_a.map(&:values).flatten


57
58
59
60
61
# File 'lib/testoscope.rb', line 57

def self.get_all_indexes
  ActiveRecord::Base.connection.tables.map { |table|
    [table, ActiveRecord::Base.connection.indexes(table).map(&:name)]
  }.to_h
end


63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/testoscope.rb', line 63

def self.print_results
  return yield(results) if block_given?

  puts "\n<UNINTENDED BEHAVIOUR>\n" unless results[:unintended_behaviour].blank?

  results[:unintended_behaviour].each do |sql, values|
    puts "\nSQL:\n\n"
    puts Niceql::Prettifier.prettify_sql( sql )
    puts "\n\nAPP BACKTRACE:\n\n"
    puts values[:backtrace].map{|arr| arr.join("\n")}.uniq.join("\n            _____________________\n\n")
    puts ''
    puts values[:explain]
    puts "\n++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n"
  end

  filtered_unused_indexes = []

  get_all_indexes.each do |table, indexes|
    unused_indexes = indexes - results[:indexes].keys
    next if unused_indexes.blank? || !sql_has_analyzing_tables?(" #{table} ")
    filtered_unused_indexes << "\n-----#{table}------\n"
    filtered_unused_indexes << unused_indexes
  end
  if !filtered_unused_indexes.blank?
    puts "\n<UNUSED INDEXES>\n"
    puts filtered_unused_indexes.join("\n")
  end
end

.resultsObject



32
33
34
35
36
37
# File 'lib/testoscope.rb', line 32

def self.results
  @results ||= {
    unintended_behaviour: {},
    indexes: {},
  }
end

.sql_has_analyzing_tables?(sql) ⇒ Boolean

Returns:

  • (Boolean)


92
93
94
# File 'lib/testoscope.rb', line 92

def self.sql_has_analyzing_tables?(sql)
  config.tables == :all || config.tables.any?{ |table| sql[table] }
end

.suspend_global_analyze(analyze) ⇒ Object



122
123
124
125
126
# File 'lib/testoscope.rb', line 122

def self.suspend_global_analyze( analyze )
  was, config.analyze = config.analyze, analyze
  yield
  config.analyze = was
end