foreign_key_checker 0.3.0 → 0.4.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: 04ba348e8860c85305d58ec740c7d49dd318a11f2d80fff5905e434281b0f75e
4
- data.tar.gz: 0e40f9cff0e3af05525c529217c28eb360e008f6e20cf1b6268d0146c71955e7
3
+ metadata.gz: cf9685280dd2c9ad8232c3b4fe7d4f72dcc56ccdbbaf0b55d826792196185f05
4
+ data.tar.gz: de9a1d21793299c0df12e92b09fd2661b160d9794d6bb01a7080478fecc3db96
5
5
  SHA512:
6
- metadata.gz: f66ab3eacf8fd3238740483184683f871bbf0cd482ae3fa644d7120a8f01bde616ef571ab19638c6bae9e2a677650838370858a9f2e8ab776829a17d48a75f8a
7
- data.tar.gz: 53fd8d518399d7b61ee42cb7e47ac01f5604852e724e78971c7fd63a2009ac8b95afbc4ddd17a433da453dc7b4b0427d3c813a241015c0525b233473af9bb319
6
+ metadata.gz: 7f3b6138b99b780ed15ab03c43c0745bfc9511089e4fe0b4bb29cc962d232f7d942d3e314dc56bc8422e193631872d7415a5a28251281fad1bca704fb3c2ab6b
7
+ data.tar.gz: d7c3c677d063a6e2210d3627caac91431024e940b44e4e2b33c912f48c532a662c81678a1f5aa32f8cccffe19c3bdf0fbd7e10c13796f129de2dc9c60044705d
data/README.md CHANGED
@@ -23,7 +23,7 @@ ForeignKeyChecker.check.each do |key, result|
23
23
  end
24
24
  ```
25
25
 
26
- Get general information about foreign keys (only postgresql, mysql2)
26
+ Get general information about foreign keys
27
27
  ```ruby
28
28
  ForeignKeyChecker::Utils.get_foreign_keys_hash
29
29
  # => {"users"=>[#<ForeignKeyChecker::Utils::Result:0x00005645e51756e8 @from_table="user_rating_changes", @from_column="user_id", @to_table="users", @to_column="id">]}
@@ -1,5 +1,9 @@
1
1
  require "foreign_key_checker/railtie"
2
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'
3
7
 
4
8
  module ForeignKeyChecker
5
9
  class TypeMismatch < StandardError; end
@@ -162,12 +166,21 @@ module ForeignKeyChecker
162
166
  def check_types(model, association)
163
167
  type_from = model.columns_hash[association.foreign_key.to_s].sql_type
164
168
  type_to = association.klass.columns_hash[association.klass.primary_key.to_s].sql_type
165
- raise TypeMismatch, "TypeMissMatch #{type_from} != #{type_to}" if type_from != type_to
169
+ raise TypeMismatch, "TypeMissMatch for relation #{model}##{association.name} #{type_from} != #{type_to}" if type_from != type_to
166
170
  end
167
171
 
168
172
  def check_foreign_key_bt_association(model, association)
169
173
  return if model.name.starts_with?('HABTM_')
170
- related = association.klass
174
+ begin
175
+ related = association.klass
176
+ rescue NameError => error
177
+ @result[:broken] << BrokenRelationResult.new(
178
+ model: model,
179
+ association: association,
180
+ error: error,
181
+ )
182
+ return
183
+ end
171
184
 
172
185
  column_name = model.connection.quote_column_name(association.foreign_key)
173
186
  scope = model.left_outer_joins(association.name).where(
@@ -188,7 +201,7 @@ module ForeignKeyChecker
188
201
  end
189
202
  end
190
203
 
191
- if foreign_keys && !model.connection.foreign_key_exists?(model.table_name, related.table_name)
204
+ if foreign_keys && !model.connection.foreign_key_exists?(model.table_name, related.table_name, column: association.foreign_key, primary_key: related.primary_key)
192
205
  scope.first
193
206
  @result[:foreign_keys] << ForeignKeyResult.new(
194
207
  model: model,
@@ -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
@@ -9,7 +9,7 @@ module ForeignKeyChecker
9
9
  class UnsupportedConnectionAdapter < StandardError; end
10
10
  def self.get_foreign_keys(model = ActiveRecord::Base)
11
11
  adapter = model.connection_config[:adapter]
12
- raise(UnsupportedConnectionAdapter, adapter) unless %w[postgresql mysql2].include?(adapter)
12
+ raise(UnsupportedConnectionAdapter, adapter) unless %w[postgresql mysql2 sqlserver sqlite3].include?(adapter)
13
13
 
14
14
  connection = model.connection
15
15
  send("get_#{adapter}_foreign_keys", connection)
@@ -22,6 +22,22 @@ module ForeignKeyChecker
22
22
  end
23
23
  end
24
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
+
25
41
  def self.get_mysql2_foreign_keys(connection)
26
42
  res = connection.select_all <<-SQL
27
43
  SELECT
@@ -57,5 +73,54 @@ module ForeignKeyChecker
57
73
  SQL
58
74
  res.to_a.map{ |i| Result.new(i) }
59
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_config[: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
60
125
  end
61
126
  end
@@ -1,3 +1,3 @@
1
1
  module ForeignKeyChecker
2
- VERSION = '0.3.0'
2
+ VERSION = '0.4.0'
3
3
  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.3.0
4
+ version: 0.4.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-31 00:00:00.000000000 Z
11
+ date: 2020-07-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -38,6 +38,104 @@ 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: activerecord-sqlserver-adapter
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: irb
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: e2mmap
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'
125
+ - !ruby/object:Gem::Dependency
126
+ name: annotate
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
41
139
  description: Run task to obtain problems with your database
42
140
  email:
43
141
  - pipocavsobake@gmail.com
@@ -49,6 +147,8 @@ files:
49
147
  - README.md
50
148
  - Rakefile
51
149
  - lib/foreign_key_checker.rb
150
+ - lib/foreign_key_checker/checkers/relations.rb
151
+ - lib/foreign_key_checker/checkers/tables.rb
52
152
  - lib/foreign_key_checker/railtie.rb
53
153
  - lib/foreign_key_checker/utils.rb
54
154
  - lib/foreign_key_checker/version.rb
@@ -60,7 +160,7 @@ licenses:
60
160
  - MIT
61
161
  metadata:
62
162
  allowed_push_host: https://rubygems.org
63
- post_install_message:
163
+ post_install_message:
64
164
  rdoc_options: []
65
165
  require_paths:
66
166
  - lib
@@ -75,8 +175,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
75
175
  - !ruby/object:Gem::Version
76
176
  version: '0'
77
177
  requirements: []
78
- rubygems_version: 3.0.6
79
- signing_key:
178
+ rubygems_version: 3.1.3
179
+ signing_key:
80
180
  specification_version: 4
81
181
  summary: Find problems with relations in active_record models
82
182
  test_files: []