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.
- data/lib/assert_efficient_sql.rb +338 -0
- 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
|
+
|