active_record_doctor 1.7.2 → 1.8.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +29 -0
  3. data/lib/active_record_doctor.rb +16 -12
  4. data/lib/active_record_doctor/detectors.rb +13 -0
  5. data/lib/active_record_doctor/detectors/base.rb +64 -0
  6. data/lib/active_record_doctor/{tasks → detectors}/extraneous_indexes.rb +11 -7
  7. data/lib/active_record_doctor/{tasks → detectors}/incorrect_boolean_presence_validation.rb +9 -6
  8. data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +71 -0
  9. data/lib/active_record_doctor/{tasks → detectors}/missing_foreign_keys.rb +13 -10
  10. data/lib/active_record_doctor/{tasks → detectors}/missing_non_null_constraint.rb +11 -7
  11. data/lib/active_record_doctor/{tasks → detectors}/missing_presence_validation.rb +11 -8
  12. data/lib/active_record_doctor/{tasks → detectors}/missing_unique_indexes.rb +8 -4
  13. data/lib/active_record_doctor/{tasks → detectors}/undefined_table_references.rb +11 -12
  14. data/lib/active_record_doctor/{tasks → detectors}/unindexed_deleted_at.rb +12 -6
  15. data/lib/active_record_doctor/{tasks → detectors}/unindexed_foreign_keys.rb +13 -10
  16. data/lib/active_record_doctor/printers.rb +3 -1
  17. data/lib/active_record_doctor/printers/io_printer.rb +63 -35
  18. data/lib/active_record_doctor/railtie.rb +2 -0
  19. data/lib/active_record_doctor/task.rb +28 -0
  20. data/lib/active_record_doctor/version.rb +3 -1
  21. data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +15 -11
  22. data/lib/tasks/active_record_doctor.rake +25 -25
  23. data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +67 -0
  24. data/test/active_record_doctor/detectors/incorrect_boolean_presence_validation_test.rb +36 -0
  25. data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +117 -0
  26. data/test/active_record_doctor/detectors/missing_foreign_keys_test.rb +24 -0
  27. data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +102 -0
  28. data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +107 -0
  29. data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +114 -0
  30. data/test/active_record_doctor/detectors/undefined_table_references_test.rb +44 -0
  31. data/test/active_record_doctor/detectors/unindexed_deleted_at_test.rb +67 -0
  32. data/test/active_record_doctor/detectors/unindexed_foreign_keys_test.rb +26 -0
  33. data/test/active_record_doctor/printers/io_printer_test.rb +14 -9
  34. data/test/model_factory.rb +78 -0
  35. data/test/setup.rb +69 -40
  36. metadata +70 -64
  37. data/lib/active_record_doctor/tasks.rb +0 -10
  38. data/lib/active_record_doctor/tasks/base.rb +0 -86
  39. data/test/active_record_doctor/tasks/extraneous_indexes_test.rb +0 -77
  40. data/test/active_record_doctor/tasks/incorrect_boolean_presence_validation_test.rb +0 -38
  41. data/test/active_record_doctor/tasks/missing_foreign_keys_test.rb +0 -23
  42. data/test/active_record_doctor/tasks/missing_non_null_constraint_test.rb +0 -113
  43. data/test/active_record_doctor/tasks/missing_presence_validation_test.rb +0 -115
  44. data/test/active_record_doctor/tasks/missing_unique_indexes_test.rb +0 -126
  45. data/test/active_record_doctor/tasks/undefined_table_references_test.rb +0 -47
  46. data/test/active_record_doctor/tasks/unindexed_deleted_at_test.rb +0 -59
  47. data/test/active_record_doctor/tasks/unindexed_foreign_keys_test.rb +0 -23
