sql_safety_net 1.1.11

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.
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