active_record_doctor 1.9.0 → 1.10.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.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +83 -19
  3. data/lib/active_record_doctor/config/default.rb +17 -0
  4. data/lib/active_record_doctor/detectors/base.rb +52 -22
  5. data/lib/active_record_doctor/detectors/extraneous_indexes.rb +25 -40
  6. data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +1 -2
  7. data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +40 -9
  8. data/lib/active_record_doctor/detectors/incorrect_length_validation.rb +63 -0
  9. data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +2 -1
  10. data/lib/active_record_doctor/detectors/missing_presence_validation.rb +3 -4
  11. data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +65 -15
  12. data/lib/active_record_doctor/detectors/short_primary_key_type.rb +5 -1
  13. data/lib/active_record_doctor/detectors/undefined_table_references.rb +1 -3
  14. data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +2 -3
  15. data/lib/active_record_doctor/version.rb +1 -1
  16. data/lib/active_record_doctor.rb +1 -0
  17. data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +5 -5
  18. data/test/active_record_doctor/detectors/disable_test.rb +30 -0
  19. data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +34 -0
  20. data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +105 -7
  21. data/test/active_record_doctor/detectors/incorrect_length_validation_test.rb +105 -0
  22. data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +34 -0
  23. data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +37 -1
  24. data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +167 -3
  25. data/test/active_record_doctor/detectors/short_primary_key_type_test.rb +27 -19
  26. data/test/active_record_doctor/detectors/unindexed_deleted_at_test.rb +9 -3
  27. data/test/setup.rb +6 -2
  28. metadata +8 -3
@@ -164,6 +164,40 @@ class ActiveRecordDoctor::Detectors::MissingNonNullConstraintTest < Minitest::Te
164
164
  refute_problems
165
165
  end
166
166
 
167
+ def test_not_null_check_constraint
168
+ skip unless postgresql?
169
+
170
+ create_table(:users) do |t|
171
+ t.string :email
172
+ end.create_model do
173
+ validates :email, presence: true
174
+ end
175
+
176
+ ActiveRecord::Base.connection.execute(<<-SQL)
177
+ ALTER TABLE users ADD CONSTRAINT email_not_null CHECK (email IS NOT NULL)
178
+ SQL
179
+
180
+ refute_problems
181
+ end
182
+
183
+ def test_not_null_check_constraint_not_valid
184
+ skip unless postgresql?
185
+
186
+ create_table(:users) do |t|
187
+ t.string :email
188
+ end.create_model do
189
+ validates :email, presence: true
190
+ end
191
+
192
+ ActiveRecord::Base.connection.execute(<<-SQL)
193
+ ALTER TABLE users ADD CONSTRAINT email_not_null CHECK (email IS NOT NULL) NOT VALID
194
+ SQL
195
+
196
+ assert_problems(<<~OUTPUT)
197
+ add `NOT NULL` to users.email - models validates its presence but it's not non-NULL in the database
198
+ OUTPUT
199
+ end
200
+
167
201
  def test_config_ignore_tables
168
202
  create_table(:users) do |t|
169
203
  t.string :name, null: true
@@ -105,9 +105,15 @@ class ActiveRecordDoctor::Detectors::MissingPresenceValidationTest < Minitest::T
105
105
 
106
106
  def test_timestamps_are_not_reported
107
107
  create_table(:users) do |t|
108
+ # Create created_at/updated_at timestamps.
108
109
  t.timestamps null: false
110
+
111
+ # Rails also supports created_on/updated_on. We used datetime, which is
112
+ # what the timestamps method users under the hood, to avoid default value
113
+ # errors in some MySQL versions when using t.timestamp.
114
+ t.datetime :created_on, null: false
115
+ t.datetime :updated_on, null: false
109
116
  end.create_model do
110
- validates :name, presence: true
111
117
  end
112
118
 
113
119
  refute_problems
@@ -119,6 +125,36 @@ class ActiveRecordDoctor::Detectors::MissingPresenceValidationTest < Minitest::T
119
125
  refute_problems
120
126
  end
121
127
 
