foreign_key_checker 0.3.0 → 0.4.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: 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: []