active_record_doctor 1.8.0 → 1.10.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 +316 -54
- data/lib/active_record_doctor/config/default.rb +76 -0
- data/lib/active_record_doctor/config/loader.rb +137 -0
- data/lib/active_record_doctor/config.rb +14 -0
- data/lib/active_record_doctor/detectors/base.rb +142 -21
- data/lib/active_record_doctor/detectors/extraneous_indexes.rb +59 -48
- data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +31 -23
- data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +102 -35
- data/lib/active_record_doctor/detectors/incorrect_length_validation.rb +63 -0
- data/lib/active_record_doctor/detectors/mismatched_foreign_key_type.rb +45 -0
- data/lib/active_record_doctor/detectors/missing_foreign_keys.rb +32 -23
- data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +41 -28
- data/lib/active_record_doctor/detectors/missing_presence_validation.rb +29 -23
- data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +92 -32
- data/lib/active_record_doctor/detectors/short_primary_key_type.rb +45 -0
- data/lib/active_record_doctor/detectors/undefined_table_references.rb +17 -20
- data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +43 -18
- data/lib/active_record_doctor/detectors/unindexed_foreign_keys.rb +31 -20
- data/lib/active_record_doctor/detectors.rb +12 -4
- data/lib/active_record_doctor/errors.rb +226 -0
- data/lib/active_record_doctor/help.rb +39 -0
- data/lib/active_record_doctor/rake/task.rb +78 -0
- data/lib/active_record_doctor/runner.rb +41 -0
- data/lib/active_record_doctor/version.rb +1 -1
- data/lib/active_record_doctor.rb +8 -3
- data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +34 -21
- data/lib/tasks/active_record_doctor.rake +9 -18
- data/test/active_record_doctor/config/loader_test.rb +120 -0
- data/test/active_record_doctor/config_test.rb +116 -0
- data/test/active_record_doctor/detectors/disable_test.rb +30 -0
- data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +165 -8
- data/test/active_record_doctor/detectors/incorrect_boolean_presence_validation_test.rb +48 -5
- data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +288 -12
- data/test/active_record_doctor/detectors/incorrect_length_validation_test.rb +105 -0
- data/test/active_record_doctor/detectors/mismatched_foreign_key_type_test.rb +82 -0
- data/test/active_record_doctor/detectors/missing_foreign_keys_test.rb +50 -4
- data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +172 -24
- data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +111 -14
- data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +223 -10
- data/test/active_record_doctor/detectors/short_primary_key_type_test.rb +72 -0
- data/test/active_record_doctor/detectors/undefined_table_references_test.rb +34 -21
- data/test/active_record_doctor/detectors/unindexed_deleted_at_test.rb +118 -8
- data/test/active_record_doctor/detectors/unindexed_foreign_keys_test.rb +56 -4
- data/test/active_record_doctor/runner_test.rb +42 -0
- data/test/generators/active_record_doctor/add_indexes/add_indexes_generator_test.rb +131 -0
- data/test/model_factory.rb +73 -23
- data/test/setup.rb +65 -71
- metadata +43 -7
- data/lib/active_record_doctor/printers/io_printer.rb +0 -133
- data/lib/active_record_doctor/task.rb +0 -28
- data/test/active_record_doctor/printers/io_printer_test.rb +0 -33
@@ -3,8 +3,6 @@
|
|
3
3
|
module ActiveRecordDoctor
|
4
4
|
# Generate migrations that add missing indexes to the database.
|
5
5
|
class AddIndexesGenerator < Rails::Generators::Base
|
6
|
-
MigrationDescription = Struct.new(:table, :columns)
|
7
|
-
|
8
6
|
desc "Generate migrations for the specified indexes"
|
9
7
|
argument :path, type: :string, default: nil, banner: "PATH"
|
10
8
|
|
@@ -12,56 +10,71 @@ module ActiveRecordDoctor
|
|
12
10
|
migration_descriptions = read_migration_descriptions(path)
|
13
11
|
now = Time.now
|
14
12
|
|
15
|
-
migration_descriptions.each_with_index do |
|
13
|
+
migration_descriptions.each_with_index do |(table, columns), index|
|
16
14
|
timestamp = (now + index).strftime("%Y%m%d%H%M%S")
|
17
|
-
file_name = "db/migrate/#{timestamp}_index_foreign_keys_in_#{
|
18
|
-
create_file(file_name, content(
|
15
|
+
file_name = "db/migrate/#{timestamp}_index_foreign_keys_in_#{table}.rb"
|
16
|
+
create_file(file_name, content(table, columns).tap { |x| puts x })
|
19
17
|
end
|
20
18
|
end
|
21
19
|
|
22
20
|
private
|
23
21
|
|
22
|
+
INPUT_LINE = /^add an index on (\w+)\.(\w+) - .*$/.freeze
|
23
|
+
private_constant :INPUT_LINE
|
24
|
+
|
24
25
|
def read_migration_descriptions(path)
|
25
|
-
|
26
|
-
table, *columns = line.split(/\s+/)
|
26
|
+
tables_to_columns = Hash.new { |hash, table| hash[table] = [] }
|
27
27
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
28
|
+
File.readlines(path).each_with_index do |line, index|
|
29
|
+
next if line.blank?
|
30
|
+
|
31
|
+
match = INPUT_LINE.match(line)
|
32
|
+
if match.nil?
|
33
|
+
raise("cannot extract table and column name from line #{index + 1}: #{line}")
|
33
34
|
end
|
34
35
|
|
35
|
-
|
36
|
+
table = match[1]
|
37
|
+
column = match[2]
|
38
|
+
|
39
|
+
tables_to_columns[table] << column
|
36
40
|
end
|
41
|
+
|
42
|
+
tables_to_columns
|
37
43
|
end
|
38
44
|
|
39
|
-
def content(
|
45
|
+
def content(table, columns)
|
40
46
|
# In order to properly indent the resulting code, we must disable the
|
41
47
|
# rubocop rule below.
|
42
48
|
|
43
49
|
<<MIGRATION
|
44
|
-
class IndexForeignKeysIn#{
|
50
|
+
class IndexForeignKeysIn#{table.camelize} < ActiveRecord::Migration#{migration_version}
|
45
51
|
def change
|
46
|
-
#{add_indexes(
|
52
|
+
#{add_indexes(table, columns)}
|
47
53
|
end
|
48
54
|
end
|
49
55
|
MIGRATION
|
50
56
|
end
|
51
57
|
|
52
|
-
def add_indexes(
|
53
|
-
|
54
|
-
add_index(
|
58
|
+
def add_indexes(table, columns)
|
59
|
+
columns.map do |column|
|
60
|
+
add_index(table, column)
|
55
61
|
end.join("\n")
|
56
62
|
end
|
57
63
|
|
58
64
|
def add_index(table, column)
|
59
|
-
|
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)}'"
|
70
|
+
else
|
71
|
+
" add_index :#{table}, :#{column}"
|
72
|
+
end
|
60
73
|
end
|
61
74
|
|
62
75
|
def migration_version
|
63
76
|
if ActiveRecord::VERSION::STRING >= "5.1"
|
64
|
-
"[#{
|
77
|
+
"[#{ActiveRecord::Migration.current_version}]"
|
65
78
|
else
|
66
79
|
""
|
67
80
|
end
|
@@ -11,23 +11,14 @@ require "active_record_doctor/detectors/missing_presence_validation"
|
|
11
11
|
require "active_record_doctor/detectors/missing_non_null_constraint"
|
12
12
|
require "active_record_doctor/detectors/incorrect_boolean_presence_validation"
|
13
13
|
require "active_record_doctor/detectors/incorrect_dependent_option"
|
14
|
-
require "active_record_doctor/
|
14
|
+
require "active_record_doctor/detectors/short_primary_key_type"
|
15
|
+
require "active_record_doctor/detectors/mismatched_foreign_key_type"
|
16
|
+
require "active_record_doctor/rake/task"
|
15
17
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
desc task.description
|
23
|
-
task task.name => :environment do
|
24
|
-
task.run or exit(1)
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
desc "Run all active_record_doctor tasks"
|
29
|
-
task :all => :environment do
|
30
|
-
results = tasks.map { |task| task.run }
|
31
|
-
results.all? or exit(1)
|
32
|
-
end
|
18
|
+
ActiveRecordDoctor::Rake::Task.new do |task|
|
19
|
+
# This file is imported when active_record_doctor is being used as part of a
|
20
|
+
# Rails app so it's the right place for all Rails-specific settings.
|
21
|
+
task.deps = [:environment]
|
22
|
+
task.config_path = ::Rails.root.join(".active_record_doctor")
|
23
|
+
task.setup = -> { ::Rails.application.eager_load! }
|
33
24
|
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ActiveRecordDoctor::LoaderTest < Minitest::Test
|
4
|
+
def test_load_config
|
5
|
+
config_path = config_file(<<-CONFIG)
|
6
|
+
ActiveRecordDoctor.configure do |config|
|
7
|
+
config.global :ignore_tables, ["schema_migrations"]
|
8
|
+
|
9
|
+
config.detector :extraneous_indexes, ignore_tables: ["users"]
|
10
|
+
end
|
11
|
+
CONFIG
|
12
|
+
|
13
|
+
config = ActiveRecordDoctor.load_config(config_path)
|
14
|
+
|
15
|
+
assert_equal(
|
16
|
+
{ ignore_tables: ["schema_migrations"] },
|
17
|
+
config.globals
|
18
|
+
)
|
19
|
+
assert_equal(
|
20
|
+
{ extraneous_indexes: { ignore_tables: ["users"] } },
|
21
|
+
config.detectors
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_load_config_raises_when_configuration_file_missing
|
26
|
+
exc = assert_raises(ActiveRecordDoctor::Error::ConfigurationFileMissing) do
|
27
|
+
ActiveRecordDoctor.load_config("/tmp/config.rb")
|
28
|
+
end
|
29
|
+
assert_equal("/tmp/config.rb", exc.config_path)
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_load_config_raises_when_configuration_file_raises
|
33
|
+
config_path = config_file("1/0")
|
34
|
+
|
35
|
+
assert_raises(ActiveRecordDoctor::Error::ConfigurationError) do
|
36
|
+
ActiveRecordDoctor.load_config(config_path)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_load_config_raises_when_configure_not_called
|
41
|
+
config_path = config_file("# configure is not called")
|
42
|
+
|
43
|
+
assert_raises(ActiveRecordDoctor::Error::ConfigureNotCalled) do
|
44
|
+
ActiveRecordDoctor.load_config(config_path)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_load_config_raises_when_configure_called_twice
|
49
|
+
config_path = config_file(<<-CONFIG)
|
50
|
+
ActiveRecordDoctor.configure { |config| }
|
51
|
+
ActiveRecordDoctor.configure { |config| }
|
52
|
+
CONFIG
|
53
|
+
|
54
|
+
assert_raises(ActiveRecordDoctor::Error::ConfigureCalledTwice) do
|
55
|
+
ActiveRecordDoctor.load_config(config_path)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_load_config_raises_when_unrecognized_global_set
|
60
|
+
config_path = config_file(<<-CONFIG)
|
61
|
+
ActiveRecordDoctor.configure do |config|
|
62
|
+
config.global :user, "acme"
|
63
|
+
end
|
64
|
+
CONFIG
|
65
|
+
|
66
|
+
assert_raises(ActiveRecordDoctor::Error::UnrecognizedGlobalSetting) do
|
67
|
+
ActiveRecordDoctor.load_config(config_path)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def test_load_config_raises_when_global_set_twice
|
72
|
+
config_path = config_file(<<-CONFIG)
|
73
|
+
ActiveRecordDoctor.configure do |config|
|
74
|
+
config.global :ignore_tables, ["schema_migrations"]
|
75
|
+
config.global :ignore_tables, ["schema_migrations"]
|
76
|
+
end
|
77
|
+
CONFIG
|
78
|
+
|
79
|
+
assert_raises(ActiveRecordDoctor::Error::DuplicateGlobalSetting) do
|
80
|
+
ActiveRecordDoctor.load_config(config_path)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_load_config_raises_when_configured_unrecognized_detector
|
85
|
+
config_path = config_file(<<-CONFIG)
|
86
|
+
ActiveRecordDoctor.configure do |config|
|
87
|
+
config.detector :other_performance_issues, {}
|
88
|
+
end
|
89
|
+
CONFIG
|
90
|
+
|
91
|
+
assert_raises(ActiveRecordDoctor::Error::UnrecognizedDetectorName) do
|
92
|
+
ActiveRecordDoctor.load_config(config_path)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_load_config_raises_when_detector_configured_twice
|
97
|
+
config_path = config_file(<<-CONFIG)
|
98
|
+
ActiveRecordDoctor.configure do |config|
|
99
|
+
config.detector :extraneous_indexes, ignore_tables: ["users"]
|
100
|
+
config.detector :extraneous_indexes, ignore_tables: ["users"]
|
101
|
+
end
|
102
|
+
CONFIG
|
103
|
+
|
104
|
+
assert_raises(ActiveRecordDoctor::Error::DetectorConfiguredTwice) do
|
105
|
+
ActiveRecordDoctor.load_config(config_path)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_load_config_raises_when_provided_unrecognized_detector_setting
|
110
|
+
config_path = config_file(<<-CONFIG)
|
111
|
+
ActiveRecordDoctor.configure do |config|
|
112
|
+
config.detector :extraneous_indexes, { delay: 1 }
|
113
|
+
end
|
114
|
+
CONFIG
|
115
|
+
|
116
|
+
assert_raises(ActiveRecordDoctor::Error::UnrecognizedDetectorSettings) do
|
117
|
+
ActiveRecordDoctor.load_config(config_path)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ActiveRecordDoctor::ConfigTest < Minitest::Test
|
4
|
+
def test_merge_globals_empty
|
5
|
+
config1 = ActiveRecordDoctor::Config.new({}, {})
|
6
|
+
config2 = ActiveRecordDoctor::Config.new({}, {})
|
7
|
+
|
8
|
+
config = config1.merge(config2)
|
9
|
+
|
10
|
+
assert_equal({}, config.globals)
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_merge_globals_in_config1
|
14
|
+
config1 = ActiveRecordDoctor::Config.new(
|
15
|
+
{ config1_global: "config1:config1_global" },
|
16
|
+
{}
|
17
|
+
)
|
18
|
+
config2 = ActiveRecordDoctor::Config.new({}, {})
|
19
|
+
|
20
|
+
config = config1.merge(config2)
|
21
|
+
|
22
|
+
assert_equal(
|
23
|
+
{ config1_global: "config1:config1_global" },
|
24
|
+
config.globals
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_merge_globals_in_config2
|
29
|
+
config1 = ActiveRecordDoctor::Config.new({}, {})
|
30
|
+
config2 = ActiveRecordDoctor::Config.new(
|
31
|
+
{ config2_global: "config2:config2_global" },
|
32
|
+
{}
|
33
|
+
)
|
34
|
+
|
35
|
+
config = config1.merge(config2)
|
36
|
+
|
37
|
+
assert_equal(
|
38
|
+
{ config2_global: "config2:config2_global" },
|
39
|
+
config.globals
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_merge_globals_in_config1_and_config2
|
44
|
+
config1 = ActiveRecordDoctor::Config.new(
|
45
|
+
{
|
46
|
+
config1_global: "config1:config1_global",
|
47
|
+
shared_global: "config1:shared_global"
|
48
|
+
},
|
49
|
+
{}
|
50
|
+
)
|
51
|
+
config2 = ActiveRecordDoctor::Config.new(
|
52
|
+
{
|
53
|
+
config2_global: "config2:config2_global",
|
54
|
+
shared_global: "config2:shared_global"
|
55
|
+
},
|
56
|
+
{}
|
57
|
+
)
|
58
|
+
|
59
|
+
config = config1.merge(config2)
|
60
|
+
|
61
|
+
assert_equal(
|
62
|
+
{
|
63
|
+
config1_global: "config1:config1_global",
|
64
|
+
shared_global: "config2:shared_global",
|
65
|
+
config2_global: "config2:config2_global"
|
66
|
+
},
|
67
|
+
config.globals
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
def test_merge_detectors
|
72
|
+
config1 = ActiveRecordDoctor::Config.new(
|
73
|
+
{},
|
74
|
+
{
|
75
|
+
config1_detector: {
|
76
|
+
config1_setting: "config1:config1_detector.config1_setting"
|
77
|
+
},
|
78
|
+
shared_detector: {
|
79
|
+
config1_setting: "config1:shared_detector.config1_setting",
|
80
|
+
shared_setting: "config1:shared_detector.shared_setting"
|
81
|
+
}
|
82
|
+
}
|
83
|
+
)
|
84
|
+
config2 = ActiveRecordDoctor::Config.new(
|
85
|
+
{},
|
86
|
+
{
|
87
|
+
config2_detector: {
|
88
|
+
config2_setting: "config2:config2_detector.config2_setting"
|
89
|
+
},
|
90
|
+
shared_detector: {
|
91
|
+
config2_setting: "config2:shared_detector.config2_setting",
|
92
|
+
shared_setting: "config2:shared_detector.shared_setting"
|
93
|
+
}
|
94
|
+
}
|
95
|
+
)
|
96
|
+
|
97
|
+
config = config1.merge(config2)
|
98
|
+
|
99
|
+
assert_equal(
|
100
|
+
{
|
101
|
+
config1_detector: {
|
102
|
+
config1_setting: "config1:config1_detector.config1_setting"
|
103
|
+
},
|
104
|
+
config2_detector: {
|
105
|
+
config2_setting: "config2:config2_detector.config2_setting"
|
106
|
+
},
|
107
|
+
shared_detector: {
|
108
|
+
config1_setting: "config1:shared_detector.config1_setting",
|
109
|
+
config2_setting: "config2:shared_detector.config2_setting",
|
110
|
+
shared_setting: "config2:shared_detector.shared_setting"
|
111
|
+
}
|
112
|
+
},
|
113
|
+
config.detectors
|
114
|
+
)
|
115
|
+
end
|
116
|
+
end
|
@@ -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.create_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
|
@@ -7,8 +7,28 @@ class ActiveRecordDoctor::Detectors::ExtraneousIndexesTest < Minitest::Test
|
|
7
7
|
end
|
8
8
|
|
9
9
|
assert_problems(<<OUTPUT)
|
10
|
-
|
11
|
-
|
10
|
+
remove index_users_on_id - coincides with the primary key on the table
|
11
|
+
OUTPUT
|
12
|
+
end
|
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
|
12
32
|
OUTPUT
|
13
33
|
end
|
14
34
|
|
@@ -22,8 +42,7 @@ OUTPUT
|
|
22
42
|
ActiveRecord::Base.connection.add_index :users, :email, name: "index_users_on_email"
|
23
43
|
|
24
44
|
assert_problems(<<OUTPUT)
|
25
|
-
|
26
|
-
index_users_on_email (can be handled by unique_index_on_users_email)
|
45
|
+
remove index_users_on_email - can be replaced by unique_index_on_users_email
|
27
46
|
OUTPUT
|
28
47
|
end
|
29
48
|
|
@@ -40,8 +59,7 @@ OUTPUT
|
|
40
59
|
end
|
41
60
|
|
42
61
|
assert_problems(<<OUTPUT)
|
43
|
-
|
44
|
-
index_users_on_last_name (can be handled by index_users_on_last_name_and_first_name_and_email, unique_index_on_users_last_name_and_first_name)
|
62
|
+
remove index_users_on_last_name - can be replaced by index_users_on_last_name_and_first_name_and_email or unique_index_on_users_last_name_and_first_name
|
45
63
|
OUTPUT
|
46
64
|
end
|
47
65
|
|
@@ -60,8 +78,147 @@ OUTPUT
|
|
60
78
|
ActiveRecord::Base.connection.add_index :users, [:last_name, :first_name]
|
61
79
|
|
62
80
|
assert_problems(<<OUTPUT)
|
63
|
-
|
64
|
-
|
81
|
+
remove index_users_on_last_name_and_first_name - can be replaced by index_users_on_last_name_and_first_name_and_email or unique_index_on_users_last_name_and_first_name
|
82
|
+
OUTPUT
|
83
|
+
end
|
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
|
65
95
|
OUTPUT
|
66
96
|
end
|
97
|
+
|
98
|
+
def test_not_covered_by_different_index_type
|
99
|
+
create_table(:users) do |t|
|
100
|
+
t.string :first_name
|
101
|
+
t.string :last_name
|
102
|
+
t.index [:last_name, :first_name], using: :btree
|
103
|
+
|
104
|
+
if mysql?
|
105
|
+
t.index :last_name, type: :fulltext
|
106
|
+
else
|
107
|
+
t.index :last_name, using: :hash
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
refute_problems
|
112
|
+
end
|
113
|
+
|
114
|
+
def test_not_covered_by_partial_index
|
115
|
+
skip("MySQL doesn't support partial indexes") if mysql?
|
116
|
+
|
117
|
+
create_table(:users) do |t|
|
118
|
+
t.string :first_name
|
119
|
+
t.string :last_name
|
120
|
+
t.boolean :active
|
121
|
+
t.index [:last_name, :first_name], where: "active"
|
122
|
+
t.index :last_name
|
123
|
+
end
|
124
|
+
|
125
|
+
refute_problems
|
126
|
+
end
|
127
|
+
|
128
|
+
def test_not_covered_with_different_opclasses
|
129
|
+
skip("ActiveRecord < 5.2 doesn't support operator classes") if ActiveRecord::VERSION::STRING < "5.2"
|
130
|
+
skip("MySQL doesn't support operator classes") if mysql?
|
131
|
+
|
132
|
+
create_table(:users) do |t|
|
133
|
+
t.string :first_name
|
134
|
+
t.string :last_name
|
135
|
+
t.index [:last_name, :first_name], opclass: :varchar_pattern_ops
|
136
|
+
t.index :last_name
|
137
|
+
end
|
138
|
+
|
139
|
+
refute_problems
|
140
|
+
end
|
141
|
+
|
142
|
+
def test_config_ignore_tables
|
143
|
+
# The detector recognizes two kinds of errors and both must take
|
144
|
+
# ignore_tables into account. We trigger those errors by indexing the
|
145
|
+
# primary key (the first extraneous index) and then indexing email twice
|
146
|
+
# (index2... is the other extraneous index).
|
147
|
+
create_table(:users) do |t|
|
148
|
+
t.index :id
|
149
|
+
t.string :email
|
150
|
+
|
151
|
+
t.index :email, name: "index1_on_users_email"
|
152
|
+
t.index :email, name: "index2_on_users_email"
|
153
|
+
end
|
154
|
+
|
155
|
+
config_file(<<-CONFIG)
|
156
|
+
ActiveRecordDoctor.configure do |config|
|
157
|
+
config.detector :extraneous_indexes,
|
158
|
+
ignore_tables: ["users"]
|
159
|
+
end
|
160
|
+
CONFIG
|
161
|
+
|
162
|
+
refute_problems
|
163
|
+
end
|
164
|
+
|
165
|
+
def test_config_global_ignore_tables
|
166
|
+
create_table(:users) do |t|
|
167
|
+
t.index :id
|
168
|
+
t.string :email
|
169
|
+
|
170
|
+
t.index :email, name: "index1_on_users_email"
|
171
|
+
t.index :email, name: "index2_on_users_email"
|
172
|
+
end
|
173
|
+
|
174
|
+
config_file(<<-CONFIG)
|
175
|
+
ActiveRecordDoctor.configure do |config|
|
176
|
+
config.global :ignore_tables, ["users"]
|
177
|
+
end
|
178
|
+
CONFIG
|
179
|
+
|
180
|
+
refute_problems
|
181
|
+
end
|
182
|
+
|
183
|
+
def test_config_global_ignore_indexes
|
184
|
+
create_table(:users) do |t|
|
185
|
+
t.index :id
|
186
|
+
t.string :email
|
187
|
+
|
188
|
+
t.index :email, name: "index1_on_users_email"
|
189
|
+
t.index :email, name: "index2_on_users_email"
|
190
|
+
end
|
191
|
+
|
192
|
+
config_file(<<-CONFIG)
|
193
|
+
ActiveRecordDoctor.configure do |config|
|
194
|
+
config.global :ignore_indexes, [
|
195
|
+
"index1_on_users_email",
|
196
|
+
"index2_on_users_email",
|
197
|
+
"index_users_on_id",
|
198
|
+
]
|
199
|
+
end
|
200
|
+
CONFIG
|
201
|
+
|
202
|
+
refute_problems
|
203
|
+
end
|
204
|
+
|
205
|
+
def test_config_detector_ignore_indexes
|
206
|
+
create_table(:users) do |t|
|
207
|
+
t.index :id
|
208
|
+
t.string :email
|
209
|
+
t.string :api_key
|
210
|
+
|
211
|
+
t.index :email, name: "index_on_users_email"
|
212
|
+
t.index [:email, :api_key], name: "index_on_users_email_and_api_key"
|
213
|
+
end
|
214
|
+
|
215
|
+
config_file(<<-CONFIG)
|
216
|
+
ActiveRecordDoctor.configure do |config|
|
217
|
+
config.detector :extraneous_indexes,
|
218
|
+
ignore_indexes: ["index_users_on_id", "index_on_users_email"]
|
219
|
+
end
|
220
|
+
CONFIG
|
221
|
+
|
222
|
+
refute_problems
|
223
|
+
end
|
67
224
|
end
|
@@ -12,10 +12,9 @@ class ActiveRecordDoctor::Detectors::IncorrectBooleanPresenceValidationTest < Mi
|
|
12
12
|
validates :email, :active, presence: true
|
13
13
|
end
|
14
14
|
|
15
|
-
assert_problems(
|
16
|
-
|
17
|
-
|
18
|
-
OUTPUT
|
15
|
+
assert_problems(<<~OUTPUT)
|
16
|
+
replace the `presence` validator on ModelFactory::Models::User.active with `inclusion` - `presence` can't be used on booleans
|
17
|
+
OUTPUT
|
19
18
|
end
|
20
19
|
|
21
20
|
def test_inclusion_is_not_reported
|
@@ -29,7 +28,51 @@ OUTPUT
|
|
29
28
|
end
|
30
29
|
|
31
30
|
def test_models_with_non_existent_tables_are_skipped
|
32
|
-
create_model(:
|
31
|
+
create_model(:User)
|
32
|
+
|
33
|
+
refute_problems
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_config_ignore_models
|
37
|
+
create_table(:users) do |t|
|
38
|
+
t.string :email, null: false
|
39
|
+
end.create_model
|
40
|
+
|
41
|
+
config_file(<<-CONFIG)
|
42
|
+
ActiveRecordDoctor.configure do |config|
|
43
|
+
config.detector :incorrect_boolean_presence_validation,
|
44
|
+
ignore_models: ["ModelFactory.User"]
|
45
|
+
end
|
46
|
+
CONFIG
|
47
|
+
|
48
|
+
refute_problems
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_global_ignore_models
|
52
|
+
create_table(:users) do |t|
|
53
|
+
t.string :email, null: false
|
54
|
+
end.create_model
|
55
|
+
|
56
|
+
config_file(<<-CONFIG)
|
57
|
+
ActiveRecordDoctor.configure do |config|
|
58
|
+
config.global :ignore_models, ["ModelFactory.User"]
|
59
|
+
end
|
60
|
+
CONFIG
|
61
|
+
|
62
|
+
refute_problems
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_config_ignore_attributes
|
66
|
+
create_table(:users) do |t|
|
67
|
+
t.string :email, null: false
|
68
|
+
end.create_model
|
69
|
+
|
70
|
+
config_file(<<-CONFIG)
|
71
|
+
ActiveRecordDoctor.configure do |config|
|
72
|
+
config.detector :incorrect_boolean_presence_validation,
|
73
|
+
ignore_attributes: ["ModelFactory.User.email"]
|
74
|
+
end
|
75
|
+
CONFIG
|
33
76
|
|
34
77
|
refute_problems
|
35
78
|
end
|