sql_safety_net 1.1.11

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,19 @@
1
+ = SQL Safety Net
2
+
3
+ This gem provides hooks into ActiveRecord to analyze SQL queries. It can be very useful in keeping an eye on tuning your code before it becomes a problem. ActiveRecord can make it very easy to send excessive load to the database.
4
+
5
+ With this gem enabled, HTML pages which have bad queries or too many queries on them will have a debug div added to the HTML with information about the queries. If you see this in development mode, you should investigate and come up with ways to reduce the load.
6
+
7
+ == Example
8
+
9
+ Add this code to an initializer:
10
+
11
+ SqlSafetyNet.init_rails
12
+
13
+ You can pass a block to this method which will yield a configuration object you can use to set threshold parameters on. See SqlSafetyNet::Configuration for all options. For example:
14
+
15
+ SqlSafetyNet.init_rails do |config|
16
+ config.query_limit = 20
17
+ end
18
+
19
+ If you need finer control over how the gem is set up, or you are using it in a non-Rails environment, you can initialize it manually. You will need to call <tt>SqlSafetyNet::Configuration.enable_on</tt> with the ActiveRecord connection object and you will need to add SqlSafetyNet::RackHandler to the Rack middleware stack.
data/Rakefile ADDED
@@ -0,0 +1,40 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ desc 'Default: run unit tests'
5
+ task :default => :test
6
+
7
+ begin
8
+ require 'rspec'
9
+ require 'rspec/core/rake_task'
10
+ desc 'Run the unit tests'
11
+ RSpec::Core::RakeTask.new(:test)
12
+ rescue LoadError
13
+ task :test do
14
+ raise "You must have rspec 2.0 installed to run the tests"
15
+ end
16
+ end
17
+
18
+ begin
19
+ require 'jeweler'
20
+ Jeweler::Tasks.new do |gem|
21
+ gem.name = "sql_safety_net"
22
+ gem.summary = %Q{Debug SQL statements in ActiveRecord}
23
+ gem.description = %Q{Debug SQL statements in ActiveRecord by displaying warnings on bad queries.}
24
+ gem.authors = ["Brian Durand"]
25
+ gem.email = ["mdobrota@tribune.com", "ddpr@tribune.com"]
26
+ gem.files = FileList["lib/**/*", "spec/**/*", "README.rdoc", "Rakefile", "License.txt"].to_a
27
+ gem.has_rdoc = true
28
+ gem.rdoc_options << '--line-numbers' << '--inline-source' << '--main' << 'README.rdoc'
29
+ gem.extra_rdoc_files = ["README.rdoc"]
30
+ gem.add_dependency('activesupport')
31
+ gem.add_dependency('activerecord', '>= 2.2.2')
32
+ gem.add_dependency('actionpack')
33
+ gem.add_development_dependency('rspec', '>= 2.0.0')
34
+ gem.add_development_dependency('mysql')
35
+ gem.add_development_dependency('pg')
36
+ gem.add_development_dependency('sqlite3-ruby')
37
+ end
38
+ Jeweler::RubygemsDotOrgTasks.new
39
+ rescue LoadError
40
+ end
@@ -0,0 +1,19 @@
1
+ module SqlSafetyNet
2
+ # Hook into ActiveSupport::Cache to set a caching flag on the QueryAnalysis whenever +fetch+ is called with a block.
3
+ module CacheStore
4
+ def self.included(base)
5
+ base.alias_method_chain(:fetch, :sql_safety_net)
6
+ end
7
+
8
+ def fetch_with_sql_safety_net(*args, &block)
9
+ analysis = QueryAnalysis.current
10
+ saved_val = analysis.caching? if analysis
11
+ begin
12
+ analysis.caching = true if analysis
13
+ fetch_without_sql_safety_net(*args, &block)
14
+ ensure
15
+ analysis.caching = saved_val if analysis
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,57 @@
1
+ module SqlSafetyNet
2
+ # Configuration for SqlSafetyNet. The various limit attributes are used for adapters that can do query analysis. Queries
3
+ # will be flagged if their query plan exceeds a limit.
4
+ class Configuration
5
+ attr_accessor :table_scan_limit, :temporary_table_limit, :filesort_limit, :return_rows_limit, :examine_rows_limit, :query_limit, :time_limit, :position
6
+
7
+ def initialize
8
+ @debug = false
9
+ @header = false
10
+ @table_scan_limit = 100
11
+ @temporary_table_limit = 100
12
+ @filesort_limit = 100
13
+ @return_rows_limit = 100
14
+ @examine_rows_limit = 5000
15
+ @query_limit = 10
16
+ @always_show = false
17
+ @position = "top:5px; right: 5px;"
18
+ @time_limit = 300
19
+ end
20
+
21
+ # Enable SqlSafetyNet on a connection. Unless this method is called, the code will not be mixed into the database
22
+ # adapter. This should normally be called only in the development environment.
23
+ def enable_on(connection_class)
24
+ connection_class = connection_class.constantize unless connection_class.is_a?(Class)
25
+ connection_class_name = connection_class.name.split('::').last
26
+ include_class = ConnectionAdapter.const_get(connection_class_name) if ConnectionAdapter.const_defined?(connection_class_name)
27
+ connection_class.send(:include, ConnectionAdapter) unless connection_class.include?(ConnectionAdapter)
28
+ connection_class.send(:include, include_class) if include_class && !connection_class.include?(include_class)
29
+ end
30
+
31
+ def debug=(val)
32
+ @debug = !!val
33
+ end
34
+
35
+ def debug?
36
+ @debug
37
+ end
38
+
39
+ def header=(val)
40
+ @header = !!val
41
+ end
42
+
43
+ def header?
44
+ @header
45
+ end
46
+
47
+ # Set a flag to always show information about queries on rendered HTML pages. If this is not set to true, the debug
48
+ # information will only be shown if the queries on the page exceed one of the limits.
49
+ def always_show=(val)
50
+ @always_show = !!val
51
+ end
52
+
53
+ def always_show?
54
+ @always_show
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,43 @@
1
+ module SqlSafetyNet
2
+ module ConnectionAdapter
3
+ # Logic for analyzing MySQL query plans.
4
+ module MysqlAdapter
5
+ def analyze_query(sql, name, *args)
6
+ if select_statement?(sql)
7
+ query_plan = select_without_sql_safety_net("EXPLAIN #{sql}", "EXPLAIN", *args)
8
+ query_plan_flags = analyze_query_plan(query_plan)
9
+ unless query_plan_flags.empty?
10
+ @logger.debug("Flagged query plan #{name} (#{query_plan_flags.join(', ')}): #{query_plan.inspect}") if @logger
11
+ return {:query_plan => query_plan, :flags => query_plan_flags}
12
+ end
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def analyze_query_plan(query_plan)
19
+ flagged = []
20
+ query_plan.each do |row|
21
+ select_type = (row['select_type'] || '').downcase
22
+ type = (row['type'] || '').downcase
23
+ rows = row['rows'].to_i
24
+ extra = (row['Extra'] || '').downcase
25
+ key = row['key']
26
+ possible_keys = row['possible_keys']
27
+
28
+ flagged << 'table scan' if (type.include?('all') and rows > SqlSafetyNet.config.table_scan_limit)
29
+ flagged << 'fulltext search' if type.include?('fulltext')
30
+ flagged << 'no index used' if (key.blank? and rows > SqlSafetyNet.config.table_scan_limit)
31
+ flagged << 'no indexes possible' if (possible_keys.blank? and rows > SqlSafetyNet.config.table_scan_limit)
32
+ flagged << 'dependent subquery' if select_type.include?('dependent')
33
+ flagged << 'uncacheable subquery' if select_type.include?('uncacheable')
34
+ flagged << 'full scan on null key' if extra.include?('full scan on null key')
35
+ flagged << "uses temporary table for #{rows} rows" if extra.include?('using temporary') and rows > SqlSafetyNet.config.temporary_table_limit
36
+ flagged << "uses filesort for #{rows} rows" if extra.include?('filesort') and rows > SqlSafetyNet.config.filesort_limit
37
+ flagged << "examines #{rows} rows" if rows > SqlSafetyNet.config.examine_rows_limit
38
+ end
39
+ return flagged
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,48 @@
1
+ module SqlSafetyNet
2
+ module ConnectionAdapter
3
+ # Logic for analyzing a query plan from PostgreSQL. These plans are not terribly useful and sometimes
4
+ # the statistics are off, so take them with a grain of salt.
5
+ module PostgreSQLAdapter
6
+ def analyze_query(sql, name, *args)
7
+ if select_statement?(sql)
8
+ query_plan = select_without_sql_safety_net("EXPLAIN #{sql}", "EXPLAIN", *args)
9
+ query_plan_flags = analyze_query_plan(query_plan)
10
+ unless query_plan_flags.empty?
11
+ @logger.debug("Flagged query plan #{name} (#{query_plan_flags.join(', ')}): #{query_plan.inspect}") if @logger
12
+ return {:query_plan => query_plan, :flags => query_plan_flags}
13
+ end
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def analyze_query_plan(query_plan)
20
+ query_plan = query_plan.collect{|r| r.values.first}
21
+ flagged = []
22
+ limit = nil
23
+ query_plan.each do |row|
24
+ row_count = query_plan_rows_value(row)
25
+ row_count = [limit, row_count].min if limit
26
+ if row =~ /^(\s|(->))*Limit\s/
27
+ limit = row_count
28
+ elsif row =~ /^(\s|(->))*Seq Scan/
29
+ flagged << 'table scan' if row_count > SqlSafetyNet.config.table_scan_limit
30
+ elsif row_count > SqlSafetyNet.config.examine_rows_limit
31
+ flagged << "examines #{row_count} rows"
32
+ end
33
+ end
34
+ return flagged
35
+ end
36
+
37
+ private
38
+
39
+ def query_plan_rows_value(plan_row)
40
+ if plan_row.match(/\brows=(\d+)/)
41
+ return $~[1].to_i
42
+ else
43
+ return 0
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,103 @@
1
+ module SqlSafetyNet
2
+ # Logic to be mixed into connection adapters allowing them to analyze queries.
3
+ module ConnectionAdapter
4
+ autoload :MysqlAdapter, File.expand_path('../connection_adapter/mysql_adapter', __FILE__)
5
+ autoload :PostgreSQLAdapter, File.expand_path('../connection_adapter/postgresql_adapter', __FILE__)
6
+
7
+ SELECT_STATEMENT = /^\s*SELECT\b/i
8
+
9
+ def self.included(base)
10
+ base.alias_method_chain :select, :sql_safety_net
11
+ base.alias_method_chain :select_rows, :sql_safety_net
12
+ base.alias_method_chain :columns, :sql_safety_net
13
+ base.alias_method_chain :active?, :sql_safety_net
14
+ end
15
+
16
+ def active_with_sql_safety_net?
17
+ active = disable_sql_safety_net{active_without_sql_safety_net?}
18
+ if @logger && !active
19
+ @logger.warn("#{adapter_name} connection not active")
20
+ end
21
+ active
22
+ end
23
+
24
+ def columns_with_sql_safety_net(table_name, name = nil)
25
+ disable_sql_safety_net do
26
+ columns_without_sql_safety_net(table_name, name)
27
+ end
28
+ end
29
+
30
+ def select_rows_with_sql_safety_net(sql, name = nil, *args)
31
+ analyze_sql_safety_net_query(sql, name, *args) do
32
+ select_rows_without_sql_safety_net(sql, name, *args)
33
+ end
34
+ end
35
+
36
+ # Disable query analysis within a block.
37
+ def disable_sql_safety_net
38
+ save_disabled = Thread.current[:sql_safety_net_disable]
39
+ begin
40
+ Thread.current[:sql_safety_net_disable] = true
41
+ yield
42
+ ensure
43
+ Thread.current[:sql_safety_net_disable] = save_disabled
44
+ end
45
+ end
46
+
47
+ def analyze_query(sql, *args)
48
+ # No op; other modules may redefine to analyze query plans
49
+ end
50
+
51
+ def select_statement?(sql)
52
+ !!sql.match(SELECT_STATEMENT)
53
+ end
54
+
55
+ protected
56
+
57
+ def select_with_sql_safety_net(sql, name = nil, *args)
58
+ analyze_sql_safety_net_query(sql, name, *args) do
59
+ select_without_sql_safety_net(sql, name, *args)
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def analyze_sql_safety_net_query(sql, name, *args)
66
+ if Thread.current[:sql_safety_net_disable]
67
+ yield
68
+ else
69
+ t = Time.now.to_f
70
+ query_results = nil
71
+ disable_sql_safety_net do
72
+ query_results = yield
73
+ end
74
+ elapsed_time = Time.now.to_f - t
75
+
76
+ unless Thread.current[:sql_safety_net_disable]
77
+ query_info = Thread.current[:sql_safety_net_query_info]
78
+ if query_info
79
+ query_info.count += 1
80
+ query_info.selects += query_results.size
81
+ end
82
+ analysis = QueryAnalysis.current
83
+ if analysis
84
+ analysis.selects += 1
85
+ analysis.rows += query_results.size
86
+ analysis.elapsed_time += elapsed_time
87
+ if SqlSafetyNet.config.debug?
88
+ flagged = (@logger ? @logger.silence{analyze_query(sql, name, *args)} : analyze_query(sql, name, *args))
89
+ if elapsed_time * 1000 >= SqlSafetyNet.config.time_limit
90
+ flagged ||= {}
91
+ flagged[:flags] ||= []
92
+ flagged[:flags] << "query time exceeded #{SqlSafetyNet.config.time_limit} ms"
93
+ end
94
+ analysis.add_query(sql, name, query_results.size, elapsed_time, flagged)
95
+ end
96
+ end
97
+ end
98
+
99
+ query_results
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,69 @@
1
+ module SqlSafetyNet
2
+ # Analysis container for the sql queries in a context.
3
+ class QueryAnalysis
4
+ attr_accessor :selects, :rows, :elapsed_time
5
+ attr_reader :flagged_queries, :non_flagged_queries
6
+ attr_writer :caching
7
+
8
+ def initialize
9
+ @selects = 0
10
+ @rows = 0
11
+ @elapsed_time = 0.0
12
+ @flagged_queries = []
13
+ @non_flagged_queries = []
14
+ @caching = false
15
+ end
16
+
17
+ # Analyze all queries within a block.
18
+ def self.analyze
19
+ Thread.current[:sql_safety_net] = new
20
+ begin
21
+ yield
22
+ return current
23
+ ensure
24
+ clear
25
+ end
26
+ end
27
+
28
+ # Get the current analysis object.
29
+ def self.current
30
+ Thread.current[:sql_safety_net]
31
+ end
32
+
33
+ # Clear all query information from the current analysis.
34
+ def self.clear
35
+ Thread.current[:sql_safety_net] = nil
36
+ end
37
+
38
+ def caching?
39
+ @caching
40
+ end
41
+
42
+ def flagged?
43
+ too_many_selects? || too_many_rows? || flagged_queries?
44
+ end
45
+
46
+ def too_many_selects?
47
+ selects > SqlSafetyNet.config.query_limit
48
+ end
49
+
50
+ def too_many_rows?
51
+ rows > SqlSafetyNet.config.return_rows_limit
52
+ end
53
+
54
+ def flagged_queries?
55
+ !flagged_queries.empty?
56
+ end
57
+
58
+ def add_query(sql, name, rows, elapsed_time, flagged)
59
+ info = {:sql => sql, :name => name, :rows => rows, :elapsed_time => elapsed_time, :cached => caching?}
60
+ if flagged.blank?
61
+ non_flagged_queries << info
62
+ else
63
+ info[:query_plan] = flagged[:query_plan]
64
+ info[:flags] = flagged[:flags]
65
+ flagged_queries << info
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,196 @@
1
+ require 'rack'
2
+
3
+ module SqlSafetyNet
4
+ # This Rack handler must be added to the middleware stack in order for query analysis to be output.
5
+ # If the configuration option for debug is set to true, it will add response headers indicating information
6
+ # about the queries executed in the course of the request.
7
+ class RackHandler
8
+
9
+ X_SQL_SAFETY_NET_HEADER = "X-SqlSafetyNet".freeze
10
+ HTML_CONTENT_TYPE_PATTERN = /text\/(x?)html/i
11
+ XML_CONTENT_TYPE_PATTERN = /application\/xml/i
12
+
13
+ def initialize(app, logger = Rails.logger)
14
+ @app = app
15
+ @logger = logger
16
+ end
17
+
18
+ def call(env)
19
+ response = nil
20
+ analysis = QueryAnalysis.analyze do
21
+ response = @app.call(env)
22
+ end
23
+
24
+ if @logger && (analysis.too_many_selects? || analysis.too_many_rows?)
25
+ request = Rack::Request.new(env)
26
+ @logger.warn("Excess database usage: request generated #{analysis.selects} queries and returned #{analysis.rows} rows [#{request.request_method} #{request.url}]")
27
+ end
28
+
29
+ # Add a response header that contains a summary of the debug info
30
+ if SqlSafetyNet.config.header? || SqlSafetyNet.config.debug?
31
+ headers = response[1]
32
+ headers[X_SQL_SAFETY_NET_HEADER] = "selects=#{analysis.selects}; rows=#{analysis.rows}; elapsed_time=#{(analysis.elapsed_time * 1000).round}; flagged_queries=#{analysis.flagged_queries.size}" if headers
33
+ end
34
+
35
+ if SqlSafetyNet.config.debug?
36
+ wrapped_response = Rack::Response.new(response[2], response[0], response[1])
37
+ if analysis.flagged? || SqlSafetyNet.config.always_show?
38
+ request = Rack::Request.new(env)
39
+ # Ignore Ajax calls
40
+ unless request.xhr?
41
+ # Only if content type is text/html
42
+ type = wrapped_response.content_type
43
+ if type.nil? || type.to_s.match(HTML_CONTENT_TYPE_PATTERN)
44
+ wrapped_response.write(flagged_sql_html(analysis))
45
+ elsif type.to_s.match(XML_CONTENT_TYPE_PATTERN)
46
+ wrapped_response.write(xml_comment(flagged_sql_text(analysis)))
47
+ end
48
+ end
49
+ end
50
+ response = wrapped_response.finish
51
+ end
52
+
53
+ response
54
+ end
55
+
56
+ def flagged_sql_html(analysis)
57
+ flagged_html = ''
58
+ cached_selects = 0
59
+ cached_rows = 0
60
+ cached_elapsed_time = 0.0
61
+
62
+ if analysis.flagged_queries?
63
+ flagged_html << '<div style="color:#C00;">'
64
+ flagged_html << "<div style=\"font-weight:bold; margin-bottom:10px;\">#{analysis.flagged_queries.size == 1 ? 'This query has' : "These #{analysis.flagged_queries.size} queries have"} flagged query plans:</div>"
65
+ analysis.flagged_queries.each do |query|
66
+ if query[:cached]
67
+ cached_selects += 1
68
+ cached_rows += query[:rows]
69
+ cached_elapsed_time += query[:elapsed_time]
70
+ end
71
+ flagged_html << '<div style="margin-bottom:10px;">'
72
+ flagged_html << "<div style=\"font-weight:bold; margin-bottom: 5px;\">#{query[:rows]} rows returned, #{(query[:elapsed_time] * 1000).round} ms#{ " <span style='color:teal;'>(CACHED)</span>" if query[:cached]}</div>"
73
+ flagged_html << "<div style=\"font-weight:bold; margin-bottom: 5px;\">#{query[:flags].join(', ')}</div>"
74
+ flagged_html << "<div style=\"margin-bottom: 5px;\">#{Rack::Utils.escape_html(query[:sql])}</div>"
75
+ flagged_html << "<div style=\"margin-bottom: 5px;\">Query Plan: #{Rack::Utils.escape_html(query[:query_plan].inspect)}</div>" if query[:query_plan]
76
+ flagged_html << '</div>'
77
+ end
78
+ flagged_html << '</div>'
79
+ end
80
+
81
+ if analysis.too_many_selects? || analysis.too_many_rows? || SqlSafetyNet.config.always_show?
82
+ flagged_html << "<div style=\"font-weight:bold; margin-bottom:10px;\">#{analysis.non_flagged_queries.size == 1 ? 'This query' : "These #{analysis.non_flagged_queries.size} queries"} did not have flagged query plans:</div>"
83
+ analysis.non_flagged_queries.each do |query|
84
+ if query[:cached]
85
+ cached_selects += 1
86
+ cached_rows += query[:rows]
87
+ cached_elapsed_time += query[:elapsed_time]
88
+ end
89
+ flagged_html << '<div style="margin-bottom:10px;">'
90
+ flagged_html << "<div style=\"font-weight:bold; margin-bottom: 5px;\">#{query[:rows]} rows returned, #{(query[:elapsed_time] * 1000).round} ms#{ " <span style='color:teal;'>(CACHED)</span>" if query[:cached]}</div>"
91
+ flagged_html << "<div style=\"margin-bottom: 5px;\">#{Rack::Utils.escape_html(query[:sql])}</div>"
92
+ flagged_html << '</div>'
93
+ end
94
+ end
95
+
96
+ color_scheme = '#060'
97
+ if analysis.flagged_queries?
98
+ color_scheme = '#C00'
99
+ elsif analysis.flagged?
100
+ color_scheme = '#C60'
101
+ end
102
+ label = (analysis.flagged?) ? 'SQL WARNING' : 'SQL INFO'
103
+
104
+ cache_html = nil
105
+ if cached_selects > 0
106
+ cache_html = <<-EOS
107
+ <div style="margin-bottom:10px; font-weight:bold;">
108
+ Some of the queries will be cached.
109
+ <div style="color:#C00;">
110
+ Uncached: #{analysis.selects - cached_selects} selects, #{analysis.rows - cached_rows} rows, #{((analysis.elapsed_time - cached_elapsed_time) * 1000).round} ms
111
+ </div>
112
+ <div style="color:teal;">
113
+ Cached: #{cached_selects} selects, #{cached_rows} rows, #{(cached_elapsed_time * 1000).round} ms
114
+ </div>
115
+ </div>
116
+ EOS
117
+ end
118
+
119
+ <<-EOS
120
+ <div id="sql_safety_net_warning" style="font-family:sans-serif; font-size:10px; position:fixed; z-index:999999999; text-align:left; #{SqlSafetyNet.config.position}">
121
+ <div style="background-color:#{color_scheme}; color:#FFF; padding:4px; width:160px; float:right;">
122
+ <a href="javascript:void(document.getElementById('sql_safety_net_warning').style.display = 'none')" style="text-decoration:none; float:right; display:block; font-size:9px;"><span style="color:#FFF; text-decoration:none; font-weight:bold;">&times;</span></a>
123
+ <a href="javascript:void(document.getElementById('sql_safety_net_flagged_queries').style.display = (document.getElementById('sql_safety_net_flagged_queries').style.display == 'block' ? 'none' : 'block'))" style="text-decoration:none;">
124
+ <span style="color:#FFF; text-decoration:none; font-weight:bold;">#{label} &raquo;</span>
125
+ </a>
126
+ <div>#{analysis.selects} selects, #{analysis.rows} rows, #{(analysis.elapsed_time * 1000).round} ms</div>
127
+ </div>
128
+ <div id="sql_safety_net_flagged_queries" style="clear:right; display:none; width:500px; padding:2px; border:1px solid #{color_scheme}; background-color:#FFF; color:#000; overflow:auto; max-height:500px;">
129
+ <div style="margin-bottom:10px; font-weight:bold;">
130
+ There are #{analysis.selects} queries on this page that return #{analysis.rows} rows and took #{(analysis.elapsed_time * 1000).round} ms to execute.
131
+ </div>
132
+ #{cache_html}
133
+ #{flagged_html}
134
+ </div>
135
+ </div>
136
+ EOS
137
+ end
138
+
139
+ def flagged_sql_text(analysis)
140
+ flagged_text = ''
141
+ cached_selects = 0
142
+ cached_rows = 0
143
+ cached_elapsed_time = 0.0
144
+
145
+ if analysis.flagged_queries?
146
+ flagged_text << "#{analysis.flagged_queries.size == 1 ? 'This query has' : "These #{analysis.flagged_queries.size} queries have"} flagged query plans:\n\n"
147
+ analysis.flagged_queries.each do |query|
148
+ if query[:cached]
149
+ cached_selects += 1
150
+ cached_rows += query[:rows]
151
+ cached_elapsed_time += query[:elapsed_time]
152
+ end
153
+ flagged_text << "#{query[:rows]} rows returned, #{(query[:elapsed_time] * 1000).round} ms#{ " (CACHED)" if query[:cached]}\n"
154
+ flagged_text << "#{query[:flags].join(', ')}\n\n"
155
+ flagged_text << "#{query[:sql]}\n\n"
156
+ flagged_text << "Query Plan: #{Rack::Utils.escape_html(query[:query_plan].inspect)}\n" if query[:query_plan]
157
+ flagged_text << "\n"
158
+ end
159
+ flagged_text << "\n"
160
+ end
161
+
162
+ if analysis.too_many_selects? || analysis.too_many_rows? || SqlSafetyNet.config.always_show?
163
+ flagged_text << "#{analysis.non_flagged_queries.size == 1 ? 'This query' : "These #{analysis.non_flagged_queries.size} queries"} did not have flagged query plans:\n\n"
164
+ analysis.non_flagged_queries.each do |query|
165
+ if query[:cached]
166
+ cached_selects += 1
167
+ cached_rows += query[:rows]
168
+ cached_elapsed_time += query[:elapsed_time]
169
+ end
170
+ flagged_text << "#{query[:rows]} rows returned, #{(query[:elapsed_time] * 1000).round} ms#{ " (CACHED)" if query[:cached]}\n\n"
171
+ flagged_text << "#{query[:sql]}\n\n"
172
+ flagged_text << "\n"
173
+ end
174
+ end
175
+
176
+ label = (analysis.flagged?) ? 'SQL WARNING' : 'SQL INFO'
177
+
178
+ cache_text = ""
179
+ if cached_selects > 0
180
+ cache_text << "Some of the queries will be cached.\n\n"
181
+ cache_text << "Uncached: #{analysis.selects - cached_selects} selects, #{analysis.rows - cached_rows} rows, #{((analysis.elapsed_time - cached_elapsed_time) * 1000).round} ms\n"
182
+ cache_text << "Cached: #{cached_selects} selects, #{cached_rows} rows, #{(cached_elapsed_time * 1000).round} ms\n\n"
183
+ end
184
+
185
+ text = "SqlSafetyNet\n\n"
186
+ text << "There are #{analysis.selects} queries on this page that return #{analysis.rows} rows and took #{(analysis.elapsed_time * 1000).round} ms to execute.\n\n"
187
+ text << cache_text
188
+ text << flagged_text
189
+ text
190
+ end
191
+
192
+ def xml_comment(text)
193
+ "<!-- #{text.gsub('-->', '\\-\\->')} -->"
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,27 @@
1
+ require 'active_record'
2
+ require 'action_controller'
3
+
4
+ # Collects and displays debugging information about SQL statements.
5
+ module SqlSafetyNet
6
+ autoload :CacheStore, File.expand_path('../sql_safety_net/cache_store', __FILE__)
7
+ autoload :Configuration, File.expand_path('../sql_safety_net/configuration', __FILE__)
8
+ autoload :ConnectionAdapter, File.expand_path('../sql_safety_net/connection_adapter', __FILE__)
9
+ autoload :QueryAnalysis, File.expand_path('../sql_safety_net/query_analysis', __FILE__)
10
+ autoload :RackHandler, File.expand_path('../sql_safety_net/rack_handler', __FILE__)
11
+
12
+ class << self
13
+ # Get the configuration for the safety net.
14
+ def config
15
+ @config ||= SqlSafetyNet::Configuration.new
16
+ end
17
+
18
+ def init_rails
19
+ SqlSafetyNet.config.enable_on(ActiveRecord::Base.connection.class)
20
+ if Rails.env == "development"
21
+ SqlSafetyNet.config.debug = true
22
+ ActiveSupport::Cache::Store.send(:include, CacheStore)
23
+ end
24
+ Rails.configuration.middleware.use(SqlSafetyNet::RackHandler)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ describe SqlSafetyNet::CacheStore do
4
+ let(:cache){ ActiveSupport::Cache::MemoryStore.new }
5
+
6
+ it "should set the caching flag on the current query analysis when fetching with a block" do
7
+ SqlSafetyNet::QueryAnalysis.analyze do
8
+ SqlSafetyNet::QueryAnalysis.current.caching?.should == false
9
+ val = cache.fetch("key") do
10
+ SqlSafetyNet::QueryAnalysis.current.caching?.should == true
11
+ "woot"
12
+ end
13
+ val.should == "woot"
14
+ end
15
+ end
16
+
17
+ it "should fetch properly even when there is not current query analysis" do
18
+ SqlSafetyNet::QueryAnalysis.current.should == nil
19
+ val = cache.fetch("key") do
20
+ SqlSafetyNet::QueryAnalysis.current.should == nil
21
+ "woot"
22
+ end
23
+ val.should == "woot"
24
+ end
25
+
26
+ end