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.
- checksums.yaml +5 -5
- data/README.md +152 -12
- data/lib/active_record_doctor.rb +20 -2
- 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 +12 -29
- data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +40 -0
- data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +71 -0
- data/lib/active_record_doctor/{tasks → detectors}/missing_foreign_keys.rb +17 -35
- data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +60 -0
- data/lib/active_record_doctor/detectors/missing_presence_validation.rb +78 -0
- data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +61 -0
- data/lib/active_record_doctor/detectors/undefined_table_references.rb +34 -0
- data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +29 -0
- data/lib/active_record_doctor/detectors/unindexed_foreign_keys.rb +48 -0
- data/lib/active_record_doctor/printers.rb +3 -1
- data/lib/active_record_doctor/printers/io_printer.rb +101 -26
- data/lib/active_record_doctor/railtie.rb +3 -1
- 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 +33 -0
- 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 +23 -10
- data/test/model_factory.rb +78 -0
- data/test/setup.rb +126 -0
- metadata +93 -149
- data/Rakefile +0 -28
- data/lib/active_record_doctor/compatibility.rb +0 -11
- data/lib/active_record_doctor/tasks.rb +0 -4
- data/lib/active_record_doctor/tasks/undefined_table_references.rb +0 -34
- data/lib/active_record_doctor/tasks/unindexed_deleted_at.rb +0 -40
- data/lib/active_record_doctor/tasks/unindexed_foreign_keys.rb +0 -66
- data/lib/tasks/active_record_doctor_tasks.rake +0 -27
- data/test/active_record_doctor/tasks/extraneous_indexes_test.rb +0 -27
- data/test/active_record_doctor/tasks/missing_foreign_keys_test.rb +0 -19
- data/test/active_record_doctor/tasks/undefined_table_references_test.rb +0 -19
- data/test/active_record_doctor/tasks/unindexed_deleted_at_test.rb +0 -19
- data/test/active_record_doctor/tasks/unindexed_foreign_keys_test.rb +0 -19
- data/test/dummy/README.rdoc +0 -28
- data/test/dummy/Rakefile +0 -6
- data/test/dummy/app/assets/javascripts/application.js +0 -13
- data/test/dummy/app/assets/stylesheets/application.css +0 -15
- data/test/dummy/app/controllers/application_controller.rb +0 -5
- data/test/dummy/app/helpers/application_helper.rb +0 -2
- data/test/dummy/app/models/application_record.rb +0 -3
- data/test/dummy/app/models/comment.rb +0 -3
- data/test/dummy/app/models/contract.rb +0 -3
- data/test/dummy/app/models/employer.rb +0 -2
- data/test/dummy/app/models/profile.rb +0 -2
- data/test/dummy/app/models/user.rb +0 -3
- data/test/dummy/app/views/layouts/application.html.erb +0 -14
- data/test/dummy/bin/bundle +0 -3
- data/test/dummy/bin/rails +0 -4
- data/test/dummy/bin/rake +0 -4
- data/test/dummy/bin/setup +0 -29
- data/test/dummy/config.ru +0 -4
- data/test/dummy/config/application.rb +0 -23
- data/test/dummy/config/boot.rb +0 -5
- data/test/dummy/config/database.yml +0 -19
- data/test/dummy/config/database.yml.travis +0 -5
- data/test/dummy/config/environment.rb +0 -5
- data/test/dummy/config/environments/development.rb +0 -41
- data/test/dummy/config/environments/production.rb +0 -79
- data/test/dummy/config/environments/test.rb +0 -47
- data/test/dummy/config/initializers/assets.rb +0 -11
- data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
- data/test/dummy/config/initializers/cookies_serializer.rb +0 -3
- data/test/dummy/config/initializers/filter_parameter_logging.rb +0 -4
- data/test/dummy/config/initializers/inflections.rb +0 -16
- data/test/dummy/config/initializers/mime_types.rb +0 -4
- data/test/dummy/config/initializers/session_store.rb +0 -3
- data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
- data/test/dummy/config/locales/en.yml +0 -23
- data/test/dummy/config/routes.rb +0 -56
- data/test/dummy/config/secrets.yml +0 -22
- data/test/dummy/db/migrate/20160213101213_create_employers.rb +0 -15
- data/test/dummy/db/migrate/20160213101221_create_users.rb +0 -23
- data/test/dummy/db/migrate/20160213101232_create_profiles.rb +0 -15
- data/test/dummy/db/migrate/20160604081452_create_comments.rb +0 -11
- data/test/dummy/db/migrate/base_migration.rb +0 -5
- data/test/dummy/db/schema.rb +0 -68
- data/test/dummy/log/development.log +0 -532
- data/test/dummy/log/test.log +0 -2699
- data/test/dummy/public/404.html +0 -67
- data/test/dummy/public/422.html +0 -67
- data/test/dummy/public/500.html +0 -66
- data/test/dummy/public/favicon.ico +0 -0
- data/test/support/spy_printer.rb +0 -52
- data/test/test_helper.rb +0 -20
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record_doctor/detectors/base"
|
4
|
+
|
5
|
+
module ActiveRecordDoctor
|
6
|
+
module Detectors
|
7
|
+
# Find has_many/has_one associations with dependent options not taking the
|
8
|
+
# related model's callbacks into account.
|
9
|
+
class IncorrectDependentOption < Base
|
10
|
+
# rubocop:disable Layout/LineLength
|
11
|
+
@description = "Detect associations that should use a different dependent option based on callbacks on the related model"
|
12
|
+
# rubocop:enable Layout/LineLength
|
13
|
+
|
14
|
+
def run
|
15
|
+
eager_load!
|
16
|
+
|
17
|
+
problems(hash_from_pairs(models.reject do |model|
|
18
|
+
model.table_name.nil?
|
19
|
+
end.map do |model|
|
20
|
+
[
|
21
|
+
model.name,
|
22
|
+
associations_with_incorrect_dependent_options(model)
|
23
|
+
]
|
24
|
+
end.reject do |_model_name, associations|
|
25
|
+
associations.empty?
|
26
|
+
end))
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def associations_with_incorrect_dependent_options(model)
|
32
|
+
reflections = model.reflect_on_all_associations(:has_many) + model.reflect_on_all_associations(:has_one)
|
33
|
+
reflections.map do |reflection|
|
34
|
+
if callback_action(reflection) == :invoke && !defines_destroy_callbacks?(reflection.klass)
|
35
|
+
suggestion =
|
36
|
+
case reflection.macro
|
37
|
+
when :has_many then :suggest_delete_all
|
38
|
+
when :has_one then :suggest_delete
|
39
|
+
else raise("unsupported association type #{reflection.macro}")
|
40
|
+
end
|
41
|
+
|
42
|
+
[reflection.name, suggestion]
|
43
|
+
elsif callback_action(reflection) == :skip && defines_destroy_callbacks?(reflection.klass)
|
44
|
+
[reflection.name, :suggest_destroy]
|
45
|
+
end
|
46
|
+
end.compact
|
47
|
+
end
|
48
|
+
|
49
|
+
def callback_action(reflection)
|
50
|
+
case reflection.options[:dependent]
|
51
|
+
when :delete_all then :skip
|
52
|
+
when :destroy then :invoke
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def defines_destroy_callbacks?(model)
|
57
|
+
# Destroying an associated model involves loading it first hence
|
58
|
+
# initialize and find are present. If they are defined on the model
|
59
|
+
# being deleted then theoretically we can't use :delete_all. It's a bit
|
60
|
+
# of an edge case as they usually are either absent or have no side
|
61
|
+
# effects but we're being pedantic -- they could be used for audit
|
62
|
+
# trial, for instance, and we don't want to skip that.
|
63
|
+
model._initialize_callbacks.present? ||
|
64
|
+
model._find_callbacks.present? ||
|
65
|
+
model._destroy_callbacks.present? ||
|
66
|
+
model._commit_callbacks.present? ||
|
67
|
+
model._rollback_callbacks.present?
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -1,28 +1,16 @@
|
|
1
|
-
|
2
|
-
require "active_record_doctor/printers/io_printer"
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
4
|
-
|
5
|
-
module Tasks
|
6
|
-
class MissingForeignKeys
|
7
|
-
include Compatibility
|
8
|
-
|
9
|
-
def self.run
|
10
|
-
new.run
|
11
|
-
end
|
3
|
+
require "active_record_doctor/detectors/base"
|
12
4
|
|
13
|
-
|
14
|
-
|
15
|
-
|
5
|
+
module ActiveRecordDoctor
|
6
|
+
module Detectors
|
7
|
+
# Find foreign-key like columns lacking an actual foreign key constraint.
|
8
|
+
class MissingForeignKeys < Base
|
9
|
+
@description = "Detect association columns without a foreign key constraint"
|
16
10
|
|
17
11
|
def run
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
private
|
22
|
-
|
23
|
-
def missing_foreign_keys
|
24
|
-
hash_from_pairs(connection_tables.select do |table|
|
25
|
-
"schema_migrations" != table
|
12
|
+
problems(hash_from_pairs(tables.reject do |table|
|
13
|
+
table == "schema_migrations"
|
26
14
|
end.map do |table|
|
27
15
|
[
|
28
16
|
table,
|
@@ -30,17 +18,19 @@ module ActiveRecordDoctor
|
|
30
18
|
# We need to skip polymorphic associations as they can reference
|
31
19
|
# multiple tables but a foreign key constraint can reference
|
32
20
|
# a single predefined table.
|
33
|
-
|
21
|
+
named_like_foreign_key?(column) &&
|
34
22
|
!foreign_key?(table, column) &&
|
35
23
|
!polymorphic_foreign_key?(table, column)
|
36
24
|
end.map(&:name)
|
37
25
|
]
|
38
|
-
end.
|
39
|
-
|
40
|
-
end)
|
26
|
+
end.reject do |_table, columns|
|
27
|
+
columns.empty?
|
28
|
+
end))
|
41
29
|
end
|
42
30
|
|
43
|
-
|
31
|
+
private
|
32
|
+
|
33
|
+
def named_like_foreign_key?(column)
|
44
34
|
column.name.end_with?("_id")
|
45
35
|
end
|
46
36
|
|
@@ -51,19 +41,11 @@ module ActiveRecordDoctor
|
|
51
41
|
end
|
52
42
|
|
53
43
|
def polymorphic_foreign_key?(table, column)
|
54
|
-
type_column_name = column.name.sub(/_id\Z/,
|
44
|
+
type_column_name = column.name.sub(/_id\Z/, "_type")
|
55
45
|
connection.columns(table).any? do |another_column|
|
56
46
|
another_column.name == type_column_name
|
57
47
|
end
|
58
48
|
end
|
59
|
-
|
60
|
-
def connection
|
61
|
-
@connection ||= ActiveRecord::Base.connection
|
62
|
-
end
|
63
|
-
|
64
|
-
def hash_from_pairs(pairs)
|
65
|
-
Hash[*pairs.flatten(1)]
|
66
|
-
end
|
67
49
|
end
|
68
50
|
end
|
69
51
|
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record_doctor/detectors/base"
|
4
|
+
|
5
|
+
module ActiveRecordDoctor
|
6
|
+
module Detectors
|
7
|
+
# Detect model-level presence validators on columns that lack a non-NULL constraint thus allowing potentially
|
8
|
+
# invalid insertions.
|
9
|
+
class MissingNonNullConstraint < Base
|
10
|
+
@description = "Detect presence validators not backed by a non-NULL constraint"
|
11
|
+
|
12
|
+
def run
|
13
|
+
eager_load!
|
14
|
+
|
15
|
+
problems(hash_from_pairs(models.reject do |model|
|
16
|
+
model.table_name.nil? ||
|
17
|
+
model.table_name == "schema_migrations" ||
|
18
|
+
!table_exists?(model.table_name)
|
19
|
+
end.map do |model|
|
20
|
+
[
|
21
|
+
model.table_name,
|
22
|
+
connection.columns(model.table_name).select do |column|
|
23
|
+
validator_needed?(model, column) &&
|
24
|
+
has_mandatory_presence_validator?(model, column) &&
|
25
|
+
column.null
|
26
|
+
end.map(&:name)
|
27
|
+
]
|
28
|
+
end.reject do |_model_name, columns|
|
29
|
+
columns.empty?
|
30
|
+
end))
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def validator_needed?(model, column)
|
36
|
+
![model.primary_key, "created_at", "updated_at"].include?(column.name)
|
37
|
+
end
|
38
|
+
|
39
|
+
def has_mandatory_presence_validator?(model, column)
|
40
|
+
# A foreign key can be validates via the column name (e.g. company_id)
|
41
|
+
# or the association name (e.g. company). We collect the allowed names
|
42
|
+
# in an array to check for their presence in the validator definition
|
43
|
+
# in one go.
|
44
|
+
attribute_name_forms = [column.name.to_sym]
|
45
|
+
belongs_to = model.reflect_on_all_associations(:belongs_to).find do |reflection|
|
46
|
+
reflection.foreign_key == column.name
|
47
|
+
end
|
48
|
+
attribute_name_forms << belongs_to.name.to_sym if belongs_to
|
49
|
+
|
50
|
+
model.validators.any? do |validator|
|
51
|
+
validator.is_a?(ActiveRecord::Validations::PresenceValidator) &&
|
52
|
+
(validator.attributes & attribute_name_forms).present? &&
|
53
|
+
!validator.options[:allow_nil] &&
|
54
|
+
!validator.options[:if] &&
|
55
|
+
!validator.options[:unless]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record_doctor/detectors/base"
|
4
|
+
|
5
|
+
module ActiveRecordDoctor
|
6
|
+
module Detectors
|
7
|
+
# Detect models with non-NULL columns that lack the corresponding model-level validator.
|
8
|
+
class MissingPresenceValidation < Base
|
9
|
+
@description = "Detect non-NULL columns without a presence validator"
|
10
|
+
|
11
|
+
def run
|
12
|
+
eager_load!
|
13
|
+
|
14
|
+
problems(hash_from_pairs(models.reject do |model|
|
15
|
+
model.table_name.nil? ||
|
16
|
+
model.table_name == "schema_migrations" ||
|
17
|
+
!table_exists?(model.table_name)
|
18
|
+
end.map do |model|
|
19
|
+
[
|
20
|
+
model.name,
|
21
|
+
connection.columns(model.table_name).select do |column|
|
22
|
+
validator_needed?(model, column) &&
|
23
|
+
!validator_present?(model, column)
|
24
|
+
end.map(&:name)
|
25
|
+
]
|
26
|
+
end.reject do |_model_name, columns|
|
27
|
+
columns.empty?
|
28
|
+
end))
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def validator_needed?(model, column)
|
34
|
+
![model.primary_key, "created_at", "updated_at"].include?(column.name) &&
|
35
|
+
!column.null
|
36
|
+
end
|
37
|
+
|
38
|
+
def validator_present?(model, column)
|
39
|
+
if column.type == :boolean
|
40
|
+
inclusion_validator_present?(model, column) ||
|
41
|
+
exclusion_validator_present?(model, column)
|
42
|
+
else
|
43
|
+
presence_validator_present?(model, column)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def inclusion_validator_present?(model, column)
|
48
|
+
model.validators.any? do |validator|
|
49
|
+
validator.is_a?(ActiveModel::Validations::InclusionValidator) &&
|
50
|
+
validator.attributes.include?(column.name.to_sym) &&
|
51
|
+
!validator.options.fetch(:in, []).include?(nil)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def exclusion_validator_present?(model, column)
|
56
|
+
model.validators.any? do |validator|
|
57
|
+
validator.is_a?(ActiveModel::Validations::ExclusionValidator) &&
|
58
|
+
validator.attributes.include?(column.name.to_sym) &&
|
59
|
+
validator.options.fetch(:in, []).include?(nil)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def presence_validator_present?(model, column)
|
64
|
+
allowed_attributes = [column.name.to_sym]
|
65
|
+
|
66
|
+
belongs_to = model.reflect_on_all_associations(:belongs_to).find do |reflection|
|
67
|
+
reflection.foreign_key == column.name
|
68
|
+
end
|
69
|
+
allowed_attributes << belongs_to.name.to_sym if belongs_to
|
70
|
+
|
71
|
+
model.validators.any? do |validator|
|
72
|
+
validator.is_a?(ActiveRecord::Validations::PresenceValidator) &&
|
73
|
+
(validator.attributes & allowed_attributes).present?
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record_doctor/detectors/base"
|
4
|
+
|
5
|
+
module ActiveRecordDoctor
|
6
|
+
module Detectors
|
7
|
+
# Detect columns covered by a uniqueness validation that lack the corresponding unique index thus risking duplicate
|
8
|
+
# inserts.
|
9
|
+
class MissingUniqueIndexes < Base
|
10
|
+
@description = "Detect columns covered by a uniqueness validator without a unique index"
|
11
|
+
|
12
|
+
def run
|
13
|
+
eager_load!
|
14
|
+
|
15
|
+
problems(hash_from_pairs(models.reject do |model|
|
16
|
+
model.table_name.nil?
|
17
|
+
end.map do |model|
|
18
|
+
[
|
19
|
+
model.table_name,
|
20
|
+
model.validators.select do |validator|
|
21
|
+
table_name = model.table_name
|
22
|
+
scope = validator.options.fetch(:scope, [])
|
23
|
+
|
24
|
+
validator.is_a?(ActiveRecord::Validations::UniquenessValidator) &&
|
25
|
+
supported_validator?(validator) &&
|
26
|
+
!unique_index?(table_name, validator.attributes, scope)
|
27
|
+
end.map do |validator|
|
28
|
+
scope = Array(validator.options.fetch(:scope, []))
|
29
|
+
attributes = validator.attributes
|
30
|
+
(scope + attributes).map(&:to_s)
|
31
|
+
end
|
32
|
+
]
|
33
|
+
end.reject do |_table_name, indexes|
|
34
|
+
indexes.empty?
|
35
|
+
end))
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def supported_validator?(validator)
|
41
|
+
validator.options[:if].nil? &&
|
42
|
+
validator.options[:unless].nil? &&
|
43
|
+
validator.options[:conditions].nil? &&
|
44
|
+
|
45
|
+
# In Rails 6, default option values are no longer explicitly set on
|
46
|
+
# options so if the key is absent we must fetch the default value
|
47
|
+
# ourselves. case_sensitive is the default in 4.2+ so it's safe to
|
48
|
+
# put true literally.
|
49
|
+
validator.options.fetch(:case_sensitive, true)
|
50
|
+
end
|
51
|
+
|
52
|
+
def unique_index?(table_name, columns, scope)
|
53
|
+
columns = (Array(scope) + columns).map(&:to_s)
|
54
|
+
|
55
|
+
indexes(table_name).any? do |index|
|
56
|
+
index.columns.to_set == columns.to_set && index.unique
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record_doctor/detectors/base"
|
4
|
+
|
5
|
+
module ActiveRecordDoctor
|
6
|
+
module Detectors
|
7
|
+
# Find models referencing non-existent database tables or views.
|
8
|
+
class UndefinedTableReferences < Base
|
9
|
+
@description = "Detect models referencing undefined tables or views"
|
10
|
+
|
11
|
+
def run
|
12
|
+
eager_load!
|
13
|
+
|
14
|
+
# If we can't list views due to old Rails version or unsupported
|
15
|
+
# database then existing_views is nil. We inform the caller we haven't
|
16
|
+
# consulted views so that it can display an appropriate warning.
|
17
|
+
existing_views = views
|
18
|
+
|
19
|
+
offending_models = models.select do |model|
|
20
|
+
model.table_name.present? &&
|
21
|
+
!tables.include?(model.table_name) &&
|
22
|
+
(
|
23
|
+
existing_views.nil? ||
|
24
|
+
!existing_views.include?(model.table_name)
|
25
|
+
)
|
26
|
+
end.map do |model|
|
27
|
+
[model.name, model.table_name]
|
28
|
+
end
|
29
|
+
|
30
|
+
problems(offending_models, views_checked: !existing_views.nil?)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record_doctor/detectors/base"
|
4
|
+
|
5
|
+
module ActiveRecordDoctor
|
6
|
+
module Detectors
|
7
|
+
# Find unindexed deleted_at columns.
|
8
|
+
class UnindexedDeletedAt < Base
|
9
|
+
PATTERN = [
|
10
|
+
"deleted_at",
|
11
|
+
"discarded_at"
|
12
|
+
].join("|").freeze
|
13
|
+
|
14
|
+
@description = "Detect unindexed deleted_at columns"
|
15
|
+
|
16
|
+
def run
|
17
|
+
problems(connection.tables.select do |table|
|
18
|
+
connection.columns(table).any? { |column| column.name =~ /^#{PATTERN}$/ }
|
19
|
+
end.flat_map do |table|
|
20
|
+
connection.indexes(table).reject do |index|
|
21
|
+
index.where =~ /\b#{PATTERN}\s+IS\s+NULL\b/i
|
22
|
+
end.map do |index|
|
23
|
+
index.name
|
24
|
+
end
|
25
|
+
end)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record_doctor/detectors/base"
|
4
|
+
|
5
|
+
module ActiveRecordDoctor
|
6
|
+
module Detectors
|
7
|
+
# Find foreign keys that lack indexes (usually recommended for performance reasons).
|
8
|
+
class UnindexedForeignKeys < Base
|
9
|
+
@description = "Detect foreign keys without an index on them"
|
10
|
+
|
11
|
+
def run
|
12
|
+
problems(hash_from_pairs(tables.reject do |table|
|
13
|
+
table == "schema_migrations"
|
14
|
+
end.map do |table|
|
15
|
+
[
|
16
|
+
table,
|
17
|
+
connection.columns(table).select do |column|
|
18
|
+
foreign_key?(column) &&
|
19
|
+
!indexed?(table, column) &&
|
20
|
+
!indexed_as_polymorphic?(table, column)
|
21
|
+
end.map(&:name)
|
22
|
+
]
|
23
|
+
end.reject do |_table, columns|
|
24
|
+
columns.empty?
|
25
|
+
end))
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def foreign_key?(column)
|
31
|
+
column.name.end_with?("_id")
|
32
|
+
end
|
33
|
+
|
34
|
+
def indexed?(table, column)
|
35
|
+
connection.indexes(table).any? do |index|
|
36
|
+
index.columns.first == column.name
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def indexed_as_polymorphic?(table, column)
|
41
|
+
type_column_name = column.name.sub(/_id\Z/, "_type")
|
42
|
+
connection.indexes(table).any? do |index|
|
43
|
+
index.columns == [type_column_name, column.name]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|