query_police 0.1.4.beta → 0.1.6.beta
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -1
- data/README.md +408 -242
- data/examples/rules/json/absent_rule.json +13 -0
- data/examples/rules/json/basic_rule.json +22 -0
- data/examples/rules/json/complex_detailed_rule.json +18 -0
- data/examples/rules/json/threshold_rule.json +15 -0
- data/examples/rules/yaml/absent_rule.yml +11 -0
- data/examples/rules/yaml/basic_rule.yml +18 -0
- data/examples/rules/yaml/complex_detailed_rule.yml +13 -0
- data/examples/rules/yaml/threshold_rule.yml +12 -0
- data/lib/query_police/analyse.rb +16 -16
- data/lib/query_police/analysis/dynamic_message.rb +3 -3
- data/lib/query_police/analysis.rb +69 -39
- data/lib/query_police/config.rb +11 -6
- data/lib/query_police/constants.rb +6 -3
- data/lib/query_police/explain.rb +15 -2
- data/lib/query_police/helper.rb +13 -3
- data/lib/query_police/rules.json +66 -57
- data/lib/query_police/version.rb +1 -1
- data/lib/query_police.rb +58 -25
- metadata +12 -4
@@ -0,0 +1,13 @@
|
|
1
|
+
{
|
2
|
+
"key": {
|
3
|
+
"description": "index key used for the table",
|
4
|
+
"value_type": "string",
|
5
|
+
"rules": {
|
6
|
+
"absent": {
|
7
|
+
"impact": "negative",
|
8
|
+
"message": "There is no index key used for $table table, and can result into full scan of the $table table",
|
9
|
+
"suggestion": "Please use index from possible_keys: $possible_keys or add new one to $table table as per the requirements."
|
10
|
+
}
|
11
|
+
}
|
12
|
+
}
|
13
|
+
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
{
|
2
|
+
"type": {
|
3
|
+
"description": "Join used in the query for a specific table.",
|
4
|
+
"value_type": "string",
|
5
|
+
"rules": {
|
6
|
+
"system": {
|
7
|
+
"impact": "positive",
|
8
|
+
"message": "Table has zero or one row, no change required.",
|
9
|
+
"suggestion": ""
|
10
|
+
},
|
11
|
+
"ALL": {
|
12
|
+
"impact": "negative",
|
13
|
+
"message": "Entire $table table is scanned to find matching rows, you have $amount_possible_keys possible keys to use.",
|
14
|
+
"suggestion": "Use index here. You can use index from possible key: $possible_keys or add new one to $table table as per the requirements.",
|
15
|
+
"debt": {
|
16
|
+
"value": 200,
|
17
|
+
"type": "base"
|
18
|
+
}
|
19
|
+
}
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
@@ -0,0 +1,18 @@
|
|
1
|
+
{
|
2
|
+
"detailed#used_columns": {
|
3
|
+
"description": "",
|
4
|
+
"value_type": "array",
|
5
|
+
"rules": {
|
6
|
+
"threshold": {
|
7
|
+
"amount": 7,
|
8
|
+
"impact": "negative",
|
9
|
+
"message": "You have selected $amount columns, You should not select too many columns.",
|
10
|
+
"suggestion": "Please only select required columns.",
|
11
|
+
"debt": {
|
12
|
+
"value": 10,
|
13
|
+
"type": "threshold_relative"
|
14
|
+
}
|
15
|
+
}
|
16
|
+
}
|
17
|
+
}
|
18
|
+
}
|
@@ -0,0 +1,15 @@
|
|
1
|
+
{
|
2
|
+
"possible_keys": {
|
3
|
+
"description": "Index keys possible for a specifc table",
|
4
|
+
"value_type": "array",
|
5
|
+
"delimiter": ",",
|
6
|
+
"rules": {
|
7
|
+
"threshold": {
|
8
|
+
"amount": 5,
|
9
|
+
"impact": "negative",
|
10
|
+
"message": "There are $amount possible keys for $table table, having too many index keys can be unoptimal",
|
11
|
+
"suggestion": "Please check if there are extra indexes in $table table."
|
12
|
+
}
|
13
|
+
}
|
14
|
+
}
|
15
|
+
}
|
@@ -0,0 +1,11 @@
|
|
1
|
+
---
|
2
|
+
key:
|
3
|
+
description: index key used for the table
|
4
|
+
value_type: string
|
5
|
+
rules:
|
6
|
+
absent:
|
7
|
+
impact: negative
|
8
|
+
message: There is no index key used for $table table, and can result into full
|
9
|
+
scan of the $table table
|
10
|
+
suggestion: 'Please use index from possible_keys: $possible_keys or add new
|
11
|
+
one to $table table as per the requirements.'
|
@@ -0,0 +1,18 @@
|
|
1
|
+
---
|
2
|
+
type:
|
3
|
+
description: Join used in the query for a specific table.
|
4
|
+
value_type: string
|
5
|
+
rules:
|
6
|
+
system:
|
7
|
+
impact: positive
|
8
|
+
message: Table has zero or one row, no change required.
|
9
|
+
suggestion: ''
|
10
|
+
ALL:
|
11
|
+
impact: negative
|
12
|
+
message: Entire $table table is scanned to find matching rows, you have $amount_possible_keys
|
13
|
+
possible keys to use.
|
14
|
+
suggestion: 'Use index here. You can use index from possible key: $possible_keys
|
15
|
+
or add new one to $table table as per the requirements.'
|
16
|
+
debt:
|
17
|
+
value: 200
|
18
|
+
type: base
|
@@ -0,0 +1,13 @@
|
|
1
|
+
---
|
2
|
+
detailed#used_columns:
|
3
|
+
description: ''
|
4
|
+
value_type: array
|
5
|
+
rules:
|
6
|
+
threshold:
|
7
|
+
amount: 7
|
8
|
+
impact: negative
|
9
|
+
message: You have selected $amount columns, You should not select too many columns.
|
10
|
+
suggestion: Please only select required columns.
|
11
|
+
debt:
|
12
|
+
value: 10
|
13
|
+
type: threshold_relative
|
@@ -0,0 +1,12 @@
|
|
1
|
+
---
|
2
|
+
possible_keys:
|
3
|
+
description: Index keys possible for a specifc table
|
4
|
+
value_type: array
|
5
|
+
delimiter: ","
|
6
|
+
rules:
|
7
|
+
threshold:
|
8
|
+
amount: 5
|
9
|
+
impact: negative
|
10
|
+
message: There are $amount possible keys for $table table, having too many index
|
11
|
+
keys can be unoptimal
|
12
|
+
suggestion: Please check if there are extra indexes in $table table.
|
data/lib/query_police/analyse.rb
CHANGED
@@ -6,38 +6,38 @@ module QueryPolice
|
|
6
6
|
module Analyse
|
7
7
|
def table(table, summary, rules_config)
|
8
8
|
table_analysis = {}
|
9
|
-
|
9
|
+
table_debt = 0
|
10
10
|
|
11
11
|
table.each do |column, value|
|
12
12
|
summary = add_summary(summary, column, value)
|
13
13
|
next unless rules_config.dig(column).present?
|
14
14
|
|
15
15
|
table_analysis.merge!({ column => apply_rules(rules_config.dig(column), value) })
|
16
|
-
|
16
|
+
table_debt += table_analysis.dig(column, "tags").map { |_, tag| tag.dig("debt") }.sum.to_f
|
17
17
|
end
|
18
18
|
|
19
|
-
[table_analysis, summary,
|
19
|
+
[table_analysis, summary, table_debt]
|
20
20
|
end
|
21
21
|
|
22
22
|
def generate_summary(rules_config, summary)
|
23
23
|
summary_analysis = {}
|
24
|
-
|
24
|
+
summary_debt = 0
|
25
25
|
|
26
26
|
summary.each do |column, value|
|
27
27
|
next unless rules_config.dig(column).present?
|
28
28
|
|
29
29
|
summary_analysis.merge!({ column => apply_rules(rules_config.dig(column), value) })
|
30
|
-
|
30
|
+
summary_debt += summary_analysis.dig(column, "tags").map { |_, tag| tag.dig("debt") }.sum.to_f
|
31
31
|
end
|
32
32
|
|
33
|
-
[summary_analysis,
|
33
|
+
[summary_analysis, summary_debt]
|
34
34
|
end
|
35
35
|
|
36
36
|
class << self
|
37
37
|
private
|
38
38
|
|
39
39
|
def add_summary(summary, column_name, value)
|
40
|
-
summary["cardinality"] = (summary.dig("cardinality") || 1)
|
40
|
+
summary["cardinality"] = (summary.dig("cardinality") || 1) * value.to_f if column_name.eql?("rows")
|
41
41
|
|
42
42
|
summary
|
43
43
|
end
|
@@ -54,7 +54,7 @@ module QueryPolice
|
|
54
54
|
next unless tag_rule.present?
|
55
55
|
|
56
56
|
column_analyse["tags"].merge!(
|
57
|
-
{ tag => Transform.tag_rule(tag_rule).merge!({ "
|
57
|
+
{ tag => Transform.tag_rule(tag_rule).merge!({ "debt" => generate_debt(tag_rule, amount) }) }
|
58
58
|
)
|
59
59
|
end
|
60
60
|
|
@@ -69,7 +69,7 @@ module QueryPolice
|
|
69
69
|
if threshold_rule.present? && amount >= threshold_rule.dig("amount")
|
70
70
|
return {
|
71
71
|
"threshold" => Transform.tag_rule(threshold_rule).merge(
|
72
|
-
{ "amount" => amount, "
|
72
|
+
{ "amount" => amount, "debt" => generate_debt(threshold_rule, amount) }
|
73
73
|
)
|
74
74
|
}
|
75
75
|
end
|
@@ -77,16 +77,16 @@ module QueryPolice
|
|
77
77
|
{}
|
78
78
|
end
|
79
79
|
|
80
|
-
def
|
81
|
-
|
80
|
+
def generate_debt(tag_rule, amount)
|
81
|
+
debt = tag_rule.dig("debt", "value")
|
82
82
|
|
83
|
-
case tag_rule.dig("
|
83
|
+
case tag_rule.dig("debt", "type").to_s
|
84
84
|
when "base"
|
85
|
-
|
85
|
+
debt.to_f
|
86
86
|
when "relative"
|
87
|
-
amount.to_f *
|
88
|
-
when "
|
89
|
-
(amount - tag_rule.dig("amount")).to_f *
|
87
|
+
amount.to_f * debt.to_f
|
88
|
+
when "threshold_relative"
|
89
|
+
(amount - tag_rule.dig("amount")).to_f * debt.to_f
|
90
90
|
else
|
91
91
|
0
|
92
92
|
end
|
@@ -54,13 +54,13 @@ module QueryPolice
|
|
54
54
|
opts.dig("colours").present? ? impact.send(IMPACTS.dig(impact, "colour")) : impact
|
55
55
|
end
|
56
56
|
|
57
|
-
def
|
57
|
+
def debt(opts)
|
58
58
|
table, column, tag = opts.values_at("table", "column", "tag")
|
59
59
|
|
60
60
|
impact = query_analytic.dig(table, "analysis", column, "tags", tag, "impact")
|
61
|
-
|
61
|
+
debt = query_analytic.dig(table, "analysis", column, "tags", tag, "debt")
|
62
62
|
|
63
|
-
opts.dig("colours").present? ?
|
63
|
+
opts.dig("colours").present? ? debt.to_s.send(IMPACTS.dig(impact, "colour")) : debt
|
64
64
|
end
|
65
65
|
|
66
66
|
def table(opts)
|
@@ -21,7 +21,7 @@ module QueryPolice
|
|
21
21
|
# "users" => {
|
22
22
|
# "id" => 1,
|
23
23
|
# "name" => "users",
|
24
|
-
# "
|
24
|
+
# "debt" => 100.0,
|
25
25
|
# "analysis" => {
|
26
26
|
# "type" => {
|
27
27
|
# "value" => all",
|
@@ -30,7 +30,7 @@ module QueryPolice
|
|
30
30
|
# "impact" => "negative",
|
31
31
|
# "warning" => "warning to represent the issue",
|
32
32
|
# "suggestions" => "some follow up suggestions",
|
33
|
-
# "
|
33
|
+
# "debt" => 100.0
|
34
34
|
# }
|
35
35
|
# }
|
36
36
|
# }
|
@@ -44,28 +44,28 @@ module QueryPolice
|
|
44
44
|
# "amount" => 10,
|
45
45
|
# "warning" => "warning to represent the issue",
|
46
46
|
# "suggestions" => "some follow up suggestions",
|
47
|
-
# "
|
47
|
+
# "debt" => 100.0
|
48
48
|
# }
|
49
49
|
# }
|
50
|
-
def initialize
|
50
|
+
def initialize(footer: nil)
|
51
|
+
@footer = footer || ""
|
52
|
+
@summary = {}
|
53
|
+
@summary_debt = 0
|
51
54
|
@table_count = 0
|
55
|
+
@table_debt = 0
|
52
56
|
@tables = {}
|
53
|
-
@table_score = 0
|
54
|
-
@summary = {}
|
55
|
-
@summary_score = 0
|
56
57
|
end
|
57
58
|
|
58
|
-
attr_accessor :
|
59
|
+
attr_accessor :summary, :tables
|
59
60
|
|
60
61
|
# register a table analysis in analysis object
|
61
62
|
# @param name [String] name of the table
|
62
63
|
# @param table_analysis [Hash] analysis of a table
|
63
|
-
# @param score [Integer] score for that table
|
64
64
|
# Eg.
|
65
65
|
# {
|
66
66
|
# "id" => 1,
|
67
67
|
# "name" => "users",
|
68
|
-
# "
|
68
|
+
# "debt" => 100.0
|
69
69
|
# "analysis" => {
|
70
70
|
# "type" => [
|
71
71
|
# {
|
@@ -73,76 +73,104 @@ module QueryPolice
|
|
73
73
|
# "impact" => "negative",
|
74
74
|
# "warning" => "warning to represent the issue",
|
75
75
|
# "suggestions" => "some follow up suggestions",
|
76
|
-
# "
|
76
|
+
# "debt" => 100.0
|
77
77
|
# }
|
78
78
|
# ]
|
79
79
|
# }
|
80
80
|
# }
|
81
|
-
|
81
|
+
# @param debt [Integer] debt for that table
|
82
|
+
def register_table(name, table_analysis, debt)
|
82
83
|
@table_count += 1
|
83
84
|
tables.merge!(
|
84
85
|
{
|
85
86
|
name => {
|
86
87
|
"id" => @table_count,
|
87
88
|
"name" => name,
|
88
|
-
"
|
89
|
+
"debt" => debt,
|
89
90
|
"analysis" => table_analysis
|
90
91
|
}
|
91
92
|
}
|
92
93
|
)
|
93
94
|
|
94
|
-
@
|
95
|
+
@table_debt += debt
|
95
96
|
end
|
96
97
|
|
97
98
|
# register summary based in different attributes
|
98
99
|
# @param summary [Hash] hash of summary of analysis
|
99
|
-
def register_summary(summary,
|
100
|
+
def register_summary(summary, debt)
|
100
101
|
self.summary.merge!(summary)
|
101
|
-
@
|
102
|
+
@summary_debt += debt
|
102
103
|
end
|
103
104
|
|
104
105
|
# to get analysis in pretty format with warnings and suggestions
|
105
|
-
# @param opts [Hash] -
|
106
|
+
# @param opts [Hash] - options
|
107
|
+
# possible keys
|
108
|
+
# [
|
109
|
+
# positive: <boolean>,
|
110
|
+
# negative: <boolean>,
|
111
|
+
# caution: <boolean>,
|
112
|
+
# wrap_width: <integer>,
|
113
|
+
# skip_footer: <boolean>
|
114
|
+
# ]
|
106
115
|
# @return [String] pretty analysis
|
107
|
-
def pretty_analysis(opts)
|
116
|
+
def pretty_analysis(opts = { "negative" => true, "caution" => true })
|
108
117
|
final_message = ""
|
109
118
|
opts = opts.with_indifferent_access
|
110
119
|
|
111
120
|
opts.slice(*IMPACTS.keys).each do |impact, value|
|
112
|
-
|
121
|
+
opts_ = opts.slice("wrap_width").merge({ "skip_footer" => true })
|
122
|
+
final_message += pretty_analysis_for(impact, opts_) if value.present?
|
113
123
|
end
|
114
124
|
|
115
|
-
final_message
|
125
|
+
return final_message unless final_message.present?
|
126
|
+
|
127
|
+
opts.dig("skip_footer").present? ? final_message : final_message + @footer
|
116
128
|
end
|
117
129
|
|
118
130
|
# to get analysis in pretty format with warnings and suggestions for a impact
|
119
131
|
# @param impact [String]
|
132
|
+
# @param opts [Hash] - options
|
133
|
+
# possible keys
|
134
|
+
# [
|
135
|
+
# wrap_width: <integer>
|
136
|
+
# skip_footer: <boolean>
|
137
|
+
# ]
|
120
138
|
# @return [String] pretty analysis
|
121
|
-
def pretty_analysis_for(impact)
|
122
|
-
final_message = "
|
139
|
+
def pretty_analysis_for(impact, opts = {})
|
140
|
+
final_message = ""
|
123
141
|
|
124
142
|
query_analytic.each_key do |table|
|
125
|
-
table_message = query_pretty_analysis(table, { impact => true })
|
143
|
+
table_message = query_pretty_analysis(table, { impact => true }.merge(opts))
|
126
144
|
|
127
145
|
final_message += "#{table_message}\n" if table_message.present?
|
128
146
|
end
|
129
147
|
|
130
|
-
final_message
|
148
|
+
return final_message unless final_message.present?
|
149
|
+
|
150
|
+
final_message = "query_debt: #{query_debt}\n\n#{final_message}"
|
151
|
+
opts.dig("skip_footer").present? ? final_message : final_message + @footer
|
131
152
|
end
|
132
153
|
|
133
|
-
# to get the final
|
134
|
-
def
|
135
|
-
@
|
154
|
+
# to get the final debt
|
155
|
+
def query_debt
|
156
|
+
@table_debt + @summary_debt
|
136
157
|
end
|
137
158
|
|
138
159
|
# to get analysis in pretty format with warnings and suggestions for a table
|
139
160
|
# @param table [String] - table name
|
140
|
-
# @param opts [Hash] -
|
161
|
+
# @param opts [Hash] - options
|
162
|
+
# possible keys
|
163
|
+
# [
|
164
|
+
# positive: <boolean>,
|
165
|
+
# negative: <boolean>,
|
166
|
+
# caution: <boolean>,
|
167
|
+
# wrap_width: <integer>
|
168
|
+
# ]
|
141
169
|
# @return [String] pretty analysis
|
142
170
|
def query_pretty_analysis(table, opts)
|
143
171
|
table_analytics = Terminal::Table.new(title: table)
|
144
172
|
table_analytics_present = false
|
145
|
-
table_analytics.add_row(["
|
173
|
+
table_analytics.add_row(["debt", query_analytic.dig(table, "debt")])
|
146
174
|
|
147
175
|
opts = opts.with_indifferent_access
|
148
176
|
|
@@ -167,7 +195,7 @@ module QueryPolice
|
|
167
195
|
query_analytic.dig(table, "analysis", column, "tags").each do |tag, tag_analysis|
|
168
196
|
next unless opts.dig(tag_analysis.dig("impact")).present?
|
169
197
|
|
170
|
-
column_analytics += tag_analytic(table, column, tag)
|
198
|
+
column_analytics += tag_analytic(table, column, tag, opts)
|
171
199
|
end
|
172
200
|
|
173
201
|
column_analytics
|
@@ -177,22 +205,24 @@ module QueryPolice
|
|
177
205
|
tables.merge(
|
178
206
|
"summary" => {
|
179
207
|
"name" => "summary",
|
180
|
-
"
|
208
|
+
"debt" => @summary_debt,
|
181
209
|
"analysis" => summary
|
182
210
|
}
|
183
211
|
)
|
184
212
|
end
|
185
213
|
|
186
|
-
def tag_analytic(table, column, tag)
|
214
|
+
def tag_analytic(table, column, tag, opts)
|
187
215
|
tag_message = []
|
188
216
|
|
189
|
-
|
190
|
-
message = dynamic_message(
|
191
|
-
suggestion = dynamic_message(
|
192
|
-
|
193
|
-
|
194
|
-
tag_message << ["
|
195
|
-
tag_message << ["
|
217
|
+
variable_opts = { "table" => table, "column" => column, "tag" => tag }
|
218
|
+
message = dynamic_message(variable_opts.merge({ "type" => "message" }))
|
219
|
+
suggestion = dynamic_message(variable_opts.merge({ "type" => "suggestion" }))
|
220
|
+
wrap_width = opts.dig("wrap_width")
|
221
|
+
|
222
|
+
tag_message << ["impact", impact(variable_opts.merge({ "colours" => true }))]
|
223
|
+
tag_message << ["tag_debt", debt(variable_opts.merge({ "colours" => true }))]
|
224
|
+
tag_message << ["message", Helper.word_wrap(message, wrap_width)]
|
225
|
+
tag_message << ["suggestion", Helper.word_wrap(suggestion, wrap_width)] if suggestion.present?
|
196
226
|
|
197
227
|
tag_message
|
198
228
|
end
|
data/lib/query_police/config.rb
CHANGED
@@ -1,17 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "constants"
|
4
|
+
|
3
5
|
module QueryPolice
|
4
6
|
# This class is used for configuration of query police
|
5
7
|
class Config
|
6
|
-
def initialize
|
7
|
-
@
|
8
|
-
@
|
8
|
+
def initialize
|
9
|
+
@action_enabled = Constants::DEFAULT_ACTION_ENABLED
|
10
|
+
@analysis_footer = Constants::DEFAULT_ANALYSIS_FOOTER
|
11
|
+
@logger_options = Constants::DEFAULT_LOGGER_OPTIONS
|
12
|
+
@rules_path = Constants::DEFAULT_RULES_PATH
|
13
|
+
@verbosity = Constants::DEFAULT_VERBOSITY
|
9
14
|
end
|
10
15
|
|
11
|
-
def
|
12
|
-
@
|
16
|
+
def action_enabled?
|
17
|
+
@action_enabled.present?
|
13
18
|
end
|
14
19
|
|
15
|
-
attr_accessor :
|
20
|
+
attr_accessor :action_enabled, :analysis_footer, :logger_options, :rules_path, :verbosity
|
16
21
|
end
|
17
22
|
end
|
@@ -2,13 +2,16 @@
|
|
2
2
|
|
3
3
|
module QueryPolice
|
4
4
|
module Constants
|
5
|
+
DEFAULT_ANALYSIS_FOOTER = ""
|
5
6
|
DEFAULT_COLUMN_RULES = {
|
6
7
|
"value_type" => "string"
|
7
8
|
}.freeze
|
8
|
-
|
9
|
-
|
10
|
-
"negative" => true
|
9
|
+
DEFAULT_ACTION_ENABLED = true
|
10
|
+
DEFAULT_LOGGER_OPTIONS = {
|
11
|
+
"negative" => true,
|
12
|
+
"caution" => true
|
11
13
|
}.freeze
|
12
14
|
DEFAULT_RULES_PATH = File.join(File.dirname(__FILE__), "rules.json")
|
15
|
+
DEFAULT_VERBOSITY = "detailed"
|
13
16
|
end
|
14
17
|
end
|
data/lib/query_police/explain.rb
CHANGED
@@ -5,12 +5,15 @@ require_relative "helper"
|
|
5
5
|
module QueryPolice
|
6
6
|
# This module provides tools to explain queries and ActiveRecord::Relation
|
7
7
|
module Explain
|
8
|
+
DETAILED_VERBOSITY = "detailed"
|
9
|
+
|
8
10
|
# to get explain result in parsable format
|
9
11
|
# @param relation [ActiveRecord::Relation, String] active record relation or raw sql query
|
12
|
+
# @param verbosity [Symbol] mode to define which EXPLAIN result should be inlcuded in final result
|
10
13
|
# @return [Array] parsed_result - array of hashes representing EXPLAIN result for each row
|
11
|
-
def full_explain(relation,
|
14
|
+
def full_explain(relation, verbosity = nil)
|
12
15
|
explain_result = explain(relation)
|
13
|
-
return explain_result unless
|
16
|
+
return explain_result.values unless verbosity.to_s.eql?(DETAILED_VERBOSITY)
|
14
17
|
|
15
18
|
detailed_explain_result = detailed_explain(relation)
|
16
19
|
|
@@ -59,10 +62,20 @@ module QueryPolice
|
|
59
62
|
|
60
63
|
def parse_detailed_explain(explain_result)
|
61
64
|
parsed_result = JSON.parse(explain_result&.first&.first || "{}").dig("query_block")
|
65
|
+
parsed_result = parse_detailed_explain_operations(parsed_result)
|
66
|
+
|
62
67
|
return parsed_result.dig("nested_loop").map { |e| e.dig("table") } if parsed_result.key?("nested_loop")
|
63
68
|
|
64
69
|
parsed_result.key?("table") ? [parsed_result.dig("table")] : []
|
65
70
|
end
|
71
|
+
|
72
|
+
def parse_detailed_explain_operations(parsed_result)
|
73
|
+
parsed_result = parsed_result.dig("ordering_operation") || parsed_result
|
74
|
+
parsed_result = parsed_result.dig("grouping_operation") || parsed_result
|
75
|
+
parsed_result = parsed_result.dig("duplicates_removal") || parsed_result
|
76
|
+
|
77
|
+
parsed_result
|
78
|
+
end
|
66
79
|
end
|
67
80
|
|
68
81
|
module_function :full_explain, :explain, :detailed_explain
|
data/lib/query_police/helper.rb
CHANGED
@@ -4,6 +4,8 @@
|
|
4
4
|
module QueryPolice
|
5
5
|
# This module define helper methods for query police
|
6
6
|
module Helper
|
7
|
+
DEFAULT_WORD_WRAP_WIDTH = 100
|
8
|
+
|
7
9
|
def flatten_hash(hash, prefix_key = "")
|
8
10
|
flat_hash = {}
|
9
11
|
|
@@ -31,12 +33,20 @@ module QueryPolice
|
|
31
33
|
"Please ensure that the file exists and the path is correct."
|
32
34
|
end
|
33
35
|
|
34
|
-
|
36
|
+
case File.extname(rules_path)
|
37
|
+
when ".yaml", ".yml"
|
38
|
+
YAML.safe_load(File.read(rules_path))
|
39
|
+
when ".json"
|
40
|
+
JSON.parse(File.read(rules_path))
|
41
|
+
else
|
42
|
+
raise Error, "'#{File.extname(rules_path)}' extension is not supported for rules."
|
43
|
+
end
|
35
44
|
end
|
36
45
|
|
37
|
-
def word_wrap(string, width =
|
46
|
+
def word_wrap(string, width = DEFAULT_WORD_WRAP_WIDTH)
|
47
|
+
width ||= DEFAULT_WORD_WRAP_WIDTH
|
38
48
|
words = string.split
|
39
|
-
wrapped_string = "
|
49
|
+
wrapped_string = ""
|
40
50
|
|
41
51
|
words.each do |word|
|
42
52
|
last_line_size = (wrapped_string.split("\n")[-1]&.size || 0)
|