query_police 0.1.0.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +487 -0
- data/Rakefile +12 -0
- data/lib/query_police/analysis/dynamic_message.rb +72 -0
- data/lib/query_police/analysis.rb +155 -0
- data/lib/query_police/config.rb +17 -0
- data/lib/query_police/constants.rb +14 -0
- data/lib/query_police/explain.rb +70 -0
- data/lib/query_police/helper.rb +44 -0
- data/lib/query_police/rules.json +171 -0
- data/lib/query_police/version.rb +5 -0
- data/lib/query_police.rb +163 -0
- data/sig/query_police.rbs +4 -0
- metadata +104 -0
data/lib/query_police.rb
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record"
|
4
|
+
require "active_support/notifications"
|
5
|
+
require "active_support/core_ext"
|
6
|
+
require "json"
|
7
|
+
require "forwardable"
|
8
|
+
|
9
|
+
require_relative "query_police/analysis"
|
10
|
+
require_relative "query_police/constants"
|
11
|
+
require_relative "query_police/config"
|
12
|
+
require_relative "query_police/explain"
|
13
|
+
require_relative "query_police/helper"
|
14
|
+
require_relative "query_police/version"
|
15
|
+
|
16
|
+
# This module provides tools to analyse your queries based on custom rules
|
17
|
+
module QueryPolice
|
18
|
+
extend Forwardable
|
19
|
+
|
20
|
+
class Error < StandardError; end
|
21
|
+
|
22
|
+
@config = Config.new(
|
23
|
+
Constants::DEFAULT_DETAILED, Constants::DEFAULT_RULES_PATH
|
24
|
+
)
|
25
|
+
|
26
|
+
CONFIG_METHODS = %i[
|
27
|
+
detailed detailed? detailed= rules_path rules_path=
|
28
|
+
].freeze
|
29
|
+
|
30
|
+
def_delegators :config, *CONFIG_METHODS
|
31
|
+
|
32
|
+
# to create analysis for ActiveRecord::Relation or a query string
|
33
|
+
# @param relation [ActiveRecord::Relation, String]
|
34
|
+
# @return [QueryPolice::Analysis] analysis - contains the analysis of the query
|
35
|
+
def analyse(relation)
|
36
|
+
rules_config = Helper.load_config(config.rules_path)
|
37
|
+
analysis = Analysis.new
|
38
|
+
summary = {}
|
39
|
+
|
40
|
+
query_plan = Explain.full_explain(relation, config.detailed?)
|
41
|
+
|
42
|
+
query_plan.each do |table|
|
43
|
+
table_analysis, summary = analyse_table(table, summary, rules_config)
|
44
|
+
|
45
|
+
analysis.register_table(table.dig("table"), table_analysis)
|
46
|
+
end
|
47
|
+
|
48
|
+
analysis.register_summary(generate_summary_analysis(rules_config, summary))
|
49
|
+
|
50
|
+
analysis
|
51
|
+
end
|
52
|
+
|
53
|
+
# to add a logger to print analysis after each query
|
54
|
+
# @param silent [Boolean] silent errors for logger
|
55
|
+
# @param logger_config [Hash] possible options [positive: <boolean>, negative: <boolean>, caution: <boolean>]
|
56
|
+
def subscribe_logger(silent: false, logger_config: Constants::DEFAULT_LOGGER_CONFIG)
|
57
|
+
ActiveSupport::Notifications.subscribe("sql.active_record") do |_, _, _, _, payload|
|
58
|
+
begin
|
59
|
+
if !payload[:exception].present? && payload[:name] =~ /.* Load/
|
60
|
+
analysis = analyse(payload[:sql])
|
61
|
+
|
62
|
+
Helper.logger(analysis.pretty_analysis(logger_config))
|
63
|
+
end
|
64
|
+
rescue => error
|
65
|
+
if silent.present?
|
66
|
+
Helper.logger("#{error.class}: #{error.message}", "error")
|
67
|
+
else
|
68
|
+
raise error
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class << self
|
75
|
+
attr_accessor :config
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def add_summary(summary, column_name, value)
|
80
|
+
summary["cardinality"] = (summary.dig("cardinality") || 1) + value.to_f if column_name.eql?("rows")
|
81
|
+
|
82
|
+
summary
|
83
|
+
end
|
84
|
+
|
85
|
+
def analyse_table(table, summary, rules_config)
|
86
|
+
table_analysis = {}
|
87
|
+
|
88
|
+
table.each do |column, value|
|
89
|
+
summary = add_summary(summary, column, value)
|
90
|
+
next unless rules_config.dig(column).present?
|
91
|
+
|
92
|
+
table_analysis.merge!({ column => apply_rules(rules_config.dig(column), value) })
|
93
|
+
end
|
94
|
+
|
95
|
+
[table_analysis, summary]
|
96
|
+
end
|
97
|
+
|
98
|
+
def apply_rules(column_rules, value)
|
99
|
+
column_rules = Constants::DEFAULT_COLUMN_RULES.merge(column_rules)
|
100
|
+
value = transform_value(value, column_rules)
|
101
|
+
amount = transform_amount(value, column_rules)
|
102
|
+
|
103
|
+
column_analyse = { "value" => value, "amount" => amount, "tags" => {} }
|
104
|
+
|
105
|
+
[*value].each do |tag|
|
106
|
+
tag_rule = column_rules.dig("rules", tag)
|
107
|
+
next unless tag_rule.present?
|
108
|
+
|
109
|
+
column_analyse["tags"].merge!({ tag => transform_tag_rule(tag_rule) })
|
110
|
+
end
|
111
|
+
|
112
|
+
column_analyse["tags"].merge!(apply_threshold_rule(column_rules, amount))
|
113
|
+
|
114
|
+
column_analyse
|
115
|
+
end
|
116
|
+
|
117
|
+
def apply_threshold_rule(column_rules, amount)
|
118
|
+
threshold_rule = column_rules.dig("rules", "threshold")
|
119
|
+
|
120
|
+
if threshold_rule.present? && amount >= threshold_rule.dig("amount")
|
121
|
+
return {
|
122
|
+
"threshold" => transform_tag_rule(threshold_rule).merge(
|
123
|
+
{ "amount" => amount }
|
124
|
+
)
|
125
|
+
}
|
126
|
+
end
|
127
|
+
|
128
|
+
{}
|
129
|
+
end
|
130
|
+
|
131
|
+
def generate_summary_analysis(rules_config, summary)
|
132
|
+
summary_analysis = {}
|
133
|
+
|
134
|
+
summary.each do |column, value|
|
135
|
+
next unless rules_config.dig(column).present?
|
136
|
+
|
137
|
+
summary_analysis.merge!({ column => apply_rules(rules_config.dig(column), value) })
|
138
|
+
end
|
139
|
+
|
140
|
+
summary_analysis
|
141
|
+
end
|
142
|
+
|
143
|
+
def transform_amount(value, column_rules)
|
144
|
+
column_rules.dig("value_type").eql?("number") ? value.to_f : value.size
|
145
|
+
end
|
146
|
+
|
147
|
+
def transform_tag_rule(tag_rule)
|
148
|
+
tag_rule.slice("impact", "suggestion", "message")
|
149
|
+
end
|
150
|
+
|
151
|
+
def transform_value(value, column_rules)
|
152
|
+
value ||= "absent"
|
153
|
+
|
154
|
+
if column_rules.dig("value_type").eql?("array") && column_rules.dig("delimiter").present?
|
155
|
+
value = value.split(column_rules.dig("delimiter")).map(&:strip)
|
156
|
+
end
|
157
|
+
|
158
|
+
value
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
module_function :analyse, :subscribe_logger, *CONFIG_METHODS
|
163
|
+
end
|
metadata
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: query_police
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0.beta
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- strikeraryu
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-04-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 3.0.0
|
20
|
+
- - "<"
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 6.0.0
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 3.0.0
|
30
|
+
- - "<"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 6.0.0
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: activerecord
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 3.0.0
|
40
|
+
- - "<"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 6.0.0
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: 3.0.0
|
50
|
+
- - "<"
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: 6.0.0
|
53
|
+
description:
|
54
|
+
email:
|
55
|
+
- striker.aryu56@gmail.com
|
56
|
+
executables: []
|
57
|
+
extensions: []
|
58
|
+
extra_rdoc_files: []
|
59
|
+
files:
|
60
|
+
- ".rspec"
|
61
|
+
- ".rubocop.yml"
|
62
|
+
- CHANGELOG.md
|
63
|
+
- CODE_OF_CONDUCT.md
|
64
|
+
- Gemfile
|
65
|
+
- LICENSE.txt
|
66
|
+
- README.md
|
67
|
+
- Rakefile
|
68
|
+
- lib/query_police.rb
|
69
|
+
- lib/query_police/analysis.rb
|
70
|
+
- lib/query_police/analysis/dynamic_message.rb
|
71
|
+
- lib/query_police/config.rb
|
72
|
+
- lib/query_police/constants.rb
|
73
|
+
- lib/query_police/explain.rb
|
74
|
+
- lib/query_police/helper.rb
|
75
|
+
- lib/query_police/rules.json
|
76
|
+
- lib/query_police/version.rb
|
77
|
+
- sig/query_police.rbs
|
78
|
+
homepage: https://github.com/strikeraryu/query_police.git
|
79
|
+
licenses:
|
80
|
+
- MIT
|
81
|
+
metadata:
|
82
|
+
homepage_uri: https://github.com/strikeraryu/query_police.git
|
83
|
+
source_code_uri: https://github.com/strikeraryu/query_police.git
|
84
|
+
changelog_uri: https://github.com/strikeraryu/query_police/blob/master/CHANGELOG.md
|
85
|
+
post_install_message:
|
86
|
+
rdoc_options: []
|
87
|
+
require_paths:
|
88
|
+
- lib
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 2.3.0
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: 1.3.6
|
99
|
+
requirements: []
|
100
|
+
rubygems_version: 3.2.3
|
101
|
+
signing_key:
|
102
|
+
specification_version: 4
|
103
|
+
summary: This gem provides tools to analyze your queries based on custom rules.
|
104
|
+
test_files: []
|