128
+ def test_not_null_check_constraint
129
+ skip unless postgresql?
130
+
131
+ create_table(:users) do |t|
132
+ t.string :name
133
+ end.create_model
134
+
135
+ ActiveRecord::Base.connection.execute(<<-SQL)
136
+ ALTER TABLE users ADD CONSTRAINT name_not_null CHECK (name IS NOT NULL)
137
+ SQL
138
+
139
+ assert_problems(<<~OUTPUT)
140
+ add a `presence` validator to ModelFactory::Models::User.name - it's NOT NULL but lacks a validator
141
+ OUTPUT
142
+ end
143
+
144
+ def test_not_null_check_constraint_not_valid
145
+ skip unless postgresql?
146
+
147
+ create_table(:users) do |t|
148
+ t.string :name
149
+ end.create_model
150
+
151
+ ActiveRecord::Base.connection.execute(<<-SQL)
152
+ ALTER TABLE users ADD CONSTRAINT name_not_null CHECK (name IS NOT NULL) NOT VALID
153
+ SQL
154
+
155
+ refute_problems
156
+ end
157
+
122
158
  def test_config_ignore_models
123
159
  create_table(:users) do |t|
124
160
  t.string :name, null: false
@@ -14,6 +14,36 @@ class ActiveRecordDoctor::Detectors::MissingUniqueIndexesTest < Minitest::Test
14
14
  OUTPUT
15
15
  end
16
16
 
17
+ def test_missing_unique_index_on_functional_index
18
+ skip if !(ActiveRecord::VERSION::STRING >= "5.0" && postgresql?)
19
+
20
+ create_table(:users) do |t|
21
+ t.string :email
22
+ t.index "lower(email)"
23
+ end.create_model do
24
+ validates :email, uniqueness: true
25
+ end
26
+
27
+ # Running the detector should NOT raise an error when a functional index
28
+ # is present. No need to assert anything -- the test is successful if no
29
+ # exception was raised.
30
+ run_detector
31
+ end
32
+
33
+ def test_validates_multiple_attributes
34
+ create_table(:users) do |t|
35
+ t.string :email
36
+ t.string :ref_token
37
+ end.create_model do
38
+ validates :email, :ref_token, uniqueness: true
39
+ end
40
+
41
+ assert_problems(<<~OUTPUT)
42
+ add a unique index on users(email) - validating uniqueness in the model without an index can lead to duplicates
43
+ add a unique index on users(ref_token) - validating uniqueness in the model without an index can lead to duplicates
44
+ OUTPUT
45
+ end
46
+
17
47
  def test_present_unique_index
18
48
  create_table(:users) do |t|
19
49
  t.string :email
@@ -25,7 +55,23 @@ class ActiveRecordDoctor::Detectors::MissingUniqueIndexesTest < Minitest::Test
25
55
  refute_problems
26
56
  end
27
57
 
28
- def test_missing_unique_index_with_scope
58
+ def test_present_partial_unique_index
59
+ skip("MySQL doesn't support partial indexes") if mysql?
60
+
61
+ create_table(:users) do |t|
62
+ t.string :email
63
+ t.boolean :active
64
+ t.index :email, unique: true, where: "active"
65
+ end.create_model do
66
+ validates :email, uniqueness: true
67
+ end
68
+
69
+ assert_problems(<<~OUTPUT)
70
+ add a unique index on users(email) - validating uniqueness in the model without an index can lead to duplicates
71
+ OUTPUT
72
+ end
73
+
74
+ def test_unique_index_with_extra_columns_with_scope
29
75
  create_table(:users) do |t|
30
76
  t.string :email
31
77
  t.integer :company_id
@@ -40,7 +86,7 @@ class ActiveRecordDoctor::Detectors::MissingUniqueIndexesTest < Minitest::Test
40
86
  OUTPUT
41
87
  end
42
88
 
43
- def test_present_unique_index_with_scope
89
+ def test_unique_index_with_exact_columns_with_scope
44
90
  create_table(:users) do |t|
45
91
  t.string :email
46
92
  t.integer :company_id
@@ -53,6 +99,73 @@ class ActiveRecordDoctor::Detectors::MissingUniqueIndexesTest < Minitest::Test
53
99
  refute_problems
54
100
  end
55
101
 
