foreign_key_checker 0.2.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: 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: []