foreign_key_checker 0.4.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 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