Module: ForeignKeyChecker::Utils::BelongsTo
- Defined in:
- lib/foreign_key_checker/utils/belongs_to.rb
Defined Under Namespace
Classes: ColumnLevel, Result, TableLevel, WayPoint
Constant Summary collapse
- CONST_REGEXP =
/\A[A-Z]\p{Alnum}*(::\p{Alnum}+)*\z/
Class Method Summary collapse
- .all_association_candidates(connection, polymorphic_suffixes: ['_type'], on_error: proc {}) ⇒ Object
- .all_candidates(connection, polymorphic_suffixes: ['_type'], on_error: proc {}) ⇒ Object
-
.build_classes(model, polymorphic_suffixes: ['_type'], on_error: proc {}) ⇒ Object
ForeignKeyChecker::Utils::BelongsTo.build_classes(User).
-
.build_delete_chain(scope, nullify = {}, **args) ⇒ Object
ForeignKeyChecker::Utils::BelongsTo.build_delete_chain(City.where(slug: ‘paris’), :capital_city_id, cities: %i[curator_id main_city_id]) ForeignKeyChecker::Utils::BelongsTo.build_delete_chain(City.where(slug: ‘paris’), :capital_city_id, cities: %i[curator_id main_city_id], local_profiles: :published_edition_id, hint_places: :place_image_id, {}).
-
.build_hm_tree(model, **args) ⇒ Object
ForeignKeyChecker::Utils::BelongsTo.build_hm_tree(User) построит дерево зависимостей от модели User 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, way + [key]); ways.each { |w| obj.push(w) } } v}.
- .build_table_mapping(model: ActiveRecord::Base, module_if_gt: 3) ⇒ Object
- .column_name_candidates(connection, polymorphic_suffixes: ['_type'], on_error: proc {}) ⇒ Object
-
.cycles_for(model) ⇒ Object
ForeignKeyChecker::Utils::BelongsTo.ways_for(User).
- .find_cycles(tree, start) ⇒ Object
- .find_ways(node, way) ⇒ Object
- .fk_candidates(connection, polymorphic_suffixes: ['_type'], on_error: proc {}) ⇒ Object
- .fks ⇒ Object
- .group_tables(tables, level: 1) ⇒ Object
- .inverse(hash) ⇒ Object
- .merge_candidates(*hashes) ⇒ Object
-
.ways_for(model) ⇒ Object
ForeignKeyChecker::Utils::BelongsTo.ways_for(User).
Class Method Details
.all_association_candidates(connection, polymorphic_suffixes: ['_type'], on_error: proc {}) ⇒ Object
349 350 351 352 353 354 355 |
# File 'lib/foreign_key_checker/utils/belongs_to.rb', line 349 def self.all_association_candidates(connection, polymorphic_suffixes: ['_type'], on_error: proc {}) belongs_to = all_candidates(connection, polymorphic_suffixes: ['_type'], on_error: proc {}) has_many = inverse(belongs_to) belongs_to.each_with_object({}) do |(table, bt_associations), object| object[table] = {belongs_to: bt_associations, has_many: has_many[table] || []} end end |
.all_candidates(connection, polymorphic_suffixes: ['_type'], on_error: proc {}) ⇒ Object
342 343 344 345 346 347 |
# File 'lib/foreign_key_checker/utils/belongs_to.rb', line 342 def self.all_candidates(connection, polymorphic_suffixes: ['_type'], on_error: proc {}) merge_candidates( fk_candidates(connection, polymorphic_suffixes: polymorphic_suffixes, on_error: on_error), column_name_candidates(connection, polymorphic_suffixes: polymorphic_suffixes, on_error: on_error) ) end |
.build_classes(model, polymorphic_suffixes: ['_type'], on_error: proc {}) ⇒ Object
ForeignKeyChecker::Utils::BelongsTo.build_classes(User)
393 394 395 396 397 398 399 400 401 402 403 404 |
# File 'lib/foreign_key_checker/utils/belongs_to.rb', line 393 def self.build_classes(model, polymorphic_suffixes: ['_type'], on_error: proc {}) @build_classes ||= begin mapping = build_table_mapping(model: model) build_class_name = proc { |table| mapping[table] } all_association_candidates(model.connection, polymorphic_suffixes: polymorphic_suffixes, on_error: on_error).map do |table, hash| hash.transform_values { |results| results.map { |result| result.build_class_name = build_class_name; result }}.merge( table_name: table, class_name: mapping[table], ) end end end |
.build_delete_chain(scope, nullify = {}, **args) ⇒ Object
ForeignKeyChecker::Utils::BelongsTo.build_delete_chain(City.where(slug: ‘paris’), :capital_city_id, cities: %i[curator_id main_city_id]) ForeignKeyChecker::Utils::BelongsTo.build_delete_chain(City.where(slug: ‘paris’), :capital_city_id, cities: %i[curator_id main_city_id], local_profiles: :published_edition_id, hint_places: :place_image_id, {})
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 |
# File 'lib/foreign_key_checker/utils/belongs_to.rb', line 408 def self.build_delete_chain(scope, nullify = {}, **args) tree = build_hm_tree(scope.model, **args) all_ids = [] dependencies = {} triggers = {} cmds = [proc { |*args| __ids = scope.pluck(:id); all_ids << [scope.model.table_name, __ids, []]; __ids}] process_tree = proc do |_tree, way| _tree.each do |table, data| cmds << proc do |*args| primary_table = data[:associations].first.primary_table ids = all_ids.select { |tn, __ids| tn == primary_table }.map(&:second).reduce(&:+).uniq _id_fks = [] data[:associations].each do |association| association.select_sql_with_fk(ids.dup).try do |sql| begin if Array.wrap(nullify[association.dependant_table.to_sym]).include?(association.foreign_key.to_sym) ids.in_groups_of(1000, false).each do |group| all_ids << [";sql;", association.nullify_sql(group), way] end next end p way _id_fks += association.connection.select_all(*sql).rows rescue => e raise e unless e..tr('"', '').include?("column id does not exist") ids.in_groups_of(1000, false).each do |group| all_ids << [";sql;", association.delete_sql(group), way] end puts e. end end end _id_fks.each do |_id, _fk| dependencies[primary_table] ||= {} dependencies[primary_table][_fk] ||= Set.new dependencies[primary_table][_fk] << [table, _id] triggers[table] ||= {} triggers[table][_id] ||= Set.new triggers[table][_id] << [primary_table, _fk] end _ids = _id_fks.map(&:first).uniq - (all_ids.select { |tn, __ids| tn == table }.map(&:second).reduce([], &:+).uniq) if _ids.any? all_ids << [table, _ids, way] data[:children].presence.try { |ch| process_tree.call(ch, way + [table]) } end _ids end end end process_tree.call(tree, [scope.model.table_name]) arg = nil cmds.each do |cmd| arg = cmd.call(arg) end sql_queries = all_ids.select { |a, b, c| a == ';sql;' }.map(&:second) t_ids = all_ids.reject { |a, b, c| a == ';sql;' }.group_by(&:first).transform_values { |vs| vs.map(&:second).reduce([], &:+).uniq }.to_a loop do break unless t_ids.map(&:second).any?(&:any?) any = false t_ids.each do |table, ids| _ids = ids.select { |id| dependencies.dig(table, id).blank? } ids.reject! { |id| dependencies.dig(table, id).blank? } if _ids.present? any = true _ids.in_groups_of(1000, false).each do |ids_group| sql_queries << ["DELETE FROM #{scope.model.connection.quote_table_name(table)} WHERE id IN (#{ids_group.map(&:to_i).join(',')})"] end _ids.each do |id| triggers.dig(table, id)&.each do |keys| dependencies.dig(*keys).delete([table, id]) end end end end unless any puts "Cannot destroy these objects. Check cyclic relation chains:\n#{find_cycles(tree, [scope.model.table_name]).inspect}" return [] end end sql_queries end |
.build_hm_tree(model, **args) ⇒ Object
ForeignKeyChecker::Utils::BelongsTo.build_hm_tree(User) построит дерево зависимостей от модели User 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, way + [key]); ways.each { |w| obj.push(w) } } v}
549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 |
# File 'lib/foreign_key_checker/utils/belongs_to.rb', line 549 def self.build_hm_tree(model, **args) mapping = build_table_mapping(model: model) hash = build_classes(model, **args).group_by do |result| result[:table_name] end found = {} processing = {} should_fill = [] find_children = proc do |c_table_name| processing[c_table_name] = true hash[c_table_name].each_with_object({}) do |results, object| perform_result = proc do |result| key = result.dependant_table object[key] ||= {associations: []} object[key][:associations].push(result) if processing[key] should_fill.push([key, object[key]]) next end found[key] ||= find_children.call(key) object[key][:children] ||= found[key] end results[:has_many].each(&perform_result) hash.each_value do |hs| hs.each do |h| h[:belongs_to].each do |r| next unless r.polymorphic && r.types.include?(mapping[c_table_name]) perform_result.call(r.inversed_association(c_table_name)) end end end end end ret = find_children.call(model.table_name) should_fill.each do |table_name, item| item[:children] = found[table_name] end ret end |
.build_table_mapping(model: ActiveRecord::Base, module_if_gt: 3) ⇒ Object
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 |
# File 'lib/foreign_key_checker/utils/belongs_to.rb', line 285 def self.build_table_mapping(model: ActiveRecord::Base, module_if_gt: 3) Rails.application.eager_load! base_class = model loop do if !base_class.superclass.respond_to?(:connection) || base_class.superclass.connection != base_class.connection break end base_class = base_class.superclass end @table_mappings ||= {} @table_mappings[base_class] ||= begin start_hash = base_class.descendants.reject do |_model| _model.abstract_class || (_model.connection != model.connection) end.group_by(&:table_name).transform_values do |models| next models.first if models.size == 1 root_models = models.map do |_model| if _model.to_s.demodulize.starts_with?("HABTM_") next end loop do break if !_model.superclass.respond_to?(:abstract_class?) || _model.superclass.table_name.nil? _model = _model.superclass end _model end.compact.uniq if root_models.size > 1 raise "More than one root model for table #{models.first.table_name}: #{root_models.inspect}" end root_models.first end all_tables = ForeignKeyChecker::Utils.get_tables(model) #group_tables(all_tables) all_tables.each_with_object(start_hash) do |table, object| object[table] ||= table.tr('.', '_').singularize.camelize end end end |
.column_name_candidates(connection, polymorphic_suffixes: ['_type'], on_error: proc {}) ⇒ Object
335 336 337 338 339 340 |
# File 'lib/foreign_key_checker/utils/belongs_to.rb', line 335 def self.column_name_candidates(connection, polymorphic_suffixes: ['_type'], on_error: proc {}) columns_of_table = ForeignKeyChecker::Utils.get_columns(connection) columns_of_table.each_with_object({}) do |(table, columns), object| object[table] = TableLevel.new(connection, table, columns_of_table, polymorphic_suffixes, on_error: on_error, fks: fks).candidates end end |
.cycles_for(model) ⇒ Object
ForeignKeyChecker::Utils::BelongsTo.ways_for(User)
542 543 544 |
# File 'lib/foreign_key_checker/utils/belongs_to.rb', line 542 def self.cycles_for(model) find_cycles(build_hm_tree(model), [model.table_name]) end |
.find_cycles(tree, start) ⇒ Object
528 529 530 531 532 533 534 |
# File 'lib/foreign_key_checker/utils/belongs_to.rb', line 528 def self.find_cycles(tree, start) find_ways(tree, start).each_with_object([]) do |way, obj| way[0..-2].index(way.last).try do |idx| obj.push ([WayPoint.new(nil, way[idx].table)] + way[idx+1..-1]) end end.uniq(&:inspect) end |
.find_ways(node, way) ⇒ Object
511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 |
# File 'lib/foreign_key_checker/utils/belongs_to.rb', line 511 def self.find_ways(node, way) way = way.map { |wp| next WayPoint.new(nil, wp) if wp.is_a?(String); wp} return [way] if node.nil? node.each_with_object([]) do |(key, item), obj| tails = if way.include?(key) [way + [key]] else find_ways(item[:children], way + [key]) end item[:associations].each do |ass| tails.each do |tail| obj.push((tail[0...way.size]) + [WayPoint.new(ass.foreign_key, ass.dependant_table)] + (tail[(way.size + 1)..-1] || [])) end end end end |
.fk_candidates(connection, polymorphic_suffixes: ['_type'], on_error: proc {}) ⇒ Object
374 375 376 377 378 379 |
# File 'lib/foreign_key_checker/utils/belongs_to.rb', line 374 def self.fk_candidates(connection, polymorphic_suffixes: ['_type'], on_error: proc {}) fks.to_a.each_with_object({}) do |datum, obj| obj[datum.from_table] ||= [] 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)) end end |
.fks ⇒ Object
370 371 372 |
# File 'lib/foreign_key_checker/utils/belongs_to.rb', line 370 def self.fks @fks ||= ForeignKeyChecker::Utils.get_foreign_keys end |
.group_tables(tables, level: 1) ⇒ Object
325 326 327 328 329 330 331 332 333 334 |
# File 'lib/foreign_key_checker/utils/belongs_to.rb', line 325 def self.group_tables(tables, level: 1) if tables.size < 2 return tables end tables.group_by do |table| table.split(/[\._]/).first(level).join('_') end.transform_values do |children| group_tables(children, level: level + 1) end end |
.inverse(hash) ⇒ Object
357 358 359 360 361 362 363 364 365 366 367 368 |
# File 'lib/foreign_key_checker/utils/belongs_to.rb', line 357 def self.inverse(hash) hash.each_with_object({}) do |(table, bt_associations), object| bt_associations.each do |association| object[association.primary_table] ||= [] object[association.primary_table].push(association.inversed_association(table)) end end.transform_values do |associations| associations.uniq do |result| "#{result.polymorphic} #{result.foreign_key} #{result.primary_table} #{result.dependant_table} #{result.name} #{result.types&.join(',')}" end end end |
.merge_candidates(*hashes) ⇒ Object
381 382 383 384 385 386 387 388 389 390 |
# File 'lib/foreign_key_checker/utils/belongs_to.rb', line 381 def self.merge_candidates(*hashes) hashes.each_with_object({}) do |hash, object| hash.each do |table, candidates| object[table] ||= [] object[table] = (object[table] + candidates).uniq do |result| "#{result.polymorphic} #{result.foreign_key} #{result.primary_table} #{result.dependant_table} #{result.name} #{result.types&.join(',')}" end end end end |
.ways_for(model) ⇒ Object
ForeignKeyChecker::Utils::BelongsTo.ways_for(User)
537 538 539 |
# File 'lib/foreign_key_checker/utils/belongs_to.rb', line 537 def self.ways_for(model) find_ways(build_hm_tree(model), [model.table_name]) end |