query_police 0.1.0.beta

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,4 @@
1
+ module QueryPolice
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ 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: []