shiba 0.1.2 → 0.2.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.
- 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
|
});
|