query_police 0.1.4.beta → 0.1.6.beta
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/.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)
|