shiba 0.2.3 → 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/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