102
+ def test_unique_index_with_fewer_columns_with_scope
103
+ create_table(:users) do |t|
104
+ t.string :email
105
+ t.integer :company_id
106
+ t.integer :department_id
107
+ t.index [:company_id, :department_id], unique: true
108
+ end.create_model do
109
+ validates :email, uniqueness: { scope: [:company_id, :department_id] }
110
+ end
111
+
112
+ refute_problems
113
+ end
114
+
115
+ def test_missing_unique_index_with_association_attribute
116
+ create_table(:users) do |t|
117
+ t.integer :account_id
118
+ end.create_model do
119
+ belongs_to :account
120
+ validates :account, uniqueness: true
121
+ end
122
+
123
+ assert_problems(<<~OUTPUT)
124
+ add a unique index on users(account_id) - validating uniqueness in the model without an index can lead to duplicates
125
+ OUTPUT
126
+ end
127
+
128
+ def test_present_unique_index_with_association_attribute
129
+ create_table(:users) do |t|
130
+ t.integer :account_id
131
+ t.index :account_id, unique: true
132
+ end.create_model do
133
+ belongs_to :account
134
+ validates :account, uniqueness: true
135
+ end
136
+
137
+ refute_problems
138
+ end
139
+
140
+ def test_missing_unique_index_with_association_scope
141
+ create_table(:comments) do |t|
142
+ t.string :title
143
+ t.integer :commentable_id
144
+ t.string :commentable_type
145
+ end.create_model do
146
+ belongs_to :commentable, polymorphic: true
147
+ validates :title, uniqueness: { scope: :commentable }
148
+ end
149
+
150
+ assert_problems(<<~OUTPUT)
151
+ add a unique index on comments(commentable_type, commentable_id, title) - validating uniqueness in the model without an index can lead to duplicates
152
+ OUTPUT
153
+ end
154
+
155
+ def test_present_unique_index_with_association_scope
156
+ create_table(:comments) do |t|
157
+ t.string :title
158
+ t.integer :commentable_id
159
+ t.string :commentable_type
160
+ t.index [:commentable_id, :commentable_type, :title], unique: true
161
+ end.create_model do
162
+ belongs_to :commentable, polymorphic: true
163
+ validates :title, uniqueness: { scope: :commentable }
164
+ end
165
+
166
+ refute_problems
167
+ end
168
+
56
169
  def test_column_order_is_ignored
57
170
  create_table(:users) do |t|
58
171
  t.string :email
@@ -93,6 +206,57 @@ class ActiveRecordDoctor::Detectors::MissingUniqueIndexesTest < Minitest::Test
93
206
  refute_problems
94
207
  end
95
208
 
209
+ def test_has_one_without_index
210
+ create_table(:users)
211
+ .create_model do
212
+ has_one :account, class_name: "ModelFactory::Models::Account"
213
+ has_one :account_history, through: :account, class_name: "ModelFactory::Models::Account"
214
+ end
215
+
216
+ create_table(:accounts) do |t|
217
+ t.integer :user_id
218
+ end.create_model do
219
+ has_one :account_history, class_name: "ModelFactory::Models::AccountHistory"
220
+ end
221
+
222
+ create_table(:account_histories) do |t|
223
+ t.integer :account_id
224
+ end.create_model do
225
+ belongs_to :account, class_name: "ModelFactory::Models::Account"
226
+ end
227
+
228
+ assert_problems(<<~OUTPUT)
229
+ add a unique index on accounts(user_id) - using `has_one` in the ModelFactory::Models::User model without an index can lead to duplicates
230
+ add a unique index on account_histories(account_id) - using `has_one` in the ModelFactory::Models::Account model without an index can lead to duplicates
231
+ OUTPUT
232
+ end
233
+
234
+ def test_has_one_with_scope_and_without_index
235
+ create_table(:users)
236
+ .create_model do
237
+ has_one :last_comment, -> { order(created_at: :desc) }, class_name: "ModelFactory::Models::Comment"
238
+ end
239
+
240
+ create_table(:comments) do |t|
241
+ t.integer :user_id
242
+ end.create_model
243
+
244
+ refute_problems
245
+ end
246
+
247
+ def test_has_one_with_index
248
+ create_table(:users)
249
+ .create_model do
250
+ has_one :account, class_name: "ModelFactory::Models::Account"
251
+ end
252
+
253
+ create_table(:accounts) do |t|
254
+ t.integer :user_id, index: { unique: true }
255
+ end.create_model
256
+
257
+ refute_problems
258
+ end
259
+
96
260
  def test_config_ignore_models
97
261
  create_table(:users) do |t|
98
262
  t.string :email
@@ -137,7 +301,7 @@ class ActiveRecordDoctor::Detectors::MissingUniqueIndexesTest < Minitest::Test
137
301
  config_file(<<-CONFIG)
138
302
  ActiveRecordDoctor.configure do |config|
139
303
  config.detector :missing_unique_indexes,
140
- ignore_columns: ["ModelFactory::Models::User(organization_id, email, role)"]
304
+ ignore_columns: ["ModelFactory::Models::User(organization_id, email)", "ModelFactory::Models::User(organization_id, role)"]
141
305
  end
