active_record_doctor 1.5.0 → 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 (99) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +152 -12
  3. data/lib/active_record_doctor.rb +20 -2
  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 +12 -29
  7. data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +40 -0
  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 +17 -35
  10. data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +60 -0
  11. data/lib/active_record_doctor/detectors/missing_presence_validation.rb +78 -0
  12. data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +61 -0
  13. data/lib/active_record_doctor/detectors/undefined_table_references.rb +34 -0
  14. data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +29 -0
  15. data/lib/active_record_doctor/detectors/unindexed_foreign_keys.rb +48 -0
  16. data/lib/active_record_doctor/printers.rb +3 -1
  17. data/lib/active_record_doctor/printers/io_printer.rb +101 -26
  18. data/lib/active_record_doctor/railtie.rb +3 -1
  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 +33 -0
  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 +23 -10
  34. data/test/model_factory.rb +78 -0
  35. data/test/setup.rb +126 -0
  36. metadata +93 -149
  37. data/Rakefile +0 -28
  38. data/lib/active_record_doctor/compatibility.rb +0 -11
  39. data/lib/active_record_doctor/tasks.rb +0 -4
  40. data/lib/active_record_doctor/tasks/undefined_table_references.rb +0 -34
  41. data/lib/active_record_doctor/tasks/unindexed_deleted_at.rb +0 -40
  42. data/lib/active_record_doctor/tasks/unindexed_foreign_keys.rb +0 -66
  43. data/lib/tasks/active_record_doctor_tasks.rake +0 -27
  44. data/test/active_record_doctor/tasks/extraneous_indexes_test.rb +0 -27
  45. data/test/active_record_doctor/tasks/missing_foreign_keys_test.rb +0 -19
  46. data/test/active_record_doctor/tasks/undefined_table_references_test.rb +0 -19
  47. data/test/active_record_doctor/tasks/unindexed_deleted_at_test.rb +0 -19
  48. data/test/active_record_doctor/tasks/unindexed_foreign_keys_test.rb +0 -19
  49. data/test/dummy/README.rdoc +0 -28
  50. data/test/dummy/Rakefile +0 -6
  51. data/test/dummy/app/assets/javascripts/application.js +0 -13
  52. data/test/dummy/app/assets/stylesheets/application.css +0 -15
  53. data/test/dummy/app/controllers/application_controller.rb +0 -5
  54. data/test/dummy/app/helpers/application_helper.rb +0 -2
  55. data/test/dummy/app/models/application_record.rb +0 -3
  56. data/test/dummy/app/models/comment.rb +0 -3
  57. data/test/dummy/app/models/contract.rb +0 -3
  58. data/test/dummy/app/models/employer.rb +0 -2
  59. data/test/dummy/app/models/profile.rb +0 -2
  60. data/test/dummy/app/models/user.rb +0 -3
  61. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  62. data/test/dummy/bin/bundle +0 -3
  63. data/test/dummy/bin/rails +0 -4
  64. data/test/dummy/bin/rake +0 -4
  65. data/test/dummy/bin/setup +0 -29
  66. data/test/dummy/config.ru +0 -4
  67. data/test/dummy/config/application.rb +0 -23
  68. data/test/dummy/config/boot.rb +0 -5
  69. data/test/dummy/config/database.yml +0 -19
  70. data/test/dummy/config/database.yml.travis +0 -5
  71. data/test/dummy/config/environment.rb +0 -5
  72. data/test/dummy/config/environments/development.rb +0 -41
  73. data/test/dummy/config/environments/production.rb +0 -79
  74. data/test/dummy/config/environments/test.rb +0 -47
  75. data/test/dummy/config/initializers/assets.rb +0 -11
  76. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  77. data/test/dummy/config/initializers/cookies_serializer.rb +0 -3
  78. data/test/dummy/config/initializers/filter_parameter_logging.rb +0 -4
  79. data/test/dummy/config/initializers/inflections.rb +0 -16
  80. data/test/dummy/config/initializers/mime_types.rb +0 -4
  81. data/test/dummy/config/initializers/session_store.rb +0 -3
  82. data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
  83. data/test/dummy/config/locales/en.yml +0 -23
  84. data/test/dummy/config/routes.rb +0 -56
  85. data/test/dummy/config/secrets.yml +0 -22
  86. data/test/dummy/db/migrate/20160213101213_create_employers.rb +0 -15
  87. data/test/dummy/db/migrate/20160213101221_create_users.rb +0 -23
  88. data/test/dummy/db/migrate/20160213101232_create_profiles.rb +0 -15
  89. data/test/dummy/db/migrate/20160604081452_create_comments.rb +0 -11
  90. data/test/dummy/db/migrate/base_migration.rb +0 -5
  91. data/test/dummy/db/schema.rb +0 -68
  92. data/test/dummy/log/development.log +0 -532
  93. data/test/dummy/log/test.log +0 -2699
  94. data/test/dummy/public/404.html +0 -67
  95. data/test/dummy/public/422.html +0 -67
  96. data/test/dummy/public/500.html +0 -66
  97. data/test/dummy/public/favicon.ico +0 -0
  98. data/test/support/spy_printer.rb +0 -52
  99. data/test/test_helper.rb +0 -20
