shiba 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/shiba/diff.rb CHANGED
@@ -12,23 +12,28 @@ module Shiba
12
12
  # The position value equals the number of lines down from the first "@@" hunk header
13
13
  # in the file you want to add a comment.
14
14
 
15
- # diff = `git diff --unified=0`
16
- # parse_diff(StringIO.new(diff))
17
- # => "hello.rb:1"
18
- # => "hello.rb:2"
19
- # => "test.rb:5"
20
-
21
- # For simplicity, the default output of git diff is not supported.
22
- # The expected format is from 'git diff unified=0'
23
-
24
15
  attr_reader :status
25
16
 
26
17
  def initialize(file)
18
+ # Fixme. seems like enumerables should work in general.
19
+ if !file.respond_to?(:pos)
20
+ raise StandardError.new("Diff file does not appear to be a seekable IO object.")
21
+ end
27
22
  @diff = file
28
23
  @status = :new
29
24
  end
30
25
 
31
26
  # Returns the file and line numbers that contain inserts. Deletions are ignored.
27
+ # For simplicity, the default output of git diff is not supported.
28
+ # The expected format is from 'git diff unified=0'
29
+ #
30
+ # Example:
31
+ # diff = `git diff --unified=0`
32
+ # Diff.new(StringIO.new(diff))
33
+ # => [ [ "hello.rb", 1..3 ]
34
+ # => [ "hello.rb", 7..7 ]
35
+ # => [ "test.rb", 23..23 ]
36
+ # => ]
32
37
  def updated_lines
33
38
  io = @diff.each_line
34
39
  path = nil
@@ -53,6 +58,11 @@ module Shiba
53
58
  # Returns the position in the diff, after the relevant file header,
54
59
  # that contains the specified file/lineno modification.
55
60
  # Only supports finding the position in the destination / newest version of the file.
61
+ #
62
+ # Example:
63
+ # diff = Diff.new(`git diff`)
64
+ # diff.find_position("test.rb", 3)
65
+ # => 5
56
66
  def find_position(path, line_number)
57
67
  io = @diff.each_line # maybe redundant?
58
68
 
@@ -101,11 +111,11 @@ module Shiba
101
111
  end
102
112
 
103
113
  def file_header?(line)
104
- line.match?(FILE_PATTERN)
114
+ line =~ FILE_PATTERN
105
115
  end
106
116
 
107
117
  def hunk_header?(line)
108
- line.match?(LINE_PATTERN)
118
+ LINE_PATTERN =~ line
109
119
  end
110
120
 
111
121
  def line_numbers_for_destination(diff_line)
data/lib/shiba/explain.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  require 'json'
2
2
  require 'shiba/index'
3
+ require 'shiba/explain/mysql_explain'
4
+ require 'shiba/explain/postgres_explain'
3
5
 
4
6
  module Shiba
5
7
  class Explain
@@ -12,9 +14,13 @@ module Shiba
12
14
  end
13
15
 
14
16
  @options = options
15
- ex = Shiba.connection.query("EXPLAIN FORMAT=JSON #{@sql}").to_a
16
- @explain_json = JSON.parse(ex.first['EXPLAIN'])
17
- @rows = self.class.transform_json(@explain_json['query_block'])
17
+ @explain_json = Shiba.connection.explain(@sql)
18
+
19
+ if Shiba.connection.mysql?
20
+ @rows = Shiba::Explain::MysqlExplain.new.transform_json(@explain_json['query_block'])
21
+ else
22
+ @rows = Shiba::Explain::PostgresExplain.new(@explain_json).transform
23
+ end
18
24
  @stats = stats
19
25
  run_checks!
20
26
  end
@@ -47,48 +53,6 @@ module Shiba
47
53
  table
48
54
  end
49
55
 
