active_record_doctor 1.9.0 → 1.11.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 +83 -19
- data/lib/active_record_doctor/config/default.rb +17 -0
- data/lib/active_record_doctor/detectors/base.rb +216 -56
- data/lib/active_record_doctor/detectors/extraneous_indexes.rb +38 -56
- data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +2 -6
- data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +88 -15
- data/lib/active_record_doctor/detectors/incorrect_length_validation.rb +60 -0
- data/lib/active_record_doctor/detectors/mismatched_foreign_key_type.rb +16 -9
- data/lib/active_record_doctor/detectors/missing_foreign_keys.rb +2 -4
- data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +14 -11
- data/lib/active_record_doctor/detectors/missing_presence_validation.rb +16 -10
- data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +61 -17
- data/lib/active_record_doctor/detectors/short_primary_key_type.rb +6 -2
- data/lib/active_record_doctor/detectors/undefined_table_references.rb +2 -4
- data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +6 -15
- data/lib/active_record_doctor/detectors/unindexed_foreign_keys.rb +2 -4
- data/lib/active_record_doctor/logger/dummy.rb +11 -0
- data/lib/active_record_doctor/logger/hierarchical.rb +22 -0
- data/lib/active_record_doctor/logger.rb +6 -0
- data/lib/active_record_doctor/rake/task.rb +10 -1
- data/lib/active_record_doctor/runner.rb +8 -3
- data/lib/active_record_doctor/version.rb +1 -1
- data/lib/active_record_doctor.rb +4 -0
- data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +5 -5
- data/test/active_record_doctor/detectors/disable_test.rb +30 -0
- data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +34 -0
- data/test/active_record_doctor/detectors/incorrect_boolean_presence_validation_test.rb +7 -7
- data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +220 -43
- data/test/active_record_doctor/detectors/incorrect_length_validation_test.rb +107 -0
- data/test/active_record_doctor/detectors/mismatched_foreign_key_type_test.rb +35 -1
- data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +78 -21
- data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +89 -25
- data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +179 -15
- data/test/active_record_doctor/detectors/short_primary_key_type_test.rb +27 -19
- data/test/active_record_doctor/detectors/undefined_table_references_test.rb +11 -13
- data/test/active_record_doctor/detectors/unindexed_deleted_at_test.rb +9 -3
- data/test/active_record_doctor/runner_test.rb +18 -19
- data/test/setup.rb +15 -7
- metadata +25 -5
- data/test/model_factory.rb +0 -128
@@ -26,26 +26,17 @@ module ActiveRecordDoctor
|
|
26
26
|
|
27
27
|
def message(index:, column_name:)
|
28
28
|
# rubocop:disable Layout/LineLength
|
29
|
-
"consider adding `WHERE #{column_name} IS NULL` to #{index} - a partial index can speed lookups of soft-deletable models"
|
29
|
+
"consider adding `WHERE #{column_name} IS NULL` or `WHERE #{column_name} IS NOT NULL` to #{index} - a partial index can speed lookups of soft-deletable models"
|
30
30
|
# rubocop:enable Layout/LineLength
|
31
31
|
end
|
32
32
|
|
33
33
|
def detect
|
34
|
-
|
35
|
-
|
36
|
-
config(:
|
37
|
-
|
38
|
-
config(:column_names).include?(column.name)
|
39
|
-
end
|
40
|
-
|
41
|
-
next if timestamp_columns.empty?
|
42
|
-
|
43
|
-
timestamp_columns.each do |timestamp_column|
|
44
|
-
indexes(table, except: config(:ignore_indexes)).each do |index|
|
45
|
-
# TODO: whole word
|
46
|
-
next if index.where =~ /\b#{timestamp_column.name}\s+IS\s+NULL\b/i
|
34
|
+
each_table(except: config(:ignore_tables)) do |table|
|
35
|
+
each_column(table, only: config(:column_names), except: config(:ignore_columns)) do |column|
|
36
|
+
each_index(table, except: config(:ignore_indexes)) do |index|
|
37
|
+
next if index.where =~ /\b#{column.name}\s+IS\s+(NOT\s+)?NULL\b/i
|
47
38
|
|
48
|
-
problem!(index: index.name, column_name:
|
39
|
+
problem!(index: index.name, column_name: column.name)
|
49
40
|
end
|
50
41
|
end
|
51
42
|
end
|
@@ -25,10 +25,8 @@ module ActiveRecordDoctor
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def detect
|
28
|
-
|
29
|
-
|
30
|
-
next if config(:ignore_columns).include?("#{table}.#{column.name}")
|
31
|
-
|
28
|
+
each_table(except: config(:ignore_tables)) do |table|
|
29
|
+
each_column(table, except: config(:ignore_columns)) do |column|
|
32
30
|
next unless foreign_key?(column)
|
33
31
|
next if indexed?(table, column)
|
34
32
|
next if indexed_as_polymorphic?(table, column)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecordDoctor
|
4
|
+
module Logger
|
5
|
+
class Hierarchical # :nodoc:
|
6
|
+
def initialize(io)
|
7
|
+
@io = io
|
8
|
+
@nesting = 0
|
9
|
+
end
|
10
|
+
|
11
|
+
def log(message)
|
12
|
+
@io.puts(" " * @nesting + message.to_s)
|
13
|
+
return if !block_given?
|
14
|
+
|
15
|
+
@nesting += 1
|
16
|
+
result = yield
|
17
|
+
@nesting -= 1
|
18
|
+
result
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -64,7 +64,7 @@ module ActiveRecordDoctor
|
|
64
64
|
private
|
65
65
|
|
66
66
|
def runner
|
67
|
-
@runner ||= ActiveRecordDoctor::Runner.new(config)
|
67
|
+
@runner ||= ActiveRecordDoctor::Runner.new(config: config, logger: logger)
|
68
68
|
end
|
69
69
|
|
70
70
|
def config
|
@@ -73,6 +73,15 @@ module ActiveRecordDoctor
|
|
73
73
|
ActiveRecordDoctor.load_config_with_defaults(path)
|
74
74
|
end
|
75
75
|
end
|
76
|
+
|
77
|
+
def logger
|
78
|
+
@logger ||=
|
79
|
+
if ENV.include?("ACTIVE_RECORD_DOCTOR_DEBUG")
|
80
|
+
ActiveRecordDoctor::Logger::Hierarchical.new($stderr)
|
81
|
+
else
|
82
|
+
ActiveRecordDoctor::Logger::Dummy.new
|
83
|
+
end
|
84
|
+
end
|
76
85
|
end
|
77
86
|
end
|
78
87
|
end
|
@@ -5,14 +5,19 @@ module ActiveRecordDoctor # :nodoc:
|
|
5
5
|
# and an output device for use by detectors.
|
6
6
|
class Runner
|
7
7
|
# io is injected via constructor parameters to facilitate testing.
|
8
|
-
def initialize(config
|
8
|
+
def initialize(config:, logger:, io: $stdout)
|
9
9
|
@config = config
|
10
|
+
@logger = logger
|
10
11
|
@io = io
|
11
12
|
end
|
12
13
|
|
13
14
|
def run_one(name)
|
14
15
|
ActiveRecordDoctor.handle_exception do
|
15
|
-
ActiveRecordDoctor.detectors.fetch(name).run(
|
16
|
+
ActiveRecordDoctor.detectors.fetch(name).run(
|
17
|
+
config: config,
|
18
|
+
logger: logger,
|
19
|
+
io: io
|
20
|
+
)
|
16
21
|
end
|
17
22
|
end
|
18
23
|
|
@@ -36,6 +41,6 @@ module ActiveRecordDoctor # :nodoc:
|
|
36
41
|
|
37
42
|
private
|
38
43
|
|
39
|
-
attr_reader :config, :io
|
44
|
+
attr_reader :config, :logger, :io
|
40
45
|
end
|
41
46
|
end
|
data/lib/active_record_doctor.rb
CHANGED
@@ -1,12 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "active_record_doctor/railtie" if defined?(Rails) && defined?(Rails::Railtie)
|
4
|
+
require "active_record_doctor/logger"
|
5
|
+
require "active_record_doctor/logger/dummy"
|
6
|
+
require "active_record_doctor/logger/hierarchical"
|
4
7
|
require "active_record_doctor/detectors"
|
5
8
|
require "active_record_doctor/detectors/base"
|
6
9
|
require "active_record_doctor/detectors/missing_presence_validation"
|
7
10
|
require "active_record_doctor/detectors/missing_foreign_keys"
|
8
11
|
require "active_record_doctor/detectors/missing_unique_indexes"
|
9
12
|
require "active_record_doctor/detectors/incorrect_boolean_presence_validation"
|
13
|
+
require "active_record_doctor/detectors/incorrect_length_validation"
|
10
14
|
require "active_record_doctor/detectors/extraneous_indexes"
|
11
15
|
require "active_record_doctor/detectors/unindexed_deleted_at"
|
12
16
|
require "active_record_doctor/detectors/undefined_table_references"
|
@@ -62,14 +62,14 @@ MIGRATION
|
|
62
62
|
end
|
63
63
|
|
64
64
|
def add_index(table, column)
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
65
|
+
connection = ActiveRecord::Base.connection
|
66
|
+
|
67
|
+
index_name = connection.index_name(table, column)
|
68
|
+
if index_name.size > connection.index_name_length
|
69
|
+
" add_index :#{table}, :#{column}, name: '#{index_name.first(connection.index_name_length)}'"
|
69
70
|
else
|
70
71
|
" add_index :#{table}, :#{column}"
|
71
72
|
end
|
72
|
-
# rubocop:enable Layout/LineLength
|
73
73
|
end
|
74
74
|
|
75
75
|
def migration_version
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ActiveRecordDoctor::Detectors::DisableTest < Minitest::Test
|
4
|
+
# Disabling detectors is implemented in the base class. It's enought to test
|
5
|
+
# it on a single detector to be reasonably certain it works on all of them.
|
6
|
+
def test_disabling
|
7
|
+
create_table(:users) do |t|
|
8
|
+
t.string :name, null: true
|
9
|
+
end.define_model do
|
10
|
+
validates :name, presence: true
|
11
|
+
end
|
12
|
+
|
13
|
+
config_file(<<-CONFIG)
|
14
|
+
ActiveRecordDoctor.configure do |config|
|
15
|
+
config.detector :missing_non_null_constraint,
|
16
|
+
enabled: false
|
17
|
+
end
|
18
|
+
CONFIG
|
19
|
+
|
20
|
+
refute_problems
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# We need to override that method in order to skip the mechanism that
|
26
|
+
# infers detector name from the test class name.
|
27
|
+
def detector_name
|
28
|
+
:missing_non_null_constraint
|
29
|
+
end
|
30
|
+
end
|
@@ -11,6 +11,27 @@ remove index_users_on_id - coincides with the primary key on the table
|
|
11
11
|
OUTPUT
|
12
12
|
end
|
13
13
|
|
14
|
+
def test_partial_index_on_primary_key
|
15
|
+
skip("MySQL doesn't support partial indexes") if mysql?
|
16
|
+
|
17
|
+
create_table(:users) do |t|
|
18
|
+
t.boolean :admin
|
19
|
+
t.index :id, where: "admin"
|
20
|
+
end
|
21
|
+
|
22
|
+
refute_problems
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_index_on_non_standard_primary_key
|
26
|
+
create_table(:profiles, primary_key: :user_id) do |t|
|
27
|
+
t.index :user_id
|
28
|
+
end
|
29
|
+
|
30
|
+
assert_problems(<<OUTPUT)
|
31
|
+
remove index_profiles_on_user_id - coincides with the primary key on the table
|
32
|
+
OUTPUT
|
33
|
+
end
|
34
|
+
|
14
35
|
def test_non_unique_version_of_index_is_duplicate
|
15
36
|
create_table(:users) do |t|
|
16
37
|
t.string :email
|
@@ -61,6 +82,19 @@ remove index_users_on_last_name_and_first_name - can be replaced by index_users_
|
|
61
82
|
OUTPUT
|
62
83
|
end
|
63
84
|
|
85
|
+
def test_unique_index_with_fewer_columns
|
86
|
+
create_table(:users) do |t|
|
87
|
+
t.string :first_name
|
88
|
+
t.string :last_name
|
89
|
+
t.index :first_name, unique: true
|
90
|
+
t.index [:last_name, :first_name], unique: true
|
91
|
+
end
|
92
|
+
|
93
|
+
assert_problems(<<OUTPUT)
|
94
|
+
remove index_users_on_last_name_and_first_name - can be replaced by index_users_on_first_name
|
95
|
+
OUTPUT
|
96
|
+
end
|
97
|
+
|
64
98
|
def test_not_covered_by_different_index_type
|
65
99
|
create_table(:users) do |t|
|
66
100
|
t.string :first_name
|
@@ -5,7 +5,7 @@ class ActiveRecordDoctor::Detectors::IncorrectBooleanPresenceValidationTest < Mi
|
|
5
5
|
create_table(:users) do |t|
|
6
6
|
t.string :email, null: false
|
7
7
|
t.boolean :active, null: false
|
8
|
-
end.
|
8
|
+
end.define_model do
|
9
9
|
# email is a non-boolean column whose presence CAN be validated in the
|
10
10
|
# usual way. We include it in the test model to ensure the detector reports
|
11
11
|
# only boolean columns.
|
@@ -13,14 +13,14 @@ class ActiveRecordDoctor::Detectors::IncorrectBooleanPresenceValidationTest < Mi
|
|
13
13
|
end
|
14
14
|
|
15
15
|
assert_problems(<<~OUTPUT)
|
16
|
-
replace the `presence` validator on
|
16
|
+
replace the `presence` validator on TransientRecord::Models::User.active with `inclusion` - `presence` can't be used on booleans
|
17
17
|
OUTPUT
|
18
18
|
end
|
19
19
|
|
20
20
|
def test_inclusion_is_not_reported
|
21
21
|
create_table(:users) do |t|
|
22
22
|
t.boolean :active, null: false
|
23
|
-
end.
|
23
|
+
end.define_model do
|
24
24
|
validates :active, inclusion: { in: [true, false] }
|
25
25
|
end
|
26
26
|
|
@@ -28,7 +28,7 @@ class ActiveRecordDoctor::Detectors::IncorrectBooleanPresenceValidationTest < Mi
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def test_models_with_non_existent_tables_are_skipped
|
31
|
-
|
31
|
+
define_model(:User)
|
32
32
|
|
33
33
|
refute_problems
|
34
34
|
end
|
@@ -36,7 +36,7 @@ class ActiveRecordDoctor::Detectors::IncorrectBooleanPresenceValidationTest < Mi
|
|
36
36
|
def test_config_ignore_models
|
37
37
|
create_table(:users) do |t|
|
38
38
|
t.string :email, null: false
|
39
|
-
end.
|
39
|
+
end.define_model
|
40
40
|
|
41
41
|
config_file(<<-CONFIG)
|
42
42
|
ActiveRecordDoctor.configure do |config|
|
@@ -51,7 +51,7 @@ class ActiveRecordDoctor::Detectors::IncorrectBooleanPresenceValidationTest < Mi
|
|
51
51
|
def test_global_ignore_models
|
52
52
|
create_table(:users) do |t|
|
53
53
|
t.string :email, null: false
|
54
|
-
end.
|
54
|
+
end.define_model
|
55
55
|
|
56
56
|
config_file(<<-CONFIG)
|
57
57
|
ActiveRecordDoctor.configure do |config|
|
@@ -65,7 +65,7 @@ class ActiveRecordDoctor::Detectors::IncorrectBooleanPresenceValidationTest < Mi
|
|
65
65
|
def test_config_ignore_attributes
|
66
66
|
create_table(:users) do |t|
|
67
67
|
t.string :email, null: false
|
68
|
-
end.
|
68
|
+
end.define_model
|
69
69
|
|
70
70
|
config_file(<<-CONFIG)
|
71
71
|
ActiveRecordDoctor.configure do |config|
|