foreign_key_checker 0.2.1 → 0.5.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 +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: []
|