query_reviewer 0.1
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 +118 -0
- data/Rakefile +24 -0
- data/lib/query_reviewer/array_extensions.rb +29 -0
- data/lib/query_reviewer/controller_extensions.rb +65 -0
- data/lib/query_reviewer/mysql_adapter_extensions.rb +90 -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 +33 -0
- data/lib/query_reviewer/sql_query.rb +130 -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 +98 -0
- data/lib/query_reviewer.rb +54 -0
- data/query_reviewer_defaults.yml +39 -0
- metadata +96 -0
@@ -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
|
+
MySQL 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="javascript: 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="javascript: query_review_toggle('warning_<%= query.id %>_profile')" title="show/hide profile">PROF</a>
|
55
|
+
<% end %>
|
56
|
+
<a href="javascript: 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", :object => query.relevant_trace, :locals => {: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="javascript: 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>
|
@@ -0,0 +1,2 @@
|
|
1
|
+
<p>SQL analysis has been disabled for you. A cookie must be set to enable analysis. This generally slows down your browser, so it's only recommended for users analyzing SQL queries.</p>
|
2
|
+
<p id="query_review_enable_link"><b><a href="javascript: createCookie('query_review_enabled', '1'); query_review_hide('query_review_enable_link'); alert('Cookie successfully set.');">Enabled it</a> on next page load and from now on.</b></p>
|
@@ -0,0 +1 @@
|
|
1
|
+
<a href="#" onclick="query_review_toggle(this.parentNode.nextSibling.nextSibling.id); return false;">SQL <%= parent_div_status %></a>
|
@@ -0,0 +1,234 @@
|
|
1
|
+
<style>
|
2
|
+
div#query_review_parent {
|
3
|
+
position: absolute;
|
4
|
+
top: 1px;
|
5
|
+
left: 1px;
|
6
|
+
z-index: 100000;
|
7
|
+
}
|
8
|
+
|
9
|
+
div.query_review_container {
|
10
|
+
float: left;
|
11
|
+
}
|
12
|
+
|
13
|
+
div.query_review {
|
14
|
+
height: 18px;
|
15
|
+
margin: 1px;
|
16
|
+
opacity: 0.75;
|
17
|
+
padding-left: 3px;
|
18
|
+
padding-right: 3px;
|
19
|
+
border: 1px solid black;
|
20
|
+
font-size: 12px;
|
21
|
+
font-weight: bold;
|
22
|
+
text-align: left;
|
23
|
+
}
|
24
|
+
|
25
|
+
div.query_review.sql_ok {
|
26
|
+
background-color: #090;
|
27
|
+
color: #020;
|
28
|
+
}
|
29
|
+
|
30
|
+
div.query_review.sql_warning {
|
31
|
+
background-color: #FAE100;
|
32
|
+
color: #F05000;
|
33
|
+
}
|
34
|
+
|
35
|
+
div.query_review.sql_critical {
|
36
|
+
background-color: #F99;
|
37
|
+
color: #A00;
|
38
|
+
}
|
39
|
+
|
40
|
+
div.query_review.sql_disabled {
|
41
|
+
background-color: #BBB;
|
42
|
+
color: #555;
|
43
|
+
}
|
44
|
+
|
45
|
+
div.query_review a {
|
46
|
+
color: inherit;
|
47
|
+
text-decoration: none;
|
48
|
+
}
|
49
|
+
|
50
|
+
div.query_review_details {
|
51
|
+
width: 900px;
|
52
|
+
height: 600px;
|
53
|
+
opacity: 0.9;
|
54
|
+
margin: 1px;
|
55
|
+
padding: 2px;
|
56
|
+
border: 1px solid black;
|
57
|
+
font-size: 12px;
|
58
|
+
overflow: auto;
|
59
|
+
background-color: #DDD;
|
60
|
+
text-align: left;
|
61
|
+
text-indent: 0px;
|
62
|
+
color: black;
|
63
|
+
}
|
64
|
+
|
65
|
+
div.query_review_details p {
|
66
|
+
margin: 2px 0 2px 0;
|
67
|
+
padding: 0px;
|
68
|
+
}
|
69
|
+
|
70
|
+
div.query_review_details a {
|
71
|
+
color: #29ABE2;
|
72
|
+
}
|
73
|
+
|
74
|
+
div.query_review_details ul {
|
75
|
+
list-style-type: circle;
|
76
|
+
padding-left: 15px;
|
77
|
+
}
|
78
|
+
|
79
|
+
div.query_review_details li {
|
80
|
+
clear: both;
|
81
|
+
}
|
82
|
+
|
83
|
+
div.query_review_details code {
|
84
|
+
white-space: normal;
|
85
|
+
line-height: 120%;
|
86
|
+
}
|
87
|
+
|
88
|
+
div.query_review_details table.explain th {
|
89
|
+
cell-padding: 5px;
|
90
|
+
}
|
91
|
+
|
92
|
+
div.query_review_details .title {
|
93
|
+
font-weight: bold;
|
94
|
+
}
|
95
|
+
|
96
|
+
div.query_review_details .indent {
|
97
|
+
padding-left: 10px;
|
98
|
+
}
|
99
|
+
|
100
|
+
div.query_review_details .number {
|
101
|
+
font-weight: bold;
|
102
|
+
}
|
103
|
+
|
104
|
+
div.query_review_details .bad {
|
105
|
+
color: #900;
|
106
|
+
}
|
107
|
+
|
108
|
+
div.query_review_details .good {
|
109
|
+
color: #090;
|
110
|
+
}
|
111
|
+
|
112
|
+
div.query_review_details div.divider {
|
113
|
+
width: 504px;
|
114
|
+
position: relative;
|
115
|
+
left: -2px;
|
116
|
+
height: 1px;
|
117
|
+
border-top: 1px dashed black;
|
118
|
+
margin: 2px 0 2px 0;
|
119
|
+
}
|
120
|
+
|
121
|
+
div.query_review_details .small {
|
122
|
+
font-size: 10px;
|
123
|
+
}
|
124
|
+
|
125
|
+
div.query_review_details .tbpadded {
|
126
|
+
padding-top: 3px;
|
127
|
+
padding-bottom: 3px;
|
128
|
+
}
|
129
|
+
|
130
|
+
div.query_review_details .trace {
|
131
|
+
background-color: #0C1021;
|
132
|
+
color: #7F908A;
|
133
|
+
padding: 5px;
|
134
|
+
margin-right: 30px;
|
135
|
+
}
|
136
|
+
|
137
|
+
div.query_review_details .trace .bold {
|
138
|
+
color: white;
|
139
|
+
}
|
140
|
+
|
141
|
+
div.query_review_details div.spectrum_container {
|
142
|
+
margin-top: 4px;
|
143
|
+
width: 35px;
|
144
|
+
display: block;
|
145
|
+
float: left;
|
146
|
+
position: relative;
|
147
|
+
height: 14px;
|
148
|
+
margin-right: 5px;
|
149
|
+
}
|
150
|
+
|
151
|
+
div.query_review_details div.spectrum_elem {
|
152
|
+
width: 1px;
|
153
|
+
display: block;
|
154
|
+
float: left;
|
155
|
+
height: 10px;
|
156
|
+
}
|
157
|
+
div.query_review_details div.spectrum_pointer {
|
158
|
+
width: 2px;
|
159
|
+
display: block;
|
160
|
+
float: left;
|
161
|
+
position: absolute;
|
162
|
+
height: 14px;
|
163
|
+
background-color: black;
|
164
|
+
top: -2px;
|
165
|
+
}
|
166
|
+
|
167
|
+
div.query_review_details table {
|
168
|
+
background-color: white;
|
169
|
+
border: 1px solid gray;
|
170
|
+
padding: 0px;
|
171
|
+
margin: 0px;
|
172
|
+
}
|
173
|
+
|
174
|
+
div.query_review_details tbody {
|
175
|
+
border: 0px;
|
176
|
+
padding: 0px;
|
177
|
+
margin: 0px;
|
178
|
+
}
|
179
|
+
|
180
|
+
div.query_review_details thead {
|
181
|
+
border-bottom: 1px solid gray;
|
182
|
+
padding: 0px;
|
183
|
+
margin: 0px;
|
184
|
+
}
|
185
|
+
|
186
|
+
|
187
|
+
div.query_review_details tr {
|
188
|
+
border: 0px;
|
189
|
+
padding: 0px;
|
190
|
+
margin: 0px;
|
191
|
+
}
|
192
|
+
|
193
|
+
div.query_review_details td {
|
194
|
+
border: 1px solid gray;
|
195
|
+
padding: 3px;
|
196
|
+
margin: 0px;
|
197
|
+
overflow: hidden;
|
198
|
+
}
|
199
|
+
|
200
|
+
div.query_review_details th {
|
201
|
+
border: 1px solid gray;
|
202
|
+
padding: 3px;
|
203
|
+
margin: 0px;
|
204
|
+
}
|
205
|
+
|
206
|
+
</style>
|
207
|
+
|
208
|
+
<style>
|
209
|
+
.sql {
|
210
|
+
color: black;
|
211
|
+
}
|
212
|
+
.sql .String {
|
213
|
+
color: #009933;
|
214
|
+
}
|
215
|
+
.sql .Keyword {
|
216
|
+
color: #0000FF;
|
217
|
+
}
|
218
|
+
.sql .Constant {
|
219
|
+
color: #6782D3;
|
220
|
+
}
|
221
|
+
.sql .Number {
|
222
|
+
color: #0066FF;
|
223
|
+
}
|
224
|
+
.sql .Comment {
|
225
|
+
color: #0066FF;
|
226
|
+
font-style: italic;
|
227
|
+
}
|
228
|
+
</style>
|
229
|
+
|
230
|
+
<% if defined?(Rails::Railtie) %>
|
231
|
+
<%= render :partial => "/js_includes_new" %>
|
232
|
+
<% else %>
|
233
|
+
<%= render :partial => "/js_includes" %>
|
234
|
+
<% end %>
|
@@ -0,0 +1,30 @@
|
|
1
|
+
<table class="explain">
|
2
|
+
<thead>
|
3
|
+
<tr>
|
4
|
+
<th>table</th>
|
5
|
+
<th>select_type</th>
|
6
|
+
<th>type</th>
|
7
|
+
<th>extra</th>
|
8
|
+
<th>possible_keys</th>
|
9
|
+
<th>key</th>
|
10
|
+
<th>key length</th>
|
11
|
+
<th>ref</th>
|
12
|
+
<th>rows</th>
|
13
|
+
</tr>
|
14
|
+
</thead>
|
15
|
+
<tbody>
|
16
|
+
<% query.subqueries.each do |subquery| %>
|
17
|
+
<tr>
|
18
|
+
<td title="<%= h subquery.table %>"><%= h subquery.table %></td>
|
19
|
+
<td title="<%= h subquery.select_type %>"><%= h subquery.select_type %></td>
|
20
|
+
<td title="<%= h subquery.query_type %>"><%= h subquery.query_type %></td>
|
21
|
+
<td title="<%= h subquery.extra %>"><%= h subquery.extra %></td>
|
22
|
+
<td title="<%= h subquery.possible_keys %>"><%= h subquery.possible_keys %></td>
|
23
|
+
<td title="<%= h subquery.key %>"><%= h subquery.key %></td>
|
24
|
+
<td title="<%= h subquery.key_len %>"><%= h subquery.key_len %></td>
|
25
|
+
<td title="<%= h subquery.ref %>"><%= h subquery.ref %></td>
|
26
|
+
<td title="<%= h subquery.rows %>"><%= h subquery.rows %></td>
|
27
|
+
</tr>
|
28
|
+
<% end %>
|
29
|
+
</tbody>
|
30
|
+
</table>
|
@@ -0,0 +1,68 @@
|
|
1
|
+
<% javascript_tag do %>
|
2
|
+
//Super lame show/hide functions so we don't need any 3rd party JS libs
|
3
|
+
function query_review_show(id) {
|
4
|
+
document.getElementById(id).style.display = "block";
|
5
|
+
}
|
6
|
+
|
7
|
+
function query_review_hide(id) {
|
8
|
+
document.getElementById(id).style.display = "none";
|
9
|
+
}
|
10
|
+
function query_review_toggle(id) {
|
11
|
+
if(document.getElementById(id).style.display == "none") {
|
12
|
+
document.getElementById(id).style.display = "block";
|
13
|
+
} else {
|
14
|
+
document.getElementById(id).style.display = "none";
|
15
|
+
}
|
16
|
+
}
|
17
|
+
function createCookie(name,value,days) {
|
18
|
+
if (days) {
|
19
|
+
var date = new Date();
|
20
|
+
date.setTime(date.getTime()+(days*24*60*60*1000));
|
21
|
+
var expires = "; expires="+date.toGMTString();
|
22
|
+
}
|
23
|
+
else var expires = "";
|
24
|
+
document.cookie = name+"="+value+expires+"; path=/";
|
25
|
+
}
|
26
|
+
|
27
|
+
function readCookie(name) {
|
28
|
+
var nameEQ = name + "=";
|
29
|
+
var ca = document.cookie.split(';');
|
30
|
+
for(var i=0;i < ca.length;i++) {
|
31
|
+
var c = ca[i];
|
32
|
+
while (c.charAt(0)==' ') c = c.substring(1,c.length);
|
33
|
+
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
|
34
|
+
}
|
35
|
+
return null;
|
36
|
+
}
|
37
|
+
|
38
|
+
function eraseCookie(name) {
|
39
|
+
createCookie(name,"",-1);
|
40
|
+
}
|
41
|
+
|
42
|
+
function ignore_list() {
|
43
|
+
var ignore_list = readCookie("query_review_ignore_list")
|
44
|
+
if (!ignore_list)
|
45
|
+
ignore_list = []
|
46
|
+
else
|
47
|
+
ignore_list = ignore_list.split(",")
|
48
|
+
return ignore_list
|
49
|
+
}
|
50
|
+
|
51
|
+
function add_ignore_hash(h){
|
52
|
+
var list = ignore_list();
|
53
|
+
list[list.length] = h
|
54
|
+
createCookie("query_review_ignore_list", list.join(","))
|
55
|
+
}
|
56
|
+
|
57
|
+
function remove_ignore_hash(h) {
|
58
|
+
var list = ignore_list();
|
59
|
+
var new_list = []
|
60
|
+
for(var i=0; i<list.length; i++)
|
61
|
+
{
|
62
|
+
if(list[i].toString() != h.toString()) {
|
63
|
+
new_list[new_list.length] = list[i]
|
64
|
+
}
|
65
|
+
}
|
66
|
+
createCookie("query_review_ignore_list", new_list.join(","))
|
67
|
+
}
|
68
|
+
<% end %>
|
@@ -0,0 +1,68 @@
|
|
1
|
+
<%= javascript_tag do %>
|
2
|
+
//Super lame show/hide functions so we don't need any 3rd party JS libs
|
3
|
+
function query_review_show(id) {
|
4
|
+
document.getElementById(id).style.display = "block";
|
5
|
+
}
|
6
|
+
|
7
|
+
function query_review_hide(id) {
|
8
|
+
document.getElementById(id).style.display = "none";
|
9
|
+
}
|
10
|
+
function query_review_toggle(id) {
|
11
|
+
if(document.getElementById(id).style.display == "none") {
|
12
|
+
document.getElementById(id).style.display = "block";
|
13
|
+
} else {
|
14
|
+
document.getElementById(id).style.display = "none";
|
15
|
+
}
|
16
|
+
}
|
17
|
+
function createCookie(name,value,days) {
|
18
|
+
if (days) {
|
19
|
+
var date = new Date();
|
20
|
+
date.setTime(date.getTime()+(days*24*60*60*1000));
|
21
|
+
var expires = "; expires="+date.toGMTString();
|
22
|
+
}
|
23
|
+
else var expires = "";
|
24
|
+
document.cookie = name+"="+value+expires+"; path=/";
|
25
|
+
}
|
26
|
+
|
27
|
+
function readCookie(name) {
|
28
|
+
var nameEQ = name + "=";
|
29
|
+
var ca = document.cookie.split(';');
|
30
|
+
for(var i=0;i < ca.length;i++) {
|
31
|
+
var c = ca[i];
|
32
|
+
while (c.charAt(0)==' ') c = c.substring(1,c.length);
|
33
|
+
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
|
34
|
+
}
|
35
|
+
return null;
|
36
|
+
}
|
37
|
+
|
38
|
+
function eraseCookie(name) {
|
39
|
+
createCookie(name,"",-1);
|
40
|
+
}
|
41
|
+
|
42
|
+
function ignore_list() {
|
43
|
+
var ignore_list = readCookie("query_review_ignore_list")
|
44
|
+
if (!ignore_list)
|
45
|
+
ignore_list = []
|
46
|
+
else
|
47
|
+
ignore_list = ignore_list.split(",")
|
48
|
+
return ignore_list
|
49
|
+
}
|
50
|
+
|
51
|
+
function add_ignore_hash(h){
|
52
|
+
var list = ignore_list();
|
53
|
+
list[list.length] = h
|
54
|
+
createCookie("query_review_ignore_list", list.join(","))
|
55
|
+
}
|
56
|
+
|
57
|
+
function remove_ignore_hash(h) {
|
58
|
+
var list = ignore_list();
|
59
|
+
var new_list = []
|
60
|
+
for(var i=0; i<list.length; i++)
|
61
|
+
{
|
62
|
+
if(list[i].toString() != h.toString()) {
|
63
|
+
new_list[new_list.length] = list[i]
|
64
|
+
}
|
65
|
+
}
|
66
|
+
createCookie("query_review_ignore_list", new_list.join(","))
|
67
|
+
}
|
68
|
+
<% end %>
|