142
306
  CONFIG
143
307
 
@@ -1,32 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class ActiveRecordDoctor::Detectors::ShortPrimaryKeyTypeTest < Minitest::Test
4
+ def setup
5
+ @connection = ActiveRecord::Base.connection
6
+ @connection.enable_extension("uuid-ossp") if postgresql?
7
+ super
8
+ end
9
+
10
+ def teardown
11
+ @connection.disable_extension("uuid-ossp") if postgresql?
12
+ super
13
+ end
14
+
4
15
  def test_short_integer_primary_key_is_reported
5
- if mysql?
6
- create_table(:companies, id: :int)
7
-
8
- assert_problems(<<~OUTPUT)
9
- change the type of companies.id to bigint
10
- OUTPUT
11
- elsif postgresql?
12
- create_table(:companies, id: :integer)
13
-
14
- assert_problems(<<~OUTPUT)
15
- change the type of companies.id to bigint
16
- OUTPUT
16
+ create_table(:companies, id: :int)
17
+
18
+ # In Rails 4.2 and MySQL primary key is not created due to a bug
19
+ if mysql? && ActiveRecord::VERSION::STRING < "5.0"
20
+ @connection.execute("ALTER TABLE companies ADD PRIMARY KEY(id)")
17
21
  end
22
+
23
+ assert_problems(<<~OUTPUT)
24
+ change the type of companies.id to bigint
25
+ OUTPUT
18
26
  end
19
27
 
20
28
  def test_long_integer_primary_key_is_not_reported
21
- if mysql?
22
- create_table(:companies, id: :bigint)
29
+ create_table(:companies, id: :bigint)
30
+ refute_problems
31
+ end
23
32
 
24
- refute_problems
25
- elsif postgresql?
26
- create_table(:companies, id: :bigserial)
33
+ def test_uuid_primary_key_is_not_reported
34
+ skip unless postgresql?
27
35
 
28
- refute_problems
29
- end
36
+ create_table(:companies, id: :uuid)
37
+ refute_problems
30
38
  end
31
39
 
32
40
  def test_no_primary_key_is_not_reported
@@ -11,6 +11,9 @@ class ActiveRecordDoctor::Detectors::UnindexedDeletedAtTest < Minitest::Test
11
11
  t.index [:first_name, :last_name],
12
12
  name: "index_profiles_on_first_name_and_last_name",
13
13
  where: "deleted_at IS NULL"
14
+ t.index [:last_name],
15
+ name: "index_deleted_profiles_on_last_name",
16
+ where: "deleted_at IS NOT NULL"
14
17
  end
15
18
 
16
19
  refute_problems
@@ -28,7 +31,7 @@ class ActiveRecordDoctor::Detectors::UnindexedDeletedAtTest < Minitest::Test
28
31
  end
29
32
 
30
33
  assert_problems(<<~OUTPUT)
31
- consider adding `WHERE deleted_at IS NULL` to index_profiles_on_first_name_and_last_name - a partial index can speed lookups of soft-deletable models
34
+ consider adding `WHERE deleted_at IS NULL` or `WHERE deleted_at IS NOT NULL` to index_profiles_on_first_name_and_last_name - a partial index can speed lookups of soft-deletable models
32
35
  OUTPUT
33
36
  end
34
37
 
@@ -42,6 +45,9 @@ class ActiveRecordDoctor::Detectors::UnindexedDeletedAtTest < Minitest::Test
42
45
  t.index [:first_name, :last_name],
43
46
  name: "index_profiles_on_first_name_and_last_name",
44
47
  where: "discarded_at IS NULL"
48
+ t.index [:last_name],
49
+ name: "index_discarded_profiles_on_last_name",
50
+ where: "discarded_at IS NOT NULL"
45
51
  end
46
52
 
47
53
  refute_problems
@@ -59,7 +65,7 @@ class ActiveRecordDoctor::Detectors::UnindexedDeletedAtTest < Minitest::Test
59
65
  end
60
66
 
61
67
  assert_problems(<<~OUTPUT)
62
- consider adding `WHERE discarded_at IS NULL` to index_profiles_on_first_name_and_last_name - a partial index can speed lookups of soft-deletable models
68
+ consider adding `WHERE discarded_at IS NULL` or `WHERE discarded_at IS NOT NULL` to index_profiles_on_first_name_and_last_name - a partial index can speed lookups of soft-deletable models
63
69
  OUTPUT
