active_record_doctor 1.9.0 → 1.10.0

Sign up to get free protection for your applications and to get access to all the features.
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