foreign_key_checker 0.1.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|