active_record_doctor 1.7.0 → 1.9.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (114) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +287 -43
  3. data/lib/active_record_doctor/config/default.rb +59 -0
  4. data/lib/active_record_doctor/config/loader.rb +137 -0
  5. data/lib/active_record_doctor/config.rb +14 -0
  6. data/lib/active_record_doctor/detectors/base.rb +155 -0
  7. data/lib/active_record_doctor/detectors/extraneous_indexes.rb +114 -0
  8. data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +49 -0
  9. data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +107 -0
  10. data/lib/active_record_doctor/detectors/mismatched_foreign_key_type.rb +45 -0
  11. data/lib/active_record_doctor/detectors/missing_foreign_keys.rb +60 -0
  12. data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +72 -0
  13. data/lib/active_record_doctor/{tasks → detectors}/missing_presence_validation.rb +35 -21
  14. data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +71 -0
  15. data/lib/active_record_doctor/detectors/short_primary_key_type.rb +41 -0
  16. data/lib/active_record_doctor/detectors/undefined_table_references.rb +33 -0
  17. data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +55 -0
  18. data/lib/active_record_doctor/detectors/unindexed_foreign_keys.rb +59 -0
  19. data/lib/active_record_doctor/detectors.rb +21 -0
  20. data/lib/active_record_doctor/errors.rb +226 -0
  21. data/lib/active_record_doctor/help.rb +39 -0
  22. data/lib/active_record_doctor/printers.rb +3 -1
  23. data/lib/active_record_doctor/railtie.rb +2 -0
  24. data/lib/active_record_doctor/rake/task.rb +78 -0
  25. data/lib/active_record_doctor/runner.rb +41 -0
  26. data/lib/active_record_doctor/version.rb +3 -1
  27. data/lib/active_record_doctor.rb +24 -2
  28. data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +46 -29
  29. data/lib/tasks/active_record_doctor.rake +21 -29
  30. data/test/active_record_doctor/config/loader_test.rb +120 -0
  31. data/test/active_record_doctor/config_test.rb +116 -0
  32. data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +190 -0
  33. data/test/active_record_doctor/detectors/incorrect_boolean_presence_validation_test.rb +79 -0
  34. data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +295 -0
  35. data/test/active_record_doctor/detectors/mismatched_foreign_key_type_test.rb +82 -0
  36. data/test/active_record_doctor/detectors/missing_foreign_keys_test.rb +70 -0
  37. data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +216 -0
  38. data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +168 -0
  39. data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +163 -0
  40. data/test/active_record_doctor/detectors/short_primary_key_type_test.rb +64 -0
  41. data/test/active_record_doctor/detectors/undefined_table_references_test.rb +57 -0
  42. data/test/active_record_doctor/detectors/unindexed_deleted_at_test.rb +171 -0
  43. data/test/active_record_doctor/detectors/unindexed_foreign_keys_test.rb +78 -0
  44. data/test/active_record_doctor/runner_test.rb +42 -0
  45. data/test/generators/active_record_doctor/add_indexes/add_indexes_generator_test.rb +131 -0
  46. data/test/model_factory.rb +128 -0
  47. data/test/setup.rb +116 -0
  48. metadata +105 -156
  49. data/Rakefile +0 -28
  50. data/lib/active_record_doctor/printers/io_printer.rb +0 -105
  51. data/lib/active_record_doctor/tasks/base.rb +0 -78
  52. data/lib/active_record_doctor/tasks/extraneous_indexes.rb +0 -82
  53. data/lib/active_record_doctor/tasks/incorrect_boolean_presence_validation.rb +0 -33
  54. data/lib/active_record_doctor/tasks/missing_foreign_keys.rb +0 -46
  55. data/lib/active_record_doctor/tasks/missing_non_null_constraint.rb +0 -49
  56. data/lib/active_record_doctor/tasks/missing_unique_indexes.rb +0 -56
  57. data/lib/active_record_doctor/tasks/undefined_table_references.rb +0 -33
  58. data/lib/active_record_doctor/tasks/unindexed_deleted_at.rb +0 -19
  59. data/lib/active_record_doctor/tasks/unindexed_foreign_keys.rb +0 -43
  60. data/lib/active_record_doctor/tasks.rb +0 -7
  61. data/test/active_record_doctor/printers/io_printer_test.rb +0 -20
  62. data/test/active_record_doctor/tasks/extraneous_indexes_test.rb +0 -81
  63. data/test/active_record_doctor/tasks/incorrect_boolean_presence_validation_test.rb +0 -33
  64. data/test/active_record_doctor/tasks/missing_foreign_keys_test.rb +0 -27
  65. data/test/active_record_doctor/tasks/missing_non_null_constraint_test.rb +0 -102
  66. data/test/active_record_doctor/tasks/missing_presence_validation_test.rb +0 -110
  67. data/test/active_record_doctor/tasks/missing_unique_indexes_test.rb +0 -95
  68. data/test/active_record_doctor/tasks/undefined_table_references_test.rb +0 -51
  69. data/test/active_record_doctor/tasks/unindexed_deleted_at_test.rb +0 -34
  70. data/test/active_record_doctor/tasks/unindexed_foreign_keys_test.rb +0 -27
  71. data/test/dummy/README.rdoc +0 -28
  72. data/test/dummy/Rakefile +0 -6
  73. data/test/dummy/app/assets/config/manifest.js +0 -1
  74. data/test/dummy/app/assets/javascripts/application.js +0 -13
  75. data/test/dummy/app/assets/stylesheets/application.css +0 -15
  76. data/test/dummy/app/controllers/application_controller.rb +0 -5
  77. data/test/dummy/app/helpers/application_helper.rb +0 -2
  78. data/test/dummy/app/models/application_record.rb +0 -3
  79. data/test/dummy/app/views/layouts/application.html.erb +0 -14
  80. data/test/dummy/bin/bundle +0 -3
  81. data/test/dummy/bin/rails +0 -4
  82. data/test/dummy/bin/rake +0 -4
  83. data/test/dummy/bin/setup +0 -29
  84. data/test/dummy/config/application.rb +0 -23
  85. data/test/dummy/config/boot.rb +0 -5
  86. data/test/dummy/config/database.yml +0 -19
  87. data/test/dummy/config/database.yml.travis +0 -5
  88. data/test/dummy/config/environment.rb +0 -5
  89. data/test/dummy/config/environments/development.rb +0 -41
  90. data/test/dummy/config/environments/production.rb +0 -79
  91. data/test/dummy/config/environments/test.rb +0 -47
  92. data/test/dummy/config/initializers/assets.rb +0 -11
  93. data/test/dummy/config/initializers/backtrace_silencers.rb +0 -7
  94. data/test/dummy/config/initializers/cookies_serializer.rb +0 -3
  95. data/test/dummy/config/initializers/filter_parameter_logging.rb +0 -4
  96. data/test/dummy/config/initializers/inflections.rb +0 -16
  97. data/test/dummy/config/initializers/mime_types.rb +0 -4
  98. data/test/dummy/config/initializers/session_store.rb +0 -3
  99. data/test/dummy/config/initializers/wrap_parameters.rb +0 -14
  100. data/test/dummy/config/locales/en.yml +0 -23
  101. data/test/dummy/config/routes.rb +0 -56
  102. data/test/dummy/config/secrets.yml +0 -22
  103. data/test/dummy/config.ru +0 -4
  104. data/test/dummy/db/migrate/base_migration.rb +0 -5
  105. data/test/dummy/db/schema.rb +0 -19
  106. data/test/dummy/log/development.log +0 -111
  107. data/test/dummy/log/test.log +0 -67508
  108. data/test/dummy/public/404.html +0 -67
  109. data/test/dummy/public/422.html +0 -67
  110. data/test/dummy/public/500.html +0 -66
  111. data/test/dummy/public/favicon.ico +0 -0
  112. data/test/support/assertions.rb +0 -11
  113. data/test/support/temping.rb +0 -25
  114. data/test/test_helper.rb +0 -17
