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.
- checksums.yaml +4 -4
- data/README.md +29 -0
- data/lib/active_record_doctor.rb +16 -12
- data/lib/active_record_doctor/detectors.rb +13 -0
- data/lib/active_record_doctor/detectors/base.rb +64 -0
- data/lib/active_record_doctor/{tasks → detectors}/extraneous_indexes.rb +11 -7
- data/lib/active_record_doctor/{tasks → detectors}/incorrect_boolean_presence_validation.rb +9 -6
- data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +71 -0
- data/lib/active_record_doctor/{tasks → detectors}/missing_foreign_keys.rb +13 -10
- data/lib/active_record_doctor/{tasks → detectors}/missing_non_null_constraint.rb +11 -7
- data/lib/active_record_doctor/{tasks → detectors}/missing_presence_validation.rb +11 -8
- data/lib/active_record_doctor/{tasks → detectors}/missing_unique_indexes.rb +8 -4
- data/lib/active_record_doctor/{tasks → detectors}/undefined_table_references.rb +11 -12
- data/lib/active_record_doctor/{tasks → detectors}/unindexed_deleted_at.rb +12 -6
- data/lib/active_record_doctor/{tasks → detectors}/unindexed_foreign_keys.rb +13 -10
- data/lib/active_record_doctor/printers.rb +3 -1
- data/lib/active_record_doctor/printers/io_printer.rb +63 -35
- data/lib/active_record_doctor/railtie.rb +2 -0
- data/lib/active_record_doctor/task.rb +28 -0
- data/lib/active_record_doctor/version.rb +3 -1
- data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +15 -11
- data/lib/tasks/active_record_doctor.rake +25 -25
- data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +67 -0
- data/test/active_record_doctor/detectors/incorrect_boolean_presence_validation_test.rb +36 -0
- data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +117 -0
- data/test/active_record_doctor/detectors/missing_foreign_keys_test.rb +24 -0
- data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +102 -0
- data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +107 -0
- data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +114 -0
- data/test/active_record_doctor/detectors/undefined_table_references_test.rb +44 -0
- data/test/active_record_doctor/detectors/unindexed_deleted_at_test.rb +67 -0
- data/test/active_record_doctor/detectors/unindexed_foreign_keys_test.rb +26 -0
- data/test/active_record_doctor/printers/io_printer_test.rb +14 -9
- data/test/model_factory.rb +78 -0
- data/test/setup.rb +69 -40
- metadata +70 -64
- data/lib/active_record_doctor/tasks.rb +0 -10
- data/lib/active_record_doctor/tasks/base.rb +0 -86
- data/test/active_record_doctor/tasks/extraneous_indexes_test.rb +0 -77
- data/test/active_record_doctor/tasks/incorrect_boolean_presence_validation_test.rb +0 -38
- data/test/active_record_doctor/tasks/missing_foreign_keys_test.rb +0 -23
- data/test/active_record_doctor/tasks/missing_non_null_constraint_test.rb +0 -113
- data/test/active_record_doctor/tasks/missing_presence_validation_test.rb +0 -115
- data/test/active_record_doctor/tasks/missing_unique_indexes_test.rb +0 -126
- data/test/active_record_doctor/tasks/undefined_table_references_test.rb +0 -47
- data/test/active_record_doctor/tasks/unindexed_deleted_at_test.rb +0 -59
- 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
|