active_record_doctor 1.10.0 → 1.12.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 +15 -15
- data/lib/active_record_doctor/detectors/base.rb +194 -53
- data/lib/active_record_doctor/detectors/extraneous_indexes.rb +36 -34
- data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +2 -5
- data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +87 -37
- data/lib/active_record_doctor/detectors/incorrect_length_validation.rb +7 -10
- 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 +13 -11
- data/lib/active_record_doctor/detectors/missing_presence_validation.rb +14 -7
- data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +70 -35
- data/lib/active_record_doctor/detectors/short_primary_key_type.rb +4 -4
- data/lib/active_record_doctor/detectors/undefined_table_references.rb +2 -2
- data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +5 -13
- data/lib/active_record_doctor/detectors/unindexed_foreign_keys.rb +35 -11
- 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/utils.rb +21 -0
- data/lib/active_record_doctor/version.rb +1 -1
- data/lib/active_record_doctor.rb +5 -0
- data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +14 -14
- data/test/active_record_doctor/detectors/disable_test.rb +1 -1
- data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +59 -6
- 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 +175 -57
- data/test/active_record_doctor/detectors/incorrect_length_validation_test.rb +16 -14
- 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 +46 -23
- data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +55 -27
- data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +216 -47
- data/test/active_record_doctor/detectors/short_primary_key_type_test.rb +5 -0
- data/test/active_record_doctor/detectors/undefined_table_references_test.rb +11 -13
- data/test/active_record_doctor/detectors/unindexed_foreign_keys_test.rb +39 -1
- data/test/active_record_doctor/runner_test.rb +18 -19
- data/test/generators/active_record_doctor/add_indexes/add_indexes_generator_test.rb +16 -6
- data/test/setup.rb +10 -6
- metadata +23 -7
- data/test/model_factory.rb +0 -128
@@ -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
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecordDoctor
|
4
|
+
module Utils # :nodoc:
|
5
|
+
class << self
|
6
|
+
def postgresql?(connection = ActiveRecord::Base.connection)
|
7
|
+
["PostgreSQL", "PostGIS"].include?(connection.adapter_name)
|
8
|
+
end
|
9
|
+
|
10
|
+
def mysql?(connection = ActiveRecord::Base.connection)
|
11
|
+
connection.adapter_name == "Mysql2"
|
12
|
+
end
|
13
|
+
|
14
|
+
def expression_indexes_unsupported?(connection = ActiveRecord::Base.connection)
|
15
|
+
(ActiveRecord::VERSION::STRING < "5.0") ||
|
16
|
+
# Active Record < 6 is unable to correctly parse expression indexes for MySQL.
|
17
|
+
(mysql?(connection) && ActiveRecord::VERSION::STRING < "6.0")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/active_record_doctor.rb
CHANGED
@@ -1,6 +1,10 @@
|
|
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/utils"
|
5
|
+
require "active_record_doctor/logger"
|
6
|
+
require "active_record_doctor/logger/dummy"
|
7
|
+
require "active_record_doctor/logger/hierarchical"
|
4
8
|
require "active_record_doctor/detectors"
|
5
9
|
require "active_record_doctor/detectors/base"
|
6
10
|
require "active_record_doctor/detectors/missing_presence_validation"
|
@@ -22,6 +26,7 @@ require "active_record_doctor/runner"
|
|
22
26
|
require "active_record_doctor/version"
|
23
27
|
require "active_record_doctor/config"
|
24
28
|
require "active_record_doctor/config/loader"
|
29
|
+
require "active_record_doctor/rake/task"
|
25
30
|
|
26
31
|
module ActiveRecordDoctor # :nodoc:
|
27
32
|
end
|
@@ -10,16 +10,16 @@ module ActiveRecordDoctor
|
|
10
10
|
migration_descriptions = read_migration_descriptions(path)
|
11
11
|
now = Time.now
|
12
12
|
|
13
|
-
migration_descriptions.each_with_index do |(table,
|
13
|
+
migration_descriptions.each_with_index do |(table, indexes), index|
|
14
14
|
timestamp = (now + index).strftime("%Y%m%d%H%M%S")
|
15
15
|
file_name = "db/migrate/#{timestamp}_index_foreign_keys_in_#{table}.rb"
|
16
|
-
create_file(file_name, content(table,
|
16
|
+
create_file(file_name, content(table, indexes).tap { |x| puts x })
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
20
|
private
|
21
21
|
|
22
|
-
INPUT_LINE = /^add an index on (\w+)
|
22
|
+
INPUT_LINE = /^add an index on (\w+)\((.+)\) - .*$/.freeze
|
23
23
|
private_constant :INPUT_LINE
|
24
24
|
|
25
25
|
def read_migration_descriptions(path)
|
@@ -34,41 +34,41 @@ module ActiveRecordDoctor
|
|
34
34
|
end
|
35
35
|
|
36
36
|
table = match[1]
|
37
|
-
|
37
|
+
columns = match[2].split(",").map(&:strip)
|
38
38
|
|
39
|
-
tables_to_columns[table] <<
|
39
|
+
tables_to_columns[table] << columns
|
40
40
|
end
|
41
41
|
|
42
42
|
tables_to_columns
|
43
43
|
end
|
44
44
|
|
45
|
-
def content(table,
|
45
|
+
def content(table, indexes)
|
46
46
|
# In order to properly indent the resulting code, we must disable the
|
47
47
|
# rubocop rule below.
|
48
48
|
|
49
49
|
<<MIGRATION
|
50
50
|
class IndexForeignKeysIn#{table.camelize} < ActiveRecord::Migration#{migration_version}
|
51
51
|
def change
|
52
|
-
#{add_indexes(table,
|
52
|
+
#{add_indexes(table, indexes)}
|
53
53
|
end
|
54
54
|
end
|
55
55
|
MIGRATION
|
56
56
|
end
|
57
57
|
|
58
|
-
def add_indexes(table,
|
59
|
-
|
60
|
-
add_index(table,
|
58
|
+
def add_indexes(table, indexes)
|
59
|
+
indexes.map do |columns|
|
60
|
+
add_index(table, columns)
|
61
61
|
end.join("\n")
|
62
62
|
end
|
63
63
|
|
64
|
-
def add_index(table,
|
64
|
+
def add_index(table, columns)
|
65
65
|
connection = ActiveRecord::Base.connection
|
66
66
|
|
67
|
-
index_name = connection.index_name(table,
|
67
|
+
index_name = connection.index_name(table, columns)
|
68
68
|
if index_name.size > connection.index_name_length
|
69
|
-
" add_index :#{table},
|
69
|
+
" add_index :#{table}, #{columns.inspect}, name: '#{index_name.first(connection.index_name_length)}'"
|
70
70
|
else
|
71
|
-
" add_index :#{table},
|
71
|
+
" add_index :#{table}, #{columns.inspect}"
|
72
72
|
end
|
73
73
|
end
|
74
74
|
|
@@ -7,7 +7,7 @@ class ActiveRecordDoctor::Detectors::ExtraneousIndexesTest < Minitest::Test
|
|
7
7
|
end
|
8
8
|
|
9
9
|
assert_problems(<<OUTPUT)
|
10
|
-
remove index_users_on_id - coincides with the primary key on the table
|
10
|
+
remove index_users_on_id from users - coincides with the primary key on the table
|
11
11
|
OUTPUT
|
12
12
|
end
|
13
13
|
|
@@ -28,7 +28,7 @@ OUTPUT
|
|
28
28
|
end
|
29
29
|
|
30
30
|
assert_problems(<<OUTPUT)
|
31
|
-
remove index_profiles_on_user_id - coincides with the primary key on the table
|
31
|
+
remove index_profiles_on_user_id from profiles - coincides with the primary key on the table
|
32
32
|
OUTPUT
|
33
33
|
end
|
34
34
|
|
@@ -42,7 +42,7 @@ OUTPUT
|
|
42
42
|
ActiveRecord::Base.connection.add_index :users, :email, name: "index_users_on_email"
|
43
43
|
|
44
44
|
assert_problems(<<OUTPUT)
|
45
|
-
remove index_users_on_email -
|
45
|
+
remove the index index_users_on_email from the table users - queries should be able to use the following index instead: unique_index_on_users_email
|
46
46
|
OUTPUT
|
47
47
|
end
|
48
48
|
|
@@ -59,7 +59,7 @@ OUTPUT
|
|
59
59
|
end
|
60
60
|
|
61
61
|
assert_problems(<<OUTPUT)
|
62
|
-
remove index_users_on_last_name -
|
62
|
+
remove the index index_users_on_last_name from the table users - queries should be able to use the following indices instead: index_users_on_last_name_and_first_name_and_email or unique_index_on_users_last_name_and_first_name
|
63
63
|
OUTPUT
|
64
64
|
end
|
65
65
|
|
@@ -78,7 +78,7 @@ OUTPUT
|
|
78
78
|
ActiveRecord::Base.connection.add_index :users, [:last_name, :first_name]
|
79
79
|
|
80
80
|
assert_problems(<<OUTPUT)
|
81
|
-
remove index_users_on_last_name_and_first_name -
|
81
|
+
remove the index index_users_on_last_name_and_first_name from the table users - queries should be able to use the following indices instead: index_users_on_last_name_and_first_name_and_email or unique_index_on_users_last_name_and_first_name
|
82
82
|
OUTPUT
|
83
83
|
end
|
84
84
|
|
@@ -91,10 +91,36 @@ OUTPUT
|
|
91
91
|
end
|
92
92
|
|
93
93
|
assert_problems(<<OUTPUT)
|
94
|
-
remove index_users_on_last_name_and_first_name -
|
94
|
+
remove the index index_users_on_last_name_and_first_name from the table users - queries should be able to use the following index instead: index_users_on_first_name
|
95
95
|
OUTPUT
|
96
96
|
end
|
97
97
|
|
98
|
+
def test_expression_index_not_covered_by_multicolumn_index
|
99
|
+
skip("Expression indexes are not supported") if ActiveRecordDoctor::Utils.expression_indexes_unsupported?
|
100
|
+
|
101
|
+
create_table(:users) do |t|
|
102
|
+
t.string :first_name
|
103
|
+
t.string :email
|
104
|
+
t.index "(lower(email))"
|
105
|
+
t.index [:first_name, :email]
|
106
|
+
end
|
107
|
+
|
108
|
+
refute_problems
|
109
|
+
end
|
110
|
+
|
111
|
+
def test_unique_expression_index_not_covered_by_unique_multicolumn_index
|
112
|
+
skip("Expression indexes are not supported") if ActiveRecordDoctor::Utils.expression_indexes_unsupported?
|
113
|
+
|
114
|
+
create_table(:users) do |t|
|
115
|
+
t.string :first_name
|
116
|
+
t.string :email
|
117
|
+
t.index "(lower(email))", unique: true
|
118
|
+
t.index [:first_name, :email], unique: true
|
119
|
+
end
|
120
|
+
|
121
|
+
refute_problems
|
122
|
+
end
|
123
|
+
|
98
124
|
def test_not_covered_by_different_index_type
|
99
125
|
create_table(:users) do |t|
|
100
126
|
t.string :first_name
|
@@ -139,6 +165,33 @@ OUTPUT
|
|
139
165
|
refute_problems
|
140
166
|
end
|
141
167
|
|
168
|
+
def test_single_column_covered_by_multi_column_on_materialized_view_is_duplicate
|
169
|
+
skip("Only PostgreSQL supports materialized views") unless postgresql?
|
170
|
+
|
171
|
+
begin
|
172
|
+
create_table(:users) do |t|
|
173
|
+
t.string :first_name
|
174
|
+
t.string :last_name
|
175
|
+
t.integer :age
|
176
|
+
end
|
177
|
+
|
178
|
+
connection = ActiveRecord::Base.connection
|
179
|
+
connection.execute(<<-SQL)
|
180
|
+
CREATE MATERIALIZED VIEW user_initials AS
|
181
|
+
SELECT first_name, last_name FROM users
|
182
|
+
SQL
|
183
|
+
|
184
|
+
connection.add_index(:user_initials, [:last_name, :first_name])
|
185
|
+
connection.add_index(:user_initials, :last_name)
|
186
|
+
|
187
|
+
assert_problems(<<OUTPUT)
|
188
|
+
remove the index index_user_initials_on_last_name from the table user_initials - queries should be able to use the following index instead: index_user_initials_on_last_name_and_first_name
|
189
|
+
OUTPUT
|
190
|
+
ensure
|
191
|
+
connection.execute("DROP MATERIALIZED VIEW user_initials")
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
142
195
|
def test_config_ignore_tables
|
143
196
|
# The detector recognizes two kinds of errors and both must take
|
144
197
|
# ignore_tables into account. We trigger those errors by indexing the
|
@@ -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|
|