@@ -1,4 +1,26 @@
1
- require "active_record_doctor/railtie" if defined?(Rails)
1
+ # frozen_string_literal: true
2
2
 
3
- module ActiveRecordDoctor
3
+ require "active_record_doctor/railtie" if defined?(Rails) && defined?(Rails::Railtie)
4
+ require "active_record_doctor/detectors"
5
+ require "active_record_doctor/detectors/base"
6
+ require "active_record_doctor/detectors/missing_presence_validation"
7
+ require "active_record_doctor/detectors/missing_foreign_keys"
8
+ require "active_record_doctor/detectors/missing_unique_indexes"
9
+ require "active_record_doctor/detectors/incorrect_boolean_presence_validation"
10
+ require "active_record_doctor/detectors/extraneous_indexes"
11
+ require "active_record_doctor/detectors/unindexed_deleted_at"
12
+ require "active_record_doctor/detectors/undefined_table_references"
13
+ require "active_record_doctor/detectors/missing_non_null_constraint"
14
+ require "active_record_doctor/detectors/unindexed_foreign_keys"
15
+ require "active_record_doctor/detectors/incorrect_dependent_option"
16
+ require "active_record_doctor/detectors/short_primary_key_type"
17
+ require "active_record_doctor/detectors/mismatched_foreign_key_type"
18
+ require "active_record_doctor/errors"
19
+ require "active_record_doctor/help"
20
+ require "active_record_doctor/runner"
21
+ require "active_record_doctor/version"
22
+ require "active_record_doctor/config"
23
+ require "active_record_doctor/config/loader"
24
+
25
+ module ActiveRecordDoctor # :nodoc:
4
26
  end
