active_record_doctor 2.0.0 → 2.0.1

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: 8bae986a04fe63835ce7e190f32471cc06a370972161c487cac195ef33f73269
4
- data.tar.gz: c5a2f92f0b31ec1faedfd65fc5abe9ef656f75b9fa933719e57f180b4fb0f28d
3
+ metadata.gz: 94e700a1c78ee5e9657d1107578c2932a84acba7f592ab6a416e5c063520a311
4
+ data.tar.gz: 8496adb8a597250ec95276fc9fbad50397667f7de2336f80405203b3a3c2a459
5
5
  SHA512:
6
- metadata.gz: 7759e1effdfcea2382966f8973c2c132bda909b847a14914fb7a46b795a7d0407f9605efa39dfece9d889ee65b35f334f78e6664928fbbe03e349f792845ae7f
7
- data.tar.gz: c563de0bbf91966325eddd2bafedb3dadcc07ca39e7c27a529fdb544113eadba4f2373e9d618a074205a8716687f7684f232a212711fcf5d50fc1484999b1074
6
+ metadata.gz: 79803ef2db49147fdf2daf5983a3f36fb895704d150347442bebdc192a6980ecb8cd438c23fe335941284e0a39aee8c3e1a725bfe3845c7a0d3dde03830068dc
7
+ data.tar.gz: 2feb5b594c369dc3d39e0f85540724474e5caa7573e0d0f18a5dcddd63d12113ec4c670ef374b475f237bd3d75fe7281bca0b0d219aac6582cbddbe59bdc2340
data/README.md CHANGED
@@ -302,16 +302,15 @@ Supported configuration options:
302
302
 
303
303
  ### Detecting Missing Foreign Key Constraints
304
304
 
305
- If `users.profile_id` references a row in `profiles` then this can be expressed
306
- at the database level with a foreign key constraint. It _forces_
307
- `users.profile_id` to point to an existing row in `profiles`. The problem is
308
- that in many legacy Rails apps, the constraint isn't enforced at the database
309
- level.
305
+ If `User` defines a `belongs_to` association to `Profile` then the underlying
306
+ column (`users.profile_id` by convention) should be marked as a foreign key in
307
+ the database. If it's not then it's possible to end up in a situation where a
308
+ user is referencing a non-existent profile.
310
309
 
311
310
  `active_record_doctor` can automatically detect foreign keys that could benefit
