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
@@ -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
|