foreign_key_checker 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a7420be9666625e34337a8a40eefd53f36acc704b2e222d563bd5811d9b5cc7b
4
- data.tar.gz: 5abb9fc0b244a85a61022fe24f7810cb39c497197caa8c526d5099fb043add9e
3
+ metadata.gz: 14d08201b2ec53461b6eeb2da97e21b0713c8dc1f62ebeb8f15644caa1ef7484
4
+ data.tar.gz: 78454098ee4372ca7c6076bb312f5e9ebdb90074f81c88871e2903898d6e1e27
5
5
  SHA512:
6
- metadata.gz: 76a2abcb77030614a6c0a7b9c8951273796ca1ed09ef0c07ee75ec0e222a7bd8ad636d1409ee3e852e9cdfb37c93a7c8f4dee7ce433ade8a6c842a46a55ee95f
7
- data.tar.gz: 400ef91dba5578aa3e84b96875a2317066501ec8b49994c9287606e5c77e8e57b02f09d0860d38fab465d3e90c4754e43a39e0d1abda4a86be46f200821bdd65
6
+ metadata.gz: 1afaaeb393ab9ea52ca22ce080074298d13164b5f730e1fb34ff643b5c736351b433c60faed578ae46f26319361d95c9d603de9b4f862142fb9895b2b16f4515
7
+ data.tar.gz: f20849ecc864ff36304cf0b54baad81df7b805cca8a7a101e1eb5865eb71ab7951265950a1ec3c562f5b6716950b7f3272856fe000ee346b657a724a90e46c24
@@ -4,6 +4,7 @@ module ForeignKeyChecker::Checkers
4
4
  end
5
5
  require 'foreign_key_checker/checkers/relations'
6
6
  require 'foreign_key_checker/checkers/tables'
7
+ require 'foreign_key_checker/utils/belongs_to'
7
8
 
8
9
  module ForeignKeyChecker
9
10
  class TypeMismatch < StandardError; end
@@ -255,5 +256,9 @@ module ForeignKeyChecker
255
256
  def check(options = {})
256
257
  Checker.new(options).check
257
258
  end
259
+
260
+ def hm_tree(model, **args)
261
+ ForeignKeyChecker::Utils::BelongsTo.build_hm_tree(model, **args)
262
+ end
258
263
  end
259
264
  end
@@ -5,6 +5,7 @@ module ForeignKeyChecker
5
5
  end
6
6
  generators do
7
7
  require 'generators/foreign_key_checker/migration/migration_generator.rb'
8
+ require 'generators/foreign_key_checker/models_generator.rb'
8
9
  end
9
10
  end
10
11
  end
@@ -8,7 +8,7 @@ module ForeignKeyChecker
8
8
  end
9
9
  class UnsupportedConnectionAdapter < StandardError; end
10
10
  def self.get_foreign_keys(model = ActiveRecord::Base)
11
- adapter = model.connection_config[:adapter]
11
+ adapter = model.connection_db_config.configuration_hash[:adapter]
12
12
  raise(UnsupportedConnectionAdapter, adapter) unless %w[postgresql mysql2 sqlserver sqlite3].include?(adapter)
13
13
 
14
14
  connection = model.connection
@@ -100,7 +100,7 @@ module ForeignKeyChecker
100
100
  end
101
101
 
102
102
  def self.get_tables(model = ActiveRecord::Base)
103
- adapter = model.connection_config[:adapter]
103
+ adapter = model.connection_db_config.configuration_hash[:adapter]
104
104
  raise(UnsupportedConnectionAdapter, adapter) unless %w[postgresql mysql2 sqlite3 sqlserver].include?(adapter)
105
105
 
106
106
  connection = model.connection
@@ -122,5 +122,11 @@ module ForeignKeyChecker
122
122
  def self.get_sqlserver_tables(connection)
123
123
  connection.tables
124
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
125
131
  end
126
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
@@ -1,3 +1,3 @@
1
1
  module ForeignKeyChecker
2
- VERSION = '0.4.1'
2
+ VERSION = '0.5.0'
3
3
  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.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - AnatolyShirykalov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-22 00:00:00.000000000 Z
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: '0'
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: '0'
26
+ version: 6.1.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: sqlite3
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -80,20 +80,6 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: activerecord-sqlserver-adapter
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
83
  - !ruby/object:Gem::Dependency
98
84
  name: irb
99
85
  requirement: !ruby/object:Gem::Requirement
@@ -151,9 +137,12 @@ files:
151
137
  - lib/foreign_key_checker/checkers/tables.rb
152
138
  - lib/foreign_key_checker/railtie.rb
153
139
  - lib/foreign_key_checker/utils.rb
140
+ - lib/foreign_key_checker/utils/belongs_to.rb
154
141
  - lib/foreign_key_checker/version.rb
155
142
  - lib/generators/foreign_key_checker/migration/migration_generator.rb
156
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
157
146
  - lib/tasks/foreign_key_checker_tasks.rake
158
147
  homepage: https://gitlab.com/pipocavsobake/foreign_key_checker
159
148
  licenses:
@@ -175,7 +164,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
175
164
  - !ruby/object:Gem::Version
176
165
  version: '0'
177
166
  requirements: []
178
- rubygems_version: 3.1.3
167
+ rubygems_version: 3.1.4
179
168
  signing_key:
180
169
  specification_version: 4
181
170
  summary: Find problems with relations in active_record models