@@ -1,65 +1,82 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecordDoctor
4
+ # Generate migrations that add missing indexes to the database.
2
5
  class AddIndexesGenerator < Rails::Generators::Base
3
- MigrationDescription = Struct.new(:table, :columns)
4
-
5
- desc 'Generate migrations for the specified indexes'
6
- argument :path, type: :string, default: nil, banner: 'PATH'
6
+ desc "Generate migrations for the specified indexes"
7
+ argument :path, type: :string, default: nil, banner: "PATH"
7
8
 
8
9
  def create_migrations
9
10
  migration_descriptions = read_migration_descriptions(path)
10
11
  now = Time.now
11
12
 
12
- migration_descriptions.each_with_index do |migration_description, index|
13
+ migration_descriptions.each_with_index do |(table, columns), index|
13
14
  timestamp = (now + index).strftime("%Y%m%d%H%M%S")
14
- file_name = "db/migrate/#{timestamp}_index_foreign_keys_in_#{migration_description.table}.rb"
15
- create_file(file_name, content(migration_description))
15
+ file_name = "db/migrate/#{timestamp}_index_foreign_keys_in_#{table}.rb"
16
+ create_file(file_name, content(table, columns).tap { |x| puts x })
16
17
  end
17
18
  end
18
19
 
19
20
  private
20
21
 
22
+ INPUT_LINE = /^add an index on (\w+)\.(\w+) - .*$/
23
+ private_constant :INPUT_LINE
24
+
21
25
  def read_migration_descriptions(path)
22
- File.readlines(path).each_with_index.map do |line, index|
23
- table, *columns = line.split(/\s+/)
26
+ tables_to_columns = Hash.new { |hash, table| hash[table] = [] }
24
27
 
25
- if table.empty?
26
- fail("No table name in #{path} on line #{index + 1}. Ensure the line doesn't start with whitespace.")
27
- end
28
- if columns.empty?
29
- fail("No columns for table #{table} in #{path} on line #{index + 1}.")
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}")
30
34
  end
31
35
 
32
- MigrationDescription.new(table, columns)
36
+ table = match[1]
37
+ column = match[2]
38
+
39
+ tables_to_columns[table] << column
33
40
  end
41
+
42
+ tables_to_columns
34
43
  end
35
44
 
36
- def content(migration_description)
37
- <<EOF
38
- class IndexForeignKeysIn#{migration_description.table.camelize} < ActiveRecord::Migration#{migration_version}
45
+ def content(table, columns)
46
+ # In order to properly indent the resulting code, we must disable the
47
+ # rubocop rule below.
48
+
49
+ <<MIGRATION
50
+ class IndexForeignKeysIn#{table.camelize} < ActiveRecord::Migration#{migration_version}
39
51
  def change
40
- #{add_indexes(migration_description)}
52
+ #{add_indexes(table, columns)}
41
53
  end
42
54
  end
43
- EOF
55
+ MIGRATION
44
56
  end
45
57
 
46
- def add_indexes(migration_description)
47
- migration_description.columns.map do |column|
48
- add_index(migration_description.table, column)
58
+ def add_indexes(table, columns)
59
+ columns.map do |column|
60
+ add_index(table, column)
49
61
  end.join("\n")
50
62
  end
51
63
 
52
64
  def add_index(table, column)
53
- " add_index :#{table}, :#{column}"
65
+ index_name = Class.new.extend(ActiveRecord::ConnectionAdapters::SchemaStatements).index_name table, column
66
+ # rubocop:disable Layout/LineLength
67
+ if index_name.size > ActiveRecord::Base.connection.allowed_index_name_length
68
+ " add_index :#{table}, :#{column}, name: '#{index_name.first ActiveRecord::Base.connection.allowed_index_name_length}'"
69
+ else
70
+ " add_index :#{table}, :#{column}"
71
+ end
72
+ # rubocop:enable Layout/LineLength
54
73
  end
55
74
 
56
75
  def migration_version
