lookout-query_reviewer 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/MIT-LICENSE +20 -0
- data/README.md +136 -0
- data/Rakefile +24 -0
- data/lib/query_reviewer.rb +66 -0
- data/lib/query_reviewer/array_extensions.rb +29 -0
- data/lib/query_reviewer/controller_extensions.rb +67 -0
- data/lib/query_reviewer/mysql_adapter_extensions.rb +92 -0
- data/lib/query_reviewer/mysql_analyzer.rb +62 -0
- data/lib/query_reviewer/query_warning.rb +17 -0
- data/lib/query_reviewer/rails.rb +37 -0
- data/lib/query_reviewer/sql_query.rb +131 -0
- data/lib/query_reviewer/sql_query_collection.rb +103 -0
- data/lib/query_reviewer/sql_sub_query.rb +45 -0
- data/lib/query_reviewer/tasks.rb +8 -0
- data/lib/query_reviewer/views/_box.html.erb +11 -0
- data/lib/query_reviewer/views/_box_ajax.js +34 -0
- data/lib/query_reviewer/views/_box_body.html.erb +73 -0
- data/lib/query_reviewer/views/_box_disabled.html.erb +2 -0
- data/lib/query_reviewer/views/_box_header.html.erb +1 -0
- data/lib/query_reviewer/views/_box_includes.html.erb +234 -0
- data/lib/query_reviewer/views/_explain.html.erb +30 -0
- data/lib/query_reviewer/views/_js_includes.html.erb +68 -0
- data/lib/query_reviewer/views/_js_includes_new.html.erb +68 -0
- data/lib/query_reviewer/views/_profile.html.erb +26 -0
- data/lib/query_reviewer/views/_query_sql.html.erb +8 -0
- data/lib/query_reviewer/views/_query_trace.html.erb +31 -0
- data/lib/query_reviewer/views/_query_with_warning.html.erb +54 -0
- data/lib/query_reviewer/views/_spectrum.html.erb +10 -0
- data/lib/query_reviewer/views/_warning_no_query.html.erb +8 -0
- data/lib/query_reviewer/views/query_review_box_helper.rb +99 -0
- data/query_reviewer_defaults.yml +39 -0
- metadata +77 -0
@@ -0,0 +1,17 @@
|
|
1
|
+
module QueryReviewer
|
2
|
+
class QueryWarning
|
3
|
+
attr_reader :query, :severity, :problem, :desc, :table, :id
|
4
|
+
|
5
|
+
cattr_accessor :next_id
|
6
|
+
self.next_id = 1
|
7
|
+
|
8
|
+
def initialize(options)
|
9
|
+
@query = options[:query]
|
10
|
+
@severity = options[:severity]
|
11
|
+
@problem = options[:problem]
|
12
|
+
@desc = options[:desc]
|
13
|
+
@table = options[:table]
|
14
|
+
@id = (self.class.next_id += 1)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'query_reviewer'
|
2
|
+
|
3
|
+
module QueryReviewer
|
4
|
+
def self.inject_reviewer
|
5
|
+
# Load adapters
|
6
|
+
ActiveRecord::Base
|
7
|
+
adapter_class = ActiveRecord::ConnectionAdapters::MysqlAdapter if defined? ActiveRecord::ConnectionAdapters::MysqlAdapter
|
8
|
+
adapter_class = ActiveRecord::ConnectionAdapters::Mysql2Adapter if defined? ActiveRecord::ConnectionAdapters::Mysql2Adapter
|
9
|
+
adapter_class.send(:include, QueryReviewer::MysqlAdapterExtensions) if adapter_class
|
10
|
+
# Load into controllers
|
11
|
+
ActionController::Base.send(:include, QueryReviewer::ControllerExtensions)
|
12
|
+
Array.send(:include, QueryReviewer::ArrayExtensions)
|
13
|
+
if ActionController::Base.respond_to?(:append_view_path)
|
14
|
+
ActionController::Base.append_view_path(File.dirname(__FILE__) + "/lib/query_reviewer/views")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
if defined?(Rails::Railtie)
|
20
|
+
module QueryReviewer
|
21
|
+
class Railtie < Rails::Railtie
|
22
|
+
rake_tasks do
|
23
|
+
load File.dirname(__FILE__) + "/tasks.rb"
|
24
|
+
end
|
25
|
+
|
26
|
+
initializer "query_reviewer.initialize" do
|
27
|
+
QueryReviewer.load_configuration
|
28
|
+
|
29
|
+
QueryReviewer.inject_reviewer if QueryReviewer.enabled?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
else # Rails 2
|
34
|
+
QueryReviewer.load_configuration
|
35
|
+
|
36
|
+
QueryReviewer.inject_reviewer if QueryReviewer.enabled?
|
37
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require "ostruct"
|
2
|
+
|
3
|
+
module QueryReviewer
|
4
|
+
# a single SQL SELECT query
|
5
|
+
class SqlQuery
|
6
|
+
attr_reader :sqls, :rows, :subqueries, :trace, :id, :command, :affected_rows, :profiles, :durations, :sanitized_sql
|
7
|
+
|
8
|
+
cattr_accessor :next_id
|
9
|
+
self.next_id = 1
|
10
|
+
|
11
|
+
def initialize(sql, rows, full_trace, duration = 0.0, profile = nil, command = "SELECT", affected_rows = 1, sanitized_sql = nil)
|
12
|
+
@trace = full_trace
|
13
|
+
@rows = rows
|
14
|
+
@sqls = [sql]
|
15
|
+
@sanitized_sql = sanitized_sql
|
16
|
+
@subqueries = rows ? rows.collect{|row| SqlSubQuery.new(self, row)} : []
|
17
|
+
@id = (self.class.next_id += 1)
|
18
|
+
@profiles = profile ? [profile.collect { |p| OpenStruct.new(p) }] : [nil]
|
19
|
+
@durations = [duration.to_f]
|
20
|
+
@warnings = []
|
21
|
+
@command = command
|
22
|
+
@affected_rows = affected_rows
|
23
|
+
end
|
24
|
+
|
25
|
+
def add(sql, duration, profile)
|
26
|
+
sql << sql
|
27
|
+
durations << duration
|
28
|
+
profiles << profile
|
29
|
+
end
|
30
|
+
|
31
|
+
def sql
|
32
|
+
sqls.first
|
33
|
+
end
|
34
|
+
|
35
|
+
def count
|
36
|
+
durations.size
|
37
|
+
end
|
38
|
+
|
39
|
+
def profile
|
40
|
+
profiles.first
|
41
|
+
end
|
42
|
+
|
43
|
+
def duration
|
44
|
+
durations.sum
|
45
|
+
end
|
46
|
+
|
47
|
+
def duration_stats
|
48
|
+
"TOTAL:#{'%.3f' % duration} AVG:#{'%.3f' % (durations.sum / durations.size)} MAX:#{'%.3f' % (durations.max)} MIN:#{'%.3f' % (durations.min)}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_table
|
52
|
+
rows.qa_columnized
|
53
|
+
end
|
54
|
+
|
55
|
+
def warnings
|
56
|
+
self.subqueries.collect(&:warnings).flatten + @warnings
|
57
|
+
end
|
58
|
+
|
59
|
+
def has_warnings?
|
60
|
+
!self.warnings.empty?
|
61
|
+
end
|
62
|
+
|
63
|
+
def max_severity
|
64
|
+
self.warnings.empty? ? 0 : self.warnings.collect(&:severity).max
|
65
|
+
end
|
66
|
+
|
67
|
+
def table
|
68
|
+
@subqueries.first.table
|
69
|
+
end
|
70
|
+
|
71
|
+
def analyze!
|
72
|
+
self.subqueries.collect(&:analyze!)
|
73
|
+
if duration
|
74
|
+
if duration >= QueryReviewer::CONFIGURATION["critical_duration_threshold"]
|
75
|
+
warn(:problem => "Query took #{duration} seconds", :severity => 9)
|
76
|
+
elsif duration >= QueryReviewer::CONFIGURATION["warn_duration_threshold"]
|
77
|
+
warn(:problem => "Query took #{duration} seconds", :severity => QueryReviewer::CONFIGURATION["critical_severity"])
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
if affected_rows >= QueryReviewer::CONFIGURATION["critical_affected_rows"]
|
82
|
+
warn(:problem => "#{affected_rows} rows affected", :severity => 9, :description => "An UPDATE or DELETE query can be slow and lock tables if it affects many rows.")
|
83
|
+
elsif affected_rows >= QueryReviewer::CONFIGURATION["warn_affected_rows"]
|
84
|
+
warn(:problem => "#{affected_rows} rows affected", :severity => QueryReviewer::CONFIGURATION["critical_severity"], :description => "An UPDATE or DELETE query can be slow and lock tables if it affects many rows.")
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_hash
|
89
|
+
@sql.hash
|
90
|
+
end
|
91
|
+
|
92
|
+
def relevant_trace
|
93
|
+
trace.collect(&:strip).select{|t| t.starts_with?(Rails.root.to_s) &&
|
94
|
+
(!t.starts_with?("#{Rails.root}/vendor") || QueryReviewer::CONFIGURATION["trace_includes_vendor"]) &&
|
95
|
+
(!t.starts_with?("#{Rails.root}/lib") || QueryReviewer::CONFIGURATION["trace_includes_lib"]) &&
|
96
|
+
!t.starts_with?("#{Rails.root}/vendor/plugins/query_reviewer") }
|
97
|
+
end
|
98
|
+
|
99
|
+
def full_trace
|
100
|
+
self.class.generate_full_trace(trace)
|
101
|
+
end
|
102
|
+
|
103
|
+
def warn(options)
|
104
|
+
options[:query] = self
|
105
|
+
options[:table] ||= self.table
|
106
|
+
@warnings << QueryWarning.new(options)
|
107
|
+
end
|
108
|
+
|
109
|
+
def select?
|
110
|
+
self.command == "SELECT"
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.generate_full_trace(trace = Kernel.caller)
|
114
|
+
trace.collect(&:strip).select{|t| !t.starts_with?("#{Rails.root}/vendor/plugins/query_reviewer") }
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.sanitize_strings_and_numbers_from_sql(sql)
|
118
|
+
new_sql = sql.clone
|
119
|
+
new_sql = new_sql.to_sql if new_sql.respond_to?(:to_sql)
|
120
|
+
new_sql.gsub!(/\b\d+\b/, "N")
|
121
|
+
new_sql.gsub!(/\b0x[0-9A-Fa-f]+\b/, "N")
|
122
|
+
new_sql.gsub!(/''/, "'S'")
|
123
|
+
new_sql.gsub!(/""/, "\"S\"")
|
124
|
+
new_sql.gsub!(/\\'/, "")
|
125
|
+
new_sql.gsub!(/\\"/, "")
|
126
|
+
new_sql.gsub!(/'[^']+'/, "'S'")
|
127
|
+
new_sql.gsub!(/"[^"]+"/, "\"S\"")
|
128
|
+
new_sql
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module QueryReviewer
|
2
|
+
# a collection of SQL SELECT queries
|
3
|
+
class SqlQueryCollection
|
4
|
+
COMMANDS = %w(SELECT DELETE INSERT UPDATE)
|
5
|
+
|
6
|
+
attr_reader :query_hash
|
7
|
+
attr_accessor :overhead_time
|
8
|
+
def initialize(query_hash = {})
|
9
|
+
@query_hash = query_hash
|
10
|
+
@overhead_time = 0.0
|
11
|
+
end
|
12
|
+
|
13
|
+
def queries
|
14
|
+
query_hash.values
|
15
|
+
end
|
16
|
+
|
17
|
+
def total_duration
|
18
|
+
self.queries.collect(&:durations).flatten.sum
|
19
|
+
end
|
20
|
+
|
21
|
+
def query_count
|
22
|
+
queries.collect(&:count).sum
|
23
|
+
end
|
24
|
+
|
25
|
+
def analyze!
|
26
|
+
self.queries.collect(&:analyze!)
|
27
|
+
|
28
|
+
@warnings = []
|
29
|
+
|
30
|
+
crit_severity = 9# ((QueryReviewer::CONFIGURATION["critical_severity"] + 10)/2).to_i
|
31
|
+
warn_severity = QueryReviewer::CONFIGURATION["critical_severity"] - 1 # ((QueryReviewer::CONFIGURATION["warn_severity"] + QueryReviewer::CONFIGURATION["critical_severity"])/2).to_i
|
32
|
+
|
33
|
+
COMMANDS.each do |command|
|
34
|
+
count = count_of_command(command)
|
35
|
+
if count > QueryReviewer::CONFIGURATION["critical_#{command.downcase}_count"]
|
36
|
+
warn(:severity => crit_severity, :problem => "#{count} #{command} queries on this page", :description => "Too many #{command} queries can severely slow down a page")
|
37
|
+
elsif count > QueryReviewer::CONFIGURATION["warn_#{command.downcase}_count"]
|
38
|
+
warn(:severity => warn_severity, :problem => "#{count} #{command} queries on this page", :description => "Too many #{command} queries can slow down a page")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def find_or_create_sql_query(sql, cols, run_time, profile, command, affected_rows)
|
44
|
+
sanitized_sql = SqlQuery.sanitize_strings_and_numbers_from_sql(sql)
|
45
|
+
trace = SqlQuery.generate_full_trace(Kernel.caller)
|
46
|
+
key = [sanitized_sql, trace]
|
47
|
+
if query_hash[key]
|
48
|
+
query_hash[key].add(sql, run_time, profile)
|
49
|
+
else
|
50
|
+
query_hash[key] = SqlQuery.new(sql, cols, trace, run_time, profile, command, affected_rows, sanitized_sql)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def warn(options)
|
55
|
+
@warnings << QueryWarning.new(options)
|
56
|
+
end
|
57
|
+
|
58
|
+
def warnings
|
59
|
+
self.queries.collect(&:warnings).flatten.sort{|a,b| b.severity <=> a.severity}
|
60
|
+
end
|
61
|
+
|
62
|
+
def without_warnings
|
63
|
+
self.queries.reject{|q| q.has_warnings?}.sort{|a,b| b.duration <=> a.duration}
|
64
|
+
end
|
65
|
+
|
66
|
+
def collection_warnings
|
67
|
+
@warnings
|
68
|
+
end
|
69
|
+
|
70
|
+
def max_severity
|
71
|
+
warnings.empty? && collection_warnings.empty? ? 0 : [warnings.empty? ? 0 : warnings.collect(&:severity).flatten.max, collection_warnings.empty? ? 0 : collection_warnings.collect(&:severity).flatten.max].max
|
72
|
+
end
|
73
|
+
|
74
|
+
def only_of_command(command, only_no_warnings = false)
|
75
|
+
qs = only_no_warnings ? self.without_warnings : self.queries
|
76
|
+
qs.select{|q| q.command == command}
|
77
|
+
end
|
78
|
+
|
79
|
+
def count_of_command(command, only_no_warnings = false)
|
80
|
+
only_of_command(command, only_no_warnings).collect(&:durations).collect(&:size).sum
|
81
|
+
end
|
82
|
+
|
83
|
+
def total_severity
|
84
|
+
warnings.collect(&:severity).sum
|
85
|
+
end
|
86
|
+
|
87
|
+
def total_with_warnings
|
88
|
+
queries.select(&:has_warnings?).length
|
89
|
+
end
|
90
|
+
|
91
|
+
def total_without_warnings
|
92
|
+
queries.length - total_with_warnings
|
93
|
+
end
|
94
|
+
|
95
|
+
def percent_with_warnings
|
96
|
+
queries.empty? ? 0 : (100.0 * total_with_warnings / queries.length).to_i
|
97
|
+
end
|
98
|
+
|
99
|
+
def percent_without_warnings
|
100
|
+
queries.empty? ? 0 : (100.0 * total_without_warnings / queries.length).to_i
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module QueryReviewer
|
2
|
+
# a single part of an SQL SELECT query
|
3
|
+
class SqlSubQuery < OpenStruct
|
4
|
+
include MysqlAnalyzer
|
5
|
+
|
6
|
+
delegate :sql, :to => :parent
|
7
|
+
attr_reader :cols, :warnings, :parent
|
8
|
+
def initialize(parent, cols)
|
9
|
+
@parent = parent
|
10
|
+
@warnings = []
|
11
|
+
@cols = cols.inject({}) {|memo, obj| memo[obj[0].to_s.downcase] = obj[1].to_s.downcase; memo }
|
12
|
+
@cols["query_type"] = @cols.delete("type")
|
13
|
+
super(@cols)
|
14
|
+
end
|
15
|
+
|
16
|
+
def analyze!
|
17
|
+
@warnings = []
|
18
|
+
adapter_name = ActiveRecord::Base.connection.instance_variable_get("@config")[:adapter]
|
19
|
+
adapter_name = 'mysql' if adapter_name == 'mysql2'
|
20
|
+
method_name = "do_#{adapter_name}_analysis!"
|
21
|
+
self.send(method_name.to_sym)
|
22
|
+
end
|
23
|
+
|
24
|
+
def table
|
25
|
+
@table[:table]
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def warn(options)
|
31
|
+
if (options[:field])
|
32
|
+
field = options.delete(:field)
|
33
|
+
val = self.send(field)
|
34
|
+
options[:problem] = ("#{field.to_s.titleize}: #{val.blank? ? "(blank)" : val}")
|
35
|
+
end
|
36
|
+
options[:query] = self
|
37
|
+
options[:table] = self.table
|
38
|
+
@warnings << QueryWarning.new(options)
|
39
|
+
end
|
40
|
+
|
41
|
+
def praise(options)
|
42
|
+
# no credit, only pain
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
namespace :query_reviewer do
|
2
|
+
desc "Create a default config/query_reviewer.yml"
|
3
|
+
task :setup do
|
4
|
+
defaults_path = File.join(File.dirname(__FILE__), "../..", "query_reviewer_defaults.yml")
|
5
|
+
dest_path = File.join(Rails.root.to_s, "config", "query_reviewer.yml")
|
6
|
+
FileUtils.copy(defaults_path, dest_path)
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<div id="query_review_parent" class="query_review_parent">
|
2
|
+
<div id="query_review_0" class="query_review_container">
|
3
|
+
<%= render :partial => "/box_includes"%>
|
4
|
+
<div class="query_review <%= parent_div_class %>" id = "query_review_header_0">
|
5
|
+
<%= render :partial => "/box_header" %>
|
6
|
+
</div>
|
7
|
+
<div class="query_review_details" id="query_review_details_0" style="display: none;">
|
8
|
+
<%= render :partial => enabled_by_cookie ? "/box_body" : "/box_disabled" %>
|
9
|
+
</div>
|
10
|
+
</div>
|
11
|
+
</div>
|
@@ -0,0 +1,34 @@
|
|
1
|
+
var i = 0;
|
2
|
+
var last = null;
|
3
|
+
for(i=0; i<10; i++) {
|
4
|
+
if(document.getElementById("query_review_"+i)) {
|
5
|
+
last = document.getElementById("query_review_"+i);
|
6
|
+
} else {
|
7
|
+
break;
|
8
|
+
}
|
9
|
+
}
|
10
|
+
if(i < 10) {
|
11
|
+
var container_div = document.createElement("div");
|
12
|
+
container_div.style.left = ""+(i * 30 + 1)+"px";
|
13
|
+
container_div.setAttribute("id", "query_review_"+i);
|
14
|
+
container_div.className = "query_review_container";
|
15
|
+
var ihtml = '<%= escape_javascript(render(:partial => "/box_includes"))%>';
|
16
|
+
ihtml += '<div class="query_review <%= parent_div_class %>" id = "query_review_header_'+i+'">';
|
17
|
+
ihtml += '<%= escape_javascript(render(:partial => "/box_header"))%>';
|
18
|
+
ihtml += '</div> ';
|
19
|
+
ihtml += '<div class="query_review_details" id="query_review_details_'+i+'" style="display: none;">';
|
20
|
+
ihtml += '<%= escape_javascript(render(:partial => enabled_by_cookie ? "/box_body" : "/box_disabled")) %>';
|
21
|
+
ihtml += '</div>';
|
22
|
+
|
23
|
+
container_div.innerHTML = ihtml;
|
24
|
+
|
25
|
+
var parent_div = document.getElementById("query_review_parent")
|
26
|
+
if(!parent_div) {
|
27
|
+
parent_div = document.createElement("div");
|
28
|
+
parent_div.setAttribute("id", "query_review_parent");
|
29
|
+
parent_div.className = "query_reivew_parent";
|
30
|
+
document.getElementById("body")[0].appendChild(parent_div);
|
31
|
+
}
|
32
|
+
|
33
|
+
parent_div.appendChild(container_div);
|
34
|
+
}
|
@@ -0,0 +1,73 @@
|
|
1
|
+
<p>Total queries: <span class="number"><%= @queries.query_count %></span>
|
2
|
+
<% if @total_time %>Total time: <span class="number" title="TOTAL TIME: <%= @total_time %>s QR_OVERHEAD: <%= @queries.overhead_time %>s">
|
3
|
+
<%= '%.3f' % (@total_time - @queries.overhead_time) %></span>s
|
4
|
+
<% end %>
|
5
|
+
Database Time: <span class="number"><%= '%.3f' % @queries.total_duration %></span>s</p>
|
6
|
+
<p class="indent">With warnings: <span class="number bad"><%= @queries.total_with_warnings %></span> (<%= @queries.percent_with_warnings %>%)</p>
|
7
|
+
<p class="indent">Without warnings: <span class="number good"><%= @queries.total_without_warnings %></span> (<%= @queries.percent_without_warnings %>%)</p>
|
8
|
+
<p>Type:
|
9
|
+
<% QueryReviewer::SqlQueryCollection::COMMANDS.each do |command| %>
|
10
|
+
<% next if @queries.count_of_command(command).zero? %>
|
11
|
+
<span class="number"><%= @queries.count_of_command(command) %></span> <%= command %>s
|
12
|
+
<% end %>
|
13
|
+
</p>
|
14
|
+
<% if warnings_no_query_sorted.length + queries_with_warnings_sorted.length > 0 %>
|
15
|
+
<div class="divider"></div>
|
16
|
+
<% if warnings_no_query_sorted_nonignored.length + queries_with_warnings_sorted_nonignored.length > 0 %>
|
17
|
+
<p class="title"><%= warnings_no_query_sorted_nonignored.length + queries_with_warnings_sorted_nonignored.length %> Errors:</p>
|
18
|
+
<ul>
|
19
|
+
<%= render :partial => "/warning_no_query", :collection => warnings_no_query_sorted_nonignored %>
|
20
|
+
<%= render :partial => "/query_with_warning", :collection => queries_with_warnings_sorted_nonignored %>
|
21
|
+
</ul>
|
22
|
+
<% end %>
|
23
|
+
<% if warnings_no_query_sorted_ignored.length + queries_with_warnings_sorted_ignored.length > 0 %>
|
24
|
+
<%= warnings_no_query_sorted_ignored.length + queries_with_warnings_sorted_ignored.length %> Warnings:
|
25
|
+
<ul id="query_review_ignored_warnings">
|
26
|
+
<%= render :partial => "/warning_no_query", :collection => warnings_no_query_sorted_ignored %>
|
27
|
+
<%= render :partial => "/query_with_warning", :collection => queries_with_warnings_sorted_ignored %>
|
28
|
+
</ul>
|
29
|
+
<% end %>
|
30
|
+
<% end %>
|
31
|
+
<div class="divider"></div>
|
32
|
+
<p class="title">Safe queries:</p>
|
33
|
+
<% if @queries.queries.empty? %>
|
34
|
+
No queries to display.
|
35
|
+
<% else %>
|
36
|
+
<% QueryReviewer::SqlQueryCollection::COMMANDS.reverse.each do |command| %>
|
37
|
+
<% next if @queries.count_of_command(command, true).zero? %>
|
38
|
+
<ul class="small">
|
39
|
+
<% @queries.only_of_command(command, true).each do |query| %>
|
40
|
+
<li>
|
41
|
+
<% if QueryReviewer::CONFIGURATION["production_data"] %>
|
42
|
+
<%= duration_with_color(query) %>s
|
43
|
+
<% end %>
|
44
|
+
<% if query.count > 1 %>
|
45
|
+
<b title="<%= query.count %> queries were executed with the same stack trace and similar SQL structure">
|
46
|
+
<%= query.count %> identical queries
|
47
|
+
</b>
|
48
|
+
<% end %>
|
49
|
+
<%= render :partial => "/query_sql", :locals=>{ :query_sql => query } %>
|
50
|
+
<% if query.select? %>
|
51
|
+
<a href="#" onclick="query_review_toggle('warning_<%= query.id %>_explain')" title="show/hide sql">EXPLN</a>
|
52
|
+
<% end %>
|
53
|
+
<% if QueryReviewer::CONFIGURATION["profiling"] && query.profile %>
|
54
|
+
<a href="#" onclick="query_review_toggle('warning_<%= query.id %>_profile')" title="show/hide profile">PROF</a>
|
55
|
+
<% end %>
|
56
|
+
<a href="#" onclick="query_review_toggle('warning_<%= query.id %>_trace')" title="show/hide stack trace">TRACE</a>
|
57
|
+
<div style="display: none" id="warning_<%= query.id %>_explain" class="indent small tbpadded">
|
58
|
+
<%= render :partial => "/explain", :locals => {:query => query} %>
|
59
|
+
</div>
|
60
|
+
<% if QueryReviewer::CONFIGURATION["profiling"] && query.profile %>
|
61
|
+
<div style="display: none" id="warning_<%= query.id %>_profile" class="indent small">
|
62
|
+
<%= render :partial => "/profile", :locals => {:query => query} %>
|
63
|
+
</div>
|
64
|
+
<% end %>
|
65
|
+
<div style="display: none" id="warning_<%= query.id %>_trace" class="indent small">
|
66
|
+
<%= render :partial => "/query_trace", :locals => {:query_trace => query.relevant_trace, :query_id => query.id, :full_trace => query.full_trace} %>
|
67
|
+
</div>
|
68
|
+
</li>
|
69
|
+
<% end %>
|
70
|
+
</ul>
|
71
|
+
<% end %>
|
72
|
+
<% end %>
|
73
|
+
<p id="query_review_disable_link"><a href="#" onclick="eraseCookie('query_review_enabled'); query_review_hide('query_review_disable_link'); alert('Cookie successfully set.');">Disable analysis report</a> on next page load and from now on.</p>
|