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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +83 -19
  3. data/lib/active_record_doctor/config/default.rb +17 -0
  4. data/lib/active_record_doctor/detectors/base.rb +216 -56
  5. data/lib/active_record_doctor/detectors/extraneous_indexes.rb +38 -56
  6. data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +2 -6
  7. data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +88 -15
  8. data/lib/active_record_doctor/detectors/incorrect_length_validation.rb +60 -0
  9. data/lib/active_record_doctor/detectors/mismatched_foreign_key_type.rb +16 -9
  10. data/lib/active_record_doctor/detectors/missing_foreign_keys.rb +2 -4
  11. data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +14 -11
  12. data/lib/active_record_doctor/detectors/missing_presence_validation.rb +16 -10
  13. data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +61 -17
  14. data/lib/active_record_doctor/detectors/short_primary_key_type.rb +6 -2
  15. data/lib/active_record_doctor/detectors/undefined_table_references.rb +2 -4
  16. data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +6 -15
  17. data/lib/active_record_doctor/detectors/unindexed_foreign_keys.rb +2 -4
  18. data/lib/active_record_doctor/logger/dummy.rb +11 -0
  19. data/lib/active_record_doctor/logger/hierarchical.rb +22 -0
  20. data/lib/active_record_doctor/logger.rb +6 -0
  21. data/lib/active_record_doctor/rake/task.rb +10 -1
  22. data/lib/active_record_doctor/runner.rb +8 -3
  23. data/lib/active_record_doctor/version.rb +1 -1
  24. data/lib/active_record_doctor.rb +4 -0
  25. data/lib/generators/active_record_doctor/add_indexes/add_indexes_generator.rb +5 -5
  26. data/test/active_record_doctor/detectors/disable_test.rb +30 -0
  27. data/test/active_record_doctor/detectors/extraneous_indexes_test.rb +34 -0
  28. data/test/active_record_doctor/detectors/incorrect_boolean_presence_validation_test.rb +7 -7
  29. data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +220 -43
  30. data/test/active_record_doctor/detectors/incorrect_length_validation_test.rb +107 -0
  31. data/test/active_record_doctor/detectors/mismatched_foreign_key_type_test.rb +35 -1
  32. data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +78 -21
  33. data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +89 -25
  34. data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +179 -15
  35. data/test/active_record_doctor/detectors/short_primary_key_type_test.rb +27 -19
  36. data/test/active_record_doctor/detectors/undefined_table_references_test.rb +11 -13
  37. data/test/active_record_doctor/detectors/unindexed_deleted_at_test.rb +9 -3
  38. data/test/active_record_doctor/runner_test.rb +18 -19
  39. data/test/setup.rb +15 -7
  40. metadata +25 -5
  41. 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
- tables(except: config(:ignore_tables)).each do |table|
35
- timestamp_columns = connection.columns(table).reject do |column|
36
- config(:ignore_columns).include?("#{table}.#{column.name}")
37
- end.select do |column|
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: timestamp_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
- tables(except: config(:ignore_tables)).each do |table|
29
- connection.columns(table).each do |column|
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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordDoctor
4
+ module Logger
5
+ class Dummy # :nodoc:
6
+ def log(_message)
7
+ yield if block_given?
8
+ end
9
+ end
10
+ end
11
+ end
@@ -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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordDoctor
4
+ module Logger # :nodoc:
5
+ end
6
+ 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, io = $stdout)
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(config, io)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecordDoctor
4
- VERSION = "1.9.0"
4
+ VERSION = "1.11.0"
5
5
  end
@@ -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
- 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}'"
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.create_model do
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 ModelFactory::Models::User.active with `inclusion` - `presence` can't be used on booleans
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.create_model do
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
- create_model(:User)
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.create_model
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.create_model
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.create_model
68
+ end.define_model
69
69
 
70
70
  config_file(<<-CONFIG)
71
71
  ActiveRecordDoctor.configure do |config|