Permalink
Browse files

initial revision

  • Loading branch information...
1 parent 8a7fb69 commit 9485a8f98951f0be7641c9c8a6b756aecba496d9 @dsboulder dsboulder committed Dec 9, 2007
View
@@ -0,0 +1,20 @@
+Copyright (c) 2007 [name of plugin creator]
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
View
13 README
@@ -0,0 +1,13 @@
+QueryReviewer
+=============
+
+Introduction goes here.
+
+
+Example
+=======
+
+Example goes here.
+
+
+Copyright (c) 2007 [name of plugin creator], released under the MIT license
View
@@ -0,0 +1,22 @@
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the query_reviewer plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+desc 'Generate documentation for the query_reviewer plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'QueryReviewer'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
View
@@ -0,0 +1,8 @@
+# Include hook code here
+
+require "active_record"
+require "action_controller"
+require 'query_reviewer'
+ActiveRecord::ConnectionAdapters::MysqlAdapter.send(:include, QueryReviewer::MysqlAdapterExtensions)
+ActionController::Base.send(:include, QueryReviewer::ControllerExtensions)
+Array.send(:include, QueryReviewer::ArrayExtensions)
View
@@ -0,0 +1 @@
+# Install hook code here
View
@@ -0,0 +1,40 @@
+# QueryReviewer
+require "ostruct"
+require "query_reviewer/array_extensions"
+require "query_reviewer/sql_query"
+require "query_reviewer/sql_sub_query"
+require "query_reviewer/mysql_adapter_extensions"
+require "query_reviewer/controller_extensions"
+
+
+module QueryReviewer
+ CONFIGURATION = YAML.load(File.read(File.join(File.dirname(__FILE__), "..", "query_reviewer.yml")))["all"] || {}
+ CONFIGURATION.merge!(YAML.load(File.read(File.join(File.dirname(__FILE__), "..", "query_reviewer.yml")))[RAILS_ENV || "development"])
+
+ if CONFIGURATION["enabled"]
+ begin
+ CONFIGURATION["uv"] ||= !Gem.searcher.find("uv").nil?
+ if CONFIGURATION["uv"]
+ require "uv"
+ end
+ rescue
+ CONFIGURATION["uv"] ||= false
+ end
+ end
+
+ class QueryWarning
+ attr_reader :query, :severity, :problem, :desc, :table, :id
+
+ cattr_accessor :next_id
+ self.next_id = 1
+
+ def initialize(options)
+ @query = options[:query]
+ @severity = options[:severity]
+ @problem = options[:problem]
+ @desc = options[:desc]
+ @table = options[:table]
+ @id = (self.class.next_id += 1)
+ end
+ end
+end
@@ -0,0 +1,29 @@
+module QueryReviewer
+ module ArrayExtensions #taken from query_analyser plugin
+ protected
+ def qa_columnized_row(fields, sized)
+ row = []
+ fields.each_with_index do |f, i|
+ row << sprintf("%0-#{sized[i]}s", f.to_s)
+ end
+ row.join(' | ')
+ end
+
+ public
+
+ def qa_columnized
+ sized = {}
+ self.each do |row|
+ row.values.each_with_index do |value, i|
+ sized[i] = [sized[i].to_i, row.keys[i].length, value.to_s.length].max
+ end
+ end
+
+ table = []
+ table << qa_columnized_row(self.first.keys, sized)
+ table << '-' * table.first.length
+ self.each { |row| table << qa_columnized_row(row.values, sized) }
+ table.join("\n ") # Spaces added to work with format_log_entry
+ end
+ end
+end
@@ -0,0 +1,37 @@
+require File.join(File.dirname(__FILE__), "views", "query_review_box_helper")
+
+module QueryReviewer
+ module ControllerExtensions
+ class QueryViewBase < ActionView::Base
+ include QueryReviewer::Views::QueryReviewBoxHelper
+ end
+
+ def self.included(base)
+ base.alias_method_chain :perform_action, :query_review
+ base.alias_method_chain :process, :query_review
+ end
+
+ def add_query_output_to_view
+ if response.body.match(/<\/body>/i) && Thread.current["queries"]
+ idx = (response.body =~ /<\/body>/i)
+ faux_view = QueryViewBase.new([File.join(File.dirname(__FILE__), "views")], {}, self)
+ queries = SqlQueryCollection.new(Thread.current["queries"])
+ queries.analyze!
+ faux_view.instance_variable_set("@queries", queries)
+ html = faux_view.render(:partial => "/box.rhtml")
+ response.body.insert(idx, html)
+ end
+ end
+
+ def perform_action_with_query_review
+ r = perform_action_without_query_review
+ add_query_output_to_view if response.content_type.blank? || response.content_type == "text/html"
+ r
+ end
+
+ def process_with_query_review(request, response, method = :perform_action, *arguments) #:nodoc:
+ Thread.current["queries"] = []
+ process_without_query_review(request, response, method, *arguments)
+ end
+ end
+end
@@ -0,0 +1,21 @@
+module QueryReviewer
+ module MysqlAdapterExtensions
+ def self.included(base)
+ base.alias_method_chain :select, :review
+ end
+
+ def select_with_review(sql, name = nil)
+ query_results = select_without_review(sql, name)
+
+ if @logger and @logger.level <= Logger::INFO and sql =~ /^select/i
+ cols = @logger.silence do
+ select_without_review("explain #{sql}", name)
+ end
+ query = SqlQuery.new(sql, cols)
+ Thread.current["queries"] << query if Thread.current["queries"] && Thread.current["queries"].respond_to?(:<<)
+ @logger.debug(format_log_entry("Analyzing #{name}\n", query.to_table))
+ end
+ query_results
+ end
+ end
+end
@@ -0,0 +1,46 @@
+module QueryReviewer
+ # a single SQL SELECT query
+ class SqlQuery
+ attr_reader :sql, :rows, :subqueries, :trace, :id
+
+ cattr_accessor :next_id
+ self.next_id = 1
+
+ def initialize(sql, rows)
+ @rows = rows
+ @sql = sql
+ @subqueries = rows.collect{|row| SqlSubQuery.new(self, row)}
+ @id = (self.class.next_id += 1)
+ # get_trace
+ end
+
+ def to_table
+ rows.qa_columnized
+ end
+
+ def warnings
+ self.subqueries.collect(&:warnings).flatten
+ end
+
+ def has_warnings?
+ !self.warnings.empty?
+ end
+
+ def max_severity
+ self.warnings.empty? ? 0 : self.warnings.collect(&:severity).max
+ end
+
+ def analyze!
+ self.subqueries.collect(&:analyze!)
+ end
+
+ def get_trace
+ begin
+ raise "not a real exception"
+ rescue
+ @trace = $!.backtrace
+ end
+ puts @trace.inspect
+ end
+ end
+end
@@ -0,0 +1,57 @@
+module QueryReviewer
+ # a collection of SQL SELECT queries
+ class SqlQueryCollection
+ attr_reader :queries
+ def initialize(queries)
+ @queries = queries
+ end
+
+ def analyze!
+ self.queries.collect(&:analyze!)
+
+ @warnings = []
+
+ if @queries.length > QueryReviewer::CONFIGURATION["critical_query_count"]
+ warn(:severity => QueryReviewer::CONFIGURATION["critical_severity"], :problem => "#{@queries.length} queries on this page", :description => "Too many queries can severely slow down a page")
+ elsif @queries.length > QueryReviewer::CONFIGURATION["warn_query_count"]
+ warn(:severity => QueryReviewer::CONFIGURATION["warn_severity"], :problem => "#{@queries.length} queries on this page", :description => "Too many queries can slow down a page")
+ end
+ end
+
+ def warn(options)
+ @warnings << QueryWarning.new(options)
+ end
+
+ def warnings
+ self.queries.collect(&:warnings).flatten.sort{|a,b| a.severity <=> b.severity}.reverse
+ end
+
+ def collection_warnings
+ @warnings
+ end
+
+ def max_severity
+ warnings.empty? && collection_warnings.empty? ? 0 : [warnings.collect(&:severity).flatten.max, collection_warnings.collect(&:severity).flatten.max].max
+ end
+
+ def total_severity
+ warnings.collect(&:severity).sum
+ end
+
+ def total_with_warnings
+ queries.select(&:has_warnings?).length
+ end
+
+ def total_without_warnings
+ queries.length - total_with_warnings
+ end
+
+ def percent_with_warnings
+ queries.empty? ? 0 : (100.0 * total_with_warnings / queries.length).to_i
+ end
+
+ def percent_without_warnings
+ queries.empty? ? 0 : (100.0 * total_without_warnings / queries.length).to_i
+ end
+ end
+end
@@ -0,0 +1,85 @@
+module QueryReviewer
+ # a single part of an SQL SELECT query
+ class SqlSubQuery < OpenStruct
+ delegate :sql, :to => :parent
+ attr_reader :cols, :warnings, :parent
+ def initialize(parent, cols)
+ @parent = parent
+ @warnings = []
+ @cols = cols.inject({}) {|memo, obj| memo[obj[0].to_s.downcase] = obj[1].to_s.downcase; memo }
+ @cols["query_type"] = @cols.delete("type")
+ super(@cols)
+ end
+
+ def analyze!
+ @warnings = []
+ analyze_select_type!
+ analyze_query_type!
+ analyze_key!
+ analyze_extras!
+ end
+
+ private
+
+ def warn(options)
+ if (options[:field])
+ field = options.delete(:field)
+ val = self.send(field)
+ options[:problem] = ("#{field.to_s.titleize}: #{val.blank? ? "(blank)" : val}")
+ end
+ options[:query] = self
+ options[:table] = @table[:table]
+ puts("Adding warning: #{options.inspect}")
+ @warnings << QueryWarning.new(options)
+ end
+
+ def praise(options)
+ # no credit, only pain
+ end
+
+ def analyze_select_type!
+ if select_type.match /uncacheable subquery/
+ warn(:severity => 10, :field => "select_type", :desc => "Subquery must be run once for EVERY row in main query")
+ elsif select_type.match /dependent/
+ warn(:severity => 2, :field => "select_type", :desc => "Dependent subqueries can not be executed while the main query is running")
+ end
+ end
+
+ def analyze_query_type!
+ case query_type
+ when "system", "const", "eq_ref":
+ praise("Yay")
+ when "ref", "ref_or_null", "range", "index_merge":
+ praise("Not bad eh...")
+ when "unique_subquery", "index_subquery":
+ #NOT SURE
+ when "index":
+ warn(:severity => 8, :field => "query_type", :desc => "Full index tree scan (slightly faster than a full table scan)") unless extra.include?("using where")
+ when "all":
+ warn(:severity => 9, :field => "query_type", :desc => "Full table scan") unless extra.include?("using where")
+ end
+ end
+
+ def analyze_key!
+ if self.key == "const"
+ praise "Way to go!"
+ elsif self.key.blank?
+ warn :severity => 6, :field => "key", :desc => "No index was used here. In this case, that meant scanning #{self.rows} rows."
+ end
+ end
+
+ def analyze_extras!
+ if self.extra.match(/range checked for each record/)
+ 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"
+ end
+
+ if self.extra.match(/using filesort/)
+ warn :severity => 2, :problem => "Using filesort", :desc => "MySQL must do an extra pass to find out how to retrieve the rows in sorted order."
+ end
+
+ if self.extra.match(/using temporary/)
+ warn :severity => 10, :problem => "Using temporary table", :desc => "To resolve the query, MySQL needs to create a temporary table to hold the result."
+ end
+ end
+ end
+end
Oops, something went wrong.

0 comments on commit 9485a8f

Please sign in to comment.