shiba 0.6.0 → 0.6.1
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/Gemfile.lock +1 -1
- data/LICENSE +13 -0
- data/Rakefile +13 -5
- data/bin/explain +22 -17
- data/lib/shiba/analyzer.rb +17 -11
- data/lib/shiba/configure.rb +4 -0
- data/lib/shiba/explain/postgres_explain.rb +3 -3
- data/lib/shiba/output.rb +14 -9
- data/lib/shiba/{explain → parsers}/postgres_explain_index_conditions.rb +7 -20
- data/lib/shiba/version.rb +1 -1
- data/web/babel.config.js +5 -0
- data/web/package-lock.json +9299 -2235
- data/web/package.json +44 -14
- data/web/src/App.vue +185 -0
- data/web/src/assets/logo.png +0 -0
- data/web/src/components/Backtrace.vue +36 -0
- data/web/src/components/HelloWorld.vue +58 -0
- data/web/src/components/Message.js +104 -0
- data/web/src/components/Message.vue +106 -0
- data/web/src/components/Query.vue +112 -0
- data/web/src/components/Sql.vue +22 -0
- data/web/src/main.js +8 -0
- data/web/src/query_data.js +47 -0
- data/web/vue.config.js +11 -0
- metadata +17 -12
- data/web/bootstrap.min.css +0 -7
- data/web/dist/bundle.js +0 -189
- data/web/main.css +0 -70
- data/web/main.js +0 -6
- data/web/results.html.erb +0 -412
- data/web/vue.js +0 -11055
- data/web/webpack.config.js +0 -14
data/web/package.json
CHANGED
@@ -1,20 +1,50 @@
|
|
1
1
|
{
|
2
|
-
"name": "
|
3
|
-
"version": "1.0
|
4
|
-
"
|
2
|
+
"name": "hello-world",
|
3
|
+
"version": "0.1.0",
|
4
|
+
"private": true,
|
5
5
|
"scripts": {
|
6
|
-
"
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
"license": "ISC",
|
11
|
-
"devDependencies": {
|
12
|
-
"webpack": "^4.29.0",
|
13
|
-
"webpack-cli": "^3.2.1"
|
6
|
+
"serve": "vue-cli-service serve",
|
7
|
+
"build": "vue-cli-service build",
|
8
|
+
"lint": "vue-cli-service lint",
|
9
|
+
"autobuild": "vue-cli-service build --watch"
|
14
10
|
},
|
15
11
|
"dependencies": {
|
16
|
-
"
|
17
|
-
"
|
12
|
+
"bootstrap": "^4.3.1",
|
13
|
+
"nanoajax": "^0.4.3",
|
14
|
+
"vue": "^2.6.6",
|
18
15
|
"vue-js-modal": "^1.3.28"
|
19
|
-
}
|
16
|
+
},
|
17
|
+
"devDependencies": {
|
18
|
+
"@vue/cli-plugin-babel": "^3.5.0",
|
19
|
+
"@vue/cli-plugin-eslint": "^3.5.0",
|
20
|
+
"@vue/cli-service": "^3.5.0",
|
21
|
+
"babel-eslint": "^10.0.1",
|
22
|
+
"eslint": "^5.8.0",
|
23
|
+
"eslint-plugin-vue": "^5.0.0",
|
24
|
+
"vue-template-compiler": "^2.5.21"
|
25
|
+
},
|
26
|
+
"eslintConfig": {
|
27
|
+
"root": true,
|
28
|
+
"env": {
|
29
|
+
"node": true
|
30
|
+
},
|
31
|
+
"extends": [
|
32
|
+
"plugin:vue/essential",
|
33
|
+
"eslint:recommended"
|
34
|
+
],
|
35
|
+
"rules": {},
|
36
|
+
"parserOptions": {
|
37
|
+
"parser": "babel-eslint"
|
38
|
+
}
|
39
|
+
},
|
40
|
+
"postcss": {
|
41
|
+
"plugins": {
|
42
|
+
"autoprefixer": {}
|
43
|
+
}
|
44
|
+
},
|
45
|
+
"browserslist": [
|
46
|
+
"> 1%",
|
47
|
+
"last 2 versions",
|
48
|
+
"not ie <= 8"
|
49
|
+
]
|
20
50
|
}
|
data/web/src/App.vue
ADDED
@@ -0,0 +1,185 @@
|
|
1
|
+
<template>
|
2
|
+
<div id="app" v-cloak>
|
3
|
+
<v-dialog :width="600"></v-dialog>
|
4
|
+
<div class="container" style="">
|
5
|
+
<div class="row" v-if="hasFuzzed">
|
6
|
+
<div class="alert alert-warning" role="alert">
|
7
|
+
This query analysis was generated using estimated table sizes.
|
8
|
+
To improve these results and find other problem queries beyond missing indexes, we'll need more stats.<br/>
|
9
|
+
<a target="_blank" 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>
|
10
|
+
</div>
|
11
|
+
</div>
|
12
|
+
<div class="row">
|
13
|
+
<div class="col-10"></div>
|
14
|
+
<div class="col-2"><input :value="search" @input="updateSearch" placeholder="search..."></div>
|
15
|
+
</div>
|
16
|
+
<div class="row">
|
17
|
+
<div class="col-12">We found {{ queries.length }} queries that
|
18
|
+
<span v-if="search == ''">deserve your attention:</span>
|
19
|
+
<span v-else>match your search term</span>
|
20
|
+
</div>
|
21
|
+
</div>
|
22
|
+
<div class="row">
|
23
|
+
<div class="col-3">Table</div>
|
24
|
+
<div class="col-5">Query</div>
|
25
|
+
<div class="col-3">Source</div>
|
26
|
+
<div class="col-1">Severity</div>
|
27
|
+
</div>
|
28
|
+
<div class="queries">
|
29
|
+
<Query v-for="query in queries" v-bind:query="query" v-bind:key="query.sql" v-bind:tags="tags" v-bind:url="url"></query>
|
30
|
+
</div>
|
31
|
+
<div v-if="search == ''">
|
32
|
+
<div class="row">
|
33
|
+
<div class="col-12">We also found <a href="#" v-on:click.prevent="lowExpanded = !lowExpanded">{{ queriesLow.length }} queries</a> that look fine.</div>
|
34
|
+
</div>
|
35
|
+
<a name="lowExapnded"></a>
|
36
|
+
<div class="queries" v-if="lowExpanded">
|
37
|
+
<query v-for="query in queriesLow" v-bind:query="query" v-bind:key="query.sql" v-bind:tags="tags" v-bind:url="url"></query>
|
38
|
+
</div>
|
39
|
+
</div>
|
40
|
+
<div style="height:50px"></div>
|
41
|
+
</div>
|
42
|
+
</div>
|
43
|
+
</template>
|
44
|
+
|
45
|
+
<script>
|
46
|
+
import Query from './components/Query.vue'
|
47
|
+
import registerMessage from './components/Message.js'
|
48
|
+
import QueryData from './query_data.js'
|
49
|
+
import VModal from 'vue-js-modal';
|
50
|
+
import _ from 'lodash'
|
51
|
+
import Vue from 'vue';
|
52
|
+
import nanoajax from 'nanoajax';
|
53
|
+
import 'bootstrap/dist/css/bootstrap.min.css'
|
54
|
+
|
55
|
+
Vue.use(VModal, { dialog: true });
|
56
|
+
|
57
|
+
function categorizeQueries(v, queries) {
|
58
|
+
queries.forEach(function(query) {
|
59
|
+
var q = new QueryData(query);
|
60
|
+
|
61
|
+
if ( q.severity == "none" ) {
|
62
|
+
v.lowQ.push(q);
|
63
|
+
} else {
|
64
|
+
v.highQ.push(q);
|
65
|
+
}
|
66
|
+
|
67
|
+
if ( q.hasTag("fuzzed_data") )
|
68
|
+
this.hasFuzzed = true;
|
69
|
+
|
70
|
+
var rCost = 0;
|
71
|
+
q.messages.forEach(function(m) {
|
72
|
+
if ( m.cost && m.cost != 0) {
|
73
|
+
rCost += m.cost;
|
74
|
+
m.running_cost = rCost;
|
75
|
+
} else {
|
76
|
+
m.running_cost = undefined;
|
77
|
+
}
|
78
|
+
});
|
79
|
+
});
|
80
|
+
|
81
|
+
var f = QueryData.sortByFunc(['severityIndex', 'table']);
|
82
|
+
v.highQ = v.highQ.sort(f);
|
83
|
+
v.lowQ = v.lowQ.sort(f);
|
84
|
+
}
|
85
|
+
|
86
|
+
|
87
|
+
export default {
|
88
|
+
name: 'app',
|
89
|
+
data: () => ({
|
90
|
+
highQ: [],
|
91
|
+
lowQ: [],
|
92
|
+
tags: {},
|
93
|
+
lowExpanded: false,
|
94
|
+
hasFuzzed: false,
|
95
|
+
search: '',
|
96
|
+
url: null
|
97
|
+
}),
|
98
|
+
mounted () {
|
99
|
+
if ( typeof(shibaData) === "undefined" ) {
|
100
|
+
nanoajax.ajax({url:'/example_data.json'}, function (code, responseText) {
|
101
|
+
if ( code == 200 ) {
|
102
|
+
var data = JSON.parse(responseText);
|
103
|
+
this.setupData(data);
|
104
|
+
}
|
105
|
+
}.bind(this));
|
106
|
+
} else {
|
107
|
+
// eslint-disable-next-line
|
108
|
+
this.setupData(shibaData);
|
109
|
+
}
|
110
|
+
},
|
111
|
+
methods: {
|
112
|
+
setupData: function(data) {
|
113
|
+
this.url = data.url;
|
114
|
+
this.tags = data.tags;
|
115
|
+
|
116
|
+
Object.keys(this.tags).forEach((k) => {
|
117
|
+
registerMessage(k, this.tags[k].title, this.tags[k].summary);
|
118
|
+
})
|
119
|
+
categorizeQueries(this, data.queries);
|
120
|
+
},
|
121
|
+
updateSearch: _.debounce(function (e) {
|
122
|
+
this.search = e.target.value;
|
123
|
+
}, 500)
|
124
|
+
},
|
125
|
+
computed: {
|
126
|
+
queries: function() {
|
127
|
+
if ( this.search != '' ) {
|
128
|
+
var filtered = [];
|
129
|
+
var lcSearch = this.search.toLowerCase();
|
130
|
+
this.highQ.concat(this.lowQ).forEach(function(q) {
|
131
|
+
if ( q.searchString.includes(lcSearch) )
|
132
|
+
filtered.push(q);
|
133
|
+
});
|
134
|
+
return filtered;
|
135
|
+
} else
|
136
|
+
return this.highQ;
|
137
|
+
},
|
138
|
+
queriesLow: function() {
|
139
|
+
return this.lowQ;
|
140
|
+
}
|
141
|
+
},
|
142
|
+
components: {
|
143
|
+
Query
|
144
|
+
}
|
145
|
+
}
|
146
|
+
</script>
|
147
|
+
|
148
|
+
<style>
|
149
|
+
.sql {
|
150
|
+
font-family: monospace;
|
151
|
+
}
|
152
|
+
|
153
|
+
.badge {
|
154
|
+
color: black;
|
155
|
+
background-color: white;
|
156
|
+
border-style: solid;
|
157
|
+
border-width: 1.5px;
|
158
|
+
margin-right: 5px;
|
159
|
+
width: 100px;
|
160
|
+
}
|
161
|
+
|
162
|
+
.shiba-badge-td {
|
163
|
+
width: 100px;
|
164
|
+
}
|
165
|
+
|
166
|
+
.shiba-messages {
|
167
|
+
margin: 0px;
|
168
|
+
margin-top: 10px;
|
169
|
+
width: 100%;
|
170
|
+
}
|
171
|
+
|
172
|
+
.shiba-messages td {
|
173
|
+
}
|
174
|
+
|
175
|
+
.shiba-message {
|
176
|
+
padding-right: 10px;
|
177
|
+
width: 90%;
|
178
|
+
}
|
179
|
+
|
180
|
+
.running-totals {
|
181
|
+
align: right;
|
182
|
+
font-family: monospace;
|
183
|
+
}
|
184
|
+
[v-cloak] { display: none }
|
185
|
+
</style>
|
Binary file
|
@@ -0,0 +1,36 @@
|
|
1
|
+
<template>
|
2
|
+
<div>
|
3
|
+
Stack Trace:<br>
|
4
|
+
<div class="backtrace">
|
5
|
+
<div v-for="bt in backtrace" v-bind:key="bt" v-html="makeURL(bt, bt)"></div>
|
6
|
+
</div>
|
7
|
+
</div>
|
8
|
+
</template>
|
9
|
+
|
10
|
+
<script>
|
11
|
+
export default {
|
12
|
+
name: 'Backtrace',
|
13
|
+
props: ['url', 'backtrace'],
|
14
|
+
methods: {
|
15
|
+
makeURL: function(line, content) {
|
16
|
+
if ( !this.url || !line )
|
17
|
+
return content;
|
18
|
+
|
19
|
+
var matches = line.match(/(.+):(\d+):/);
|
20
|
+
var f = matches[1].replace(/^\/+/, '');
|
21
|
+
var l = matches[2];
|
22
|
+
|
23
|
+
return `<a href='${this.url}/${f}#L${l}' target='_new'>${content}</a>`;
|
24
|
+
}
|
25
|
+
}
|
26
|
+
}
|
27
|
+
</script>
|
28
|
+
<style>
|
29
|
+
.backtrace {
|
30
|
+
font-family: monospace;
|
31
|
+
background-color: #EEEEEE;
|
32
|
+
padding: 5px;
|
33
|
+
margin: 10px
|
34
|
+
}
|
35
|
+
</style>
|
36
|
+
|
@@ -0,0 +1,58 @@
|
|
1
|
+
<template>
|
2
|
+
<div class="hello">
|
3
|
+
<h1>{{ msg }}</h1>
|
4
|
+
<p>
|
5
|
+
For a guide and recipes on how to configure / customize this project,<br>
|
6
|
+
check out the
|
7
|
+
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
|
8
|
+
</p>
|
9
|
+
<h3>Installed CLI Plugins</h3>
|
10
|
+
<ul>
|
11
|
+
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
|
12
|
+
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
|
13
|
+
</ul>
|
14
|
+
<h3>Essential Links</h3>
|
15
|
+
<ul>
|
16
|
+
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
|
17
|
+
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
|
18
|
+
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
|
19
|
+
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
|
20
|
+
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
|
21
|
+
</ul>
|
22
|
+
<h3>Ecosystem</h3>
|
23
|
+
<ul>
|
24
|
+
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
|
25
|
+
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
|
26
|
+
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
|
27
|
+
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
|
28
|
+
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
|
29
|
+
</ul>
|
30
|
+
</div>
|
31
|
+
</template>
|
32
|
+
|
33
|
+
<script>
|
34
|
+
export default {
|
35
|
+
name: 'HelloWorld',
|
36
|
+
props: {
|
37
|
+
msg: String
|
38
|
+
}
|
39
|
+
}
|
40
|
+
</script>
|
41
|
+
|
42
|
+
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
43
|
+
<style scoped>
|
44
|
+
h3 {
|
45
|
+
margin: 40px 0 0;
|
46
|
+
}
|
47
|
+
ul {
|
48
|
+
list-style-type: none;
|
49
|
+
padding: 0;
|
50
|
+
}
|
51
|
+
li {
|
52
|
+
display: inline-block;
|
53
|
+
margin: 0 10px;
|
54
|
+
}
|
55
|
+
a {
|
56
|
+
color: #42b983;
|
57
|
+
}
|
58
|
+
</style>
|
@@ -0,0 +1,104 @@
|
|
1
|
+
var template = `
|
2
|
+
<tr>
|
3
|
+
<td class="shiba-badge-td">
|
4
|
+
<a class="badge" v-bind:style="costToColor">::TITLE::</a>
|
5
|
+
</td>
|
6
|
+
<td class="shiba-message">
|
7
|
+
::SUMMARY::
|
8
|
+
</td>
|
9
|
+
<td class="running-totals">
|
10
|
+
{{ formattedRunningCost }}
|
11
|
+
</td>
|
12
|
+
</tr>
|
13
|
+
`
|
14
|
+
|
15
|
+
var greenToRedGradient = [
|
16
|
+
'#57bb8a','#63b682', '#73b87e', '#84bb7b', '#94bd77', '#a4c073', '#b0be6e',
|
17
|
+
'#c4c56d', '#d4c86a', '#e2c965', '#f5ce62', '#f3c563', '#e9b861', '#e6ad61',
|
18
|
+
'#ecac67', '#e9a268', '#e79a69', '#e5926b', '#e2886c', '#e0816d', '#dd776e'
|
19
|
+
];
|
20
|
+
|
21
|
+
var templateComputedFunctions = {
|
22
|
+
key_parts: function() {
|
23
|
+
if ( this.index_used && this.index_used.length > 0 )
|
24
|
+
return this.index_used.join(',');
|
25
|
+
else
|
26
|
+
return "";
|
27
|
+
},
|
28
|
+
fuzz_table_sizes: function() {
|
29
|
+
var h = {};
|
30
|
+
var tables = this.tables;
|
31
|
+
|
32
|
+
Object.keys(tables).forEach(function(k) {
|
33
|
+
var size = tables[k];
|
34
|
+
if ( !h[size] )
|
35
|
+
h[size] = [];
|
36
|
+
|
37
|
+
h[size].push(k);
|
38
|
+
});
|
39
|
+
|
40
|
+
var sizesDesc = Object.keys(h).sort(function(a, b) { return b - a });
|
41
|
+
var str = "";
|
42
|
+
|
43
|
+
sizesDesc.forEach(function(size) {
|
44
|
+
str = str + h[size].join(", ") + ": " + size.toLocaleString() + " rows. ";
|
45
|
+
});
|
46
|
+
|
47
|
+
return str;
|
48
|
+
},
|
49
|
+
formatted_cost: function() {
|
50
|
+
var readPercentage = (this.rows_read / this.table_size) * 100.0;
|
51
|
+
if ( this.rows_read > 100 && readPercentage > 1 ) // todo: make better
|
52
|
+
return `${readPercentage.toFixed()}% (${this.rows_read.toLocaleString()}) of the`;
|
53
|
+
else
|
54
|
+
return this.rows_read.toLocaleString();
|
55
|
+
},
|
56
|
+
costToColor: function() {
|
57
|
+
var costScale = this.cost ? this.cost / 0.5 : 0;
|
58
|
+
|
59
|
+
if ( costScale > 1 )
|
60
|
+
costScale = 1;
|
61
|
+
|
62
|
+
var pos = (costScale * (greenToRedGradient.length - 1)).toFixed();
|
63
|
+
|
64
|
+
return "border-color: " + greenToRedGradient[pos];
|
65
|
+
},
|
66
|
+
formattedRunningCost: function() {
|
67
|
+
if ( this.running_cost === undefined )
|
68
|
+
return "-";
|
69
|
+
else if ( this.running_cost < 1.0 )
|
70
|
+
return (this.running_cost * 100).toFixed() + "ms";
|
71
|
+
else
|
72
|
+
return this.running_cost.toFixed(1) + "s";
|
73
|
+
},
|
74
|
+
formatted_result: function() {
|
75
|
+
var rb = this.result_bytes;
|
76
|
+
var result;
|
77
|
+
if ( rb == 0 )
|
78
|
+
return "" + this.result_size + " rows";
|
79
|
+
else if ( rb < 1000 )
|
80
|
+
result = rb + " bytes ";
|
81
|
+
else if ( rb < 1000000 )
|
82
|
+
result = (rb / 1000).toFixed() + "kb ";
|
83
|
+
else
|
84
|
+
result = (rb / 1000000 ).toFixed(1) + "mb ";
|
85
|
+
|
86
|
+
return result + " (" + this.result_size.toLocaleString() + " rows)";
|
87
|
+
}
|
88
|
+
}
|
89
|
+
|
90
|
+
import Vue from 'vue'
|
91
|
+
|
92
|
+
export default function (tag, title, summary) {
|
93
|
+
var tmpl = template.replace("::TITLE::", title).replace("::SUMMARY::", summary);
|
94
|
+
|
95
|
+
Vue.component(`tag-${tag}`, {
|
96
|
+
template: tmpl,
|
97
|
+
props: [ 'table_size', 'result_size', 'table', 'cost', 'index', 'join_to', 'index_used', 'running_cost', 'tables', 'rows_read', 'result_bytes' ],
|
98
|
+
computed: templateComputedFunctions,
|
99
|
+
data: function () {
|
100
|
+
return { lastRunnningCost: undefined };
|
101
|
+
}
|
102
|
+
});
|
103
|
+
}
|
104
|
+
|