active_record_doctor 1.10.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/active_record_doctor/detectors/base.rb +180 -50
  4. data/lib/active_record_doctor/detectors/extraneous_indexes.rb +24 -27
  5. data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +2 -5
  6. data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +63 -21
  7. data/lib/active_record_doctor/detectors/incorrect_length_validation.rb +7 -10
  8. data/lib/active_record_doctor/detectors/mismatched_foreign_key_type.rb +16 -9
  9. data/lib/active_record_doctor/detectors/missing_foreign_keys.rb +2 -4
  10. data/lib/active_record_doctor/detectors/missing_non_null_constraint.rb +13 -11
  11. data/lib/active_record_doctor/detectors/missing_presence_validation.rb +14 -7
  12. data/lib/active_record_doctor/detectors/missing_unique_indexes.rb +5 -11
  13. data/lib/active_record_doctor/detectors/short_primary_key_type.rb +1 -1
  14. data/lib/active_record_doctor/detectors/undefined_table_references.rb +2 -2
  15. data/lib/active_record_doctor/detectors/unindexed_deleted_at.rb +5 -13
  16. data/lib/active_record_doctor/detectors/unindexed_foreign_keys.rb +2 -4
  17. data/lib/active_record_doctor/logger/dummy.rb +11 -0
  18. data/lib/active_record_doctor/logger/hierarchical.rb +22 -0
  19. data/lib/active_record_doctor/logger.rb +6 -0
  20. data/lib/active_record_doctor/rake/task.rb +10 -1
  21. data/lib/active_record_doctor/runner.rb +8 -3
  22. data/lib/active_record_doctor/version.rb +1 -1
  23. data/lib/active_record_doctor.rb +3 -0
  24. data/test/active_record_doctor/detectors/disable_test.rb +1 -1
  25. data/test/active_record_doctor/detectors/incorrect_boolean_presence_validation_test.rb +7 -7
  26. data/test/active_record_doctor/detectors/incorrect_dependent_option_test.rb +136 -57
  27. data/test/active_record_doctor/detectors/incorrect_length_validation_test.rb +16 -14
  28. data/test/active_record_doctor/detectors/mismatched_foreign_key_type_test.rb +35 -1
  29. data/test/active_record_doctor/detectors/missing_non_null_constraint_test.rb +46 -23
  30. data/test/active_record_doctor/detectors/missing_presence_validation_test.rb +55 -27
  31. data/test/active_record_doctor/detectors/missing_unique_indexes_test.rb +36 -36
  32. data/test/active_record_doctor/detectors/undefined_table_references_test.rb +11 -13
  33. data/test/active_record_doctor/runner_test.rb +18 -19
  34. data/test/setup.rb +10 -6
  35. metadata +19 -4
  36. data/test/model_factory.rb +0 -128
@@ -23,13 +23,10 @@ module ActiveRecordDoctor
23
23
  end
24
24
 
25
25
  def detect
26
- models(except: config(:ignore_models)).each do |model|
27
- next unless model.table_exists?
28
-
29
- connection.columns(model.table_name).each do |column|
26
+ each_model(except: config(:ignore_models), existing_tables_only: true) do |model|
27
+ each_attribute(model, except: config(:ignore_attributes)) do |column|
30
28
  next unless validator_needed?(model, column)
31
29
  next if validator_present?(model, column)
32
- next if config(:ignore_attributes).include?("#{model}.#{column.name}")
33
30
 
34
31
  problem!(column: column.name, model: model.name)
35
32
  end
@@ -52,17 +49,23 @@ module ActiveRecordDoctor
52
49
 
53
50
  def inclusion_validator_present?(model, column)
54
51
  model.validators.any? do |validator|
52
+ validator_items = inclusion_validator_items(validator)
53
+ return true if validator_items.is_a?(Proc)
54
+
55
55
  validator.is_a?(ActiveModel::Validations::InclusionValidator) &&
56
56
  validator.attributes.include?(column.name.to_sym) &&
57
- !validator.options.fetch(:in, []).include?(nil)
57
+ !validator_items.include?(nil)
58
58
  end
59
59
  end
60
60
 
61
61
  def exclusion_validator_present?(model, column)