@@ -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
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveRecordDoctor::Detectors::MissingUniqueIndexesTest < Minitest::Test
4
+ def test_missing_unique_index
5
+ create_table(:users) do |t|
6
+ t.string :email
7
+ t.index :email
8
+ end.create_model do
9
+ validates :email, uniqueness: true
10
+ end
11
+
12
+ assert_problems(<<OUTPUT)
13
+ The following indexes should be created to back model-level uniqueness validations:
14
+ users: email
15
+ OUTPUT
16
+ end
17
+
18
+ def test_present_unique_index
19
+ create_table(:users) do |t|
20
+ t.string :email
21
+ t.index :email, unique: true
22
+ end.create_model do
23
+ validates :email, uniqueness: true
24
+ end
25
+
26
+ refute_problems
27
+ end
28
+
29
+ def test_missing_unique_index_with_scope
30
+ create_table(:users) do |t|
31
+ t.string :email
32
+ t.integer :company_id
33
+ t.integer :department_id
34
+ t.index [:company_id, :department_id, :email]
35
+ end.create_model do
36
+ validates :email, uniqueness: { scope: [:company_id, :department_id] }
37
+ end
38
+
39
+ assert_problems(<<OUTPUT)
40
+ The following indexes should be created to back model-level uniqueness validations:
41
+ users: company_id, department_id, email
42
+ OUTPUT
43
+ end
44
+
45
+ def test_present_unique_index_with_scope
46
+ create_table(:users) do |t|
47
+ t.string :email
48
+ t.integer :company_id
49
+ t.integer :department_id
50
+ t.index [:company_id, :department_id, :email], unique: true
51
+ end.create_model do
52
+ validates :email, uniqueness: { scope: [:company_id, :department_id] }
53
+ end
54
+
55
+ refute_problems
56
+ end
57
+
58
+ def test_column_order_is_ignored
59
+ create_table(:users) do |t|
60
+ t.string :email
61
+ t.integer :organization_id
62
+
63
+ t.index [:email, :organization_id], unique: true
64
+ end.create_model do
65
+ validates :email, uniqueness: { scope: :organization_id }
66
+ end
67
+
68
+ refute_problems
69
+ end
70
+
71
+ def test_conditions_is_skipped
72
+ assert_skipped(conditions: -> { where.not(email: nil) })
73
+ end
74
+
75
+ def test_case_insensitive_is_skipped
76
+ assert_skipped(case_sensitive: false)
77
+ end
78
+
79
+ def test_if_is_skipped
80
+ assert_skipped(if: ->(_model) { true })
81
+ end
82
+
83
+ def test_unless_is_skipped
84
+ assert_skipped(unless: ->(_model) { true })
85
+ end
86
+
87
+ def test_skips_validator_without_attributes
88
+ create_table(:users) do |t|
89
+ t.string :email
90
+ t.index :email
91
+ end.create_model do
92
+ validates_with DummyValidator
93
+ end
94
+
95
+ refute_problems
96
+ end
97
+
98
+ class DummyValidator < ActiveModel::Validator
99
+ def validate(record)
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ def assert_skipped(options)
106
+ create_table(:users) do |t|
107
+ t.string :email
108
+ end.create_model do
109
+ validates :email, uniqueness: options
110
+ end
111
+
112
+ refute_problems
113
+ end
114
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveRecordDoctor::Detectors::UndefinedTableReferencesTest < Minitest::Test
4
+ def test_table_exists
5
+ create_table(:users) do
6
+ end.create_model do
7
+ end
8
+
9
+ refute_problems
10
+ end
11
+
12
+ def test_table_does_not_exist_when_views_supported
13
+ create_model(:users)
14
+
15
+ if mysql? && ActiveRecord::VERSION::STRING < "5.0"
16
+ assert_problems(<<OUTPUT)
17
+ WARNING: Models backed by database views are supported only in Rails 5+ OR
18
+ Rails 4.2 + PostgreSQL. It seems this is NOT your setup. Therefore, such models
19
+ will be erroneously reported below as not having their underlying tables/views.
20
+ Consider upgrading Rails or disabling this task temporarily.
21
+ The following models reference undefined tables:
22
+ ModelFactory::Models::User (the table users is undefined)
23
+ OUTPUT
24
+ else
25
+ assert_problems(<<OUTPUT)
26
+ The following models reference undefined tables:
27
+ ModelFactory::Models::User (the table users is undefined)
28
+ OUTPUT
29
+ end
30
+ end
31
+
32
+ def test_view_instead_of_table
33
+ # We replace the underlying table with a view. The view doesn't have to be
34
+ # backed by an actual table - it can simply return a predefined tuple.
35
+ ActiveRecord::Base.connection.execute("CREATE VIEW users AS SELECT 1")
36
+ create_model(:users)
37
+
38
+ begin
39
+ refute_problems
40
+ ensure
41
+ ActiveRecord::Base.connection.execute("DROP VIEW users")
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveRecordDoctor::Detectors::UnindexedDeletedAtTest < Minitest::Test
4
+ def test_indexed_deleted_at_is_not_reported
5
+ skip("MySQL doesn't support partial indexes") if mysql?
6
+
7
+ create_table(:users) do |t|
8
+ t.string :first_name
9
+ t.string :last_name
10
+ t.datetime :deleted_at
11
+ t.index [:first_name, :last_name],
12
+ name: "index_profiles_on_first_name_and_last_name",
13
+ where: "deleted_at IS NULL"
14
+ end
15
+
16
+ refute_problems
17
+ end
18
+
19
+ def test_unindexed_deleted_at_is_reported
20
+ skip("MySQL doesn't support partial indexes") if mysql?
21
+
22
+ create_table(:users) do |t|
23
+ t.string :first_name
24
+ t.string :last_name
25
+ t.datetime :deleted_at
26
+ t.index [:first_name, :last_name],
27
+ name: "index_profiles_on_first_name_and_last_name"
28
+ end
29
+
30
+ assert_problems(<<OUTPUT)
31
+ The following indexes should include `deleted_at IS NULL`:
32
+ index_profiles_on_first_name_and_last_name
33
+ OUTPUT
34
+ end
35
+
36
+ def test_indexed_discarded_at_is_not_reported
37
+ skip("MySQL doesn't support partial indexes") if mysql?
38
+
39
+ create_table(:users) do |t|
40
+ t.string :first_name
41
+ t.string :last_name
42
+ t.datetime :discarded_at
43
+ t.index [:first_name, :last_name],
44
+ name: "index_profiles_on_first_name_and_last_name",
45
+ where: "discarded_at IS NULL"
46
+ end
47
+
48
+ refute_problems
49
+ end
50
+
51
+ def test_unindexed_discarded_at_is_reported
52
+ skip("MySQL doesn't support partial indexes") if mysql?
53
+
54
+ create_table(:users) do |t|
55
+ t.string :first_name
56
+ t.string :last_name
57
+ t.datetime :discarded_at
58
+ t.index [:first_name, :last_name],
59
+ name: "index_profiles_on_first_name_and_last_name"
60
+ end
61
+
62
+ assert_problems(<<OUTPUT)
63
+ The following indexes should include `deleted_at IS NULL`:
64
+ index_profiles_on_first_name_and_last_name
65
+ OUTPUT
66
+ end
67
+ end