foreign_key_checker 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 76ab463a1538404e230ac579e9e2c3494ac47f809637bcaddbf36066228f8d2c
4
+ data.tar.gz: 83c9a1f74842daa7b447269068e177cb74c50a84fe508b05523e5e33a99c6124
5
+ SHA512:
6
+ metadata.gz: c710144b4e055d92568ec109b1bb3933a3a8db272f213f584e660bed6b3917316ab1b1a2413a970c0b9c29873d7033d3927e4e4197bd6dbdf61e879b0c609f01
7
+ data.tar.gz: b515941c4d3a970c089cbe853016624c32b5dcfa8a5b33a0a670483b14cb46f0424749aa0d9b6cb829ef99c2ff37ae023f980f42fba8c4869c7b206a6bb8320e
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2019 AnatolyShirykalov
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # ForeignKeyChecker
2
+ This gem checks `belongs_to` ActiveRecord relations. It finds
3
+ 1. Nonpolymorphic relations without a `foreign_key`
4
+ 2. records without a record in related table (example: `city_id` is 1000, but there is not city with id 1000)
5
+ 3. broken relations: if you try to join such relation (example: `City.joins(:country)`), there is an exception.
6
+
7
+ ## Usage
8
+ ```bash
9
+ bundle exec rake foreign_key_check
10
+ ```
11
+
12
+ Or use it inside your application
13
+ ```ruby
14
+ ForeignKeyChecker.check.each do |key, result|
15
+ ...
16
+ end
17
+ ```
18
+
19
+ ## Installation
20
+ Add this line to your application's Gemfile:
21
+
22
+ ```ruby
23
+ gem 'foreign_key_checker'
24
+ ```
25
+
26
+ And then execute:
27
+ ```bash
28
+ $ bundle
29
+ ```
30
+
31
+ Or install it yourself as:
32
+ ```bash
33
+ $ gem install foreign_key_checker
34
+ ```
35
+
36
+ ## License
37
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'ForeignKeyChecker'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ require 'bundler/gem_tasks'
18
+
19
+ require 'rake/testtask'
20
+
21
+ Rake::TestTask.new(:test) do |t|
22
+ t.libs << 'test'
23
+ t.pattern = 'test/**/*_test.rb'
24
+ t.verbose = false
25
+ end
26
+
27
+ task default: :test
@@ -0,0 +1,7 @@
1
+ module ForeignKeyChecker
2
+ class Railtie < ::Rails::Railtie
3
+ rake_tasks do
4
+ load 'tasks/foreign_key_checker_tasks.rake'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module ForeignKeyChecker
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,188 @@
1
+ require "foreign_key_checker/railtie"
2
+
3
+ module ForeignKeyChecker
4
+ class Result
5
+ attr_reader :model, :association
6
+ def initialize(data)
7
+ data.each do |key, value|
8
+ instance_variable_set("@#{key}", value)
9
+ end
10
+ end
11
+
12
+ def from_table
13
+ model.table_name
14
+ end
15
+
16
+ def to_table
17
+ association.klass.table_name
18
+ end
19
+
20
+ def from_column
21
+ association.foreign_key
22
+ end
23
+
24
+ def to_column
25
+ association.klass.primary_key
26
+ end
27
+
28
+ def human_relation
29
+ "#{from_table} belongs_to #{to_table} (by column #{from_column} to #{to_column})"
30
+ end
31
+
32
+ def message; end
33
+
34
+ def inspect
35
+ "#<#{self.class.name}:#{self.object_id} #{message}>"
36
+ end
37
+ end
38
+
39
+ class ForeignKeyResult < Result
40
+ def message
41
+ "There is no foreign_key for relation #{human_relation}\n"
42
+ end
43
+ end
44
+
45
+ class IndexResult < Result
46
+ def message
47
+ "There is no index for relation #{human_relation}\n"
48
+ end
49
+ end
50
+
51
+ class ZombieResult < Result
52
+ attr_reader :zombies, :scope
53
+ def sql
54
+ scope.to_sql
55
+ end
56
+
57
+ def delete_sql
58
+ from_t = model.connection.quote_table_name(from_table)
59
+ from_c = model.connection.quote_column_name(from_column)
60
+ to_t = model.connection.quote_table_name(to_table)
61
+ to_c = model.connection.quote_column_name(to_column)
62
+ "DELETE #{from_t} FROM #{from_t}.#{from_c} LEFT OUTER JOIN #{to_t} ON #{to_t}.#{to_c} = #{from_t}.#{from_c} WHERE #{to_t}.#{to_c} IS NULL"
63
+ end
64
+
65
+ def message
66
+ "#{human_relation} with #{zombies} zombies; processed by statement:\n#{sql}\n"
67
+ end
68
+ end
69
+
70
+ class BrokenRelationResult < Result
71
+ attr_reader :error
72
+ def message
73
+ "#{human_relation} is bloken with error #{error.class.name}: #{error.message}"
74
+ end
75
+ end
76
+ class Checker
77
+
78
+ DEFAULT_OPTIONS = {
79
+ excluded_modules: [],
80
+ specification_names: ['primary'],
81
+ foreign_keys: true,
82
+ indexes: true,
83
+ zombies: true,
84
+ polymorphic_zombies: true,
85
+ }
86
+
87
+ DEFAULT_OPTIONS.keys.each { |key| attr_reader key }
88
+
89
+ def initialize(options = {})
90
+ @options = DEFAULT_OPTIONS.merge(options)
91
+ @options.each do |key, value|
92
+ if DEFAULT_OPTIONS.has_key?(key)
93
+ instance_variable_set("@#{key}", value)
94
+ end
95
+ end
96
+ @result = {
97
+ zombies: [],
98
+ foreign_keys: [],
99
+ indexes: [],
100
+ broken: [],
101
+ }
102
+ end
103
+
104
+ def specification_names=(value)
105
+ value.map(&:to_s)
106
+ end
107
+
108
+ def excluded_model?(model)
109
+ excluded_modules.each do |mod_name|
110
+ return true if model.to_s.starts_with?(mod_name)
111
+ end
112
+ false
113
+ end
114
+
115
+ def excluded_specification?(model)
116
+ !specification_names.include?(model.connection_specification_name.to_s)
117
+ end
118
+
119
+ def check_polymorphic_bt_association(model, association)
120
+
121
+ end
122
+
123
+ def check_foreign_key_bt_association(model, association)
124
+ return if model.name.starts_with?('HABTM_')
125
+ related = association.klass
126
+
127
+ column_name = model.connection.quote_column_name(association.foreign_key)
128
+ scope = model.left_outer_joins(association.name).where(
129
+ "#{related.quoted_table_name}.#{related.quoted_primary_key} IS NULL AND #{model.quoted_table_name}.#{column_name} IS NOT NULL"
130
+ )
131
+
132
+ if zombies
133
+ number = scope.count
134
+ if number > 0
135
+ @result[:zombies] << ZombieResult.new(
136
+ model: model,
137
+ association: association,
138
+ scope: scope,
139
+ zombies: number,
140
+ )
141
+ end
142
+ end
143
+
144
+ if foreign_keys && !model.connection.foreign_key_exists?(model.table_name, related.table_name)
145
+ @result[:foreign_keys] << ForeignKeyResult.new(
146
+ model: model,
147
+ association: association
148
+ )
149
+ end
150
+
151
+ if indexes && !model.connection.index_exists?(model.table_name, association.foreign_key)
152
+ @result[:indexes] << IndexResult.new(
153
+ model: model,
154
+ association: association,
155
+ )
156
+ end
157
+ rescue ActiveRecord::InverseOfAssociationNotFoundError, ActiveRecord::StatementInvalid => error
158
+ @result[:broken] << BrokenRelationResult.new(
159
+ model: model,
160
+ association: association,
161
+ error: error,
162
+ )
163
+ end
164
+
165
+ def check
166
+ Rails.application.eager_load!
167
+ ActiveRecord::Base.descendants.each do |model|
168
+ next if excluded_model?(model)
169
+ next if excluded_specification?(model)
170
+
171
+ model.reflect_on_all_associations(:belongs_to).each do |association|
172
+ if association.options[:polymorphic] && polymorphic_zombies
173
+ check_polymorphic_bt_association(model, association)
174
+ next
175
+ end
176
+
177
+ check_foreign_key_bt_association(model, association)
178
+ end
179
+ end
180
+ @result
181
+ end
182
+ end
183
+ class << self
184
+ def check(options = {})
185
+ Checker.new(options).check
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,9 @@
1
+ desc "Explaining what the task does"
2
+ task foreign_key_check: :environment do
3
+ ForeignKeyChecker.check.each do |key, results|
4
+ puts key if results.any?
5
+ results.each do |result|
6
+ puts result.message
7
+ end
8
+ end
9
+ end
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: foreign_key_checker
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - AnatolyShirykalov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-10-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: sqlite3
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Run task to obtain problems with your database
42
+ email:
43
+ - pipocavsobake@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - MIT-LICENSE
49
+ - README.md
50
+ - Rakefile
51
+ - lib/foreign_key_checker.rb
52
+ - lib/foreign_key_checker/railtie.rb
53
+ - lib/foreign_key_checker/version.rb
54
+ - lib/tasks/foreign_key_checker_tasks.rake
55
+ homepage: https://rubygems.org/foreign_key_checker
56
+ licenses:
57
+ - MIT
58
+ metadata:
59
+ allowed_push_host: https://rubygems.org
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubygems_version: 3.0.6
76
+ signing_key:
77
+ specification_version: 4
78
+ summary: Find problems with relations in active_record models
79
+ test_files: []