foreign_key_checker 0.1.1 → 0.4.0
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/README.md +13 -0
- data/Rakefile +0 -10
- data/lib/foreign_key_checker.rb +76 -5
- data/lib/foreign_key_checker/checkers/relations.rb +122 -0
- data/lib/foreign_key_checker/checkers/tables.rb +88 -0
- data/lib/foreign_key_checker/railtie.rb +3 -0
- data/lib/foreign_key_checker/utils.rb +126 -0
- data/lib/foreign_key_checker/version.rb +1 -1
- data/lib/generators/foreign_key_checker/migration/migration_generator.rb +29 -0
- data/lib/generators/foreign_key_checker/migration/templates/migrations/fix_foreign_keys.rb.erb +18 -0
- metadata +109 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf9685280dd2c9ad8232c3b4fe7d4f72dcc56ccdbbaf0b55d826792196185f05
|
|
4
|
+
data.tar.gz: de9a1d21793299c0df12e92b09fd2661b160d9794d6bb01a7080478fecc3db96
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7f3b6138b99b780ed15ab03c43c0745bfc9511089e4fe0b4bb29cc962d232f7d942d3e314dc56bc8422e193631872d7415a5a28251281fad1bca704fb3c2ab6b
|
|
7
|
+
data.tar.gz: d7c3c677d063a6e2210d3627caac91431024e940b44e4e2b33c912f48c532a662c81678a1f5aa32f8cccffe19c3bdf0fbd7e10c13796f129de2dc9c60044705d
|
data/README.md
CHANGED
|
@@ -5,6 +5,13 @@ This gem checks `belongs_to` ActiveRecord relations. It finds
|
|
|
5
5
|
3. broken relations: if you try to join such relation (example: `City.joins(:country)`), there is an exception.
|
|
6
6
|
|
|
7
7
|
## Usage
|
|
8
|
+
Generate migration to fix foreign key problems
|
|
9
|
+
```bash
|
|
10
|
+
bundle exec rails generate foreign_key_checker:migration
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
Print information about foreign key problems
|
|
8
15
|
```bash
|
|
9
16
|
bundle exec rake foreign_key_check
|
|
10
17
|
```
|
|
@@ -16,6 +23,12 @@ ForeignKeyChecker.check.each do |key, result|
|
|
|
16
23
|
end
|
|
17
24
|
```
|
|
18
25
|
|
|
26
|
+
Get general information about foreign keys
|
|
27
|
+
```ruby
|
|
28
|
+
ForeignKeyChecker::Utils.get_foreign_keys_hash
|
|
29
|
+
# => {"users"=>[#<ForeignKeyChecker::Utils::Result:0x00005645e51756e8 @from_table="user_rating_changes", @from_column="user_id", @to_table="users", @to_column="id">]}
|
|
30
|
+
```
|
|
31
|
+
|
|
19
32
|
## Installation
|
|
20
33
|
Add this line to your application's Gemfile:
|
|
21
34
|
|
data/Rakefile
CHANGED
|
@@ -4,16 +4,6 @@ rescue LoadError
|
|
|
4
4
|
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
|
5
5
|
end
|
|
6
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
7
|
require 'bundler/gem_tasks'
|
|
18
8
|
|
|
19
9
|
require 'rake/testtask'
|
data/lib/foreign_key_checker.rb
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
require "foreign_key_checker/railtie"
|
|
2
|
+
require 'foreign_key_checker/utils'
|
|
3
|
+
module ForeignKeyChecker::Checkers
|
|
4
|
+
end
|
|
5
|
+
require 'foreign_key_checker/checkers/relations'
|
|
6
|
+
require 'foreign_key_checker/checkers/tables'
|
|
2
7
|
|
|
3
8
|
module ForeignKeyChecker
|
|
9
|
+
class TypeMismatch < StandardError; end
|
|
4
10
|
class Result
|
|
5
11
|
attr_reader :model, :association
|
|
6
12
|
def initialize(data)
|
|
@@ -34,12 +40,30 @@ module ForeignKeyChecker
|
|
|
34
40
|
def inspect
|
|
35
41
|
"#<#{self.class.name}:#{self.object_id} #{message}>"
|
|
36
42
|
end
|
|
43
|
+
|
|
44
|
+
def nullable?
|
|
45
|
+
model.columns_hash[from_column.to_s].null
|
|
46
|
+
end
|
|
47
|
+
|
|
37
48
|
end
|
|
38
49
|
|
|
39
50
|
class ForeignKeyResult < Result
|
|
51
|
+
attr_reader :scope
|
|
40
52
|
def message
|
|
41
53
|
"There is no foreign_key for relation #{human_relation}\n"
|
|
42
54
|
end
|
|
55
|
+
|
|
56
|
+
def migration
|
|
57
|
+
"add_foreign_key :#{from_table}, :#{to_table}#{
|
|
58
|
+
", column: :#{from_column}" if from_column.to_s != "#{to_table.singularize}_id"
|
|
59
|
+
}#{
|
|
60
|
+
", primary_key: :#{to_column}" if to_column.to_s != 'id'
|
|
61
|
+
}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def to_zombie
|
|
65
|
+
ZombieResult.new(scope: scope, model: model, association: association)
|
|
66
|
+
end
|
|
43
67
|
end
|
|
44
68
|
|
|
45
69
|
class IndexResult < Result
|
|
@@ -59,7 +83,26 @@ module ForeignKeyChecker
|
|
|
59
83
|
from_c = model.connection.quote_column_name(from_column)
|
|
60
84
|
to_t = model.connection.quote_table_name(to_table)
|
|
61
85
|
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"
|
|
86
|
+
#"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"
|
|
87
|
+
"DELETE FROM #{from_t} WHERE #{from_c} IS NOT NULL AND #{from_c} NOT IN (SELECT #{to_c} FROM #{to_t})"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def set_null_sql
|
|
91
|
+
from_t = model.connection.quote_table_name(from_table)
|
|
92
|
+
from_c = model.connection.quote_column_name(from_column)
|
|
93
|
+
to_t = model.connection.quote_table_name(to_table)
|
|
94
|
+
to_c = model.connection.quote_column_name(to_column)
|
|
95
|
+
#"UPDATE #{from_t} SET #{from_t}.#{from_c} = NULL FROM #{from_t} LEFT OUTER JOIN #{to_t} ON #{to_t}.#{to_c} = #{from_t}.#{from_c} WHERE #{to_t}.#{to_c} IS NULL"
|
|
96
|
+
"UPDATE #{from_t} SET #{from_c} = NULL WHERE #{from_c} IS NOT NULL AND #{from_c} NOT IN (SELECT #{to_c} FROM #{to_t})"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def set_null_migration
|
|
100
|
+
"execute('#{set_null_sql}')"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def migration(set_null: false)
|
|
104
|
+
return set_null_migration if set_null
|
|
105
|
+
"execute('#{delete_sql}')"
|
|
63
106
|
end
|
|
64
107
|
|
|
65
108
|
def message
|
|
@@ -120,15 +163,32 @@ module ForeignKeyChecker
|
|
|
120
163
|
|
|
121
164
|
end
|
|
122
165
|
|
|
166
|
+
def check_types(model, association)
|
|
167
|
+
type_from = model.columns_hash[association.foreign_key.to_s].sql_type
|
|
168
|
+
type_to = association.klass.columns_hash[association.klass.primary_key.to_s].sql_type
|
|
169
|
+
raise TypeMismatch, "TypeMissMatch for relation #{model}##{association.name} #{type_from} != #{type_to}" if type_from != type_to
|
|
170
|
+
end
|
|
171
|
+
|
|
123
172
|
def check_foreign_key_bt_association(model, association)
|
|
124
173
|
return if model.name.starts_with?('HABTM_')
|
|
125
|
-
|
|
174
|
+
begin
|
|
175
|
+
related = association.klass
|
|
176
|
+
rescue NameError => error
|
|
177
|
+
@result[:broken] << BrokenRelationResult.new(
|
|
178
|
+
model: model,
|
|
179
|
+
association: association,
|
|
180
|
+
error: error,
|
|
181
|
+
)
|
|
182
|
+
return
|
|
183
|
+
end
|
|
126
184
|
|
|
127
185
|
column_name = model.connection.quote_column_name(association.foreign_key)
|
|
128
186
|
scope = model.left_outer_joins(association.name).where(
|
|
129
187
|
"#{related.quoted_table_name}.#{related.quoted_primary_key} IS NULL AND #{model.quoted_table_name}.#{column_name} IS NOT NULL"
|
|
130
188
|
)
|
|
131
189
|
|
|
190
|
+
check_types(model, association)
|
|
191
|
+
|
|
132
192
|
if zombies
|
|
133
193
|
number = scope.count
|
|
134
194
|
if number > 0
|
|
@@ -141,10 +201,12 @@ module ForeignKeyChecker
|
|
|
141
201
|
end
|
|
142
202
|
end
|
|
143
203
|
|
|
144
|
-
if foreign_keys && !model.connection.foreign_key_exists?(model.table_name, related.table_name)
|
|
204
|
+
if foreign_keys && !model.connection.foreign_key_exists?(model.table_name, related.table_name, column: association.foreign_key, primary_key: related.primary_key)
|
|
205
|
+
scope.first
|
|
145
206
|
@result[:foreign_keys] << ForeignKeyResult.new(
|
|
146
207
|
model: model,
|
|
147
|
-
association: association
|
|
208
|
+
association: association,
|
|
209
|
+
scope: scope,
|
|
148
210
|
)
|
|
149
211
|
end
|
|
150
212
|
|
|
@@ -154,7 +216,7 @@ module ForeignKeyChecker
|
|
|
154
216
|
association: association,
|
|
155
217
|
)
|
|
156
218
|
end
|
|
157
|
-
rescue ActiveRecord::InverseOfAssociationNotFoundError, ActiveRecord::StatementInvalid => error
|
|
219
|
+
rescue ActiveRecord::InverseOfAssociationNotFoundError, ActiveRecord::StatementInvalid, TypeMismatch => error
|
|
158
220
|
@result[:broken] << BrokenRelationResult.new(
|
|
159
221
|
model: model,
|
|
160
222
|
association: association,
|
|
@@ -162,6 +224,13 @@ module ForeignKeyChecker
|
|
|
162
224
|
)
|
|
163
225
|
end
|
|
164
226
|
|
|
227
|
+
def already_done_fk?(model, association)
|
|
228
|
+
@_done ||= {}
|
|
229
|
+
ret = @_done[[model.table_name, association.foreign_key]]
|
|
230
|
+
@_done[[model.table_name, association.foreign_key]] = true
|
|
231
|
+
ret
|
|
232
|
+
end
|
|
233
|
+
|
|
165
234
|
def check
|
|
166
235
|
Rails.application.eager_load!
|
|
167
236
|
ActiveRecord::Base.descendants.each do |model|
|
|
@@ -169,11 +238,13 @@ module ForeignKeyChecker
|
|
|
169
238
|
next if excluded_specification?(model)
|
|
170
239
|
|
|
171
240
|
model.reflect_on_all_associations(:belongs_to).each do |association|
|
|
241
|
+
|
|
172
242
|
if association.options[:polymorphic] && polymorphic_zombies
|
|
173
243
|
check_polymorphic_bt_association(model, association)
|
|
174
244
|
next
|
|
175
245
|
end
|
|
176
246
|
|
|
247
|
+
next if already_done_fk?(model, association)
|
|
177
248
|
check_foreign_key_bt_association(model, association)
|
|
178
249
|
end
|
|
179
250
|
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
require 'foreign_key_checker/utils'
|
|
2
|
+
|
|
3
|
+
module ForeignKeyChecker::Checkers::Relations
|
|
4
|
+
class Result
|
|
5
|
+
attr_reader :ok, :model, :association, :fk, :error, :table
|
|
6
|
+
def initialize(**args)
|
|
7
|
+
%i[model association ok error fk table].each do |key|
|
|
8
|
+
instance_variable_set("@#{key}", args[key])
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
class JoinResult < Result
|
|
13
|
+
def message
|
|
14
|
+
"expect { #{model.to_s}.joins(:#{association.name}).first }.to_not raise_exception" if !ok
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
class ErrorResult < Result
|
|
18
|
+
def message
|
|
19
|
+
"`#{model.to_s}##{association.name}` is broken\n#{error.class.to_s}\n#{error.message}\n#{error.backtrace.join("\n")}" if !ok
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
class HasOneOrManyResult < Result
|
|
23
|
+
def message
|
|
24
|
+
"expected has_many or has_one association for #{fk.inspect}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
class HasOneOrManyDependentResult < Result
|
|
28
|
+
def message
|
|
29
|
+
"expected has_many or has_one association with dependent option for #{fk.inspect}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
class NoModelResult < Result
|
|
33
|
+
def message
|
|
34
|
+
"expected find model for table #{table}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
def self.check_by_join
|
|
38
|
+
Rails.application.eager_load!
|
|
39
|
+
models = ActiveRecord::Base.descendants
|
|
40
|
+
models.each_with_object([]) do |model, results|
|
|
41
|
+
next if model.to_s.include?('HABTM')
|
|
42
|
+
model.reflect_on_all_associations.each do |association|
|
|
43
|
+
begin
|
|
44
|
+
next if association.options[:polymorphic]
|
|
45
|
+
next if association.scope && association.scope.is_a?(Proc) && association.scope.arity > 0
|
|
46
|
+
next if model.connection_specification_name != association.klass.connection_specification_name
|
|
47
|
+
|
|
48
|
+
model.joins(association.name).first
|
|
49
|
+
result = JoinResult.new(model: model, association: association, ok: true)
|
|
50
|
+
yield result if block_given?
|
|
51
|
+
results << result
|
|
52
|
+
rescue => e
|
|
53
|
+
result = JoinResult.new(model: model, association: association, ok: false, error: e)
|
|
54
|
+
yield result if block_given?
|
|
55
|
+
results << result
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.check_by_fks
|
|
62
|
+
Rails.application.eager_load!
|
|
63
|
+
models = ActiveRecord::Base.descendants.group_by(&:table_name)
|
|
64
|
+
check_relation = proc do |model, fk, association|
|
|
65
|
+
begin
|
|
66
|
+
pk = association.options[:primary_key] || model.primary_key
|
|
67
|
+
association.klass.table_name.to_s == fk.from_table.to_s && association.foreign_key.to_s == fk.from_column.to_s && pk.to_s == fk.to_column.to_s
|
|
68
|
+
rescue => e
|
|
69
|
+
if block_given?
|
|
70
|
+
yield ErrorResult.new(model: model, association: association, fk: fk, ok: false, error: e)
|
|
71
|
+
else
|
|
72
|
+
p model
|
|
73
|
+
p fk
|
|
74
|
+
p association
|
|
75
|
+
raise e
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
ForeignKeyChecker::Utils.get_foreign_keys_hash.each do |to_table, fks|
|
|
80
|
+
fks.each do |fk|
|
|
81
|
+
unless models.key?(fk.to_table)
|
|
82
|
+
result = NoModelResult.new(ok: false, table: fk.to_table)
|
|
83
|
+
if block_given?
|
|
84
|
+
yield result
|
|
85
|
+
else
|
|
86
|
+
raise result.message
|
|
87
|
+
end
|
|
88
|
+
next
|
|
89
|
+
end
|
|
90
|
+
models[fk.to_table].each do |model|
|
|
91
|
+
next if model.connection_specification_name != 'primary'
|
|
92
|
+
ok = false
|
|
93
|
+
ok ||= !!model.reflect_on_all_associations(:has_many).find do |association|
|
|
94
|
+
check_relation.call(model, fk, association)
|
|
95
|
+
end
|
|
96
|
+
ok ||= !!model.reflect_on_all_associations(:has_one).find do |association|
|
|
97
|
+
check_relation.call(model, fk, association)
|
|
98
|
+
end
|
|
99
|
+
if block_given?
|
|
100
|
+
yield HasOneOrManyResult.new(model: model, fk: fk, ok: ok)
|
|
101
|
+
else
|
|
102
|
+
raise "expected has_many or has_one association for #{fk.inspect}" if !ok
|
|
103
|
+
end
|
|
104
|
+
next if !ok
|
|
105
|
+
|
|
106
|
+
dep = false
|
|
107
|
+
dep ||= !!model.reflect_on_all_associations(:has_many).find do |association|
|
|
108
|
+
check_relation.call(model, fk, association) && association.options[:dependent]
|
|
109
|
+
end
|
|
110
|
+
dep ||= !!model.reflect_on_all_associations(:has_one).find do |association|
|
|
111
|
+
check_relation.call(model, fk, association) && association.options[:dependent]
|
|
112
|
+
end
|
|
113
|
+
if block_given?
|
|
114
|
+
yield HasOneOrManyDependentResult.new(model: model, fk: fk, ok: ok)
|
|
115
|
+
else
|
|
116
|
+
raise "expected has_many or has_one association with dependent option for #{fk.inspect}" if !dep
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
require 'foreign_key_checker/utils'
|
|
2
|
+
|
|
3
|
+
# Стоит использовать в тех случаях, когда подключается rails к уже существующей базе
|
|
4
|
+
# Можно проверить, для каких таблиц всё ещё нет ActiveRecord-моделей
|
|
5
|
+
module ForeignKeyChecker::Checkers::Tables
|
|
6
|
+
SPECIAL_TABLES = ['schema_migrations', 'ar_internal_metadata'].freeze
|
|
7
|
+
# Список таблиц, не описанных моделями (в том числе HABTM-моделями, сгенерированными автоматически rails)
|
|
8
|
+
def self.without_models(specification_name = 'primary')
|
|
9
|
+
Rails.application.eager_load!
|
|
10
|
+
|
|
11
|
+
actual_tables = ForeignKeyChecker::Utils.get_tables
|
|
12
|
+
actual_tables - modelised_tables(specification_name) - SPECIAL_TABLES
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.without_foreign_keys(specification_name = 'primary')
|
|
16
|
+
all_fks = ForeignKeyChecker::Utils.get_foreign_keys
|
|
17
|
+
results = []
|
|
18
|
+
models(specification_name).each do |model|
|
|
19
|
+
model.column_names.each do |column_name|
|
|
20
|
+
next unless column_name.ends_with?('_id')
|
|
21
|
+
next if all_fks.find { |fk| fk.from_table = model.table_name && fk.from_column == column_name }
|
|
22
|
+
|
|
23
|
+
table_name = column_name.delete_suffix('_id')
|
|
24
|
+
results << ["#{table_name}.#{column_name}"]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.modelised_tables(specification_name = 'primary')
|
|
30
|
+
models(specification_name).map(&:table_name)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.models(specification_name = 'primary')
|
|
34
|
+
ActiveRecord::Base.descendants.select do |model|
|
|
35
|
+
model.connection_specification_name == specification_name
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def self.common_tables
|
|
41
|
+
ForeignKeyChecker::Utils.get_tables - SPECIAL_TABLES
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class Result
|
|
45
|
+
attr_reader :table_name, :foreign_keys, :internal_references
|
|
46
|
+
def initialize(**args)
|
|
47
|
+
%i[table_name foreign_keys internal_references].each do |key|
|
|
48
|
+
instance_variable_set("@#{key}", args[key] || args[key].to_s)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def ok?
|
|
53
|
+
foreign_keys.blank?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def referenced?
|
|
57
|
+
foreign_keys.present?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def ext_ref?
|
|
61
|
+
!internal_references
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.check
|
|
67
|
+
tables = without_models
|
|
68
|
+
fks = ForeignKeyChecker::Utils.get_foreign_keys_hash
|
|
69
|
+
fks.default = []
|
|
70
|
+
tables.map do |table|
|
|
71
|
+
Result.new(
|
|
72
|
+
table_name: table,
|
|
73
|
+
foreign_keys: fks[table] || [],
|
|
74
|
+
internal_references: (fks[table].map(&:from_table) - tables).empty?,
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# TODO вспомнить, что я тут задумал
|
|
80
|
+
def self.ordered(results = check)
|
|
81
|
+
return results if results.size == 0
|
|
82
|
+
oks = results.select(&:ok?)
|
|
83
|
+
if oks.size == 0
|
|
84
|
+
raise "no ok"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
module ForeignKeyChecker
|
|
2
|
+
module Utils
|
|
3
|
+
class Result
|
|
4
|
+
attr_reader :from_table, :to_table, :from_column, :to_column
|
|
5
|
+
def initialize(args)
|
|
6
|
+
args.each { |k, v| instance_variable_set("@#{k}", v) }
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
class UnsupportedConnectionAdapter < StandardError; end
|
|
10
|
+
def self.get_foreign_keys(model = ActiveRecord::Base)
|
|
11
|
+
adapter = model.connection_config[:adapter]
|
|
12
|
+
raise(UnsupportedConnectionAdapter, adapter) unless %w[postgresql mysql2 sqlserver sqlite3].include?(adapter)
|
|
13
|
+
|
|
14
|
+
connection = model.connection
|
|
15
|
+
send("get_#{adapter}_foreign_keys", connection)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.get_foreign_keys_hash(model = ActiveRecord::Base)
|
|
19
|
+
get_foreign_keys(model).to_a.each_with_object({}) do |datum, obj|
|
|
20
|
+
obj[datum.to_table] ||= []
|
|
21
|
+
obj[datum.to_table].push(datum)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.get_sqlite3_foreign_keys(connection)
|
|
26
|
+
res = connection.select_all <<-SQL
|
|
27
|
+
SELECT
|
|
28
|
+
m.name as from_table,
|
|
29
|
+
p."from" as from_column,
|
|
30
|
+
p."table" as to_table,
|
|
31
|
+
p."to" as to_column
|
|
32
|
+
FROM
|
|
33
|
+
sqlite_master m
|
|
34
|
+
JOIN pragma_foreign_key_list(m.name) p ON 1
|
|
35
|
+
WHERE m.type = 'table'
|
|
36
|
+
ORDER BY m.name ;
|
|
37
|
+
SQL
|
|
38
|
+
res.to_a.map{|i| Result.new(i) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.get_mysql2_foreign_keys(connection)
|
|
42
|
+
res = connection.select_all <<-SQL
|
|
43
|
+
SELECT
|
|
44
|
+
fks.TABLE_NAME AS from_table,
|
|
45
|
+
fks.COLUMN_NAME AS from_column,
|
|
46
|
+
fks.REFERENCED_TABLE_NAME AS to_table,
|
|
47
|
+
fks.REFERENCED_COLUMN_NAME AS to_column
|
|
48
|
+
FROM information_schema.KEY_COLUMN_USAGE AS fks
|
|
49
|
+
INNER JOIN information_schema.REFERENTIAL_CONSTRAINTS rules ON rules.CONSTRAINT_NAME = fks.CONSTRAINT_NAME
|
|
50
|
+
WHERE
|
|
51
|
+
fks.CONSTRAINT_SCHEMA = DATABASE()
|
|
52
|
+
AND rules.CONSTRAINT_SCHEMA = DATABASE();
|
|
53
|
+
SQL
|
|
54
|
+
res.to_a.map{|i| Result.new(i) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.get_postgresql_foreign_keys(connection)
|
|
58
|
+
res = connection.select_all <<-SQL
|
|
59
|
+
SELECT
|
|
60
|
+
tc.table_name AS from_table,
|
|
61
|
+
kcu.column_name AS from_column,
|
|
62
|
+
ccu.table_name AS to_table,
|
|
63
|
+
ccu.column_name AS to_column
|
|
64
|
+
FROM
|
|
65
|
+
information_schema.table_constraints AS tc
|
|
66
|
+
JOIN information_schema.key_column_usage AS kcu
|
|
67
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
68
|
+
AND tc.table_schema = kcu.table_schema
|
|
69
|
+
JOIN information_schema.constraint_column_usage AS ccu
|
|
70
|
+
ON ccu.constraint_name = tc.constraint_name
|
|
71
|
+
AND ccu.table_schema = tc.table_schema
|
|
72
|
+
WHERE tc.constraint_type = 'FOREIGN KEY';
|
|
73
|
+
SQL
|
|
74
|
+
res.to_a.map{ |i| Result.new(i) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.get_sqlserver_foreign_keys(connection)
|
|
78
|
+
res = connection.select_all <<-SQL
|
|
79
|
+
SELECT obj.name AS FK_NAME,
|
|
80
|
+
sch.name AS [schema_name],
|
|
81
|
+
tab1.name AS [from_table],
|
|
82
|
+
col1.name AS [from_column],
|
|
83
|
+
tab2.name AS [to_table],
|
|
84
|
+
col2.name AS [to_column]
|
|
85
|
+
FROM sys.foreign_key_columns fkc
|
|
86
|
+
INNER JOIN sys.objects obj
|
|
87
|
+
ON obj.object_id = fkc.constraint_object_id
|
|
88
|
+
INNER JOIN sys.tables tab1
|
|
89
|
+
ON tab1.object_id = fkc.parent_object_id
|
|
90
|
+
INNER JOIN sys.schemas sch
|
|
91
|
+
ON tab1.schema_id = sch.schema_id
|
|
92
|
+
INNER JOIN sys.columns col1
|
|
93
|
+
ON col1.column_id = parent_column_id AND col1.object_id = tab1.object_id
|
|
94
|
+
INNER JOIN sys.tables tab2
|
|
95
|
+
ON tab2.object_id = fkc.referenced_object_id
|
|
96
|
+
INNER JOIN sys.columns col2
|
|
97
|
+
ON col2.column_id = referenced_column_id AND col2.object_id = tab2.object_id
|
|
98
|
+
SQL
|
|
99
|
+
res.to_a.map { |i| Result.new(i) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def self.get_tables(model = ActiveRecord::Base)
|
|
103
|
+
adapter = model.connection_config[:adapter]
|
|
104
|
+
raise(UnsupportedConnectionAdapter, adapter) unless %w[postgresql mysql2 sqlite3 sqlserver].include?(adapter)
|
|
105
|
+
|
|
106
|
+
connection = model.connection
|
|
107
|
+
send("get_#{adapter}_tables", connection)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def self.get_mysql2_tables(connection)
|
|
111
|
+
connection.select_all("SELECT table_name FROM information_schema.tables WHERE TABLE_SCHEMA = '#{connection.current_database}'").to_a.pluck('table_name')
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.get_postgresql_tables(connection)
|
|
115
|
+
connection.select_all("SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname != 'pg_catalog' AND schemaname != 'information_schema'").to_a.pluck('tablename')
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def self.get_sqlite3_tables(connection)
|
|
119
|
+
connection.select_all("SELECT name FROM sqlite_master WHERE type='table'").to_a.pluck('name') - ['sqlite_sequence']
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def self.get_sqlserver_tables(connection)
|
|
123
|
+
connection.tables
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require 'rails/generators/active_record'
|
|
2
|
+
|
|
3
|
+
module ForeignKeyChecker
|
|
4
|
+
module Generators
|
|
5
|
+
class MigrationGenerator < ActiveRecord::Generators::Base
|
|
6
|
+
desc "generates the necessary migrations to fix foreign_key problems"
|
|
7
|
+
argument :name, type: :string, default: 'FixForeignKeys'
|
|
8
|
+
|
|
9
|
+
source_root File.expand_path('templates', __dir__)
|
|
10
|
+
|
|
11
|
+
def create_migrations
|
|
12
|
+
@class_name = name.camelcase.tr(':', '')
|
|
13
|
+
file_name = name.underscore.tr('/', '_')
|
|
14
|
+
@done = Dir.glob(Rails.root.join('db', 'migrate', '*.rb')).map do |path|
|
|
15
|
+
File.open(path).readlines.find { |line| line.include?('ActiveRecord::Migration') && line.include?(@class_name) }
|
|
16
|
+
end.compact.size
|
|
17
|
+
@checks = ForeignKeyChecker.check(zombies: false) unless @behavior == :revoke
|
|
18
|
+
if @behavior == :revoke
|
|
19
|
+
@file_suffix = "_v#{@done}" if @done > 1
|
|
20
|
+
@class_suffix = "V#{@done}" if @done > 1
|
|
21
|
+
else
|
|
22
|
+
@file_suffix = "_v#{@done + 1}" if @done > 0
|
|
23
|
+
@class_suffix = "V#{@done + 1}" if @done > 0
|
|
24
|
+
end
|
|
25
|
+
migration_template 'migrations/fix_foreign_keys.rb.erb', "db/migrate/#{file_name}#{@file_suffix}.rb"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/generators/foreign_key_checker/migration/templates/migrations/fix_foreign_keys.rb.erb
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class <%= @class_name %><%= @class_suffix %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version.to_s %>]
|
|
2
|
+
def change
|
|
3
|
+
<% @checks[:foreign_keys].each do |fk| %>
|
|
4
|
+
reversible do |dir|
|
|
5
|
+
dir.up do
|
|
6
|
+
<% if fk.nullable? %>
|
|
7
|
+
# <%= fk.to_zombie.migration %>
|
|
8
|
+
<%= fk.to_zombie.migration(set_null: true) %>
|
|
9
|
+
<% else %>
|
|
10
|
+
# column <%= fk.from_column %> can't be set to null
|
|
11
|
+
<%= fk.to_zombie.migration %>
|
|
12
|
+
<% end %>
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
<%= fk.migration %>
|
|
16
|
+
<% end %>
|
|
17
|
+
end
|
|
18
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: foreign_key_checker
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- AnatolyShirykalov
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2020-07-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -38,6 +38,104 @@ dependencies:
|
|
|
38
38
|
- - ">="
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
40
|
version: '0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: pg
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: mysql2
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: tiny_tds
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: activerecord-sqlserver-adapter
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: irb
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '0'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '0'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: e2mmap
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - ">="
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - ">="
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '0'
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: annotate
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - ">="
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '0'
|
|
132
|
+
type: :development
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - ">="
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '0'
|
|
41
139
|
description: Run task to obtain problems with your database
|
|
42
140
|
email:
|
|
43
141
|
- pipocavsobake@gmail.com
|
|
@@ -49,15 +147,20 @@ files:
|
|
|
49
147
|
- README.md
|
|
50
148
|
- Rakefile
|
|
51
149
|
- lib/foreign_key_checker.rb
|
|
150
|
+
- lib/foreign_key_checker/checkers/relations.rb
|
|
151
|
+
- lib/foreign_key_checker/checkers/tables.rb
|
|
52
152
|
- lib/foreign_key_checker/railtie.rb
|
|
153
|
+
- lib/foreign_key_checker/utils.rb
|
|
53
154
|
- lib/foreign_key_checker/version.rb
|
|
155
|
+
- lib/generators/foreign_key_checker/migration/migration_generator.rb
|
|
156
|
+
- lib/generators/foreign_key_checker/migration/templates/migrations/fix_foreign_keys.rb.erb
|
|
54
157
|
- lib/tasks/foreign_key_checker_tasks.rake
|
|
55
158
|
homepage: https://gitlab.com/pipocavsobake/foreign_key_checker
|
|
56
159
|
licenses:
|
|
57
160
|
- MIT
|
|
58
161
|
metadata:
|
|
59
162
|
allowed_push_host: https://rubygems.org
|
|
60
|
-
post_install_message:
|
|
163
|
+
post_install_message:
|
|
61
164
|
rdoc_options: []
|
|
62
165
|
require_paths:
|
|
63
166
|
- lib
|
|
@@ -72,8 +175,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
72
175
|
- !ruby/object:Gem::Version
|
|
73
176
|
version: '0'
|
|
74
177
|
requirements: []
|
|
75
|
-
rubygems_version: 3.
|
|
76
|
-
signing_key:
|
|
178
|
+
rubygems_version: 3.1.3
|
|
179
|
+
signing_key:
|
|
77
180
|
specification_version: 4
|
|
78
181
|
summary: Find problems with relations in active_record models
|
|
79
182
|
test_files: []
|