shiba 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +13 -0
- data/.travis/my.cnf +3 -0
- data/Gemfile.lock +14 -1
- data/README.md +93 -30
- data/Rakefile +9 -1
- data/TODO +25 -7
- data/bin/check +0 -0
- data/bin/dump_stats +38 -0
- data/bin/explain +67 -28
- data/bin/shiba +4 -4
- data/lib/shiba.rb +3 -1
- data/lib/shiba/analyzer.rb +6 -5
- data/lib/shiba/backtrace.rb +56 -0
- data/lib/shiba/checker.rb +103 -0
- data/lib/shiba/configure.rb +28 -8
- data/lib/shiba/diff.rb +119 -0
- data/lib/shiba/explain.rb +149 -49
- data/lib/shiba/fuzzer.rb +77 -0
- data/lib/shiba/index.rb +8 -129
- data/lib/shiba/index_stats.rb +210 -0
- data/lib/shiba/output.rb +24 -18
- data/lib/shiba/output/tags.yaml +34 -13
- data/lib/shiba/query_watcher.rb +3 -46
- data/lib/shiba/railtie.rb +31 -8
- data/lib/shiba/table_stats.rb +34 -0
- data/lib/shiba/version.rb +1 -1
- data/shiba.gemspec +1 -0
- data/shiba.yml.example +4 -0
- data/web/main.css +32 -2
- data/web/results.html.erb +132 -58
- metadata +26 -6
- data/bin/analyze +0 -77
- data/bin/inspect +0 -0
- data/bin/parse +0 -0
- data/bin/watch.rb +0 -19
data/lib/shiba/output/tags.yaml
CHANGED
@@ -1,4 +1,12 @@
|
|
1
1
|
---
|
2
|
+
fuzzed_data:
|
3
|
+
title: Fuzzed Data
|
4
|
+
summary: Shiba doesn't know the size of <b>{{query.table}}</b>. For these purposes we set the table size to <b>{{query.table_size}}</b>.
|
5
|
+
description: |
|
6
|
+
We're not sure how much data this table will hold in the future, so we've pretended
|
7
|
+
there's 6000 rows in it. This can lead to a lot of false positives. To
|
8
|
+
improve results, please give shiba your index statistics.
|
9
|
+
level: info
|
2
10
|
possible_key_check:
|
3
11
|
title: Oddly reported possible keys
|
4
12
|
description: |
|
@@ -7,18 +15,21 @@ possible_key_check:
|
|
7
15
|
key possible. Sometimes "possible_keys" will be inaccurate and no keys were possible.
|
8
16
|
level: info
|
9
17
|
access_type_const:
|
10
|
-
title:
|
18
|
+
title: One row
|
19
|
+
summary: The database only needs to read a single row from <b>{{query.table}}</b>.
|
11
20
|
description: |
|
12
21
|
This query selects at *most* one row, which is about as good as things get.
|
13
22
|
level: success
|
14
23
|
access_type_ref:
|
15
|
-
title:
|
24
|
+
title: Indexed
|
25
|
+
summary: The database reads {{ formattedCost }} rows in <b>{{ query.table }}</b> via the <b>{{ query.key }}</b> index ({{ key_parts }}).
|
16
26
|
description: |
|
17
27
|
This query uses an index to find rows that match a single value. Often this
|
18
28
|
has very good performance, but it depends on how many rows match that value.
|
19
29
|
level: success
|
20
30
|
access_type_range:
|
21
|
-
title:
|
31
|
+
title: Indexed
|
32
|
+
summary: The database uses a "range scan" to read more than {{ formattedCost }} rows in {{ query.table }} via the <b>{{ query.key }}</b> index ({{ key_parts }})
|
22
33
|
description: |
|
23
34
|
This query uses an index to find rows that match a range of values, for instance
|
24
35
|
`WHERE indexed_value in (1,2,5,6)` or `WHERE indexed_value >= 5 AND indexed_value <= 15`.
|
@@ -27,6 +38,7 @@ access_type_range:
|
|
27
38
|
level: info
|
28
39
|
access_type_tablescan:
|
29
40
|
title: Table Scan
|
41
|
+
summary: The database reads <b>100%</b> ({{ query.table_size }}) of the rows in <b>{{ query.table }}</b>, skipping any indexes.
|
30
42
|
description: |
|
31
43
|
This query doesn't use any indexes to find data, meaning this query will need to evaluate
|
32
44
|
every single row in the table. This is about the worst of all possible worlds.
|
@@ -35,15 +47,24 @@ access_type_tablescan:
|
|
35
47
|
but be aware that if this table is not effectively tiny or constant-sized you're entering
|
36
48
|
a world of pain.
|
37
49
|
level: danger
|
38
|
-
fuzzed_data:
|
39
|
-
title: Guessed Table Size
|
40
|
-
description: |
|
41
|
-
We're not sure how much data this table will hold in the future, so we've pretended
|
42
|
-
there's 6000 rows in it. This can lead to a lot of false positives. To
|
43
|
-
improve results, please give shiba your index statistics.
|
44
|
-
level: info
|
45
50
|
ignored:
|
46
|
-
title: Ignored
|
47
|
-
|
48
|
-
|
51
|
+
title: Ignored
|
52
|
+
summary: This query matched an "ignore" rule in shiba.yml. Any further analysis was skipped.
|
53
|
+
description:
|
49
54
|
level: info
|
55
|
+
index_walk:
|
56
|
+
title: Index Walk
|
57
|
+
description: |
|
58
|
+
This query is sorted in the same order as the index used, which means that the database
|
59
|
+
can read the index and simply pluck {LIMIT} rows out of the index. It's a very
|
60
|
+
fast way to look up stuff by index.
|
61
|
+
level: success
|
62
|
+
retsize_bad:
|
63
|
+
title: Big Results
|
64
|
+
summary: The database returns {{ query.return_size.toLocaleString() }} rows to the client.
|
65
|
+
level: danger
|
66
|
+
retsize_good:
|
67
|
+
title: Small Results
|
68
|
+
summary: The database returns {{ query.return_size.toLocaleString() }} row(s) to the client.
|
69
|
+
level: success
|
70
|
+
|
data/lib/shiba/query_watcher.rb
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
require 'shiba/query'
|
2
|
+
require 'shiba/backtrace'
|
2
3
|
require 'json'
|
3
4
|
require 'rails'
|
4
5
|
|
5
6
|
module Shiba
|
6
7
|
class QueryWatcher
|
7
|
-
IGNORE = /\.rvm|gem|vendor\/|rbenv|seed|db|shiba|test|spec/
|
8
8
|
|
9
9
|
def self.watch(file)
|
10
|
-
new(file).watch
|
10
|
+
new(file).tap { |w| w.watch }
|
11
11
|
end
|
12
12
|
|
13
13
|
attr_reader :queries
|
@@ -26,7 +26,7 @@ module Shiba
|
|
26
26
|
if sql.start_with?("SELECT")
|
27
27
|
fingerprint = Query.get_fingerprint(sql)
|
28
28
|
if !@queries[fingerprint]
|
29
|
-
if lines =
|
29
|
+
if lines = Backtrace.from_app
|
30
30
|
@file.puts("#{sql} /*shiba#{lines}*/")
|
31
31
|
end
|
32
32
|
end
|
@@ -35,48 +35,5 @@ module Shiba
|
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
38
|
-
protected
|
39
|
-
|
40
|
-
# 8 backtrace lines starting from the app caller, cleaned of app/project cruft.
|
41
|
-
def app_backtrace
|
42
|
-
app_line_idx = caller_locations.index { |line| line.to_s !~ IGNORE }
|
43
|
-
if app_line_idx == nil
|
44
|
-
return
|
45
|
-
end
|
46
|
-
|
47
|
-
caller_locations(app_line_idx+1, 8).map do |loc|
|
48
|
-
line = loc.to_s
|
49
|
-
line.sub!(backtrace_ignore_pattern, '')
|
50
|
-
line
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def backtrace_ignore_pattern
|
55
|
-
@roots ||= begin
|
56
|
-
paths = Gem.path
|
57
|
-
paths << Rails.root.to_s if Rails.root
|
58
|
-
paths << repo_root
|
59
|
-
paths << ENV['HOME']
|
60
|
-
paths.uniq!
|
61
|
-
paths.compact!
|
62
|
-
# match and replace longest path first
|
63
|
-
paths.sort_by!(&:size).reverse!
|
64
|
-
|
65
|
-
Regexp.new(paths.map {|r| Regexp.escape(r) }.join("|"))
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
# /user/git_repo => "/user/git_repo"
|
70
|
-
# /user/not_a_repo => nil
|
71
|
-
def repo_root
|
72
|
-
root = nil
|
73
|
-
Open3.popen3('git rev-parse --show-toplevel') {|_,o,_,_|
|
74
|
-
if root = o.gets
|
75
|
-
root = root.chomp
|
76
|
-
end
|
77
|
-
}
|
78
|
-
root
|
79
|
-
end
|
80
|
-
|
81
38
|
end
|
82
39
|
end
|
data/lib/shiba/railtie.rb
CHANGED
@@ -1,20 +1,43 @@
|
|
1
1
|
require 'shiba/query_watcher'
|
2
2
|
|
3
3
|
class Shiba::Railtie < Rails::Railtie
|
4
|
+
# Logging is enabled when:
|
5
|
+
# 1. SHIBA_OUT environment variable is set to an existing file path.
|
6
|
+
# 2. RSpec/MiniTest exists, in which case a fallback query log is generated at /tmp
|
4
7
|
config.after_initialize do
|
5
8
|
begin
|
6
|
-
path = ENV['SHIBA_OUT']
|
7
|
-
|
9
|
+
path = ENV['SHIBA_OUT'] || "/tmp/shiba-query.log-#{Time.now.to_i}"
|
10
|
+
f = File.open(path, 'a')
|
11
|
+
watcher = Shiba::QueryWatcher.watch(f)
|
12
|
+
next if watcher.nil?
|
8
13
|
|
9
|
-
|
10
|
-
f
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
+
at_exit do
|
15
|
+
f.close
|
16
|
+
puts ""
|
17
|
+
cmd = "shiba explain #{database_args} --file #{path}"
|
18
|
+
if ENV['SHIBA_DEBUG']
|
19
|
+
$stderr.puts("running:")
|
20
|
+
$stderr.puts(cmd)
|
21
|
+
end
|
22
|
+
system(cmd)
|
14
23
|
end
|
24
|
+
|
15
25
|
rescue => e
|
16
26
|
$stderr.puts("Shiba failed to load")
|
17
27
|
$stderr.puts(e.message, e.backtrace.join("\n"))
|
18
28
|
end
|
19
29
|
end
|
20
|
-
|
30
|
+
|
31
|
+
def self.database_args
|
32
|
+
c = ActiveRecord::Base.configurations['test']
|
33
|
+
options = {
|
34
|
+
'host': c['host'],
|
35
|
+
'database': c['database'],
|
36
|
+
'user': c['username'],
|
37
|
+
'password': c['password']
|
38
|
+
}
|
39
|
+
|
40
|
+
options.reject { |k,v| v.nil? }.map { |k,v| "--#{k} #{v}" }.join(" ")
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'shiba/index_stats'
|
2
|
+
require 'shiba/fuzzer'
|
3
|
+
|
4
|
+
module Shiba
|
5
|
+
class TableStats
|
6
|
+
def initialize(dump_stats, connection, manual_stats)
|
7
|
+
@dump_stats = Shiba::IndexStats.new(dump_stats)
|
8
|
+
@db_stats = Shiba::Fuzzer.new(connection).fuzz!
|
9
|
+
@manual_stats = Shiba::IndexStats.new(manual_stats)
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
def estimate_key(table_name, key, parts)
|
14
|
+
ask_each(:estimate_key, table_name, key, parts)
|
15
|
+
end
|
16
|
+
|
17
|
+
def table_count(table)
|
18
|
+
ask_each(:table_count, table)
|
19
|
+
end
|
20
|
+
|
21
|
+
def fuzzed?(table)
|
22
|
+
!@dump_stats.tables[table] && !@manual_stats.tables[table] && @db_stats.tables[table]
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def ask_each(method, *args)
|
27
|
+
[@dump_stats, @db_stats].each do |stat|
|
28
|
+
result = stat.send(method, *args)
|
29
|
+
return result unless result.nil?
|
30
|
+
end
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/shiba/version.rb
CHANGED
data/shiba.gemspec
CHANGED
data/shiba.yml.example
ADDED
data/web/main.css
CHANGED
@@ -12,7 +12,37 @@
|
|
12
12
|
margin: 10px
|
13
13
|
}
|
14
14
|
|
15
|
+
.sql {
|
16
|
+
font-family: monospace;
|
17
|
+
}
|
18
|
+
|
15
19
|
.badge {
|
16
|
-
color:
|
17
|
-
|
20
|
+
color: black;
|
21
|
+
background-color: white;
|
22
|
+
border-style: solid;
|
23
|
+
border-width: 2px;
|
24
|
+
margin-right: 5px;
|
25
|
+
width: 100px;
|
26
|
+
}
|
27
|
+
|
28
|
+
.shiba-badge-info {
|
29
|
+
border-color: #78b0ec;
|
30
|
+
}
|
31
|
+
|
32
|
+
.shiba-badge-danger {
|
33
|
+
border-color: #ff655d;
|
34
|
+
}
|
35
|
+
|
36
|
+
.shiba-badge-success {
|
37
|
+
border-color: green;
|
38
|
+
}
|
39
|
+
|
40
|
+
.shiba-badge-warning {
|
41
|
+
border-color: #ffb100;
|
42
|
+
}
|
43
|
+
|
44
|
+
.shiba-info-list {
|
45
|
+
list-style-type:none;
|
46
|
+
margin: 5px;
|
47
|
+
padding: 0;
|
18
48
|
}
|
data/web/results.html.erb
CHANGED
@@ -1,44 +1,84 @@
|
|
1
1
|
<html>
|
2
2
|
<head>
|
3
3
|
<title>Shiba results for <%= Time.now %></title>
|
4
|
-
<% data[:js].each do |js| %>
|
5
|
-
<script type="text/javascript" src="./js/<%= js %>"></script>
|
6
|
-
<% end %>
|
7
|
-
<link rel="stylesheet" href="bootstrap.min.css">
|
8
|
-
<link rel="stylesheet" href="main.css">
|
9
4
|
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons" rel="stylesheet">
|
10
5
|
</head>
|
11
6
|
<body>
|
7
|
+
<% data[:js].each do |js| %>
|
8
|
+
<script type="text/javascript">
|
9
|
+
<%= File.read(js) %>
|
10
|
+
</script>
|
11
|
+
<% end %>
|
12
|
+
|
13
|
+
<% data[:css].each do |css| %>
|
14
|
+
<style type="text/css">
|
15
|
+
<%= File.read(css) %>
|
16
|
+
</style>
|
17
|
+
<% end %>
|
18
|
+
|
12
19
|
<script language="javascript">
|
13
20
|
var data = <%= data.to_json %>;
|
14
21
|
var queriesByTable = [];
|
15
22
|
var queriesByTableLow = [];
|
23
|
+
var queriesHaveFuzzed = false;
|
24
|
+
var severityIndexes = { high: 1, medium: 2, low: 3 };
|
16
25
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
26
|
+
function sortByFunc(fields) {
|
27
|
+
return function(a, b) {
|
28
|
+
for ( var i = 0 ; i < fields.length; i++ ) {
|
29
|
+
if ( a[fields[i]] < b[fields[i]] )
|
30
|
+
return -1;
|
31
|
+
else if ( a[fields[i]] > b[fields[i]] )
|
32
|
+
return 1;
|
33
|
+
}
|
34
|
+
return 0;
|
25
35
|
}
|
26
36
|
}
|
27
37
|
|
38
|
+
function Query(obj) {
|
39
|
+
Object.assign(this, obj);
|
40
|
+
this.severityIndex = severityIndexes[this.severity];
|
41
|
+
this.splitSQL();
|
42
|
+
};
|
43
|
+
|
44
|
+
Query.prototype = {
|
45
|
+
hasTag: function(tag) {
|
46
|
+
this.tags.includes(tag);
|
47
|
+
},
|
48
|
+
splitSQL: function() {
|
49
|
+
this.sqlFragments = this.sql.match(/(SELECT\s)(.*?)(\s+FROM .*)/i);
|
50
|
+
},
|
51
|
+
select: function () {
|
52
|
+
return this.sqlFragments[1];
|
53
|
+
},
|
54
|
+
fields: function () {
|
55
|
+
return this.sqlFragments[2];
|
56
|
+
},
|
57
|
+
rest: function(index) {
|
58
|
+
return this.sqlFragments.slice(index).join('');
|
59
|
+
}
|
60
|
+
};
|
61
|
+
|
28
62
|
data.queries.forEach(function(query) {
|
29
|
-
|
30
|
-
|
63
|
+
var q = new Query(query);
|
64
|
+
|
65
|
+
if ( q.cost < 100 ) {
|
66
|
+
queriesByTableLow.push(q);
|
31
67
|
} else {
|
32
|
-
queriesByTable.push(
|
68
|
+
queriesByTable.push(q);
|
33
69
|
}
|
34
70
|
|
35
|
-
if (
|
36
|
-
|
71
|
+
if ( q.hasTag("fuzzed_data" ) )
|
72
|
+
queriesHaveFuzzed = true;
|
73
|
+
|
74
|
+
q.expandSelect = false;
|
37
75
|
});
|
38
76
|
|
39
|
-
|
40
|
-
|
77
|
+
var f = sortByFunc(['severityIndex', 'table']);
|
78
|
+
queriesByTable = queriesByTable.sort(f);
|
79
|
+
queriesByTableLow = queriesByTableLow.sort(f);
|
41
80
|
</script>
|
81
|
+
|
42
82
|
<script type="text/x-template" id="query-template">
|
43
83
|
<div class="query">
|
44
84
|
<div class="row">
|
@@ -50,38 +90,94 @@
|
|
50
90
|
</div>
|
51
91
|
<div class="col-5">{{ truncate(query.sql, 50) }}</div>
|
52
92
|
<div class="col-3" v-html="makeURL(query.backtrace[0], shortLocation(query))"></div>
|
53
|
-
<div class="col-1">{{ severity
|
93
|
+
<div class="col-1">{{ query.severity }}</div>
|
54
94
|
</div>
|
55
95
|
<div class="row" v-if="expanded">
|
56
96
|
<div class="col-12">
|
57
97
|
<div class="query-info-box">
|
58
|
-
<
|
98
|
+
<query-sql v-bind:query="query"></query-sql>
|
59
99
|
<div v-if="query.backtrace && query.backtrace.length > 0">
|
60
100
|
Stack Trace:<br>
|
61
101
|
<div class="backtrace">
|
62
102
|
<div v-for="backtrace in query.backtrace" v-html="makeURL(backtrace, backtrace)"></div>
|
63
103
|
</div>
|
64
104
|
</div>
|
65
|
-
|
66
|
-
<
|
67
|
-
|
105
|
+
<ul class="shiba-info-list">
|
106
|
+
<li v-for="tag in query.tags">
|
107
|
+
<component v-bind:is="'tag-' + tag" v-bind:query="query"></component>
|
108
|
+
</li>
|
109
|
+
</ul>
|
110
|
+
<div v-if="!rawExpanded">
|
111
|
+
<a href="#" v-on:click.prevent="rawExpanded = !rawExpanded">See full EXPLAIN</a>
|
112
|
+
</div>
|
113
|
+
<div v-else>
|
114
|
+
<a href="#" v-on:click.prevent="rawExpanded = !rawExpanded">hide EXPLAIN</a>
|
115
|
+
<pre class="backtrace">{{ JSON.stringify(query.raw_explain, null, 2) }}</pre>
|
68
116
|
</div>
|
69
|
-
Cost: {{ query.cost }}<br>
|
70
|
-
<a class="badge"
|
71
|
-
v-for="tag in query.tags"
|
72
|
-
href="#"
|
73
|
-
v-bind:class="tagClass(tag)"
|
74
|
-
v-on:click="expandInfo(tag, $event)">{{ tagTitle(tag) }}</a>
|
75
117
|
</div>
|
76
118
|
</div>
|
77
119
|
</div>
|
78
120
|
</div>
|
79
121
|
</script>
|
80
122
|
|
123
|
+
<% data[:tags].each do |tag, h| %>
|
124
|
+
<script type="text/x-template" id="tag-<%= tag %>-template">
|
125
|
+
<span><a class="badge shiba-badge-<%= h['level'] %>"><%= h['title'] %></a><%= h['summary'] %></span>
|
126
|
+
</script>
|
127
|
+
<script>
|
128
|
+
Vue.component('tag-<%= tag %>', {
|
129
|
+
template: '#tag-<%= tag %>-template',
|
130
|
+
props: ['query'],
|
131
|
+
computed: {
|
132
|
+
key_parts: function() {
|
133
|
+
if ( this.query.used_key_parts && this.query.used_key_parts.length > 0 )
|
134
|
+
return this.query.used_key_parts.join(',');
|
135
|
+
else
|
136
|
+
return "";
|
137
|
+
},
|
138
|
+
formattedCost: function() {
|
139
|
+
var costPercentage = (this.query.cost / this.query.table_size) * 100.0;
|
140
|
+
if ( this.query.cost > 100 && costPercentage > 1 ) // todo: make better
|
141
|
+
return `${costPercentage.toFixed()}% (${this.query.cost.toLocaleString()}) of the`;
|
142
|
+
else
|
143
|
+
return this.query.cost.toLocaleString();
|
144
|
+
}
|
145
|
+
}
|
146
|
+
});
|
147
|
+
</script>
|
148
|
+
<% end %>
|
149
|
+
|
150
|
+
|
151
|
+
<script type="text/x-template" id="sql-template">
|
152
|
+
<div class="sql">
|
153
|
+
<span>{{ query.select() }}</span>
|
154
|
+
<span v-if="!expandFields && query.fields().length > 80">
|
155
|
+
<a href="#" v-on:click.prevent="expandFields = !expandFields">...</a>
|
156
|
+
</span>
|
157
|
+
<span v-else>{{ query.fields() }}</span>
|
158
|
+
<span>{{ query.rest(3) }}</span>
|
159
|
+
</div>
|
160
|
+
</script>
|
161
|
+
|
162
|
+
<script>
|
163
|
+
Vue.component('query-sql', {
|
164
|
+
template: '#sql-template',
|
165
|
+
props: ['query'],
|
166
|
+
data: function () {
|
167
|
+
return { expandFields: false }
|
168
|
+
}
|
169
|
+
});
|
170
|
+
</script>
|
171
|
+
|
81
172
|
<div id="app">
|
82
173
|
<v-dialog :width="600"></v-dialog>
|
83
174
|
<div class="container" style="">
|
84
175
|
<div class="row">
|
176
|
+
<div v-if="hasFuzzed" class="alert alert-warning" role="alert">
|
177
|
+
This query analysis was generated using estimated table sizes.
|
178
|
+
<a href="https://github.com/burrito-brothers/shiba/blob/master/README.md#going-beyond-table-scans">Find out how to get a more accurate analysis by feeding Shiba index stats</a>
|
179
|
+
</div>
|
180
|
+
|
85
181
|
<div class="col-12">We found {{ queries.length }} queries that deserve your attention: </div>
|
86
182
|
</div>
|
87
183
|
<div class="row">
|
@@ -110,7 +206,8 @@
|
|
110
206
|
props: ['query', 'tags', 'github'],
|
111
207
|
data: function () {
|
112
208
|
return {
|
113
|
-
expanded: false
|
209
|
+
expanded: false,
|
210
|
+
rawExpanded: false
|
114
211
|
};
|
115
212
|
},
|
116
213
|
methods: {
|
@@ -137,18 +234,6 @@
|
|
137
234
|
if (event) event.preventDefault()
|
138
235
|
this.expanded = !this.expanded;
|
139
236
|
},
|
140
|
-
severity: function(query) {
|
141
|
-
if ( query.cost > 1 )
|
142
|
-
return "high";
|
143
|
-
else
|
144
|
-
return "low";
|
145
|
-
},
|
146
|
-
castNull: function (string) {
|
147
|
-
if ( !string )
|
148
|
-
return "(none)";
|
149
|
-
else
|
150
|
-
return string;
|
151
|
-
},
|
152
237
|
shortLocation: function(query) {
|
153
238
|
if ( !query.backtrace )
|
154
239
|
return null;
|
@@ -164,24 +249,12 @@
|
|
164
249
|
var line = matches[2];
|
165
250
|
|
166
251
|
return `<a href='${data.url}/${file}#L${line}' target='_new'>${content}</a>`;
|
167
|
-
},
|
168
|
-
tagTitle: function(tag) {
|
169
|
-
if ( !this.tags[tag] )
|
170
|
-
return tag;
|
171
|
-
else
|
172
|
-
return this.tags[tag].title;
|
173
|
-
},
|
174
|
-
tagClass: function(tag) {
|
175
|
-
if ( !this.tags[tag] )
|
176
|
-
return '';
|
177
|
-
else
|
178
|
-
return "badge-" + this.tags[tag].level;
|
179
252
|
}
|
180
253
|
},
|
181
254
|
computed: {
|
182
255
|
expandText: function() {
|
183
256
|
return this.expanded ? "-" : "+";
|
184
|
-
}
|
257
|
+
}
|
185
258
|
}
|
186
259
|
});
|
187
260
|
|
@@ -191,7 +264,8 @@
|
|
191
264
|
queries: queriesByTable,
|
192
265
|
queriesLow: queriesByTableLow,
|
193
266
|
tags: data.tags,
|
194
|
-
lowExpanded: false
|
267
|
+
lowExpanded: false,
|
268
|
+
hasFuzzed: queriesHaveFuzzed
|
195
269
|
},
|
196
270
|
methods: { }
|
197
271
|
});
|