50
- def self.transform_table(table, extra = {})
51
- t = table
52
- res = {}
53
- res['table'] = t['table_name']
54
- res['access_type'] = t['access_type']
55
- res['key'] = t['key']
56
- res['used_key_parts'] = t['used_key_parts'] if t['used_key_parts']
57
- res['rows'] = t['rows_examined_per_scan']
58
- res['filtered'] = t['filtered']
59
-
60
- if t['possible_keys'] && t['possible_keys'] != [res['key']]
61
- res['possible_keys'] = t['possible_keys']
62
- end
63
- res['using_index'] = t['using_index'] if t['using_index']
64
-
65
- res.merge!(extra)
66
-
67
- res
68
- end
69
-
70
- def self.transform_json(json, res = [], extra = {})
71
- rows = []
72
-
73
- if (ordering = json['ordering_operation'])
74
- index_walk = (ordering['using_filesort'] == false)
75
- return transform_json(json['ordering_operation'], res, { "index_walk" => index_walk } )
76
- elsif json['duplicates_removal']
77
- return transform_json(json['duplicates_removal'], res, extra)
78
- elsif json['grouping_operation']
79
- return transform_json(json['grouping_operation'], res, extra)
80
- elsif !json['nested_loop'] && !json['table']
81
- return [{'Extra' => json['message']}]
82
- elsif json['nested_loop']
83
- json['nested_loop'].map do |nested|
84
- transform_json(nested, res, extra)
85
- end
86
- elsif json['table']
87
- res << transform_table(json['table'], extra)
88
- end
89
- res
90
- end
91
-
92
56
  # [{"id"=>1, "select_type"=>"SIMPLE", "table"=>"interwiki", "partitions"=>nil, "type"=>"const", "possible_keys"=>"PRIMARY", "key"=>"PRIMARY", "key_len"=>"34", "ref"=>"const", "rows"=>1, "filtered"=>100.0, "Extra"=>nil}]
93
57
  attr_reader :cost
94
58
 
@@ -143,7 +107,8 @@ module Shiba
143
107
 
144
108
  # TODO: need to parse SQL here I think
145
109
  def simple_table_scan?
146
- @rows.size == 1 && first['using_index'] && (@sql !~ /order by/i)
110
+ @rows.size == 1 && (@sql !~ /order by/i) &&
111
+ (first['using_index'] || !(@sql =~ /\s+WHERE\s+/i))
147
112
  end
148
113
 
149
114
  def severity
@@ -216,15 +181,14 @@ module Shiba
216
181
  messages << "fuzzed_data" if fuzzed?(first_table)
217
182
  end
218
183
 
184
+ # TODO: we don't catch some cases like SELECT * from foo where index_col = 1 limit 1
185
+ # bcs we really just need to parse the SQL.
219
186
  check :check_simple_table_scan
220
187
  def check_simple_table_scan
221
188
  if simple_table_scan?
222
189
  if limit
223
- messages << 'limited_tablescan'
190
+ messages << 'limited_scan'
224
191
  @cost = limit
225
- else
226
- tag_query_type
227
- @cost = @stats.estimate_key(first_table, first_key, first['used_key_parts'])
228
192
  end
229
193
  end
230
194
  end
@@ -349,9 +313,10 @@ module Shiba
349
313
  end
350
314
 
351
315
  def humanized_explain
352
- h = @explain_json['query_block'].dup
353
- %w(select_id cost_info).each { |i| h.delete(i) }
354
- h
316
+ #h = @explain_json['query_block'].dup
317
+ #%w(select_id cost_info).each { |i| h.delete(i) }
318
+ #h
319
+ @explain_json
355
320
  end
356
321
  end
357
322
  end
