assert_efficient_sql 0.3.0

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