57
- major = ActiveRecord::VERSION::MAJOR
58
- minor = ActiveRecord::VERSION::MINOR
59
- if major >= 5 && minor >= 1
60
- "[#{major}.#{minor}]"
76
+ if ActiveRecord::VERSION::STRING >= "5.1"
77
+ "[#{ActiveRecord::Migration.current_version}]"
61
78
  else
62
- ''
79
+ ""
63
80
  end
64
81
  end
65
82
  end
@@ -1,32 +1,24 @@
1
- require "active_record_doctor/tasks"
2
- require "active_record_doctor/tasks/unindexed_foreign_keys"
3
- require "active_record_doctor/tasks/extraneous_indexes"
4
- require "active_record_doctor/tasks/missing_foreign_keys"
5
- require "active_record_doctor/tasks/undefined_table_references"
6
- require "active_record_doctor/tasks/unindexed_deleted_at"
7
- require "active_record_doctor/tasks/missing_unique_indexes"
8
- require "active_record_doctor/tasks/missing_presence_validation"
9
- require "active_record_doctor/tasks/missing_non_null_constraint"
10
- require "active_record_doctor/tasks/incorrect_boolean_presence_validation"
1
+ # frozen_string_literal: true
11
2
 
12
- namespace :active_record_doctor do
13
- def mount(task_class)
14
- name = task_class.name.demodulize.underscore.to_sym
3
+ require "active_record_doctor/detectors"
4
+ require "active_record_doctor/detectors/unindexed_foreign_keys"
5
+ require "active_record_doctor/detectors/extraneous_indexes"
6
+ require "active_record_doctor/detectors/missing_foreign_keys"
7
+ require "active_record_doctor/detectors/undefined_table_references"
8
+ require "active_record_doctor/detectors/unindexed_deleted_at"
9
+ require "active_record_doctor/detectors/missing_unique_indexes"
10
+ require "active_record_doctor/detectors/missing_presence_validation"
11
+ require "active_record_doctor/detectors/missing_non_null_constraint"
12
+ require "active_record_doctor/detectors/incorrect_boolean_presence_validation"
13
+ require "active_record_doctor/detectors/incorrect_dependent_option"
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
- task name => :environment do
17
- result, success = task_class.run
18
- success = true if success.nil?
19
-
20
- printer = ActiveRecordDoctor::Printers::IOPrinter.new
21
- printer.public_send(name, result)
22
-
23
- # nil doesn't indicate a failure but rather no explicit result. We assume
24
- # success by default hence only false results in an erroneous exit code.
25
- exit(1) if success == false
26
- end
27
- end
28
-
29
- ActiveRecordDoctor::Tasks.all.each do |task|
30
- mount task
31
- 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! }
32
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,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveRecordDoctor::Detectors::ExtraneousIndexesTest < Minitest::Test
4
+ def test_index_on_primary_key_is_duplicate
5
+ create_table(:users) do |t|
6
+ t.index :id
7
+ end
8
+
9
+ assert_problems(<<OUTPUT)
10
+ remove index_users_on_id - coincides with the primary key on the table
11
+ OUTPUT
12
+ end
13
+
14
+ def test_non_unique_version_of_index_is_duplicate
15
+ create_table(:users) do |t|
16
+ t.string :email
17
+ t.index :email, unique: true, name: "unique_index_on_users_email"
18
+ end
19
+
20
+ # Rails 4.2 compatibility - can't be pulled into the block above.
21
+ ActiveRecord::Base.connection.add_index :users, :email, name: "index_users_on_email"
22
+
23
+ assert_problems(<<OUTPUT)
24
+ remove index_users_on_email - can be replaced by unique_index_on_users_email
25
+ OUTPUT
26
+ end
27
+
28
+ def test_single_column_covered_by_unique_and_non_unique_multi_column_is_duplicate
29
+ create_table(:users) do |t|
30
+ t.string :first_name
31
+ t.string :last_name
32
+ t.string :email
33
+ t.index [:last_name, :first_name, :email]
34
+ t.index [:last_name, :first_name],
35
+ unique: true,
36
+ name: "unique_index_on_users_last_name_and_first_name"
37
+ t.index :last_name
38
+ end
39
+
40
+ assert_problems(<<OUTPUT)
41
+ 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
42
+ OUTPUT
43
+ end
44
+
45
+ def test_multi_column_covered_by_unique_and_non_unique_multi_column_is_duplicate
46
+ create_table(:users) do |t|
47
+ t.string :first_name
48
+ t.string :last_name
49
+ t.string :email
50
+ t.index [:last_name, :first_name, :email]
51
+ t.index [:last_name, :first_name],
52
+ unique: true,
53
+ name: "unique_index_on_users_last_name_and_first_name"
54
+ end
55
+
56
+ # Rails 4.2 compatibility - can't be pulled into the block above.
57
+ ActiveRecord::Base.connection.add_index :users, [:last_name, :first_name]
58
+
59
+ assert_problems(<<OUTPUT)
60
+ 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
61
+ OUTPUT
62
+ end
63
+
64
+ def test_not_covered_by_different_index_type
65
+ create_table(:users) do |t|
66
+ t.string :first_name
67
+ t.string :last_name
68
+ t.index [:last_name, :first_name], using: :btree
69
+
70
+ if mysql?
71
+ t.index :last_name, type: :fulltext
72
+ else
73
+ t.index :last_name, using: :hash
74
+ end
75
+ end
76
+
77
+ refute_problems
78
+ end
79
+
80
+ def test_not_covered_by_partial_index
81
+ skip("MySQL doesn't support partial indexes") if mysql?
82
+
83
+ create_table(:users) do |t|
84
+ t.string :first_name
85
+ t.string :last_name
86
+ t.boolean :active
87
+ t.index [:last_name, :first_name], where: "active"
88
+ t.index :last_name
89
+ end
90
+
91
+ refute_problems
92
+ end
93
+
94
+ def test_not_covered_with_different_opclasses
95
+ skip("ActiveRecord < 5.2 doesn't support operator classes") if ActiveRecord::VERSION::STRING < "5.2"
96
+ skip("MySQL doesn't support operator classes") if mysql?
97
+
98
+ create_table(:users) do |t|
99
+ t.string :first_name
100
+ t.string :last_name
101
+ t.index [:last_name, :first_name], opclass: :varchar_pattern_ops
102
+ t.index :last_name
103
+ end
104
+
105
+ refute_problems
106
+ end
107
+
108
+ def test_config_ignore_tables
109
+ # The detector recognizes two kinds of errors and both must take
110
+ # ignore_tables into account. We trigger those errors by indexing the
111
+ # primary key (the first extraneous index) and then indexing email twice
112
+ # (index2... is the other extraneous index).
113
+ create_table(:users) do |t|
114
+ t.index :id
115
+ t.string :email
116
+
117
+ t.index :email, name: "index1_on_users_email"
118
+ t.index :email, name: "index2_on_users_email"
119
+ end
120
+
121
+ config_file(<<-CONFIG)
122
+ ActiveRecordDoctor.configure do |config|
123
+ config.detector :extraneous_indexes,
124
+ ignore_tables: ["users"]
125
+ end
126
+ CONFIG
127
+
128
+ refute_problems
129
+ end
130
+
131
+ def test_config_global_ignore_tables
132
+ create_table(:users) do |t|
133
+ t.index :id
134
+ t.string :email
135
+
136
+ t.index :email, name: "index1_on_users_email"
137
+ t.index :email, name: "index2_on_users_email"
138
+ end
139
+
140
+ config_file(<<-CONFIG)
141
+ ActiveRecordDoctor.configure do |config|
142
+ config.global :ignore_tables, ["users"]
143
+ end
144
+ CONFIG
145
+
146
+ refute_problems
147
+ end
148
+
149
+ def test_config_global_ignore_indexes
150
+ create_table(:users) do |t|
151
+ t.index :id
152
+ t.string :email
153
+
154
+ t.index :email, name: "index1_on_users_email"
155
+ t.index :email, name: "index2_on_users_email"
156
+ end
157
+
158
+ config_file(<<-CONFIG)
159
+ ActiveRecordDoctor.configure do |config|
160
+ config.global :ignore_indexes, [
161
+ "index1_on_users_email",
162
+ "index2_on_users_email",
163
+ "index_users_on_id",
164
+ ]
165
+ end
166
+ CONFIG
167
+
168
+ refute_problems
169
+ end
170
+
171
+ def test_config_detector_ignore_indexes
172
+ create_table(:users) do |t|
173
+ t.index :id
174
+ t.string :email
175
+ t.string :api_key
176
+
177
+ t.index :email, name: "index_on_users_email"
178
+ t.index [:email, :api_key], name: "index_on_users_email_and_api_key"
179
+ end
180
+
181
+ config_file(<<-CONFIG)
182
+ ActiveRecordDoctor.configure do |config|
183
+ config.detector :extraneous_indexes,
184
+ ignore_indexes: ["index_users_on_id", "index_on_users_email"]
185
+ end
186
+ CONFIG
187
+
188
+ refute_problems
189
+ end
190
+ end