@@ -1,23 +0,0 @@
1
- class ActiveRecordDoctor::Tasks::MissingForeignKeysTest < Minitest::Test
2
- def test_missing_foreign_key_is_reported
3
- Temping.create(:companies, temporary: false)
4
- Temping.create(:users, temporary: false) do
5
- with_columns do |t|
6
- t.references :company, foreign_key: false
7
- end
8
- end
9
-
10
- assert_equal({'users' => ['company_id']}, run_task)
11
- end
12
-
13
- def test_present_foreign_key_is_not_reported
14
- Temping.create(:companies, temporary: false)
15
- Temping.create(:users, temporary: false) do
16
- with_columns do |t|
17
- t.references :company, foreign_key: true
18
- end
19
- end
20
-
21
- assert_equal({}, run_task)
22
- end
23
- end
@@ -1,113 +0,0 @@
1
- class ActiveRecordDoctor::Tasks::MissingNonNullConstraintTest < Minitest::Test
2
- def test_presence_true_and_null_true
3
- Temping.create(:users, temporary: false) do
4
- validates :name, presence: true
5
-
6
- with_columns do |t|
7
- t.string :name, null: true
8
- end
9
- end
10
-
11
- assert_equal({ 'users' => ['name'] }, run_task)
12
- end
13
-
14
- def test_association_presence_true_and_null_true
15
- Temping.create(:companies, temporary: false)
16
- Temping.create(:users, temporary: false) do
17
- belongs_to :company, required: true
18
-
19
- with_columns do |t|
20
- t.references :company
21
- end
22
- end
23
-
24
- assert_equal({ 'users' => ['company_id'] }, run_task)
25
- end
26
-
27
- def test_presence_true_and_null_false
28
- Temping.create(:users, temporary: false) do
29
- validates :name, presence: true
30
-
31
- with_columns do |t|
32
- t.string :name, null: false
33
- end
34
- end
35
-
36
- assert_equal({}, run_task)
37
- end
38
-
39
- def test_presence_false_and_null_true
40
- Temping.create(:users, temporary: false) do
41
- # The age validator is a form of regression test against a bug that
42
- # caused false positives. In this test case, name is NOT validated
43
- # for presence so it does NOT need be marked non-NULL. However, the
44
- # bug would match the age presence validator with the NULL-able name
45
- # column which would result in a false positive error report.
46
- validates :age, presence: true
47
- validates :name, presence: false
48
-
49
- with_columns do |t|
50
- t.string :name, null: true
51
- end
52
- end
53
-
54
- assert_equal({}, run_task)
55
- end
56
-
57
- def test_presence_false_and_null_false
58
- Temping.create(:users, temporary: false) do
59
- validates :name, presence: false
60
-
61
- with_columns do |t|
62
- t.string :name, null: false
63
- end
64
- end
65
-
66
- assert_equal({}, run_task)
67
- end
68
-
69
- def test_presence_true_with_if
70
- Temping.create(:users, temporary: false) do
71
- validates :name, presence: true, if: -> { false }
72
-
73
- with_columns do |t|
74
- t.string :name, null: true
75
- end
76
- end
77
-
78
- assert_equal({}, run_task)
79
- end
80
-
81
- def test_presence_true_with_unless
82
- Temping.create(:users, temporary: false) do
83
- validates :name, presence: true, unless: -> { false }
84
-
85
- with_columns do |t|
86
- t.string :name, null: true
87
- end
88
- end
89
-
90
- assert_equal({}, run_task)
91
- end
92
-
93
- def test_presence_true_with_allow_nil
94
- Temping.create(:users, temporary: false) do
95
- validates :name, presence: true, allow_nil: true
96
-
97
- with_columns do |t|
98
- t.string :name, null: true
99
- end
100
- end
101
-
102
- assert_equal({}, run_task)
103
- end
104
-
105
- def test_models_with_non_existent_tables_are_skipped
106
- klass = Class.new(ActiveRecord::Base) do
107
- self.table_name = 'action_text_rich_texts'
108
- end
109
-
110
- # No need to assert anything as merely not raising an exception is a success.
111
- run_task
112
- end
113
- end
@@ -1,115 +0,0 @@
1
- class ActiveRecordDoctor::Tasks::MissingPresenceValidationTest < Minitest::Test
2
- def test_null_column_is_not_reported_if_validation_absent
3
- Temping.create(:users, temporary: false) do
4
- with_columns do |t|
5
- t.string :name
6
- end
7
- end
8
-
9
- assert_equal({}, run_task)
10
- end
11
-
12
- def test_non_null_column_is_reported_if_validation_absent
13
- Temping.create(:users, temporary: false) do
14
- with_columns do |t|
15
- t.string :name, null: false
16
- end
17
- end
18
-
19
- assert_equal({ 'User' => ['name'] }, run_task)
20
- end
21
-
22
- def test_non_null_column_is_not_reported_if_validation_present
23
- Temping.create(:users, temporary: false) do
24
- validates :name, presence: true
25
-
26
- with_columns do |t|
27
- t.string :name, null: false
28
- end
29
- end
30
-
31
- assert_equal({}, run_task)
32
- end
33
-
34
- def test_non_null_column_is_not_reported_if_association_validation_present
35
- Temping.create(:companies, temporary: false)
36
- Temping.create(:users, temporary: false) do
37
- belongs_to :company, required: true
38
-
39
- with_columns do |t|
40
- t.references :company, null: false
41
- end
42
- end
43
-
44
- assert_equal({}, run_task)
45
- end
46
-
47
- def test_non_null_boolean_is_reported_if_nil_included
48
- Temping.create(:users, temporary: false) do
49
- validates :active, inclusion: { in: [nil, true, false] }
50
-
51
- with_columns do |t|
52
- t.boolean :active, null: false
53
- end
54
- end
55
-
56
- assert_equal({ 'User' => ['active'] }, run_task)
57
- end
58
-
59
- def test_non_null_boolean_is_not_reported_if_nil_not_included
60
- Temping.create(:users, temporary: false) do
61
- validates :active, inclusion: { in: [true, false] }
62
-
63
- with_columns do |t|
64
- t.boolean :active, null: false
65
- end
66
- end
67
-
68
- assert_equal({}, run_task)
69
- end
70
-
71
- def test_non_null_boolean_is_not_reported_if_nil_excluded
72
- Temping.create(:users, temporary: false) do
73
- validates :active, exclusion: { in: [nil] }
74
-
75
- with_columns do |t|
76
- t.boolean :active, null: false
77
- end
78
- end
79
-
80
- assert_equal({}, run_task)
81
- end
82
-
83
- def test_non_null_boolean_is_reported_if_nil_not_excluded
84
- Temping.create(:users, temporary: false) do
85
- validates :active, exclusion: { in: [false] }
86
-
87
- with_columns do |t|
88
- t.boolean :active, null: false
89
- end
90
- end
91
-
92
- assert_equal({ 'User' => ['active'] }, run_task)
93
- end
94
-
95
- def test_timestamps_are_not_reported
96
- Temping.create(:users, temporary: false) do
97
- validates :name, presence: true
98
-
99
- with_columns do |t|
100
- t.timestamps null: false
101
- end
102
- end
103
-
104
- assert_equal({}, run_task)
105
- end
106
-
107
- def test_models_with_non_existent_tables_are_skipped
108
- klass = Class.new(ActiveRecord::Base) do
109
- self.table_name = 'action_text_rich_texts'
110
- end
111
-
112
- # No need to assert anything as merely not raising an exception is a success.
113
- run_task
114
- end
115
- end
@@ -1,126 +0,0 @@
1
- class ActiveRecordDoctor::Tasks::MissingUniqueIndexesTest < Minitest::Test
2
- def test_missing_unique_index
3
- Temping.create(:users, temporary: false) do
4
- with_columns do |t|
5
- t.string :email
6
- t.index :email
7
- end
8
-
9
- validates :email, uniqueness: true
10
- end
11
-
12
- assert_result([
13
- ['users', [['email']]]
14
- ])
15
- end
16
-
17
- def test_present_unique_index
18
- Temping.create(:users, temporary: false) do
19
- with_columns do |t|
20
- t.string :email
21
- t.index :email, unique: true
22
- end
23
-
24
- validates :email, uniqueness: true
25
- end
26
-
27
- assert_result([])
28
- end
29
-
30
- def test_missing_unique_index_with_scope
31
- Temping.create(:users, temporary: false) do
32
- with_columns do |t|
33
- t.string :email
34
- t.integer :company_id
35
- t.integer :department_id
36
- t.index [:company_id, :department_id, :email]
37
- end
38
-
39
- validates :email, uniqueness: { scope: [:company_id, :department_id] }
40
- end
41
-
42
- assert_result([
43
- ['users', [['company_id', 'department_id', 'email']]]
44
- ])
45
- end
46
-
47
- def test_present_unique_index_with_scope
48
- Temping.create(:users, temporary: false) do
49
- with_columns do |t|
50
- t.string :email
51
- t.integer :company_id
52
- t.integer :department_id
53
- t.index [:company_id, :department_id, :email], unique: true
54
- end
55
-
56
- validates :email, uniqueness: { scope: [:company_id, :department_id] }
57
- end
58
-
59
- assert_result([])
60
- end
61
-
62
- def test_column_order_is_ignored
63
- Temping.create(:users, temporary: false) do
64
- with_columns do |t|
65
- t.string :email
66
- t.integer :organization_id
67
-
68
- t.index [:email, :organization_id], unique: true
69
- end
70
-
71
- validates :email, uniqueness: { scope: :organization_id }
72
- end
73
-
74
- assert_result([])
75
- end
76
-
77
- def test_conditions_is_skipped
78
- assert_skipped(conditions: -> { where.not(email: nil) })
79
- end
80
-
81
- def test_case_insensitive_is_skipped
82
- assert_skipped(case_sensitive: false)
83
- end
84
-
85
- def test_if_is_skipped
86
- assert_skipped(if: ->(model) { true })
87
- end
88
-
89
- def test_unless_is_skipped
90
- assert_skipped(unless: ->(model) { true })
91
- end
92
-
93
- def test_skips_validator_without_attributes
94
- Temping.create(:users, temporary: false) do
95
- with_columns do |t|
96
- t.string :email
97
- t.index :email
98
- end
99
-
100
- validates_with DummyValidator
101
- end
102
-
103
- # There's no need for assert/refute as it's enough the line below doesn't
104
- # raise an exception.
105
- run_task
106
- end
107
-
108
- class DummyValidator < ActiveModel::Validator
109
- def validate(record)
110
- end
111
- end
112
-
113
- private
114
-
115
- def assert_skipped(options)
116
- Temping.create(:users, temporary: false) do
117
- with_columns do |t|
118
- t.string :email
119
- end
120
-
121
- validates :email, uniqueness: options
122
- end
123
-
124
- assert_result([])
125
- end
126
- end
@@ -1,47 +0,0 @@
1
- class ActiveRecordDoctor::Tasks::UndefinedTableReferencesTest < Minitest::Test
2
- def test_table_exists
3
- # No columns needed, just the table.
4
- Temping.create(:users, temporary: false)
5
-
6
- assert_equal([[], true], run_task)
7
- end
8
-
9
- def test_table_does_not_exist
10
- # No columns needed, just the table.
11
- Temping.create(:users, temporary: false)
12
-
13
- # We drop the underlying table to make the model invalid.
14
- ActiveRecord::Base.connection.drop_table(User.table_name)
15
-
16
- # We wrap the assertion in begin/ensure because we must recreate the
17
- # table as otherwise Temping will raise an error. Assertion errors are
18
- # signalled via exceptions which we shouldn't swallow if we don't want to
19
- # break the test suite hence the choice of begin/ensure.
20
- begin
21
- assert_equal([[[User.name, User.table_name]], true], run_task)
22
- ensure
23
- ActiveRecord::Base.connection.create_table(User.table_name)
24
- end
25
- end
26
-
27
- def test_view_instead_of_table
28
- # No columns needed, just the table.
29
- Temping.create(:users, temporary: false)
30
-
31
- # We replace the underlying table with a view. The view doesn't have to be
32
- # backed by an actual table - it can simply return a predefined tuple.
33
- ActiveRecord::Base.connection.drop_table(User.table_name)
34
- ActiveRecord::Base.connection.execute("CREATE VIEW users AS SELECT 1")
35
-
36
- # We wrap the assertion in begin/ensure because we must recreate the
37
- # table as otherwise Temping will raise an error. Assertion errors are
38
- # signalled via exceptions which we shouldn't swallow if we don't want to
39
- # break the test suite hence the choice of begin/ensure.
40
- begin
41
- assert_equal([[], true], run_task)
42
- ensure
43
- ActiveRecord::Base.connection.execute("DROP VIEW users")
44
- ActiveRecord::Base.connection.create_table(User.table_name)
45
- end
46
- end
47
- end
@@ -1,59 +0,0 @@
1
- class ActiveRecordDoctor::Tasks::UnindexedDeletedAtTest < Minitest::Test
2
- def test_indexed_deleted_at_is_not_reported
3
- Temping.create(:users, temporary: false) do
4
- with_columns do |t|
5
- t.string :first_name
6
- t.string :last_name
7
- t.datetime :deleted_at
8
- t.index [:first_name, :last_name],
9
- name: 'index_profiles_on_first_name_and_last_name',
10
- where: 'deleted_at IS NULL'
11
- end
12
- end
13
-
14
- assert_result([])
15
- end
16
-
17
- def test_unindexed_deleted_at_is_reported
18
- Temping.create(:users, temporary: false) do
19
- with_columns do |t|
20
- t.string :first_name
21
- t.string :last_name
22
- t.datetime :deleted_at
23
- t.index [:first_name, :last_name],
24
- name: 'index_profiles_on_first_name_and_last_name'
25
- end
26
- end
27
-
28
- assert_result(['index_profiles_on_first_name_and_last_name'])
29
- end
30
-
31
- def test_indexed_discarded_at_is_not_reported
32
- Temping.create(:users, temporary: false) do
33
- with_columns do |t|
34
- t.string :first_name
35
- t.string :last_name
36
- t.datetime :discarded_at
37
- t.index [:first_name, :last_name],
38
- name: 'index_profiles_on_first_name_and_last_name',
39
- where: 'discarded_at IS NULL'
40
- end
41
- end
42
-
43
- assert_result([])
44
- end
45
-
46
- def test_unindexed_discarded_at_is_reported
47
- Temping.create(:users, temporary: false) do
48
- with_columns do |t|
49
- t.string :first_name
50
- t.string :last_name
51
- t.datetime :discarded_at
52
- t.index [:first_name, :last_name],
53
- name: 'index_profiles_on_first_name_and_last_name'
54
- end
55
- end
56
-
57
- assert_result(['index_profiles_on_first_name_and_last_name'])
58
- end
59
- end