113 lines
		
	
	
		
			3.3 KiB
		
	
	
	
		
			Ruby
		
	
	
	
			
		
		
	
	
			113 lines
		
	
	
		
			3.3 KiB
		
	
	
	
		
			Ruby
		
	
	
	
# frozen_string_literal: true
 | 
						|
 | 
						|
module Gitlab
 | 
						|
  module Sherlock
 | 
						|
    class Query
 | 
						|
      attr_reader :id, :query, :started_at, :finished_at, :backtrace
 | 
						|
 | 
						|
      # SQL identifiers that should be prefixed with newlines.
 | 
						|
      PREFIX_NEWLINE = %r{
 | 
						|
        \s+(FROM
 | 
						|
          |(LEFT|RIGHT)?INNER\s+JOIN
 | 
						|
          |(LEFT|RIGHT)?OUTER\s+JOIN
 | 
						|
          |WHERE
 | 
						|
          |AND
 | 
						|
          |GROUP\s+BY
 | 
						|
          |ORDER\s+BY
 | 
						|
          |LIMIT
 | 
						|
          |OFFSET)\s+}ix.freeze # Vim indent breaks when this is on a newline :<
 | 
						|
 | 
						|
      # Creates a new Query using a String and a separate Array of bindings.
 | 
						|
      #
 | 
						|
      # query - A String containing a SQL query, optionally with numeric
 | 
						|
      #         placeholders (`$1`, `$2`, etc).
 | 
						|
      #
 | 
						|
      # bindings - An Array of ActiveRecord columns and their values.
 | 
						|
      # started_at - The start time of the query as a Time-like object.
 | 
						|
      # finished_at - The completion time of the query as a Time-like object.
 | 
						|
      #
 | 
						|
      # Returns a new Query object.
 | 
						|
      def self.new_with_bindings(query, bindings, started_at, finished_at)
 | 
						|
        bindings.each_with_index do |(_, value), index|
 | 
						|
          quoted_value = ActiveRecord::Base.connection.quote(value)
 | 
						|
 | 
						|
          query = query.gsub("$#{index + 1}", quoted_value)
 | 
						|
        end
 | 
						|
 | 
						|
        new(query, started_at, finished_at)
 | 
						|
      end
 | 
						|
 | 
						|
      # query - The SQL query as a String (without placeholders).
 | 
						|
      # started_at - The start time of the query as a Time-like object.
 | 
						|
      # finished_at - The completion time of the query as a Time-like object.
 | 
						|
      def initialize(query, started_at, finished_at)
 | 
						|
        @id = SecureRandom.uuid
 | 
						|
        @query = query
 | 
						|
        @started_at = started_at
 | 
						|
        @finished_at = finished_at
 | 
						|
        @backtrace = caller_locations.map do |loc|
 | 
						|
          Location.from_ruby_location(loc)
 | 
						|
        end
 | 
						|
 | 
						|
        unless @query.end_with?(';')
 | 
						|
          @query = "#{@query};"
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
      # Returns the query duration in milliseconds.
 | 
						|
      def duration
 | 
						|
        @duration ||= (@finished_at - @started_at) * 1000.0
 | 
						|
      end
 | 
						|
 | 
						|
      def to_param
 | 
						|
        @id
 | 
						|
      end
 | 
						|
 | 
						|
      # Returns a human readable version of the query.
 | 
						|
      def formatted_query
 | 
						|
        @formatted_query ||= format_sql(@query)
 | 
						|
      end
 | 
						|
 | 
						|
      # Returns the last application frame of the backtrace.
 | 
						|
      def last_application_frame
 | 
						|
        @last_application_frame ||= @backtrace.find(&:application?)
 | 
						|
      end
 | 
						|
 | 
						|
      # Returns an Array of application frames (excluding Gems and the likes).
 | 
						|
      def application_backtrace
 | 
						|
        @application_backtrace ||= @backtrace.select(&:application?)
 | 
						|
      end
 | 
						|
 | 
						|
      # Returns the query plan as a String.
 | 
						|
      def explain
 | 
						|
        unless @explain
 | 
						|
          ActiveRecord::Base.connection.transaction do
 | 
						|
            @explain = raw_explain(@query).values.flatten.join("\n")
 | 
						|
 | 
						|
            # Roll back any queries that mutate data so we don't mess up
 | 
						|
            # anything when running explain on an INSERT, UPDATE, DELETE, etc.
 | 
						|
            raise ActiveRecord::Rollback
 | 
						|
          end
 | 
						|
        end
 | 
						|
 | 
						|
        @explain
 | 
						|
      end
 | 
						|
 | 
						|
      private
 | 
						|
 | 
						|
      def raw_explain(query)
 | 
						|
        explain = "EXPLAIN ANALYZE #{query};"
 | 
						|
 | 
						|
        ActiveRecord::Base.connection.execute(explain)
 | 
						|
      end
 | 
						|
 | 
						|
      def format_sql(query)
 | 
						|
        query.each_line
 | 
						|
          .map { |line| line.strip }
 | 
						|
          .join("\n")
 | 
						|
          .gsub(PREFIX_NEWLINE) { "\n#{$1} " }
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
end
 |