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
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveRecordDoctor::Detectors::ExtraneousIndexesTest < Minitest::Test
4
+ def test_index_on_primary_key_is_duplicate
5
+ create_table(:users) do |t|
6
+ t.index :id
7
+ end
8
+
9
+ assert_problems(<<OUTPUT)
10
+ The following indexes are extraneous and can be removed:
11
+ index_users_on_id (is a primary key of users)
12
+ OUTPUT
13
+ end
14
+
15
+ def test_non_unique_version_of_index_is_duplicate
16
+ create_table(:users) do |t|
17
+ t.string :email
18
+ t.index :email, unique: true, name: "unique_index_on_users_email"
19
+ end
20
+
21
+ # Rails 4.2 compatibility - can't be pulled into the block above.
22
+ ActiveRecord::Base.connection.add_index :users, :email, name: "index_users_on_email"
23
+
24
+ assert_problems(<<OUTPUT)
25
+ The following indexes are extraneous and can be removed:
26
+ index_users_on_email (can be handled by unique_index_on_users_email)
27
+ OUTPUT
28
+ end
29
+
30
+ def test_single_column_covered_by_unique_and_non_unique_multi_column_is_duplicate
31
+ create_table(:users) do |t|
32
+ t.string :first_name
33
+ t.string :last_name
34
+ t.string :email
35
+ t.index [:last_name, :first_name, :email]
36
+ t.index [:last_name, :first_name],
37
+ unique: true,
38
+ name: "unique_index_on_users_last_name_and_first_name"
39
+ t.index :last_name
40
+ end
41
+
42
+ assert_problems(<<OUTPUT)
43
+ The following indexes are extraneous and can be removed:
44
+ index_users_on_last_name (can be handled by index_users_on_last_name_and_first_name_and_email, unique_index_on_users_last_name_and_first_name)
45
+ OUTPUT
46
+ end
47
+
48
+ def test_multi_column_covered_by_unique_and_non_unique_multi_column_is_duplicate
49
+ create_table(:users) do |t|
50
+ t.string :first_name
51
+ t.string :last_name
52
+ t.string :email
53
+ t.index [:last_name, :first_name, :email]
54
+ t.index [:last_name, :first_name],
55
+ unique: true,
56
+ name: "unique_index_on_users_last_name_and_first_name"
57
+ end
58
+
59
+ # Rails 4.2 compatibility - can't be pulled into the block above.
60
+ ActiveRecord::Base.connection.add_index :users, [:last_name, :first_name]
61
+
62
+ assert_problems(<<OUTPUT)
63
+ The following indexes are extraneous and can be removed:
64
+ index_users_on_last_name_and_first_name (can be handled by index_users_on_last_name_and_first_name_and_email, unique_index_on_users_last_name_and_first_name)
65
+ OUTPUT
66
+ end
67
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveRecordDoctor::Detectors::IncorrectBooleanPresenceValidationTest < Minitest::Test
4
+ def test_presence_true_is_reported_on_boolean_only
5
+ create_table(:users) do |t|
6
+ t.string :email, null: false
7
+ t.boolean :active, null: false
8
+ end.create_model do
9
+ # email is a non-boolean column whose presence CAN be validated in the
10
+ # usual way. We include it in the test model to ensure the detector reports
11
+ # only boolean columns.
12
+ validates :email, :active, presence: true
13
+ end
14
+
15
+ assert_problems(<<OUTPUT)
16
+ The presence of the following boolean columns is validated incorrectly:
17
+ ModelFactory::Models::User: active
18
+ OUTPUT
19
+ end
20
+
21
+ def test_inclusion_is_not_reported
22
+ create_table(:users) do |t|
23
+ t.boolean :active, null: false
24
+ end.create_model do
25
+ validates :active, inclusion: { in: [true, false] }
26
+ end
27
+
28
+ refute_problems
29
+ end
30
+
31
+ def test_models_with_non_existent_tables_are_skipped
32
+ create_model(:users)
33
+
34
+ refute_problems
35
+ end
36
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveRecordDoctor::Detectors::IncorrectDependentOptionTest < Minitest::Test
4
+ def test_invoking_no_callbacks_suggests_delete_all
5
+ create_table(:companies) do
6
+ end.create_model do
7
+ has_many :users, dependent: :destroy
8
+ end
9
+
10
+ create_table(:users) do |t|
11
+ t.references :companies
12
+ end.create_model do
13
+ belongs_to :company
14
+ end
15
+
16
+ assert_problems(<<OUTPUT)
17
+ The following associations might be using invalid dependent settings:
18
+ ModelFactory::Models::Company: users loads models one-by-one to invoke callbacks even though the related model defines none - consider using `dependent: :delete_all`
19
+ OUTPUT
20
+ end
21
+
22
+ def test_invoking_callbacks_does_not_suggest_delete_all
23
+ create_table(:companies) do
24
+ end.create_model do
25
+ has_many :users, dependent: :destroy
26
+ end
27
+
28
+ create_table(:users) do |t|
29
+ t.references :companies
30
+ end.create_model do
31
+ belongs_to :company
32
+
33
+ before_destroy :log
34
+
35
+ def log
36
+ end
37
+ end
38
+
39
+ refute_problems
40
+ end
41
+
42
+ def test_skipping_callbacks_suggests_destroy
43
+ create_table(:companies) do
44
+ end.create_model do
45
+ has_many :users, dependent: :delete_all
46
+ end
47
+
48
+ create_table(:users) do |t|
49
+ t.references :companies
50
+ end.create_model do
51
+ belongs_to :company
52
+
53
+ before_destroy :log
54
+
55
+ def log
56
+ end
57
+ end
58
+
59
+ assert_problems(<<OUTPUT)
60
+ The following associations might be using invalid dependent settings:
61
+ ModelFactory::Models::Company: users skips callbacks that are defined on the associated model - consider changing to `dependent: :destroy` or similar
62
+ OUTPUT
63
+ end
64
+
65
+ def test_invoking_callbacks_does_not_suggest_destroy
66
+ create_table(:companies) do
67
+ end.create_model do
68
+ has_many :users, dependent: :destroy
69
+ end
70
+
71
+ create_table(:users) do |t|
72
+ t.references :companies
73
+ end.create_model do
74
+ belongs_to :company
75
+
76
+ before_destroy :log
77
+
78
+ def log
79
+ end
80
+ end
81
+
82
+ refute_problems
83
+ end
84
+
85
+ def test_works_on_has_one
86
+ create_table(:companies) do
87
+ end.create_model do
88
+ has_one :owner, class_name: "ModelFactory::Models::User", dependent: :destroy
89
+ end
90
+
91
+ create_table(:users) do |t|
92
+ t.references :companies
93
+ end.create_model do
94
+ belongs_to :company
95
+ end
96
+
97
+ assert_problems(<<OUTPUT)
98
+ The following associations might be using invalid dependent settings:
99
+ ModelFactory::Models::Company: owner loads the associated model before deleting it - consider using `dependent: :delete`
100
+ OUTPUT
101
+ end
102
+
103
+ def test_no_dependent_suggests_nothing
104
+ create_table(:companies) do
105
+ end.create_model do
106
+ has_many :users
107
+ end
108
+
109
+ create_table(:users) do |t|
110
+ t.references :companies
111
+ end.create_model do
112
+ belongs_to :company
113
+ end
114
+
115
+ refute_problems
116
+ end
117
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveRecordDoctor::Detectors::MissingForeignKeysTest < Minitest::Test
4
+ def test_missing_foreign_key_is_reported
5
+ create_table(:companies)
6
+ create_table(:users) do |t|
7
+ t.references :company, foreign_key: false
8
+ end
9
+
10
+ assert_problems(<<OUTPUT)
11
+ The following columns lack a foreign key constraint:
12
+ users company_id
13
+ OUTPUT
14
+ end
15
+
16
+ def test_present_foreign_key_is_not_reported
17
+ create_table(:companies)
18
+ create_table(:users) do |t|
19
+ t.references :company, foreign_key: true
20
+ end
21
+
22
+ refute_problems
23
+ end
24
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveRecordDoctor::Detectors::MissingNonNullConstraintTest < Minitest::Test
4
+ def test_presence_true_and_null_true
5
+ create_table(:users) do |t|
6
+ t.string :name, null: true
7
+ end.create_model do
8
+ validates :name, presence: true
9
+ end
10
+
11
+ assert_problems(<<OUTPUT)
12
+ The following columns should be marked as `null: false`:
13
+ users: name
14
+ OUTPUT
15
+ end
16
+
17
+ def test_association_presence_true_and_null_true
18
+ create_table(:companies)
19
+ create_table(:users) do |t|
20
+ t.references :company
21
+ end.create_model do
22
+ belongs_to :company, required: true
23
+ end
24
+
25
+ assert_problems(<<OUTPUT)
26
+ The following columns should be marked as `null: false`:
27
+ users: company_id
28
+ OUTPUT
29
+ end
30
+
31
+ def test_presence_true_and_null_false
32
+ create_table(:users) do |t|
33
+ t.string :name, null: false
34
+ end.create_model do
35
+ validates :name, presence: true
36
+ end
37
+
38
+ refute_problems
39
+ end
40
+
41
+ def test_presence_false_and_null_true
42
+ create_table(:users) do |t|
43
+ t.string :name, null: true
44
+ end.create_model do
45
+ # The age validator is a form of regression test against a bug that
46
+ # caused false positives. In this test case, name is NOT validated
47
+ # for presence so it does NOT need be marked non-NULL. However, the
48
+ # bug would match the age presence validator with the NULL-able name
49
+ # column which would result in a false positive error report.
50
+ validates :age, presence: true
51
+ validates :name, presence: false
52
+ end
53
+
54
+ refute_problems
55
+ end
56
+
57
+ def test_presence_false_and_null_false
58
+ create_table(:users) do |t|
59
+ t.string :name, null: false
60
+ end.create_model do
61
+ validates :name, presence: false
62
+ end
63
+
64
+ refute_problems
65
+ end
66
+
67
+ def test_presence_true_with_if
68
+ create_table(:users) do |t|
69
+ t.string :name, null: true
70
+ end.create_model do
71
+ validates :name, presence: true, if: -> { false }
72
+ end
73
+
74
+ refute_problems
75
+ end
76
+
77
+ def test_presence_true_with_unless
78
+ create_table(:users) do |t|
79
+ t.string :name, null: true
80
+ end.create_model do
81
+ validates :name, presence: true, unless: -> { false }
82
+ end
83
+
84
+ refute_problems
85
+ end
86
+
87
+ def test_presence_true_with_allow_nil
88
+ create_table(:users) do |t|
89
+ t.string :name, null: true
90
+ end.create_model do
91
+ validates :name, presence: true, allow_nil: true
92
+ end
93
+
94
+ refute_problems
95
+ end
96
+
97
+ def test_models_with_non_existent_tables_are_skipped
98
+ create_model(:users)
99
+
100
+ refute_problems
101
+ end
102
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveRecordDoctor::Detectors::MissingPresenceValidationTest < Minitest::Test
4
+ def test_null_column_is_not_reported_if_validation_absent
5
+ create_table(:users) do |t|
6
+ t.string :name
7
+ end.create_model do
8
+ end
9
+
10
+ refute_problems
11
+ end
12
+
13
+ def test_non_null_column_is_reported_if_validation_absent
14
+ create_table(:users) do |t|
15
+ t.string :name, null: false
16
+ end.create_model do
17
+ end
18
+
19
+ assert_problems(<<OUTPUT)
20
+ The following models and columns should have presence validations:
21
+ ModelFactory::Models::User: name
22
+ OUTPUT
23
+ end
24
+
25
+ def test_non_null_column_is_not_reported_if_validation_present
26
+ create_table(:users) do |t|
27
+ t.string :name, null: false
28
+ end.create_model do
29
+ validates :name, presence: true
30
+ end
31
+
32
+ refute_problems
33
+ end
34
+
35
+ def test_non_null_column_is_not_reported_if_association_validation_present
36
+ create_table(:companies).create_model
37
+ create_table(:users) do |t|
38
+ t.references :company, null: false
39
+ end.create_model do
40
+ belongs_to :company, required: true
41
+ end
42
+
43
+ refute_problems
44
+ end
45
+
46
+ def test_non_null_boolean_is_reported_if_nil_included
47
+ create_table(:users) do |t|
48
+ t.boolean :active, null: false
49
+ end.create_model do
50
+ validates :active, inclusion: { in: [nil, true, false] }
51
+ end
52
+
53
+ assert_problems(<<OUTPUT)
54
+ The following models and columns should have presence validations:
55
+ ModelFactory::Models::User: active
56
+ OUTPUT
57
+ end
58
+
59
+ def test_non_null_boolean_is_not_reported_if_nil_not_included
60
+ create_table(:users) do |t|
61
+ t.boolean :active, null: false
62
+ end.create_model do
63
+ validates :active, inclusion: { in: [true, false] }
64
+ end
65
+
66
+ refute_problems
67
+ end
68
+
69
+ def test_non_null_boolean_is_not_reported_if_nil_excluded
70
+ create_table(:users) do |t|
71
+ t.boolean :active, null: false
72
+ end.create_model do
73
+ validates :active, exclusion: { in: [nil] }
74
+ end
75
+
76
+ refute_problems
77
+ end
78
+
79
+ def test_non_null_boolean_is_reported_if_nil_not_excluded
80
+ create_table(:users) do |t|
81
+ t.boolean :active, null: false
82
+ end.create_model do
83
+ validates :active, exclusion: { in: [false] }
84
+ end
85
+
86
+ assert_problems(<<OUTPUT)
87
+ The following models and columns should have presence validations:
88
+ ModelFactory::Models::User: active
89
+ OUTPUT
90
+ end
91
+
92
+ def test_timestamps_are_not_reported
93
+ create_table(:users) do |t|
94
+ t.timestamps null: false
95
+ end.create_model do
96
+ validates :name, presence: true
97
+ end
98
+
99
+ refute_problems
100
+ end
101
+
102
+ def test_models_with_non_existent_tables_are_skipped
103
+ create_model(:users)
104
+
105
+ refute_problems
106
+ end
107
+ end