lookout-query_reviewer 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- 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>
|