64
70
  end
65
71
 
@@ -165,7 +171,7 @@ class ActiveRecordDoctor::Detectors::UnindexedDeletedAtTest < Minitest::Test
165
171
  CONFIG
166
172
 
167
173
  assert_problems(<<~OUTPUT)
168
- consider adding `WHERE obliverated_at IS NULL` to index_profiles_on_first_name_and_last_name - a partial index can speed lookups of soft-deletable models
174
+ consider adding `WHERE obliverated_at IS NULL` or `WHERE obliverated_at IS NOT NULL` to index_profiles_on_first_name_and_last_name - a partial index can speed lookups of soft-deletable models
169
175
  OUTPUT
170
176
  end
171
177
  end
data/test/setup.rb CHANGED
@@ -104,13 +104,17 @@ class Minitest::Test
104
104
 
105
105
  def assert_problems(expected_output)
106
106
  success, output = run_detector
107
- assert_equal(expected_output, output)
107
+ assert_equal(sort_lines(expected_output), sort_lines(output))
108
108
  refute(success, "Expected the detector to return failure.")
109
109
  end
110
110
 
111
111
  def refute_problems(expected_output = "")
112
112
  success, output = run_detector
113
- assert_equal(expected_output, output)
113
+ assert_equal(sort_lines(expected_output), sort_lines(output))
114
114
  assert(success, "Expected the detector to return success.")
115
115
  end
116
+
117
+ def sort_lines(string)
118
+ string.split("\n").sort.join("\n")
119
+ end
116
120
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_record_doctor
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.0
4
+ version: 1.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Greg Navis
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-03 00:00:00.000000000 Z
11
+ date: 2022-07-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -126,6 +126,7 @@ files:
126
126
  - lib/active_record_doctor/detectors/extraneous_indexes.rb
127
127
  - lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb
128
128
  - lib/active_record_doctor/detectors/incorrect_dependent_option.rb
129
+ - lib/active_record_doctor/detectors/incorrect_length_validation.rb
129
130
  - lib/active_record_doctor/detectors/mismatched_foreign_key_type.rb
130
131
  - lib/active_record_doctor/detectors/missing_foreign_keys.rb
131
132
  - lib/active_record_doctor/detectors/missing_non_null_constraint.rb
@@ -147,9 +148,11 @@ files:
147
148
  - lib/tasks/active_record_doctor.rake
148
149
  - test/active_record_doctor/config/loader_test.rb
149
150
  - test/active_record_doctor/config_test.rb
151
+ - test/active_record_doctor/detectors/disable_test.rb
150
152
  - test/active_record_doctor/detectors/extraneous_indexes_test.rb
151
153
  - test/active_record_doctor/detectors/incorrect_boolean_presence_validation_test.rb
152
154
  - test/active_record_doctor/detectors/incorrect_dependent_option_test.rb
155
+ - test/active_record_doctor/detectors/incorrect_length_validation_test.rb
153
156
  - test/active_record_doctor/detectors/mismatched_foreign_key_type_test.rb
154
157
  - test/active_record_doctor/detectors/missing_foreign_keys_test.rb
155
158
  - test/active_record_doctor/detectors/missing_non_null_constraint_test.rb
@@ -182,16 +185,18 @@ required_rubygems_version: !ruby/object:Gem::Requirement
182
185
  - !ruby/object:Gem::Version
183
186
  version: '0'
184
187
  requirements: []
185
- rubygems_version: 3.2.32
188
+ rubygems_version: 3.2.33
186
189
  signing_key:
187
190
  specification_version: 4
188
191
  summary: Identify database issues before they hit production.
189
192
  test_files:
190
193
  - test/active_record_doctor/config/loader_test.rb
191
194
  - test/active_record_doctor/config_test.rb
195
+ - test/active_record_doctor/detectors/disable_test.rb
192
196
  - test/active_record_doctor/detectors/extraneous_indexes_test.rb
193
197
  - test/active_record_doctor/detectors/incorrect_boolean_presence_validation_test.rb
194
198
  - test/active_record_doctor/detectors/incorrect_dependent_option_test.rb
199
+ - test/active_record_doctor/detectors/incorrect_length_validation_test.rb
195
200
  - test/active_record_doctor/detectors/mismatched_foreign_key_type_test.rb
196
201
  - test/active_record_doctor/detectors/missing_foreign_keys_test.rb
197
202
  - test/active_record_doctor/detectors/missing_non_null_constraint_test.rb