assert_efficient_sql 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (2) hide show
  1. data/lib/assert_efficient_sql.rb +338 -0
  2. metadata +70 -0
@@ -0,0 +1,338 @@
1
+ require 'assert2'
2
+
3
+ #:stopdoc:
4
+
5
+ # ERGO reach out to an SQL lint?
6
+ # ERGO cite, and peacibly coexist with mysql_helper.rb
7
+ # ERGO report all failings, not one at a time
8
+ # ERGO check for valid options
9
+ # ERGO highlite the offending row in the analysis
10
+ # ERGO cite http://hackmysql.com/selectandsort
11
+ # ERGO link from http://efficient-sql.rubyforge.org/files/README.html to
12
+ # project page
13
+ # ERGO hamachi.cc
14
+ # ERGO is flunk susceptible to <?> bug?
15
+ # ERGO One catch that jumps out right away is that you’re going to have to run this against a DB that looks a lot like production, since MySQL will punt to full table scans on smaller tables, and your unit test data probably qualifies as “smaller tables”.
16
+ # (and cross-cite) http://enfranchisedmind.com/blog/2008/01/14/assert_efficient_sql/
17
+ # ERGO all with no possible keys is worse than ALL with possible keys
18
+ # ERGO retire _exec
19
+
20
+ class Array
21
+ protected
22
+ def qa_columnized_row(fields, sized)
23
+ row = []
24
+ fields.each_with_index do |f, i|
25
+ row << sprintf("%0-#{sized[i]}s", f.to_s)
26
+ end
27
+ row.join(' | ')
28
+ end
29
+
30
+ public
31
+ def qa_columnized
32
+ sized = {}
33
+ self.each do |row|
34
+ row.values.each_with_index do |value, i|
35
+ sized[i] = [sized[i].to_i, row.keys[i].length, value.to_s.length].max
36
+ end
37
+ end
38
+
39
+ table = []
40
+ table << qa_columnized_row(self.first.keys, sized)
41
+ table << '-' * table.first.length
42
+ self.each { |row| table << qa_columnized_row(row.values, sized) }
43
+ table.join("\n ") # Spaces added to work with format_log_entry
44
+ end
45
+ end
46
+
47
+ module ActiveRecord
48
+ module ConnectionAdapters
49
+ class MysqlAdapter < AbstractAdapter
50
+ attr_accessor :analyses
51
+
52
+ private
53
+ alias_method :select_without_analyzer, :select
54
+
55
+ def select(sql, name = nil)
56
+ query_results = select_without_analyzer(sql, name)
57
+
58
+ if sql =~ /^select /i
59
+ analysis = select_without_analyzer("EXPLAIN extended #{sql}", name)
60
+
61
+ # TODO use extended?
62
+ # http://www.mysqlperformanceblog.com/2006/07/24/extended-explain/
63
+ #
64
+ # ERGO p select_without_analyzer('show warnings')
65
+ # hence, show warnings caused by your queries, too
66
+
67
+ if @logger and @logger.level <= Logger::INFO
68
+ @logger.debug(
69
+ @logger.silence do
70
+ format_log_entry("Analyzing #{ name }\n",
71
+ "#{ analysis.qa_columnized }\n"
72
+ )
73
+ end
74
+ ) if sql =~ /^select/i
75
+ end
76
+
77
+ explained = [sql, name, analysis.map(&:with_indifferent_access)]
78
+ (@analyses ||= []) << explained
79
+ end
80
+
81
+ query_results
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ module AssertEfficientSql; end
88
+
89
+ class AssertEfficientSql::SqlEfficiencyAsserter
90
+
91
+ def initialize(options, context)
92
+ @issues = []
93
+ @options, @context = options, context
94
+ @analyses = ActiveRecord::Base.connection.analyses
95
+ @session_before = fetch_database_session
96
+ yield
97
+ check_for_query_statements
98
+ @session_after = fetch_database_session
99
+ check_session_status
100
+
101
+ @analyses.each do |@name, @sql, @analysis|
102
+ @analysis.each do |@explanation|
103
+ analyze_efficiency
104
+ end
105
+ end
106
+
107
+ puts explain_all if @options[:verbose]
108
+ end
109
+
110
+ def explain_all
111
+ @analyses.map{ |@name, @sql, @analysis|
112
+ format_explanation
113
+ }.join("\n")
114
+ end
115
+
116
+ def fetch_database_session
117
+ result = ActiveRecord::Base.connection.execute('show session status')
118
+ hashes = []
119
+ result.each_hash{|h| hashes << h }
120
+ zz = {}.with_indifferent_access
121
+ hashes.each{|v| zz[v['Variable_name'].to_sym] = v['Value'].to_i }
122
+ return zz
123
+ end
124
+
125
+ def check(bool)
126
+ @issues << yield unless bool
127
+ end
128
+
129
+ def check_session_status
130
+ @options.each do |key, value|
131
+ if @session_before[key] # ERGO and not true
132
+ if (before = @session_before[key]) + value <=
133
+ (after = @session_after[key])
134
+ flunk "Status variable #{ key } incremented > #{ value },\n" +
135
+ "from #{ before } to #{ after }, during one of these:\n" +
136
+ explain_all
137
+ end
138
+ end
139
+ end
140
+ end
141
+
142
+ def analyze_efficiency
143
+ rows = @explanation[:rows].to_i
144
+ throttle = @options[:throttle]
145
+
146
+ check rows <= throttle do
147
+ "row count #{ rows } is more than :throttle => #{ throttle }"
148
+ end
149
+
150
+ check @options[:ALL] || 'ALL' != @explanation[:type] do
151
+ 'full table scan'
152
+ end
153
+
154
+ check @options[:Using_filesort] ||
155
+ @explanation[:Extra] !~ /(Using filesort)/ do
156
+ $1
157
+ end
158
+
159
+ flunk 'Pessimistic ' + format_explanation unless @issues.empty?
160
+ end
161
+
162
+ def check_for_query_statements
163
+ flunk 'assert_efficient_sql saw no queries!' if @analyses.empty?
164
+ end
165
+
166
+ def flunk(why)
167
+ @context.flunk @context.build_message(@options[:diagnostic], why)
168
+ end
169
+
170
+ def format_explanation
171
+ @name = 'for ' + @name unless @name.blank?
172
+
173
+ return "\nquery #{ @name }\n" +
174
+ @issues.join("\n") +
175
+ "\n#{ @sql }\n " +
176
+ @analysis.qa_columnized
177
+ end
178
+
179
+ end
180
+
181
+ #:startdoc:
182
+
183
+
184
+ module AssertEfficientSql
185
+
186
+ # See: http://www.oreillynet.com/onlamp/blog/2007/07/assert_latest_and_greatest.html
187
+
188
+ def assert_latest(*models, &block)
189
+ models, diagnostic = _get_latest_args(models, 'assert')
190
+ get_latest(models, &block) or _flunk_latest(models, diagnostic, true, block)
191
+ end
192
+
193
+ def _get_latest_args(models, what)
194
+ diagnostic = nil
195
+ diagnostic = models.pop if models.last.kind_of? String
196
+
197
+ unless models.length > 0 and
198
+ (diagnostic.nil? or diagnostic.kind_of? String)
199
+ raise "call #{ what }_latest(models..., diagnostic) with any number " +
200
+ 'of Model classes, followed by an optional diagnostic message'
201
+ end
202
+ return models, diagnostic
203
+ end
204
+ private :_get_latest_args
205
+
206
+ def deny_latest(*models, &block)
207
+ models, diagnostic = _get_latest_args(models, 'deny')
208
+ return unless got = get_latest(models, &block)
209
+ models = [got].flatten.compact.map(&:class)
210
+ _flunk_latest(models, diagnostic, false, block)
211
+ end
212
+
213
+ def get_latest(models, &block)
214
+ max_ids = models.map{|model| model.maximum(:id) || 0 }
215
+ block.call
216
+ index = -1
217
+ return *models.map{|model|
218
+ all = *model.find( :all,
219
+ :conditions => "id > #{max_ids[index += 1]}",
220
+ :order => "id asc" )
221
+ all # * returns nil for [],
222
+ # one object for [x],
223
+ # or an array with more than one item
224
+ }
225
+ end
226
+
227
+ def _flunk_latest(models, diagnostic, polarity, block)
228
+ model_names = models.map(&:name).join(', ')
229
+ rationale = "should#{ ' not' unless polarity
230
+ } create new #{ model_names
231
+ } record(s) in block:\n\t\t#{
232
+ reflect_source(&block).gsub("\n", "\n\t\t")
233
+ }\n"
234
+ # RubyNodeReflector::RubyReflector.new(block, false).result }"
235
+ # note we don't evaluate...
236
+ flunk build_message(diagnostic, rationale)
237
+ end
238
+ private :_flunk_latest
239
+
240
+
241
+ def _exec(cmd) #:nodoc:
242
+ ActiveRecord::Base.connection.execute(cmd)
243
+ end
244
+
245
+ def assert_efficient_sql(options = {}, &block)
246
+ options = { :verbose => true } if options == :verbose
247
+
248
+ if options.class == Hash
249
+ options.reverse_merge! default_options
250
+
251
+ if current_adapter?(:MysqlAdapter)
252
+ return assert_efficient_mysql(options, &block)
253
+ else
254
+ warn_adapter_required(options)
255
+ block.call if block
256
+ end
257
+ else
258
+ print_syntax
259
+ end
260
+
261
+ return []
262
+ end
263
+
264
+ class BufferStdout #:nodoc:
265
+ def write(stuff)
266
+ (@output ||= '') << stuff
267
+ end
268
+ def output; @output || '' end
269
+ end
270
+
271
+ def assert_stdout(matcher = nil, diagnostic = nil) #:nodoc:
272
+ waz = $stdout
273
+ $stdout = BufferStdout.new
274
+ yield
275
+ assert_match matcher, $stdout.output, diagnostic if matcher
276
+ return $stdout.output
277
+ ensure
278
+ $stdout = waz
279
+ end
280
+
281
+ def deny_stdout(unmatcher, diagnostic = nil, &block) #:nodoc:
282
+ got = assert_stdout(nil, nil, &block)
283
+ assert_no_match unmatcher, got, diagnostic
284
+ end
285
+
286
+ private
287
+
288
+ def current_adapter?(type) #:nodoc:
289
+ ActiveRecord::ConnectionAdapters.const_defined?(type) and
290
+ ActiveRecord::Base.connection.instance_of?(ActiveRecord::ConnectionAdapters.const_get(type))
291
+ end
292
+
293
+ def warn_adapter_required(options)
294
+ if options[:warn_mysql_required]
295
+ puts 'assert_efficient_sql requires MySQL' unless $warned_once
296
+ $warned_once = true
297
+ end
298
+ end
299
+
300
+ def assert_efficient_mysql(options, &block)
301
+ outer_block_analyses = ActiveRecord::Base.connection.analyses
302
+ ActiveRecord::Base.connection.analyses = []
303
+ _exec('flush tables') if options[:flush]
304
+ SqlEfficiencyAsserter.new(options, self, &block)
305
+ return ActiveRecord::Base.connection.analyses # in case someone would like to use it!
306
+ ensure
307
+ ActiveRecord::Base.connection.analyses = outer_block_analyses
308
+ end
309
+
310
+ def syntax
311
+ return {
312
+ :diagnostic => [nil , 'supplementary message in failure reports'],
313
+ :flush => [true , 'flush memory before evaluation'],
314
+ :throttle => [1000 , 'maximum permitted rows scanned'],
315
+ :Using_filesort => [false, 'permission to write a temporary file to sort'],
316
+ :verbose => [false, 'if the test passes, print the EXPLAIN'],
317
+ :warn_mysql_required => [true , 'disable the spew advising we only work with MySQL'] }
318
+ end
319
+
320
+ def default_options
321
+ options = syntax.dup
322
+ options.each{|k,(v,m)| options[k] = v}
323
+ return options
324
+ end
325
+
326
+ def print_syntax
327
+ puts "\n\nassert_efficient_sql called with invalid argument.\n"
328
+ puts " __flag__ __default__ __effect__"
329
+
330
+ syntax.each do |k,(v,m)|
331
+ printf " :%-14s => %-8s # %s\n", k, v.inspect, m
332
+ end
333
+ end
334
+
335
+ end
336
+
337
+ #:stopdoc:
338
+ Test::Unit::TestCase.send :include, AssertEfficientSql
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: assert_efficient_sql
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: ruby
6
+ authors:
7
+ - Phlip
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-03-12 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rubynode
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: "0"
23
+ version:
24
+ - !ruby/object:Gem::Dependency
25
+ name: assert2
26
+ version_requirement:
27
+ version_requirements: !ruby/object:Gem::Requirement
28
+ requirements:
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: "0"
32
+ version:
33
+ description:
34
+ email: phlip2005@gmail.com
35
+ executables: []
36
+
37
+ extensions: []
38
+
39
+ extra_rdoc_files: []
40
+
41
+ files:
42
+ - lib/assert_efficient_sql.rb
43
+ has_rdoc: false
44
+ homepage: http://rubyforge.org/projects/efficient-sql
45
+ post_install_message:
46
+ rdoc_options: []
47
+
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ version:
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: "0"
61
+ version:
62
+ requirements: []
63
+
64
+ rubyforge_project: efficient-sql
65
+ rubygems_version: 1.0.1
66
+ signing_key:
67
+ specification_version: 2
68
+ summary: efficient assertions for ActiveRecord tests
69
+ test_files: []
70
+