@@ -0,0 +1,47 @@
1
+ module Shiba
2
+ class Explain
3
+ class MysqlExplain
4
+ def transform_table(table, extra = {})
5
+ t = table
6
+ res = {}
7
+ res['table'] = t['table_name']
8
+ res['access_type'] = t['access_type']
9
+ res['key'] = t['key']
10
+ res['used_key_parts'] = t['used_key_parts'] if t['used_key_parts']
11
+ res['rows'] = t['rows_examined_per_scan']
12
+ res['filtered'] = t['filtered']
13
+
14
+ if t['possible_keys'] && t['possible_keys'] != [res['key']]
15
+ res['possible_keys'] = t['possible_keys']
16
+ end
17
+ res['using_index'] = t['using_index'] if t['using_index']
18
+
19
+ res.merge!(extra)
20
+
21
+ res
22
+ end
23
+
24
+ def transform_json(json, res = [], extra = {})
25
+ rows = []
26
+
27
+ if (ordering = json['ordering_operation'])
28
+ index_walk = (ordering['using_filesort'] == false)
29
+ return transform_json(json['ordering_operation'], res, { "index_walk" => index_walk } )
30
+ elsif json['duplicates_removal']
31
+ return transform_json(json['duplicates_removal'], res, extra)
32
+ elsif json['grouping_operation']
33
+ return transform_json(json['grouping_operation'], res, extra)
34
+ elsif !json['nested_loop'] && !json['table']
35
+ return [{'Extra' => json['message']}]
36
+ elsif json['nested_loop']
37
+ json['nested_loop'].map do |nested|
38
+ transform_json(nested, res, extra)
39
+ end
40
+ elsif json['table']
41
+ res << transform_table(json['table'], extra)
42
+ end
43
+ res
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,91 @@
1
+ require 'shiba/explain/postgres_explain_index_conditions'
2
+
3
+ module Shiba
4
+ class Explain
5
+ class PostgresExplain
6
+ def initialize(json)
7
+ @json = json
8
+ @state = {}
9
+ end
10
+
11
+ def with_state(hash)
12
+ old_state = @state
13
+ @state = @state.merge(hash)
14
+ yield
15
+ @state = old_state
16
+ end
17
+
18
+ def transform_node(node, array)
19
+ case node['Node Type']
20
+ when "Limit", "LockRows", "Aggregate", "Unique", "Sort", "Hash", "ProjectSet"
21
+ recurse_plans(node, array)
22
+ when "Nested Loop"
23
+ with_state(join_type: node["Join Type"]) do
24
+ recurse_plans(node, array)
25
+ end
26
+ when "Hash Join"
27
+ join_fields = extract_join_key_parts(node['Hash Cond'])
28
+ with_state(join_fields: join_fields, join_type: "Hash") do
29
+ recurse_plans(node, array)
30
+ end
31
+ when "Bitmap Heap Scan"
32
+ with_state(table: node['Relation Name']) do
33
+ recurse_plans(node, array)
34
+ end
35
+ when "Seq Scan"
36
+ array << {
37
+ "table" => node["Relation Name"],
38
+ "access_type" => "ALL",
39
+ "key" => nil,
40
+ "filter" => node["Filter"]
41
+ }
42
+ when "Index Scan", "Bitmap Index Scan", "Index Only Scan"
43
+ table = node["Relation Name"] || @state[:table]
44
+
45
+ if node['Index Cond']
46
+ used_key_parts = extract_used_key_parts(node['Index Cond'])
47
+ else
48
+ used_key_parts = []
49
+ end
50
+
51
+ h = {
52
+ "table" => node["Relation Name"] || @state[:table],
53
+ "access_type" => "ref",
54
+ "key" => node["Index Name"],
55
+ "used_key_parts" => used_key_parts
56
+ }
57
+
58
+ if node['Node Type'] == "Index Only Scan"
59
+ h['using_index'] = true
60
+ end
61
+
62
+ array << h
63
+ else
64
+ raise "unhandled node: #{node}"
65
+ end
66
+ array
67
+ end
68
+
69
+ def extract_used_key_parts(cond)
70
+ conds = PostgresExplainIndexConditions.new(cond)
71
+ conds.fields
72
+ end
73
+
74
+ def extract_join_key_parts(cond)
75
+ conds = PostgresExplainIndexConditions.new(cond)
76
+ conds.join_fields
77
+ end
78
+
79
+ def recurse_plans(node, array)
80
+ node['Plans'].each do |n|
81
+ transform_node(n, array)
82
+ end
83
+ end
84
+
85
+ def transform
86
+ plan = @json.first['Plan']
87
+ transform_node(plan, [])
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,137 @@
1
+ require 'strscan'
2
+
3
+ module Shiba
4
+ class Explain
5
+ class PostgresExplainIndexConditions
6
+ def initialize(string)
7
+ @string = string
8
+ @sc = StringScanner.new(string)
9
+ @fields = nil
10
+ end
11
+
12
+ attr_reader :sc
13
+ def parse!
14
+ return if @fields
15
+ @fields = {}
16
+ sc.scan(LPAREN)
17
+ if sc.peek(1) == "(" && !sc.match?(/\(\w+\)::/)
18
+
19
+ while sc.peek(1) == "("
20
+ sc.getch
21
+ extract_field(sc)
22
+ sc.scan(/\s+AND\s+/)
23
+ end
24
+ else
25
+ extract_field(sc)
26
+ end
27
+ end
28
+
29
+ def fields
30
+ parse!
31
+ @fields[nil]
32
+ end
33
+
34
+ def join_fields
35
+ parse!
36
+ @fields
37
+ end
38
+
39
+ private
40
+ LPAREN = /\(/
41
+ RPAREN = /\)/
42
+
43
+ def parse_string(sc)
44
+ v = ""
45
+ qchar = sc.getch
46
+ double_quote = qchar * 2
47
+ while true
48
+ if sc.peek(1) == qchar
49
+ if sc.peek(2) == double_quote
50
+ sc.scan(/#{double_quote}/)
51
+ else
52
+ # end of string
53
+ sc.getch
54
+ # optional type hint
55
+ sc.scan(/::\w+(\[\])?/)
56
+ return v
57
+ end
58
+ end
59
+ v += sc.getch
60
+ end
61
+ end
62
+
63
+ def parse_value(sc)
64
+ peek = sc.peek(1)
65
+ if peek == "'"
66
+ parse_string(sc)
67
+ elsif peek == '"'
68
+ parse_field(sc)
69
+ elsif (v = sc.scan(/\d+\.?\d*/))
70
+ if v.include?('.')
71
+ v.to_f
72
+ else
73
+ v.to_i
74
+ end
75
+ elsif sc.scan(/ANY \(/)
76
+ # parse as string
77
+ v = parse_value(sc)
78
+ sc.scan(/\)/)
79
+ v
80
+ else
81
+ parse_field(sc)
82
+ end
83
+ end
84
+
85
+ def parse_ident(sc)
86
+ peek = sc.peek(1)
87
+ if peek == "("
88
+ sc.getch
89
+ # typed column like (name)::text = 'ben'
90
+ ident = sc.scan(/[^\)]+/)
91
+ sc.scan(/\)::\S+/)
92
+ elsif peek == '"'
93
+ ident = parse_string(sc)
94
+ else
95
+ ident = sc.scan(/[^ \.\)\[]+/)
96
+ # field[1] for array fields, not bothering to do brace matching here yet, oy vey
97
+ sc.scan(/\[.*?\]/)
98
+ end
99
+ ident
100
+ end
101
+
102
+ def parse_field(sc)
103
+ first = nil
104
+ second = nil
105
+
106
+ first = parse_ident(sc)
107
+ if sc.scan(/\./)
108
+ second = parse_ident(sc)
109
+ table = first
110
+ field = second
111
+ else
112
+ table = nil
113
+ field = first
114
+ end
115
+
116
+ @fields[table] ||= []
117
+ @fields[table] << field unless @fields[table].include?(field)
118
+ end
119
+
120
+
121
+ def extract_field(sc)
122
+ # (type = 1)
123
+ # ((type)::text = 1)
124
+ # (((type)::text = ANY ('{User,AnonymousUser}'::text[])) AND ((type)::text = 'User'::text))
125
+ table = nil
126
+
127
+ parse_field(sc)
128
+ sc.scan(/\s+\S+\s+/) # operator
129
+ parse_value(sc)
130
+
131
+ if sc.scan(RPAREN).nil?
132
+ raise "bad scan; #{sc.inspect}"
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end