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.
Files changed (32) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.md +118 -0
  3. data/Rakefile +24 -0
  4. data/lib/query_reviewer/array_extensions.rb +29 -0
  5. data/lib/query_reviewer/controller_extensions.rb +65 -0
  6. data/lib/query_reviewer/mysql_adapter_extensions.rb +90 -0
  7. data/lib/query_reviewer/mysql_analyzer.rb +62 -0
  8. data/lib/query_reviewer/query_warning.rb +17 -0
  9. data/lib/query_reviewer/rails.rb +33 -0
  10. data/lib/query_reviewer/sql_query.rb +130 -0
  11. data/lib/query_reviewer/sql_query_collection.rb +103 -0
  12. data/lib/query_reviewer/sql_sub_query.rb +45 -0
  13. data/lib/query_reviewer/tasks.rb +8 -0
  14. data/lib/query_reviewer/views/_box.html.erb +11 -0
  15. data/lib/query_reviewer/views/_box_ajax.js +34 -0
  16. data/lib/query_reviewer/views/_box_body.html.erb +73 -0
  17. data/lib/query_reviewer/views/_box_disabled.html.erb +2 -0
  18. data/lib/query_reviewer/views/_box_header.html.erb +1 -0
  19. data/lib/query_reviewer/views/_box_includes.html.erb +234 -0
  20. data/lib/query_reviewer/views/_explain.html.erb +30 -0
  21. data/lib/query_reviewer/views/_js_includes.html.erb +68 -0
  22. data/lib/query_reviewer/views/_js_includes_new.html.erb +68 -0
  23. data/lib/query_reviewer/views/_profile.html.erb +26 -0
  24. data/lib/query_reviewer/views/_query_sql.html.erb +8 -0
  25. data/lib/query_reviewer/views/_query_trace.html.erb +31 -0
  26. data/lib/query_reviewer/views/_query_with_warning.html.erb +54 -0
  27. data/lib/query_reviewer/views/_spectrum.html.erb +10 -0
  28. data/lib/query_reviewer/views/_warning_no_query.html.erb +8 -0
  29. data/lib/query_reviewer/views/query_review_box_helper.rb +98 -0
  30. data/lib/query_reviewer.rb +54 -0
  31. data/query_reviewer_defaults.yml +39 -0
  32. metadata +96 -0
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007 [name of plugin creator]
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # QueryReviewer #
2
+
3
+ ## Introduction ##
4
+
5
+ QueryReviewer is an advanced SQL query analyzer. It accomplishes the following goals:
6
+
7
+ * View all EXPLAIN output for all SELECT queries to generate a page
8
+ * Rate a page's SQL usage into one of three categories: OK, WARNING, CRITICAL
9
+ * Attach meaningful warnings to individual queries, and collections of queries
10
+ * Display interactive summary on page
11
+
12
+ ## This Fork ##
13
+
14
+ I use this utility for most of my rails projects. Still the best out there in my opinion for analyzing and understanding your generated SQL queries. I forked the original [query_reviewer](https://github.com/dsboulder/query_reviewer) and applied a collection of patches that have been made since the plugin was created. A list of the biggest additions below:
15
+
16
+ * Snazzed up the README into markdown for better readability
17
+ * Full compatibility for Rails 3 (including Railtie)
18
+ * Cleanup and move rake task to `lib/tasks` to fix deprecation warnings
19
+ * Added gemspec for use with Bundler (as a gem)
20
+ * Fixed missing tags and additional XHTML escaping
21
+ * Fix SQL escaping for better XHTML compatibility
22
+ * Fixes for deprecation warnings and for 1.9 compatiblity
23
+ * Converts templates to more modern foo.html.erb naming
24
+
25
+ Last commit to the main repository was on March 30th, 2009. This fork compiles a variety of patches that were made since that time along with additional work to support compatibility with 1.9 and Rails 3. **Also:** If anyone else creates generally useful enhancements to this utility please start by forking this and then issue me a pull request.
26
+
27
+ **Note:** This plugin should work for Rails 2.X and Rails 3. Support for Rails 3 has been confirmed in the latest revision (with fixed deprecation warnings).
28
+
29
+ ## Installation ##
30
+
31
+ All you have to do is install it into your Rails 2 or 3 project.
32
+
33
+ Right now if you use bundler, simply add this to your Gemfile:
34
+
35
+ # Gemfile
36
+ gem "query_reviewer", :git => "git://github.com/nesquena/query_reviewer.git"
37
+
38
+ If you are not using bundler, you might want to [start using it](http://gembundler.com/rails23.html). You can also install this as a plugin:
39
+
40
+ script/plugin install git://github.com/nesquena/query_reviewer.git
41
+
42
+ In Rails 2, if the rake tasks are not loaded automatically (as a gem), you’ll need to add the following to your Rakefile:
43
+
44
+ # Rakefile
45
+ begin
46
+ require 'query_reviewer/tasks'
47
+ rescue LoadError
48
+ STDERR.puts "The query_reviewer gem could not be found!"
49
+ end
50
+
51
+ You can then run:
52
+
53
+ $ rake query_reviewer:setup
54
+
55
+ Which will create `config/query_reviewer.yml` in your application, see below for what these options mean.
56
+ If you don't create a config file, the gem will use the default in `vendor/plugins/query_reviewer`.
57
+
58
+ ## Configuration ##
59
+
60
+ The configuration file allows you to set configuration parameters shared across all rails environment, as well as overriding those shared parameteres with environment-specific parameters (such as disabling analysis on production!)
61
+
62
+ * `enabled`: whether any output or query analysis is performed. Set this false in production!
63
+ * `inject_view`: controls whether the output automatically is injected before the </body> in HTML output.
64
+ * `profiling`: when enabled, runs the MySQL SET PROFILING=1 for queries longer than the `warn_duration_threshold` / 2.0
65
+ * `production_data`: whether the duration of a query should be taken into account
66
+ * `stack_trace_lines`: number of lines of call stack to include in the "short" version of the stack trace
67
+ * `trace_includes_vendor`: whether the "short" verison of the stack trace should include files in /vendor
68
+ * `trace_includes_lib`: whether the "short" verison of the stack trace should include files in /lib
69
+ * `warn_severity`: the severity of problem that merits "WARNING" status
70
+ * `critical_severity`: the severity of problem that merits "CRITICAL" status
71
+ * `warn_query_count`: the number of queries in a single request that merits "WARNING" status
72
+ * `critical_query_count`: the number of queries in a single request that merits "CRITICAL" status
73
+ * `warn_duration_threshold`: how long a query must take in seconds (float) before it's considered "WARNING"
74
+ * `critical_duration_threshold`: how long a query must take in seconds (float) before it's considered "CRITICIAL"
75
+
76
+ ## Example ##
77
+
78
+ If you disable the inject_view option above, you'll need to manually put the analyzer's output into your view:
79
+
80
+ # view.html.haml
81
+ = query_review_output
82
+
83
+ and that will display the analyzer view!
84
+
85
+ ## Resources ##
86
+
87
+ Random collection of resources that might be interesting related to this utility:
88
+
89
+ * <http://blog.purifyapp.com/2010/06/15/optimise-your-mysql/>
90
+ * <http://www.tatvartha.com/2009/09/rails-optimizing-database-indexes-using-query_analyzer-and-query_reviewer/>
91
+ * <http://www.geekskillz.com/articles/using-indexes-to-improve-rails-performance>
92
+ * <http://www.williambharding.com/blog/rails/rails-mysql-indexes-step-1-in-pitiful-to-prime-performance/>
93
+ * <http://guides.rubyonrails.org/performance_testing.html>
94
+
95
+ Other related gems that prove useful for database optimization:
96
+
97
+ * [bullet](https://github.com/flyerhzm/bullet)
98
+ * [slim-scrooge](https://github.com/sdsykes/slim_scrooge)
99
+ * [slim-attributes](https://github.com/sdsykes/slim-attributes)
100
+
101
+ ## Alternatives ##
102
+
103
+ There have been other alternatives created since this was originally released. A few of the best are listed below. I for one still prefer this utility over the other options:
104
+
105
+ * [rack-bug](https://github.com/brynary/rack-bug)
106
+ * [rails-footnotes](https://github.com/josevalim/rails-footnotes)
107
+ * [newrelic-development](http://support.newrelic.com/kb/docs/developer-mode)
108
+ * [palmist](https://github.com/flyingmachine/palmist)
109
+ * [query_diet](https://github.com/makandra/query_diet)
110
+ * [query_trace](https://github.com/ntalbott/query_trace)
111
+
112
+ Know of a better alternative? Let me know!
113
+
114
+ ## Acknowledgements ##
115
+
116
+ Created by Kongregate & David Stevenson. Refactorings and compilations of all fixes since was done by Nathan Esquenazi.
117
+
118
+ Copyright (c) 2007-2008 Kongregate & David Stevenson, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'bundler'
5
+ Bundler::GemHelper.install_tasks
6
+
7
+ desc 'Default: run unit tests.'
8
+ task :default => :test
9
+
10
+ desc 'Test the query_reviewer plugin.'
11
+ Rake::TestTask.new(:test) do |t|
12
+ t.libs << 'lib'
13
+ t.pattern = 'test/**/*_test.rb'
14
+ t.verbose = true
15
+ end
16
+
17
+ desc 'Generate documentation for the query_reviewer plugin.'
18
+ Rake::RDocTask.new(:rdoc) do |rdoc|
19
+ rdoc.rdoc_dir = 'rdoc'
20
+ rdoc.title = 'QueryReviewer'
21
+ rdoc.options << '--line-numbers' << '--inline-source'
22
+ rdoc.rdoc_files.include('README')
23
+ rdoc.rdoc_files.include('lib/**/*.rb')
24
+ end
@@ -0,0 +1,29 @@
1
+ module QueryReviewer
2
+ module ArrayExtensions #taken from query_analyser plugin
3
+ protected
4
+ def qa_columnized_row(fields, sized)
5
+ row = []
6
+ fields.each_with_index do |f, i|
7
+ row << sprintf("%0-#{sized[i]}s", f.to_s)
8
+ end
9
+ row.join(' | ')
10
+ end
11
+
12
+ public
13
+
14
+ def qa_columnized
15
+ sized = {}
16
+ self.each do |row|
17
+ row.values.each_with_index do |value, i|
18
+ sized[i] = [sized[i].to_i, row.keys[i].length, value.to_s.length].max
19
+ end
20
+ end
21
+
22
+ table = []
23
+ table << qa_columnized_row(self.first.keys, sized)
24
+ table << '-' * table.first.length
25
+ self.each { |row| table << qa_columnized_row(row.values, sized) }
26
+ table.join("\n ") # Spaces added to work with format_log_entry
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,65 @@
1
+ require "action_view"
2
+ require File.join(File.dirname(__FILE__), "views", "query_review_box_helper")
3
+
4
+ module QueryReviewer
5
+ module ControllerExtensions
6
+ class QueryViewBase < ActionView::Base
7
+ include QueryReviewer::Views::QueryReviewBoxHelper
8
+ end
9
+
10
+ def self.included(base)
11
+ if QueryReviewer::CONFIGURATION["inject_view"]
12
+ alias_name = defined?(Rails::Railtie) ? :process_action : :perform_action
13
+ base.alias_method_chain(alias_name, :query_review)
14
+ end
15
+ base.alias_method_chain :process, :query_review
16
+ base.helper_method :query_review_output
17
+ end
18
+
19
+ def query_review_output(ajax = false, total_time = nil)
20
+ faux_view = QueryViewBase.new([File.join(File.dirname(__FILE__), "views")], {}, self)
21
+ queries = Thread.current["queries"]
22
+ queries.analyze!
23
+ faux_view.instance_variable_set("@queries", queries)
24
+ faux_view.instance_variable_set("@total_time", total_time)
25
+ if ajax
26
+ js = faux_view.render(:partial => "/box_ajax.js")
27
+ else
28
+ html = faux_view.render(:partial => "/box")
29
+ end
30
+ end
31
+
32
+ def add_query_output_to_view(total_time)
33
+ if request.xhr?
34
+ if cookies["query_review_enabled"]
35
+ if !response.content_type || response.content_type.include?("text/html")
36
+ response.body += "<script type=\"text/javascript\">"+query_review_output(true, total_time)+"</script>"
37
+ elsif response.content_type && response.content_type.include?("text/javascript")
38
+ response.body += ";\n"+query_review_output(true, total_time)
39
+ end
40
+ end
41
+ else
42
+ if response.body.is_a?(String) && response.body.match(/<\/body>/i) && Thread.current["queries"]
43
+ idx = (response.body =~ /<\/body>/i)
44
+ html = query_review_output(false, total_time)
45
+ response.body = response.body.insert(idx, html)
46
+ end
47
+ end
48
+ end
49
+
50
+ def perform_action_with_query_review(*args)
51
+ Thread.current["query_reviewer_enabled"] = cookies["query_review_enabled"]
52
+ t1 = Time.now
53
+ r = defined?(Rails::Railtie) ? process_action_without_query_review(*args) : perform_action_without_query_review(*args)
54
+ t2 = Time.now
55
+ add_query_output_to_view(t2 - t1)
56
+ r
57
+ end
58
+ alias_method :process_action_with_query_review, :perform_action_with_query_review
59
+
60
+ def process_with_query_review(*args) #:nodoc:
61
+ Thread.current["queries"] = SqlQueryCollection.new
62
+ process_without_query_review(*args)
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,90 @@
1
+ module QueryReviewer
2
+ module MysqlAdapterExtensions
3
+ def self.included(base)
4
+ base.alias_method_chain :select, :review
5
+ base.alias_method_chain :update, :review
6
+ base.alias_method_chain :insert, :review
7
+ base.alias_method_chain :delete, :review
8
+ end
9
+
10
+ def update_with_review(sql, *args)
11
+ t1 = Time.now
12
+ result = update_without_review(sql, *args)
13
+ t2 = Time.now
14
+
15
+ create_or_add_query_to_query_reviewer!(sql, nil, t2 - t1, nil, "UPDATE", result)
16
+
17
+ result
18
+ end
19
+
20
+ def insert_with_review(sql, *args)
21
+ t1 = Time.now
22
+ result = insert_without_review(sql, *args)
23
+ t2 = Time.now
24
+
25
+ create_or_add_query_to_query_reviewer!(sql, nil, t2 - t1, nil, "INSERT")
26
+
27
+ result
28
+ end
29
+
30
+ def delete_with_review(sql, *args)
31
+ t1 = Time.now
32
+ result = delete_without_review(sql, *args)
33
+ t2 = Time.now
34
+
35
+ create_or_add_query_to_query_reviewer!(sql, nil, t2 - t1, nil, "DELETE", result)
36
+
37
+ result
38
+ end
39
+
40
+ def select_with_review(sql, *args)
41
+ sql.gsub!(/^SELECT /i, "SELECT SQL_NO_CACHE ") if QueryReviewer::CONFIGURATION["disable_sql_cache"]
42
+ @logger.silence { execute("SET PROFILING=1") } if QueryReviewer::CONFIGURATION["profiling"]
43
+ t1 = Time.now
44
+ query_results = select_without_review(sql, *args)
45
+ t2 = Time.now
46
+
47
+ if @logger && sql =~ /^select/i && query_reviewer_enabled?
48
+ use_profiling = QueryReviewer::CONFIGURATION["profiling"]
49
+ use_profiling &&= (t2 - t1) >= QueryReviewer::CONFIGURATION["warn_duration_threshold"].to_f / 2.0 if QueryReviewer::CONFIGURATION["production_data"]
50
+
51
+ if use_profiling
52
+ t5 = Time.now
53
+ @logger.silence { execute("SET PROFILING=1") }
54
+ t3 = Time.now
55
+ select_without_review(sql, *args)
56
+ t4 = Time.now
57
+ profile = @logger.silence { select_without_review("SHOW PROFILE ALL", *args) }
58
+ @logger.silence { execute("SET PROFILING=0") }
59
+ t6 = Time.now
60
+ Thread.current["queries"].overhead_time += t6 - t5
61
+ else
62
+ profile = nil
63
+ end
64
+
65
+ cols = @logger.silence do
66
+ select_without_review("explain #{sql}", *args)
67
+ end
68
+
69
+ duration = t3 ? [t2 - t1, t4 - t3].min : t2 - t1
70
+ create_or_add_query_to_query_reviewer!(sql, cols, duration, profile)
71
+
72
+ #@logger.debug(format_log_entry("Analyzing #{name}\n", query.to_table)) if @logger.level <= Logger::INFO
73
+ end
74
+ query_results
75
+ end
76
+
77
+ def query_reviewer_enabled?
78
+ Thread.current["queries"] && Thread.current["queries"].respond_to?(:find_or_create_sql_query) && Thread.current["query_reviewer_enabled"]
79
+ end
80
+
81
+ def create_or_add_query_to_query_reviewer!(sql, cols, run_time, profile, command = "SELECT", affected_rows = 1)
82
+ if query_reviewer_enabled?
83
+ t1 = Time.now
84
+ Thread.current["queries"].find_or_create_sql_query(sql, cols, run_time, profile, command, affected_rows)
85
+ t2 = Time.now
86
+ Thread.current["queries"].overhead_time += t2 - t1
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,62 @@
1
+ module QueryReviewer
2
+ module MysqlAnalyzer
3
+ def do_mysql_analysis!
4
+ analyze_select_type!
5
+ analyze_query_type!
6
+ analyze_key!
7
+ analyze_extras!
8
+ analyze_keylen!
9
+ end
10
+
11
+ def analyze_select_type!
12
+ if select_type.match /uncacheable subquery/
13
+ warn(:severity => 10, :field => "select_type", :desc => "Subquery must be run once for EVERY row in main query")
14
+ elsif select_type.match /dependent/
15
+ warn(:severity => 2, :field => "select_type", :desc => "Dependent subqueries can not be executed while the main query is running")
16
+ end
17
+ end
18
+
19
+ def analyze_query_type!
20
+ case query_type
21
+ when "system", "const", "eq_ref" then
22
+ praise("Yay")
23
+ when "ref", "ref_or_null", "range", "index_merge" then
24
+ praise("Not bad eh...")
25
+ when "unique_subquery", "index_subquery" then
26
+ #NOT SURE
27
+ when "index" then
28
+ warn(:severity => 8, :field => "query_type", :desc => "Full index tree scan (slightly faster than a full table scan)") unless !extra.include?("using where")
29
+ when "all" then
30
+ warn(:severity => 9, :field => "query_type", :desc => "Full table scan") unless !extra.include?("using where")
31
+ end
32
+ end
33
+
34
+ def analyze_key!
35
+ if self.key == "const"
36
+ praise "Way to go!"
37
+ elsif self.key.blank? && !self.extra.include?("select tables optimized away")
38
+ warn :severity => 6, :field => "key", :desc => "No index was used here. In this case, that meant scanning #{self.rows} rows."
39
+ end
40
+ end
41
+
42
+ def analyze_extras!
43
+ if self.extra.match(/range checked for each record/)
44
+ warn :severity => 4, :problem => "Range checked for each record", :desc => "MySQL found no good index to use, but found that some of indexes might be used after column values from preceding tables are known"
45
+ end
46
+
47
+ if self.extra.match(/using filesort/)
48
+ warn :severity => 2, :problem => "Using filesort", :desc => "MySQL must do an extra pass to find out how to retrieve the rows in sorted order."
49
+ end
50
+
51
+ if self.extra.match(/using temporary/)
52
+ warn :severity => 10, :problem => "Using temporary table", :desc => "To resolve the query, MySQL needs to create a temporary table to hold the result."
53
+ end
54
+ end
55
+
56
+ def analyze_keylen!
57
+ if self.key_len && !self.key_len.to_i.nil? && (self.key_len.to_i > QueryReviewer::CONFIGURATION["max_safe_key_length"])
58
+ warn :severity => 4, :problem => "Long key length (#{self.key_len.to_i})", :desc => "The key used for the index was rather long, potentially affecting indices in memory"
59
+ end
60
+ end
61
+ end
62
+ end
@@ -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,33 @@
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.inject_reviewer if QueryReviewer.enabled?
28
+ end
29
+ end
30
+ end
31
+ else # Rails 2
32
+ QueryReviewer.inject_reviewer
33
+ end
@@ -0,0 +1,130 @@
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.gsub!(/\b\d+\b/, "N")
120
+ new_sql.gsub!(/\b0x[0-9A-Fa-f]+\b/, "N")
121
+ new_sql.gsub!(/''/, "'S'")
122
+ new_sql.gsub!(/""/, "\"S\"")
123
+ new_sql.gsub!(/\\'/, "")
124
+ new_sql.gsub!(/\\"/, "")
125
+ new_sql.gsub!(/'[^']+'/, "'S'")
126
+ new_sql.gsub!(/"[^"]+"/, "\"S\"")
127
+ new_sql
128
+ end
129
+ end
130
+ end