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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 61d367175ce6fba75c746ebc5425f463c20d903e31885edd04e407a100dbe72e
4
- data.tar.gz: 1a6cb3b2f4d0123e884f432a7d6a4c48acf2d9e16a7fb3439ddfd4f06df198db
3
+ metadata.gz: 14d08201b2ec53461b6eeb2da97e21b0713c8dc1f62ebeb8f15644caa1ef7484
4
+ data.tar.gz: 78454098ee4372ca7c6076bb312f5e9ebdb90074f81c88871e2903898d6e1e27
5
5
  SHA512:
6
- metadata.gz: 0437adb8468a7018a11e77d5e77fec0cd7fcd0b5cf37ccfb8d31a2da35da3ebd4a706a5d373ebb8f50ae91e3e698c73b1cd5cd4e28049b520156e90d9e65b619
7
- data.tar.gz: 91f2dda0e5a8036cdc330a673a8d2aaba235435f870b02c99ea828140fca646456d555d979447fc78c3f9174c53ad23ff2c295084588c2f54ac211f606f684ac
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
 
@@ -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
- related = association.klass
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
@@ -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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module ForeignKeyChecker
2
- VERSION = '0.2.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.2.1
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: 2019-10-24 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
@@ -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.0.6
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: []