62
62
  model.validators.any? do |validator|
63
+ validator_items = inclusion_validator_items(validator)
64
+ return true if validator_items.is_a?(Proc)
65
+
63
66
  validator.is_a?(ActiveModel::Validations::ExclusionValidator) &&
64
67
  validator.attributes.include?(column.name.to_sym) &&
65
- validator.options.fetch(:in, []).include?(nil)
68
+ validator_items.include?(nil)
66
69
  end
67
70
  end
68
71
 
@@ -79,6 +82,10 @@ module ActiveRecordDoctor
79
82
  (validator.attributes & allowed_attributes).present?
80
83
  end
81
84
  end
85
+
86
+ def inclusion_validator_items(validator)
87
+ validator.options[:in] || validator.options[:within] || []
88
+ end
82
89
  end
83
90
  end
84
91
  end
@@ -35,9 +35,7 @@ module ActiveRecordDoctor
35
35
  end
36
36
 
37
37
  def validations_without_indexes
38
- models(except: config(:ignore_models)).each do |model|
39
- next unless model.table_exists?
40
-
38
+ each_model(except: config(:ignore_models), existing_tables_only: true) do |model|
41
39
  model.validators.each do |validator|
42
40
  scope = Array(validator.options.fetch(:scope, []))
43
41
 
@@ -57,18 +55,14 @@ module ActiveRecordDoctor
57
55
  end
58
56
 
59
57
  def has_ones_without_indexes # rubocop:disable Naming/PredicateName
60
- models.each do |model|
61
- has_ones = model.reflect_on_all_associations(:has_one)
62
- has_ones.each do |has_one|
63
- next if has_one.is_a?(ActiveRecord::Reflection::ThroughReflection) || has_one.scope
64
-
65
- association_model = has_one.klass
66
- next if config(:ignore_models).include?(association_model.name)
58
+ each_model do |model|
59
+ each_association(model, type: :has_one, has_scope: false, through: false) do |has_one|
60
+ next if config(:ignore_models).include?(has_one.klass.name)
67
61
 
68
62
  foreign_key = has_one.foreign_key
69
63
  next if ignore_columns.include?(foreign_key.to_s)
70
64
 
71
- table_name = association_model.table_name
65
+ table_name = has_one.klass.table_name
72
66
  next if unique_index?(table_name, [foreign_key])
73
67
 
74
68
  problem!(model: model, table: table_name, columns: [foreign_key], problem: :has_ones)
@@ -20,7 +20,7 @@ module ActiveRecordDoctor
20
20
  end
21
21
 
22
22
  def detect
23
- tables(except: config(:ignore_tables)).each do |table|
23
+ each_table(except: config(:ignore_tables)) do |table|
24
24
  column = primary_key(table)
25
25
  next if column.nil?
26
26
  next if bigint?(column) || uuid?(column)
@@ -20,8 +20,8 @@ module ActiveRecordDoctor
20
20
  end
21
21
 
22
22
  def detect
23
- models(except: config(:ignore_models)).each do |model|
24
- next if model.table_exists? || views.include?(model.table_name)
23
+ each_model(except: config(:ignore_models), abstract: false) do |model|
24
+ next if connection.data_source_exists?(model.table_name)
25
25
 
26
26
  problem!(model: model.name, table: model.table_name)
27
27
  end
@@ -31,20 +31,12 @@ module ActiveRecordDoctor
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
- next if index.where =~ /\b#{timestamp_column.name}\s+IS\s+(NOT\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
46
38
 
47
- problem!(index: index.name, column_name: timestamp_column.name)
39
+ problem!(index: index.name, column_name: column.name)
48
40
  end
49
41
  end
50
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.10.0"
4
+ VERSION = "1.11.0"
5
5
  end
@@ -1,6 +1,9 @@
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"
@@ -6,7 +6,7 @@ class ActiveRecordDoctor::Detectors::DisableTest < Minitest::Test
6
6
  def test_disabling
7
7
  create_table(:users) do |t|
8
8
  t.string :name, null: true
9
- end.create_model do
9
+ end.define_model do
10
10
  validates :name, presence: true
11
11
  end
12
12
 
@@ -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|