312
311
  from a foreign key constraint (a future version will generate a migration that
313
- add the constraint; for now, it's your job). You can obtain the list of foreign
314
- keys with the following command:
312
+ add the constraint; for now, it's your job). You can obtain the list of missing
313
+ foreign key constraints with the following command:
315
314
 
316
315
  ```bash
317
316
  bundle exec rake active_record_doctor:missing_foreign_keys
@@ -331,9 +330,9 @@ end
331
330
  Supported configuration options:
332
331
 
333
332
  - `enabled` - set to `false` to disable the detector altogether
334
- - `ignore_tables` - tables whose columns should not be checked.
335
- - `ignore_columns` - columns, written as table.column, that should not be
336
- checked.
333
+ - `ignore_models` - models whose associations should not be checked.
334
+ - `ignore_associations` - associations, written as Model.association, that
335
+ should not be checked.
337
336
 
338
337
  ### Detecting Models Referencing Undefined Tables
339
338
 
@@ -36,8 +36,8 @@ ActiveRecordDoctor.configure do
36
36
 
37
37
  detector :missing_foreign_keys,
38
38
  enabled: true,
39
- ignore_tables: [],
40
- ignore_columns: []
39
+ ignore_models: [],
40
+ ignore_associations: []
41
41
 
42
42
  detector :missing_non_null_constraint,
43
43
  enabled: true,
@@ -7,12 +7,12 @@ module ActiveRecordDoctor
7
7
  class MissingForeignKeys < Base # :nodoc:
8
8
  @description = "detect foreign-key-like columns lacking an actual foreign key constraint"
9
9
  @config = {
10
- ignore_tables: {
11
- description: "tables whose columns should not be checked",
10
+ ignore_models: {
11
+ description: "models whose columns should not be checked",
12
12
  global: true
13
13
  },
14
- ignore_columns: {
15
- description: "columns, written as table.column, that should not be checked"
14
+ ignore_associations: {
15
+ description: "associations, written as Model.association, that should not be checked"
16
16
  }
17
17
  }
18
18
 
@@ -23,42 +23,18 @@ module ActiveRecordDoctor
23
23
  end
24
24
 
25
25
  def detect
26
- each_table(except: config(:ignore_tables)) do |table|
27
- each_column(table, except: config(:ignore_columns)) do |column|
28
- # We need to skip polymorphic associations as they can reference
29
- # multiple tables but a foreign key constraint can reference
30
- # a single predefined table.
31
- next unless looks_like_foreign_key?(column)
32
- next if foreign_key?(table, column)
33
- next if polymorphic_foreign_key?(table, column)
34
- next if model_destroyed_async?(table, column)
26
+ each_model(except: config(:ignore_models), existing_tables_only: true) do |model|
27
+ foreign_keys = connection.foreign_keys(model.table_name)
28
+ foreign_key_columns = foreign_keys.map { |key| key.options[:column] }
35
29
 
36
- problem!(table: table, column: column.name)
37
- end
38
- end
39
- end
40
-
41
- def foreign_key?(table, column)
42
- connection.foreign_keys(table).any? do |foreign_key|
43
- foreign_key.options[:column] == column.name
44
- end
45
- end
30
+ each_association(model, type: :belongs_to) do |association|
31
+ next if ignored?("#{model.name}.#{association.name}", config(:ignore_associations))
32
+ next if association.options[:polymorphic]
46
33
 
47
- def polymorphic_foreign_key?(table, column)
48
- type_column_name = column.name.sub(/_id\Z/, "_type")
49
- connection.columns(table).any? do |another_column|
50
- another_column.name == type_column_name
51
- end
52
- end
34
+ has_foreign_key = foreign_key_columns.include?(association.foreign_key)
35
+ next if has_foreign_key
53
36
 
54
- def model_destroyed_async?(table, column)
55
- # Check if there are any models having `has_many ..., dependent: :destroy_async`
56
- # referencing the specified table.
57
- models.any? do |model|
58
- model.reflect_on_all_associations(:has_many).any? do |reflection|
59
- reflection.options[:dependent] == :destroy_async &&
60
- reflection.foreign_key == column.name &&
61
- reflection.klass.table_name == table
37
+ problem!(table: model.table_name, column: association.foreign_key)
62
38
  end
63
39
  end
64
40
  end
@@ -21,14 +21,14 @@ module ActiveRecordDoctor
21
21
 
22
22
  private
23
23
 
24
- def message(type:, column_or_association:, model:)
24
+ def message(type:, column:, reflection:, model:)
25
25
  case type
26
26
  when :missing_validator
27
- "add a `presence` validator to #{model}.#{column_or_association} - it's NOT NULL but lacks a validator"
27
+ "add a `presence` validator to #{model}.#{column} - it's NOT NULL but lacks a validator"
28
28
  when :optional_association
29
- "add `optional: false` to #{model}.#{column_or_association} - the foreign key #{column_or_association}_id is NOT NULL" # rubocop:disable Layout/LineLength
29
+ "add `optional: false` to #{model}.#{reflection.name} - the foreign key #{reflection.foreign_key} is NOT NULL"
30
30
  when :optional_polymorphic_association
31
- "add `optional: false` to #{model}.#{column_or_association} - the foreign key #{column_or_association}_id or type #{column_or_association}_type are NOT NULL" # rubocop:disable Layout/LineLength
31
+ "add `optional: false` to #{model}.#{reflection.name} - the foreign key #{reflection.foreign_key} or type #{reflection.foreign_type} are NOT NULL" # rubocop:disable Layout/LineLength
32
32
  end
33
33
  end
34
34
 
@@ -56,7 +56,7 @@ module ActiveRecordDoctor
56
56
  (config(:ignore_columns_with_default) && (column.default || column.default_function)) ||
57
57
 
58
58
  # Explicitly ignored columns should be skipped.
59
- config(:ignore_attributes).include?("#{model.name}.#{column.name}")
59
+ ignored?("#{model.name}.#{column.name}", config(:ignore_attributes))
60
60
  end
61
61
 
62
62
  # At this point the only columns that are left are those that DO
@@ -76,28 +76,30 @@ module ActiveRecordDoctor
76
76
  # that are validated directly or via an association name.
77
77
  model.validators.each do |validator|
78
78
  problematic_columns.reject! do |column|
79
- # Translate a foreign key or type to the association name.
80
- attribute = column_name_to_association_name[column.name] || column.name.to_sym
79
+ attribute_names = [
80
+ column.name.to_sym,
81
+ column_name_to_association_name[column.name]
82
+ ].compact
81
83
 
82
84
  case validator
83
85
 
84
86
  # A regular presence validator is enough if the column name is
85
87
  # listed among the attributes it's validating.
86
88
  when ActiveRecord::Validations::PresenceValidator
87
- validator.attributes.include?(attribute)
89
+ (validator.attributes & attribute_names).present?
88
90
 
89
91
  # An inclusion validator ensures the column is not nil if it covers
90
92
  # the column and nil is NOT one of the values it allows.
91
93
  when ActiveModel::Validations::InclusionValidator
92
94
  validator_items = inclusion_or_exclusion_validator_items(validator)
93
- validator.attributes.include?(attribute) &&
95
+ (validator.attributes & attribute_names).present? &&
94
96
  (validator_items.is_a?(Proc) || validator_items.exclude?(nil))
95
97
 
96
98
  # An exclusion validator ensures the column is not nil if it covers
97
99
  # the column and excludes nil as an allowed value explicitly.
98
100
  when ActiveModel::Validations::ExclusionValidator
99
101
  validator_items = inclusion_or_exclusion_validator_items(validator)
100
- validator.attributes.include?(attribute) &&
102
+ (validator.attributes & attribute_names).present? &&
101
103
  (validator_items.is_a?(Proc) || validator_items.include?(nil))
102
104
 
103
105
  end
@@ -109,7 +111,7 @@ module ActiveRecordDoctor
109
111
  problematic_associations = []
110
112
  problematic_polymorphic_associations = []
111
113
 
112
- model.reflect_on_all_associations.each do |reflection|
114
+ model.reflect_on_all_associations(:belongs_to).each do |reflection|
113
115
  foreign_key_column = problematic_columns.find { |column| column.name == reflection.foreign_key }
114
116
  if reflection.polymorphic?
115
117
  # If the foreign key and type are not one of the columns that lack
@@ -125,7 +127,7 @@ module ActiveRecordDoctor
125
127
 
126
128
  # ... report an error about an incorrectly configured polymorphic
127
129
  # association.
128
- problematic_polymorphic_associations << reflection.name
130
+ problematic_polymorphic_associations << reflection
129
131
  else
130
132
  # If the foreign key is not one of the columns that lack a
131
133
  # validator then it means the association added a validator and is
@@ -137,7 +139,7 @@ module ActiveRecordDoctor
137
139
  problematic_columns.delete(foreign_key_column)
138
140
 
139
141
  # ... report an error about an incorrectly configured association.
140
- problematic_associations << reflection.name
142
+ problematic_associations << reflection
141
143
  end
142
144
  end
143
145
 
@@ -145,22 +147,22 @@ module ActiveRecordDoctor
145
147
  # ignored should be removed from the output. It's NOT enough to skip
146
148
  # processing them in the loop above because their underlying foreign
147
149
  # key and type columns must be removed from output, too.
148
- problematic_associations.reject! do |name|
149
- config(:ignore_attributes).include?("#{model.name}.#{name}")
150
+ problematic_associations.reject! do |reflection|
151
+ config(:ignore_attributes).include?("#{model.name}.#{reflection.name}")
150
152
  end
151
- problematic_polymorphic_associations.reject! do |name|
152
- config(:ignore_attributes).include?("#{model.name}.#{name}")
153
+ problematic_polymorphic_associations.reject! do |reflection|
154
+ config(:ignore_attributes).include?("#{model.name}.#{reflection.name}")
153
155
  end
154
156
 
155
157
  # Job is done and all accumulated errors can be reported.
156
- problematic_polymorphic_associations.each do |name|
157
- problem!(type: :optional_polymorphic_association, column_or_association: name, model: model.name)
158
+ problematic_polymorphic_associations.each do |reflection|
159
+ problem!(type: :optional_polymorphic_association, column: nil, reflection: reflection, model: model.name)
158
160
  end
159
- problematic_associations.each do |name|
160
- problem!(type: :optional_association, column_or_association: name, model: model.name)
161
+ problematic_associations.each do |reflection|
162
+ problem!(type: :optional_association, column: nil, reflection: reflection, model: model.name)
161
163
  end
162
164
  problematic_columns.each do |column|
163
- problem!(type: :missing_validator, column_or_association: column.name, model: model.name)
165
+ problem!(type: :missing_validator, column: column.name, reflection: nil, model: model.name)
164
166
  end
165
167
  end
166
168
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordDoctor
4
- VERSION = "2.0.0"
4
+ VERSION = "2.0.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_record_doctor
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Greg Navis
@@ -144,9 +144,6 @@ files:
144
144
  - MIT-LICENSE.txt
145
145
  - README.md
146
146
  - lib/active_record_doctor.rb
147
- - lib/active_record_doctor/adapters/database.rb
148
- - lib/active_record_doctor/adapters/table.rb
149
- - lib/active_record_doctor/adapters/validators.rb
150
147
  - lib/active_record_doctor/config.rb
151
148
  - lib/active_record_doctor/config/default.rb
152
149
  - lib/active_record_doctor/config/loader.rb
@@ -1,17 +0,0 @@
1
- module ActiveRecordDoctor
2
- module Adapters
3
- class Database
4
- def initialize(connection)
5
- @connection = connection
6
- end
7
-
8
- def tables
9
- connection.tables
10
- end
11
-
12
- private
13
-
14
- attr_reader :connection
15
- end
16
- end
17
- end
@@ -1,18 +0,0 @@
1
- module ActiveRecordDoctor
2
- module Adapters
3
- class Table
4
- def initialize(connection, table_name)
5
- @connection = connection
6
- @table_name = table_name
7
- end
8
-
9
- def columns
10
- connection.columns(table_name)
11
- end
12
-
13
- private
14
-
15
- attr_reader :connection, :table_name
16
- end
17
- end
18
- end
@@ -1,16 +0,0 @@
1
- module ActiveRecordDoctor
2
- module Adapters
3
- class Validators
4
- def initialize(model_class, kind: :any)
5
- @model_class = model_class
6
- end
7
-
8
- def each
9
- end
10
-
11
- private
12
-
13
- attr_reader :model_class
14
- end
15
- end
16
- end