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.
@@ -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: []