foreign_key_checker 0.2.1 → 0.5.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 +6 -0
- data/lib/foreign_key_checker.rb +33 -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 +1 -0
- data/lib/foreign_key_checker/utils.rb +132 -0
- data/lib/foreign_key_checker/utils/belongs_to.rb +594 -0
- data/lib/foreign_key_checker/version.rb +1 -1
- data/lib/generators/foreign_key_checker/models_generator.rb +21 -0
- data/lib/generators/foreign_key_checker/templates/models/model.rb.erb +9 -0
- metadata +98 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 14d08201b2ec53461b6eeb2da97e21b0713c8dc1f62ebeb8f15644caa1ef7484
|
4
|
+
data.tar.gz: 78454098ee4372ca7c6076bb312f5e9ebdb90074f81c88871e2903898d6e1e27
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1afaaeb393ab9ea52ca22ce080074298d13164b5f730e1fb34ff643b5c736351b433c60faed578ae46f26319361d95c9d603de9b4f862142fb9895b2b16f4515
|
7
|
+
data.tar.gz: f20849ecc864ff36304cf0b54baad81df7b805cca8a7a101e1eb5865eb71ab7951265950a1ec3c562f5b6716950b7f3272856fe000ee346b657a724a90e46c24
|
data/README.md
CHANGED
@@ -23,6 +23,12 @@ ForeignKeyChecker.check.each do |key, result|
|
|
23
23
|
end
|
24
24
|
```
|
25
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
|
+
|
26
32
|
## Installation
|
27
33
|
Add this line to your application's Gemfile:
|
28
34
|
|
data/lib/foreign_key_checker.rb
CHANGED
@@ -1,4 +1,10 @@
|
|
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'
|
7
|
+
require 'foreign_key_checker/utils/belongs_to'
|
2
8
|
|
3
9
|
module ForeignKeyChecker
|
4
10
|
class TypeMismatch < StandardError; end
|
@@ -79,7 +85,7 @@ module ForeignKeyChecker
|
|
79
85
|
to_t = model.connection.quote_table_name(to_table)
|
80
86
|
to_c = model.connection.quote_column_name(to_column)
|
81
87
|
#"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"
|
82
|
-
"DELETE FROM #{from_t} WHERE #{from_c} IS NOT NULL AND #{from_c} NOT IN (SELECT #{to_c} FROM #{to_t})"
|
88
|
+
"DELETE FROM #{from_t} WHERE #{from_c} IS NOT NULL AND #{from_c} NOT IN (SELECT * FROM (SELECT #{to_c} FROM #{to_t}) AS t )"
|
83
89
|
end
|
84
90
|
|
85
91
|
def set_null_sql
|
@@ -88,7 +94,7 @@ module ForeignKeyChecker
|
|
88
94
|
to_t = model.connection.quote_table_name(to_table)
|
89
95
|
to_c = model.connection.quote_column_name(to_column)
|
90
96
|
#"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"
|
91
|
-
"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
|
+
"UPDATE #{from_t} SET #{from_c} = NULL WHERE #{from_c} IS NOT NULL AND #{from_c} NOT IN (SELECT * FROM (SELECT #{to_c} FROM #{to_t}) AS t )"
|
92
98
|
end
|
93
99
|
|
94
100
|
def set_null_migration
|
@@ -161,12 +167,21 @@ module ForeignKeyChecker
|
|
161
167
|
def check_types(model, association)
|
162
168
|
type_from = model.columns_hash[association.foreign_key.to_s].sql_type
|
163
169
|
type_to = association.klass.columns_hash[association.klass.primary_key.to_s].sql_type
|
164
|
-
raise TypeMismatch, "TypeMissMatch #{type_from} != #{type_to}" if type_from != type_to
|
170
|
+
raise TypeMismatch, "TypeMissMatch for relation #{model}##{association.name} #{type_from} != #{type_to}" if type_from != type_to
|
165
171
|
end
|
166
172
|
|
167
173
|
def check_foreign_key_bt_association(model, association)
|
168
174
|
return if model.name.starts_with?('HABTM_')
|
169
|
-
|
175
|
+
begin
|
176
|
+
related = association.klass
|
177
|
+
rescue NameError => error
|
178
|
+
@result[:broken] << BrokenRelationResult.new(
|
179
|
+
model: model,
|
180
|
+
association: association,
|
181
|
+
error: error,
|
182
|
+
)
|
183
|
+
return
|
184
|
+
end
|
170
185
|
|
171
186
|
column_name = model.connection.quote_column_name(association.foreign_key)
|
172
187
|
scope = model.left_outer_joins(association.name).where(
|
@@ -187,7 +202,7 @@ module ForeignKeyChecker
|
|
187
202
|
end
|
188
203
|
end
|
189
204
|
|
190
|
-
if foreign_keys && !model.connection.foreign_key_exists?(model.table_name, related.table_name)
|
205
|
+
if foreign_keys && !model.connection.foreign_key_exists?(model.table_name, related.table_name, column: association.foreign_key, primary_key: related.primary_key)
|
191
206
|
scope.first
|
192
207
|
@result[:foreign_keys] << ForeignKeyResult.new(
|
193
208
|
model: model,
|
@@ -210,6 +225,13 @@ module ForeignKeyChecker
|
|
210
225
|
)
|
211
226
|
end
|
212
227
|
|
228
|
+
def already_done_fk?(model, association)
|
229
|
+
@_done ||= {}
|
230
|
+
ret = @_done[[model.table_name, association.foreign_key]]
|
231
|
+
@_done[[model.table_name, association.foreign_key]] = true
|
232
|
+
ret
|
233
|
+
end
|
234
|
+
|
213
235
|
def check
|
214
236
|
Rails.application.eager_load!
|
215
237
|
ActiveRecord::Base.descendants.each do |model|
|
@@ -217,11 +239,13 @@ module ForeignKeyChecker
|
|
217
239
|
next if excluded_specification?(model)
|
218
240
|
|
219
241
|
model.reflect_on_all_associations(:belongs_to).each do |association|
|
242
|
+
|
220
243
|
if association.options[:polymorphic] && polymorphic_zombies
|
221
244
|
check_polymorphic_bt_association(model, association)
|
222
245
|
next
|
223
246
|
end
|
224
247
|
|
248
|
+
next if already_done_fk?(model, association)
|
225
249
|
check_foreign_key_bt_association(model, association)
|
226
250
|
end
|
227
251
|
end
|
@@ -232,5 +256,9 @@ module ForeignKeyChecker
|
|
232
256
|
def check(options = {})
|
233
257
|
Checker.new(options).check
|
234
258
|
end
|
259
|
+
|
260
|
+
def hm_tree(model, **args)
|
261
|
+
ForeignKeyChecker::Utils::BelongsTo.build_hm_tree(model, **args)
|
262
|
+
end
|
235
263
|
end
|
236
264
|
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,132 @@
|
|
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_db_config.configuration_hash[: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_db_config.configuration_hash[: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
|
+
|
126
|
+
def self.get_columns(connection)
|
127
|
+
get_tables.each_with_object({}) do |table, object|
|
128
|
+
object[table] = connection.columns(table)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,594 @@
|
|
1
|
+
module ForeignKeyChecker
|
2
|
+
module Utils
|
3
|
+
module BelongsTo
|
4
|
+
CONST_REGEXP = /\A[A-Z]\p{Alnum}*(::\p{Alnum}+)*\z/
|
5
|
+
class Result
|
6
|
+
attr_reader :primary_table, :dependant_table, :foreign_key, :foreign_type, :types, :polymorphic, :inverse_of, :name, :connection
|
7
|
+
attr_writer :build_class_name
|
8
|
+
def initialize(**args)
|
9
|
+
args.each do |key, value|
|
10
|
+
instance_variable_set("@#{key}", value)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def association_code
|
15
|
+
if inverse_of
|
16
|
+
has_many_code
|
17
|
+
else
|
18
|
+
belongs_to_code
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def inversed_association(table)
|
23
|
+
@inversed_associations ||= {}
|
24
|
+
@inversed_associations[table] = build_inversed_association(table)
|
25
|
+
@inversed_associations[table].build_class_name = @build_class_name if @build_class_name
|
26
|
+
@inversed_associations[table]
|
27
|
+
end
|
28
|
+
|
29
|
+
def class_name
|
30
|
+
build_class_name
|
31
|
+
end
|
32
|
+
|
33
|
+
def model
|
34
|
+
return class_name.constantize if class_name.is_a?(String)
|
35
|
+
|
36
|
+
class_name
|
37
|
+
end
|
38
|
+
|
39
|
+
def primary_model
|
40
|
+
kls = build_primary_class_name
|
41
|
+
return kls.constantize if kls.is_a?(String)
|
42
|
+
|
43
|
+
kls
|
44
|
+
end
|
45
|
+
|
46
|
+
def dependant_model
|
47
|
+
kls = build_dependant_class_name
|
48
|
+
return kls.constantize if kls.is_a?(String)
|
49
|
+
|
50
|
+
kls
|
51
|
+
rescue NameError => e
|
52
|
+
puts "#{e.class.to_s} #{e.message} #{e.backtrace.first}"
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
|
56
|
+
def delete_sql(ids)
|
57
|
+
dependent_sql(ids, prefix: 'DELETE FROM')
|
58
|
+
end
|
59
|
+
|
60
|
+
def select_sql_with_fk(ids)
|
61
|
+
dependent_sql(ids, prefix: "SELECT id, #{connection.quote_column_name(foreign_key)} FROM")
|
62
|
+
end
|
63
|
+
|
64
|
+
def select_sql(ids)
|
65
|
+
dependent_sql(ids)
|
66
|
+
end
|
67
|
+
|
68
|
+
def nullify_sql(ids)
|
69
|
+
dependent_sql(ids, prefix: 'UPDATE', suffix: nullify_suffix)
|
70
|
+
end
|
71
|
+
|
72
|
+
def has_many?
|
73
|
+
!belongs_to?
|
74
|
+
end
|
75
|
+
|
76
|
+
def belongs_to?
|
77
|
+
!inverse_of
|
78
|
+
end
|
79
|
+
|
80
|
+
def inspect
|
81
|
+
"#{class_name}.#{association_code}"
|
82
|
+
end
|
83
|
+
|
84
|
+
def conflict_with_fk?(fk)
|
85
|
+
return false if polymorphic
|
86
|
+
|
87
|
+
fk.from_column == foreign_key && fk.from_table == dependant_table && (fk.to_table != primary_table || fk.to_column != 'id')
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
def dependent_sql(ids, prefix: 'SELECT id FROM', suffix: '')
|
92
|
+
return nil if belongs_to?
|
93
|
+
return nil if ids.empty?
|
94
|
+
return if class_name.is_a?(ActiveRecord::Base) && class_name.primary_key.nil?
|
95
|
+
|
96
|
+
conn = connection
|
97
|
+
if polymorphic
|
98
|
+
[
|
99
|
+
"#{prefix} #{conn.quote_table_name(dependant_table.to_s)} #{suffix} WHERE #{conn.quote_column_name(foreign_key)} IN (#{ids.map(&:to_i).join(',')}) AND #{conn.quote_column_name(foreign_type)} = $1",
|
100
|
+
nil,
|
101
|
+
[class_name.to_s],
|
102
|
+
]
|
103
|
+
else
|
104
|
+
["#{prefix} #{conn.quote_table_name(dependant_table.to_s)} #{suffix} WHERE #{conn.quote_column_name(foreign_key)} IN (#{ids.map(&:to_i).join(',')})"]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def nullify_suffix
|
109
|
+
conn = connection
|
110
|
+
if polymorphic
|
111
|
+
"SET #{conn.quote_column_name(foreign_key)} = NULL, #{conn.quote_column_name(foreign_type)} = NULL"
|
112
|
+
else
|
113
|
+
"SET #{conn.quote_column_name(foreign_key)} = NULL"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def build_inversed_association(table)
|
118
|
+
table = table.table_name if table.is_a?(ActiveRecord::Base)
|
119
|
+
if polymorphic
|
120
|
+
self.class.new(primary_table: table, dependant_table: dependant_table, foreign_key: foreign_key, foreign_type: foreign_type, polymorphic: true, inverse_of: self, connection: connection)
|
121
|
+
else
|
122
|
+
self.class.new(primary_table: primary_table, connection: connection, dependant_table: dependant_table, foreign_key: foreign_key, inverse_of: self)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def has_many_code
|
127
|
+
if polymorphic
|
128
|
+
has_many_polymorphic_code
|
129
|
+
else
|
130
|
+
has_many_simple_code
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def belongs_to_code
|
135
|
+
if polymorphic
|
136
|
+
"belongs_to :#{belongs_to_relation_name}, polymorphic: true, foreign_key: :#{foreign_key}, foreign_type: :#{foreign_type}"
|
137
|
+
else
|
138
|
+
"belongs_to :#{belongs_to_relation_name}, class_name: '#{build_primary_class_name}', foreign_key: :#{foreign_key}"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def has_many_polymorphic_code
|
143
|
+
"has_many :#{has_many_relation_name}, class_name: '#{build_dependant_class_name}', polymorphic: true, foreign_key: :#{foreign_key}, foreign_type: :#{foreign_type}, dependent: :destroy"
|
144
|
+
end
|
145
|
+
|
146
|
+
def has_many_simple_code
|
147
|
+
"has_many :#{has_many_relation_name}, class_name: '#{build_dependant_class_name}', foreign_key: :#{foreign_key}, dependent: :destroy"
|
148
|
+
end
|
149
|
+
|
150
|
+
def has_many_relation_name
|
151
|
+
dependant_table.tr('.', '_').underscore.pluralize
|
152
|
+
end
|
153
|
+
|
154
|
+
def belongs_to_relation_name
|
155
|
+
foreign_key.delete_suffix('_id')
|
156
|
+
#return polymorphic_belongs_to_relation_name if polymorphic
|
157
|
+
#primary_table.tr('.', '_').underscore.singularize
|
158
|
+
end
|
159
|
+
|
160
|
+
def polymorphic_belongs_to_relation_name
|
161
|
+
@name
|
162
|
+
end
|
163
|
+
|
164
|
+
def build_class_name(arg = _table)
|
165
|
+
@build_class_name.call(arg)
|
166
|
+
end
|
167
|
+
|
168
|
+
def build_primary_class_name
|
169
|
+
build_class_name(primary_table)
|
170
|
+
end
|
171
|
+
|
172
|
+
def build_dependant_class_name
|
173
|
+
build_class_name(dependant_table)
|
174
|
+
end
|
175
|
+
|
176
|
+
def type_to_table(type)
|
177
|
+
@type_to_table.is_a?(Proc) ? @type_to_table.call(type) : type.underscore.pluralize
|
178
|
+
end
|
179
|
+
|
180
|
+
def _table
|
181
|
+
return dependant_table if belongs_to?
|
182
|
+
|
183
|
+
primary_table
|
184
|
+
end
|
185
|
+
end
|
186
|
+
class TableLevel
|
187
|
+
attr_reader :connection, :table, :columns, :columns_of_table, :polymorphic_suffixes, :fks
|
188
|
+
def initialize(connection, table, columns_of_table, polymorphic_suffixes, fks:, on_error: proc { } )
|
189
|
+
@connection = connection
|
190
|
+
@table = table
|
191
|
+
@columns = columns_of_table[table]
|
192
|
+
@columns_of_table = columns_of_table
|
193
|
+
@polymorphic_suffixes = polymorphic_suffixes
|
194
|
+
@on_error = on_error
|
195
|
+
@fks = fks
|
196
|
+
end
|
197
|
+
|
198
|
+
def foreign_key_suffix
|
199
|
+
'_id'
|
200
|
+
end
|
201
|
+
|
202
|
+
def candidates
|
203
|
+
columns.select { |column| column.name.ends_with?(foreign_key_suffix) }.each_with_object([]) do |column, object|
|
204
|
+
level = ColumnLevel.new(self, column)
|
205
|
+
level.errors.each(&@on_error)
|
206
|
+
[level.association].compact.each do |ass|
|
207
|
+
next if fks.find { |fk| ass.conflict_with_fk?(fk) }
|
208
|
+
|
209
|
+
object.push(ass)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
class ColumnLevel
|
215
|
+
attr_reader :table_level, :column
|
216
|
+
def initialize(table_level, column)
|
217
|
+
@table_level = table_level
|
218
|
+
@column = column
|
219
|
+
@errors = []
|
220
|
+
@association_name = column.name.delete_suffix(table_level.foreign_key_suffix)
|
221
|
+
@polymorphic_column_names = polymorphic_column_names
|
222
|
+
end
|
223
|
+
|
224
|
+
def association
|
225
|
+
perform if !@done
|
226
|
+
@association
|
227
|
+
end
|
228
|
+
|
229
|
+
def errors
|
230
|
+
perform if !@done
|
231
|
+
@errors
|
232
|
+
end
|
233
|
+
|
234
|
+
def polymorphic?
|
235
|
+
perform if !@done
|
236
|
+
@polymorphic
|
237
|
+
end
|
238
|
+
|
239
|
+
private
|
240
|
+
|
241
|
+
def perform
|
242
|
+
check_polymorphic!
|
243
|
+
check_common!
|
244
|
+
@done = true
|
245
|
+
end
|
246
|
+
|
247
|
+
def check_polymorphic!
|
248
|
+
if @polymorphic_column_names.size > 1
|
249
|
+
@errors.push("Can't determine polymorphic column for association #{@association_name}; candidates: #{@polymorphic_columns.join(', ')}")
|
250
|
+
elsif @polymorphic_column_names.size == 1
|
251
|
+
conn = table_level.connection
|
252
|
+
foreign_type = @polymorphic_column_names.first
|
253
|
+
types = conn.select_all("SELECT DISTINCT #{conn.quote_column_name(foreign_type)} FROM #{conn.quote_table_name(table_level.table)}").rows.map(&:first).compact.select { |tp| tp.match(CONST_REGEXP) }
|
254
|
+
@association = Result.new(polymorphic: true, foreign_key: column.name, foreign_type: foreign_type, types: types, name: @association_name, dependant_table: table_level.table, connection: conn)
|
255
|
+
@polymorphic = true
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def check_common!
|
260
|
+
if @polymorphic_column_names.size != 0
|
261
|
+
return
|
262
|
+
end
|
263
|
+
matched_table = table_level.columns_of_table.keys.find do |table1|
|
264
|
+
next false if table1.singularize != @association_name
|
265
|
+
next false if table_level.columns_of_table[table1].find { |col| col.name == 'id' }&.type != column.type
|
266
|
+
true
|
267
|
+
end
|
268
|
+
if matched_table
|
269
|
+
@association = Result.new(polymorphic: false, foreign_key: column.name, primary_table: matched_table, dependant_table: table_level.table, connection: table_level.connection)
|
270
|
+
@polymorphic = false
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
def polymorphic_column_names
|
275
|
+
table_level.polymorphic_suffixes.map do |polymorphic_suffix|
|
276
|
+
column_candidate_name = "#{@association_name}#{polymorphic_suffix}"
|
277
|
+
next unless table_level.columns.find { |col| col.name == column_candidate_name && col.type == :string }
|
278
|
+
|
279
|
+
column_candidate_name
|
280
|
+
end.compact
|
281
|
+
end
|
282
|
+
|
283
|
+
end
|
284
|
+
|
285
|
+
def self.build_table_mapping(model: ActiveRecord::Base, module_if_gt: 3)
|
286
|
+
Rails.application.eager_load!
|
287
|
+
base_class = model
|
288
|
+
loop do
|
289
|
+
if !base_class.superclass.respond_to?(:connection) || base_class.superclass.connection != base_class.connection
|
290
|
+
break
|
291
|
+
end
|
292
|
+
base_class = base_class.superclass
|
293
|
+
end
|
294
|
+
@table_mappings ||= {}
|
295
|
+
@table_mappings[base_class] ||= begin
|
296
|
+
start_hash = base_class.descendants.reject do |_model|
|
297
|
+
_model.abstract_class || (_model.connection != model.connection)
|
298
|
+
end.group_by(&:table_name).transform_values do |models|
|
299
|
+
next models.first if models.size == 1
|
300
|
+
|
301
|
+
root_models = models.map do |_model|
|
302
|
+
if _model.to_s.demodulize.starts_with?("HABTM_")
|
303
|
+
next
|
304
|
+
end
|
305
|
+
loop do
|
306
|
+
break if !_model.superclass.respond_to?(:abstract_class?) || _model.superclass.table_name.nil?
|
307
|
+
|
308
|
+
_model = _model.superclass
|
309
|
+
end
|
310
|
+
_model
|
311
|
+
end.compact.uniq
|
312
|
+
if root_models.size > 1
|
313
|
+
raise "More than one root model for table #{models.first.table_name}: #{root_models.inspect}"
|
314
|
+
end
|
315
|
+
root_models.first
|
316
|
+
end
|
317
|
+
all_tables = ForeignKeyChecker::Utils.get_tables(model)
|
318
|
+
#group_tables(all_tables)
|
319
|
+
all_tables.each_with_object(start_hash) do |table, object|
|
320
|
+
object[table] ||= table.tr('.', '_').singularize.camelize
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
def self.group_tables(tables, level: 1)
|
326
|
+
if tables.size < 2
|
327
|
+
return tables
|
328
|
+
end
|
329
|
+
tables.group_by do |table|
|
330
|
+
table.split(/[\._]/).first(level).join('_')
|
331
|
+
end.transform_values do |children|
|
332
|
+
group_tables(children, level: level + 1)
|
333
|
+
end
|
334
|
+
end
|
335
|
+
def self.column_name_candidates(connection, polymorphic_suffixes: ['_type'], on_error: proc {})
|
336
|
+
columns_of_table = ForeignKeyChecker::Utils.get_columns(connection)
|
337
|
+
columns_of_table.each_with_object({}) do |(table, columns), object|
|
338
|
+
object[table] = TableLevel.new(connection, table, columns_of_table, polymorphic_suffixes, on_error: on_error, fks: fks).candidates
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
def self.all_candidates(connection, polymorphic_suffixes: ['_type'], on_error: proc {})
|
343
|
+
merge_candidates(
|
344
|
+
fk_candidates(connection, polymorphic_suffixes: polymorphic_suffixes, on_error: on_error),
|
345
|
+
column_name_candidates(connection, polymorphic_suffixes: polymorphic_suffixes, on_error: on_error)
|
346
|
+
)
|
347
|
+
end
|
348
|
+
|
349
|
+
def self.all_association_candidates(connection, polymorphic_suffixes: ['_type'], on_error: proc {})
|
350
|
+
belongs_to = all_candidates(connection, polymorphic_suffixes: ['_type'], on_error: proc {})
|
351
|
+
has_many = inverse(belongs_to)
|
352
|
+
belongs_to.each_with_object({}) do |(table, bt_associations), object|
|
353
|
+
object[table] = {belongs_to: bt_associations, has_many: has_many[table] || []}
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
def self.inverse(hash)
|
358
|
+
hash.each_with_object({}) do |(table, bt_associations), object|
|
359
|
+
bt_associations.each do |association|
|
360
|
+
object[association.primary_table] ||= []
|
361
|
+
object[association.primary_table].push(association.inversed_association(table))
|
362
|
+
end
|
363
|
+
end.transform_values do |associations|
|
364
|
+
associations.uniq do |result|
|
365
|
+
"#{result.polymorphic} #{result.foreign_key} #{result.primary_table} #{result.dependant_table} #{result.name} #{result.types&.join(',')}"
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
def self.fks
|
371
|
+
@fks ||= ForeignKeyChecker::Utils.get_foreign_keys
|
372
|
+
end
|
373
|
+
|
374
|
+
def self.fk_candidates(connection, polymorphic_suffixes: ['_type'], on_error: proc {})
|
375
|
+
fks.to_a.each_with_object({}) do |datum, obj|
|
376
|
+
obj[datum.from_table] ||= []
|
377
|
+
obj[datum.from_table].push(Result.new(polymorphic: false, foreign_key: datum.from_column, dependant_table: datum.from_table, primary_table: datum.to_table, connection: connection))
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
def self.merge_candidates(*hashes)
|
382
|
+
hashes.each_with_object({}) do |hash, object|
|
383
|
+
hash.each do |table, candidates|
|
384
|
+
object[table] ||= []
|
385
|
+
object[table] = (object[table] + candidates).uniq do |result|
|
386
|
+
"#{result.polymorphic} #{result.foreign_key} #{result.primary_table} #{result.dependant_table} #{result.name} #{result.types&.join(',')}"
|
387
|
+
end
|
388
|
+
end
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
# ForeignKeyChecker::Utils::BelongsTo.build_classes(User)
|
393
|
+
def self.build_classes(model, polymorphic_suffixes: ['_type'], on_error: proc {})
|
394
|
+
@build_classes ||= begin
|
395
|
+
mapping = build_table_mapping(model: model)
|
396
|
+
build_class_name = proc { |table| mapping[table] }
|
397
|
+
all_association_candidates(model.connection, polymorphic_suffixes: polymorphic_suffixes, on_error: on_error).map do |table, hash|
|
398
|
+
hash.transform_values { |results| results.map { |result| result.build_class_name = build_class_name; result }}.merge(
|
399
|
+
table_name: table,
|
400
|
+
class_name: mapping[table],
|
401
|
+
)
|
402
|
+
end
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
# ForeignKeyChecker::Utils::BelongsTo.build_delete_chain(City.where(slug: 'paris'), {countries: :capital_city_id, cities: %i[curator_id main_city_id]})
|
407
|
+
# ForeignKeyChecker::Utils::BelongsTo.build_delete_chain(City.where(slug: 'paris'), {countries: :capital_city_id, cities: %i[curator_id main_city_id], local_profiles: :published_edition_id, hint_places: :place_image_id}, {})
|
408
|
+
def self.build_delete_chain(scope, nullify = {}, **args)
|
409
|
+
tree = build_hm_tree(scope.model, **args)
|
410
|
+
all_ids = []
|
411
|
+
dependencies = {}
|
412
|
+
triggers = {}
|
413
|
+
cmds = [proc { |*args| __ids = scope.pluck(:id); all_ids << [scope.model.table_name, __ids, []]; __ids}]
|
414
|
+
process_tree = proc do |_tree, way|
|
415
|
+
_tree.each do |table, data|
|
416
|
+
cmds << proc do |*args|
|
417
|
+
primary_table = data[:associations].first.primary_table
|
418
|
+
ids = all_ids.select { |tn, __ids| tn == primary_table }.map(&:second).reduce(&:+).uniq
|
419
|
+
_id_fks = []
|
420
|
+
data[:associations].each do |association|
|
421
|
+
association.select_sql_with_fk(ids.dup).try do |sql|
|
422
|
+
begin
|
423
|
+
if Array.wrap(nullify[association.dependant_table.to_sym]).include?(association.foreign_key.to_sym)
|
424
|
+
ids.in_groups_of(1000, false).each do |group|
|
425
|
+
all_ids << [";sql;", association.nullify_sql(group), way]
|
426
|
+
end
|
427
|
+
next
|
428
|
+
end
|
429
|
+
|
430
|
+
p way
|
431
|
+
_id_fks += association.connection.select_all(*sql).rows
|
432
|
+
rescue => e
|
433
|
+
raise e unless e.message.tr('"', '').include?("column id does not exist")
|
434
|
+
ids.in_groups_of(1000, false).each do |group|
|
435
|
+
all_ids << [";sql;", association.delete_sql(group), way]
|
436
|
+
end
|
437
|
+
puts e.message
|
438
|
+
end
|
439
|
+
end
|
440
|
+
end
|
441
|
+
_id_fks.each do |_id, _fk|
|
442
|
+
dependencies[primary_table] ||= {}
|
443
|
+
dependencies[primary_table][_fk] ||= Set.new
|
444
|
+
dependencies[primary_table][_fk] << [table, _id]
|
445
|
+
|
446
|
+
triggers[table] ||= {}
|
447
|
+
triggers[table][_id] ||= Set.new
|
448
|
+
triggers[table][_id] << [primary_table, _fk]
|
449
|
+
end
|
450
|
+
_ids = _id_fks.map(&:first).uniq - (all_ids.select { |tn, __ids| tn == table }.map(&:second).reduce([], &:+).uniq)
|
451
|
+
if _ids.any?
|
452
|
+
all_ids << [table, _ids, way]
|
453
|
+
data[:children].presence.try { |ch| process_tree.call(ch, way + [table]) }
|
454
|
+
end
|
455
|
+
_ids
|
456
|
+
end
|
457
|
+
end
|
458
|
+
end
|
459
|
+
process_tree.call(tree, [scope.model.table_name])
|
460
|
+
arg = nil
|
461
|
+
cmds.each do |cmd|
|
462
|
+
arg = cmd.call(arg)
|
463
|
+
end
|
464
|
+
sql_queries = all_ids.select { |a, b, c| a == ';sql;' }.map(&:second)
|
465
|
+
t_ids = all_ids.reject { |a, b, c| a == ';sql;' }.group_by(&:first).transform_values { |vs| vs.map(&:second).reduce([], &:+).uniq }.to_a
|
466
|
+
loop do
|
467
|
+
break unless t_ids.map(&:second).any?(&:any?)
|
468
|
+
any = false
|
469
|
+
t_ids.each do |table, ids|
|
470
|
+
_ids = ids.select { |id| dependencies.dig(table, id).blank? }
|
471
|
+
ids.reject! { |id| dependencies.dig(table, id).blank? }
|
472
|
+
if _ids.present?
|
473
|
+
any = true
|
474
|
+
_ids.in_groups_of(1000, false).each do |ids_group|
|
475
|
+
sql_queries << ["DELETE FROM #{scope.model.connection.quote_table_name(table)} WHERE id IN (#{ids_group.map(&:to_i).join(',')})"]
|
476
|
+
end
|
477
|
+
_ids.each do |id|
|
478
|
+
triggers.dig(table, id)&.each do |keys|
|
479
|
+
dependencies.dig(*keys).delete([table, id])
|
480
|
+
end
|
481
|
+
end
|
482
|
+
end
|
483
|
+
end
|
484
|
+
unless any
|
485
|
+
binding.pry
|
486
|
+
puts "Cannot destroy these objects. Check cyclic relation chains:\n#{find_cycles(tree, [scope.model.table_name]).inspect}"
|
487
|
+
return []
|
488
|
+
end
|
489
|
+
end
|
490
|
+
sql_queries
|
491
|
+
end
|
492
|
+
class WayPoint
|
493
|
+
attr_reader :table
|
494
|
+
def initialize(foreign_key, table)
|
495
|
+
@foreign_key = foreign_key
|
496
|
+
@table = table
|
497
|
+
end
|
498
|
+
|
499
|
+
def inspect
|
500
|
+
return @table if @foreign_key.blank?
|
501
|
+
|
502
|
+
"->#{@foreign_key}(#{@table})"
|
503
|
+
end
|
504
|
+
|
505
|
+
def ==(value)
|
506
|
+
return @table == value if value.is_a?(String)
|
507
|
+
|
508
|
+
value.table == @table
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
def self.find_ways(node, way)
|
513
|
+
way = way.map { |wp| next WayPoint.new(nil, wp) if wp.is_a?(String); wp}
|
514
|
+
return [way] if node.nil?
|
515
|
+
node.each_with_object([]) do |(key, item), obj|
|
516
|
+
tails = if way.include?(key)
|
517
|
+
[way + [key]]
|
518
|
+
else
|
519
|
+
find_ways(item[:children], way + [key])
|
520
|
+
end
|
521
|
+
item[:associations].each do |ass|
|
522
|
+
tails.each do |tail|
|
523
|
+
obj.push((tail[0...way.size]) + [WayPoint.new(ass.foreign_key, ass.dependant_table)] + (tail[(way.size + 1)..-1] || []))
|
524
|
+
end
|
525
|
+
end
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
def self.find_cycles(tree, start)
|
530
|
+
find_ways(tree, start).each_with_object([]) do |way, obj|
|
531
|
+
way[0..-2].index(way.last).try do |idx|
|
532
|
+
obj.push ([WayPoint.new(nil, way[idx].table)] + way[idx+1..-1])
|
533
|
+
end
|
534
|
+
end.uniq(&:inspect)
|
535
|
+
end
|
536
|
+
|
537
|
+
# ForeignKeyChecker::Utils::BelongsTo.ways_for(User)
|
538
|
+
def self.ways_for(model)
|
539
|
+
find_ways(build_hm_tree(model), [model.table_name])
|
540
|
+
end
|
541
|
+
|
542
|
+
# ForeignKeyChecker::Utils::BelongsTo.ways_for(User)
|
543
|
+
def self.cycles_for(model)
|
544
|
+
find_cycles(build_hm_tree(model), [model.table_name])
|
545
|
+
end
|
546
|
+
|
547
|
+
# ForeignKeyChecker::Utils::BelongsTo.build_hm_tree(User)
|
548
|
+
# построит дерево зависимостей от модели User
|
549
|
+
#find_cycle = proc { |node, way| puts way.join('->'); next [way] if node.nil?; node.each_with_object([]) { |(key, item), obj| ways = way.include?(key) ? [way + [key]] : find_cycle.call(item[:children], way + [key]); ways.each { |w| obj.push(w) } } v}
|
550
|
+
def self.build_hm_tree(model, **args)
|
551
|
+
mapping = build_table_mapping(model: model)
|
552
|
+
hash = build_classes(model, **args).group_by do |result|
|
553
|
+
result[:table_name]
|
554
|
+
end
|
555
|
+
found = {}
|
556
|
+
processing = {}
|
557
|
+
should_fill = []
|
558
|
+
find_children = proc do |c_table_name|
|
559
|
+
processing[c_table_name] = true
|
560
|
+
hash[c_table_name].each_with_object({}) do |results, object|
|
561
|
+
perform_result = proc do |result|
|
562
|
+
key = result.dependant_table
|
563
|
+
object[key] ||= {associations: []}
|
564
|
+
object[key][:associations].push(result)
|
565
|
+
|
566
|
+
if processing[key]
|
567
|
+
should_fill.push([key, object[key]])
|
568
|
+
next
|
569
|
+
end
|
570
|
+
|
571
|
+
found[key] ||= find_children.call(key)
|
572
|
+
object[key][:children] ||= found[key]
|
573
|
+
end
|
574
|
+
results[:has_many].each(&perform_result)
|
575
|
+
hash.each_value do |hs|
|
576
|
+
hs.each do |h|
|
577
|
+
h[:belongs_to].each do |r|
|
578
|
+
next unless r.polymorphic && r.types.include?(mapping[c_table_name])
|
579
|
+
|
580
|
+
perform_result.call(r.inversed_association(c_table_name))
|
581
|
+
end
|
582
|
+
end
|
583
|
+
end
|
584
|
+
end
|
585
|
+
end
|
586
|
+
ret = find_children.call(model.table_name)
|
587
|
+
should_fill.each do |table_name, item|
|
588
|
+
item[:children] = found[table_name]
|
589
|
+
end
|
590
|
+
ret
|
591
|
+
end
|
592
|
+
end
|
593
|
+
end
|
594
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'rails/generators/active_record'
|
2
|
+
module ForeignKeyChecker
|
3
|
+
module Generators
|
4
|
+
class ModelsGenerator < ActiveRecord::Generators::Base
|
5
|
+
desc "generates models for all tables in development database"
|
6
|
+
argument :name, type: :string, default: 'FixForeignKeys'
|
7
|
+
|
8
|
+
source_root File.expand_path('templates', __dir__)
|
9
|
+
|
10
|
+
def install
|
11
|
+
ForeignKeyChecker::Utils::BelongsTo.build_classes(ActiveRecord::Base.connection).each do |object|
|
12
|
+
file_path = "app/models/#{object[:class_name].underscore}.rb"
|
13
|
+
@object = object
|
14
|
+
template 'models/model.rb.erb', file_path
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
class <%= @object[:class_name] %> < ApplicationRecord
|
2
|
+
self.table_name = '<%= @object[:table_name] %>'
|
3
|
+
<% @object[:belongs_to].each do |bt| %>
|
4
|
+
<%= bt.association_code %>
|
5
|
+
<% end %>
|
6
|
+
<% @object[:has_many].each do |bt| %>
|
7
|
+
<%= bt.association_code %>
|
8
|
+
<% end %>
|
9
|
+
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.5.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: 2021-03-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 6.1.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 6.1.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: sqlite3
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -38,6 +38,90 @@ 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: irb
|
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: e2mmap
|
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: annotate
|
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'
|
41
125
|
description: Run task to obtain problems with your database
|
42
126
|
email:
|
43
127
|
- pipocavsobake@gmail.com
|
@@ -49,17 +133,23 @@ files:
|
|
49
133
|
- README.md
|
50
134
|
- Rakefile
|
51
135
|
- lib/foreign_key_checker.rb
|
136
|
+
- lib/foreign_key_checker/checkers/relations.rb
|
137
|
+
- lib/foreign_key_checker/checkers/tables.rb
|
52
138
|
- lib/foreign_key_checker/railtie.rb
|
139
|
+
- lib/foreign_key_checker/utils.rb
|
140
|
+
- lib/foreign_key_checker/utils/belongs_to.rb
|
53
141
|
- lib/foreign_key_checker/version.rb
|
54
142
|
- lib/generators/foreign_key_checker/migration/migration_generator.rb
|
55
143
|
- lib/generators/foreign_key_checker/migration/templates/migrations/fix_foreign_keys.rb.erb
|
144
|
+
- lib/generators/foreign_key_checker/models_generator.rb
|
145
|
+
- lib/generators/foreign_key_checker/templates/models/model.rb.erb
|
56
146
|
- lib/tasks/foreign_key_checker_tasks.rake
|
57
147
|
homepage: https://gitlab.com/pipocavsobake/foreign_key_checker
|
58
148
|
licenses:
|
59
149
|
- MIT
|
60
150
|
metadata:
|
61
151
|
allowed_push_host: https://rubygems.org
|
62
|
-
post_install_message:
|
152
|
+
post_install_message:
|
63
153
|
rdoc_options: []
|
64
154
|
require_paths:
|
65
155
|
- lib
|
@@ -74,8 +164,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
164
|
- !ruby/object:Gem::Version
|
75
165
|
version: '0'
|
76
166
|
requirements: []
|
77
|
-
rubygems_version: 3.
|
78
|
-
signing_key:
|
167
|
+
rubygems_version: 3.1.4
|
168
|
+
signing_key:
|
79
169
|
specification_version: 4
|
80
170
|
summary: Find problems with relations in active_record models
|
81
171
|
test_files: []
|