BigW Consortium Gitlab

query.rb 3.37 KB
Newer Older
1 2 3 4 5
module Gitlab
  module Sherlock
    class Query
      attr_reader :id, :query, :started_at, :finished_at, :backtrace

6
      # SQL identifiers that should be prefixed with newlines.
7 8 9 10 11 12 13 14 15
      PREFIX_NEWLINE = /
        \s+(FROM
          |(LEFT|RIGHT)?INNER\s+JOIN
          |(LEFT|RIGHT)?OUTER\s+JOIN
          |WHERE
          |AND
          |GROUP\s+BY
          |ORDER\s+BY
          |LIMIT
16 17 18 19 20 21 22 23 24 25 26 27
          |OFFSET)\s+/ix # 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.
28
      def self.new_with_bindings(query, bindings, started_at, finished_at)
29
        bindings.each_with_index do |(_, value), index|
30 31 32 33 34 35 36 37
          quoted_value = ActiveRecord::Base.connection.quote(value)

          query = query.gsub("$#{index + 1}", quoted_value)
        end

        new(query, started_at, finished_at)
      end

38 39 40
      # 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.
41 42 43 44 45 46 47 48 49 50 51 52 53 54
      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 += ';'
        end
      end

55
      # Returns the query duration in milliseconds.
56 57 58 59 60 61 62 63
      def duration
        @duration ||= (@finished_at - @started_at) * 1000.0
      end

      def to_param
        @id
      end

64
      # Returns a human readable version of the query.
65 66 67 68
      def formatted_query
        @formatted_query ||= format_sql(@query)
      end

69
      # Returns the last application frame of the backtrace.
70 71 72 73
      def last_application_frame
        @last_application_frame ||= @backtrace.find(&:application?)
      end

74
      # Returns an Array of application frames (excluding Gems and the likes).
75 76 77 78
      def application_backtrace
        @application_backtrace ||= @backtrace.select(&:application?)
      end

79
      # Returns the query plan as a String.
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
      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)
        if Gitlab::Database.postgresql?
          explain = "EXPLAIN ANALYZE #{query};"
        else
          explain = "EXPLAIN #{query};"
        end

        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