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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/active_record_doctor/detectors/base.rb +180 -50
- data/lib/active_record_doctor/detectors/extraneous_indexes.rb +24 -27
- data/lib/active_record_doctor/detectors/incorrect_boolean_presence_validation.rb +2 -5
- data/lib/active_record_doctor/detectors/incorrect_dependent_option.rb +63 -21
- 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 +5 -11
- data/lib/active_record_doctor/detectors/short_primary_key_type.rb +1 -1
- 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 +2 -4
- 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/version.rb +1 -1
- data/lib/active_record_doctor.rb +3 -0
- data/test/active_record_doctor/detectors/disable_test.rb +1 -1
- 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 +136 -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 +36 -36
- data/test/active_record_doctor/detectors/undefined_table_references_test.rb +11 -13
- data/test/active_record_doctor/runner_test.rb +18 -19
- data/test/setup.rb +10 -6
- metadata +19 -4
- data/test/model_factory.rb +0 -128
@@ -23,13 +23,10 @@ module ActiveRecordDoctor
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def detect
|
26
|
-
|
27
|
-
|
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
|
-
!
|
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
|
-
|
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
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
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 =
|
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
|
-
|
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
|
-
|
24
|
-
next if
|
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
|
-
|
35
|
-
|
36
|
-
config(:
|
37
|
-
|
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:
|
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
|
-
|
29
|
-
|
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,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
|
data/lib/active_record_doctor.rb
CHANGED
@@ -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"
|
@@ -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|
|