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
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecordDoctor # :nodoc:
|
4
|
+
class << self
|
5
|
+
# The config file that's currently being processed by .load_config.
|
6
|
+
attr_reader :current_config
|
7
|
+
|
8
|
+
# This method is part of the public API that is intended for use by
|
9
|
+
# active_record_doctor users. The remaining methods are considered to be
|
10
|
+
# public-not-published.
|
11
|
+
def configure(&block)
|
12
|
+
# If current_config is set it means that .configure was already called
|
13
|
+
# so we must raise an error.
|
14
|
+
raise ActiveRecordDoctor::Error::ConfigureCalledTwice if current_config
|
15
|
+
|
16
|
+
# Determine the recognized global and detector settings based on detector
|
17
|
+
# metadata. recognizedd_detectors maps detector names to setting names.
|
18
|
+
# recognized_globals contains global setting names.
|
19
|
+
recognized_detectors = {}
|
20
|
+
recognized_globals = []
|
21
|
+
|
22
|
+
ActiveRecordDoctor.detectors.each do |name, detector|
|
23
|
+
locals, globals = detector.locals_and_globals
|
24
|
+
|
25
|
+
recognized_detectors[name] = locals
|
26
|
+
recognized_globals.concat(globals)
|
27
|
+
end
|
28
|
+
|
29
|
+
# The same global can be used by multiple detectors so we must remove
|
30
|
+
# duplicates to ensure they aren't reported mutliple times via the user
|
31
|
+
# interface (e.g. in error messages).
|
32
|
+
recognized_globals.uniq!
|
33
|
+
|
34
|
+
# Prepare an empty configuration and call the loader. After .new returns
|
35
|
+
# @current_config will contain the configuration provided by the block.
|
36
|
+
@current_config = Config.new({}, {})
|
37
|
+
Loader.new(current_config, recognized_globals, recognized_detectors, &block)
|
38
|
+
|
39
|
+
# This method is part of the public API expected to be called by users.
|
40
|
+
# In order to avoid leaking internal objects, we return an explicit nil.
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def load_config(path)
|
45
|
+
begin
|
46
|
+
load(path)
|
47
|
+
rescue ActiveRecordDoctor::Error
|
48
|
+
raise
|
49
|
+
rescue LoadError
|
50
|
+
raise ActiveRecordDoctor::Error::ConfigurationFileMissing
|
51
|
+
rescue StandardError => e
|
52
|
+
raise ActiveRecordDoctor::Error::ConfigurationError[e]
|
53
|
+
end
|
54
|
+
raise ActiveRecordDoctor::Error::ConfigureNotCalled if current_config.nil?
|
55
|
+
|
56
|
+
# Store the configuration and reset @current_config. We cannot reset
|
57
|
+
# @current_config in .configure because that would prevent us from
|
58
|
+
# detecting multiple calls to that method.
|
59
|
+
config = @current_config
|
60
|
+
@current_config = nil
|
61
|
+
|
62
|
+
config
|
63
|
+
rescue ActiveRecordDoctor::Error => e
|
64
|
+
e.config_path = path
|
65
|
+
raise e
|
66
|
+
end
|
67
|
+
|
68
|
+
DEFAULT_CONFIG_PATH = File.join(__dir__, "default.rb").freeze
|
69
|
+
private_constant :DEFAULT_CONFIG_PATH
|
70
|
+
|
71
|
+
def load_config_with_defaults(path)
|
72
|
+
default_config = load_config(DEFAULT_CONFIG_PATH)
|
73
|
+
return default_config if path.nil?
|
74
|
+
|
75
|
+
config = load_config(path)
|
76
|
+
default_config.merge(config)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# A class used for loading user-provided configuration files.
|
81
|
+
class Loader
|
82
|
+
def initialize(config, recognized_globals, recognized_detectors, &block)
|
83
|
+
@config = config
|
84
|
+
@recognized_globals = recognized_globals
|
85
|
+
@recognized_detectors = recognized_detectors
|
86
|
+
instance_eval(&block)
|
87
|
+
end
|
88
|
+
|
89
|
+
def global(name, value)
|
90
|
+
name = name.to_sym
|
91
|
+
|
92
|
+
unless recognized_globals.include?(name)
|
93
|
+
raise ActiveRecordDoctor::Error::UnrecognizedGlobalSetting[
|
94
|
+
name,
|
95
|
+
recognized_globals
|
96
|
+
]
|
97
|
+
end
|
98
|
+
|
99
|
+
if config.globals.include?(name)
|
100
|
+
raise ActiveRecordDoctor::Error::DuplicateGlobalSetting[name]
|
101
|
+
end
|
102
|
+
|
103
|
+
config.globals[name] = value
|
104
|
+
end
|
105
|
+
|
106
|
+
def detector(name, settings)
|
107
|
+
name = name.to_sym
|
108
|
+
|
109
|
+
recognized_settings = recognized_detectors[name]
|
110
|
+
if recognized_settings.nil?
|
111
|
+
raise ActiveRecordDoctor::Error::UnrecognizedDetectorName[
|
112
|
+
name,
|
113
|
+
recognized_detectors.keys
|
114
|
+
]
|
115
|
+
end
|
116
|
+
|
117
|
+
if config.detectors.include?(name)
|
118
|
+
raise ActiveRecordDoctor::Error::DetectorConfiguredTwice[name]
|
119
|
+
end
|
120
|
+
|
121
|
+
unrecognized_settings = settings.keys - recognized_settings
|
122
|
+
unless unrecognized_settings.empty?
|
123
|
+
raise ActiveRecordDoctor::Error::UnrecognizedDetectorSettings[
|
124
|
+
name,
|
125
|
+
unrecognized_settings,
|
126
|
+
recognized_settings
|
127
|
+
]
|
128
|
+
end
|
129
|
+
|
130
|
+
config.detectors[name] = settings
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
attr_reader :config, :recognized_globals, :recognized_detectors
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecordDoctor # :nodoc:
|
4
|
+
Config = Struct.new(:globals, :detectors) do
|
5
|
+
def merge(config)
|
6
|
+
globals = self.globals.merge(config.globals)
|
7
|
+
detectors = self.detectors.merge(config.detectors) do |_name, self_settings, config_settings|
|
8
|
+
self_settings.merge(config_settings)
|
9
|
+
end
|
10
|
+
|
11
|
+
Config.new(globals, detectors)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -4,60 +4,181 @@ module ActiveRecordDoctor
|
|
4
4
|
module Detectors
|
5
5
|
# Base class for all active_record_doctor detectors.
|
6
6
|
class Base
|
7
|
+
BASE_CONFIG = {
|
8
|
+
enabled: {
|
9
|
+
description: "set to false to disable the detector altogether"
|
10
|
+
}
|
11
|
+
}.freeze
|
12
|
+
|
7
13
|
class << self
|
8
14
|
attr_reader :description
|
9
15
|
|
10
|
-
def run
|
11
|
-
new.run
|
16
|
+
def run(config, io)
|
17
|
+
new(config, io).run
|
18
|
+
end
|
19
|
+
|
20
|
+
def underscored_name
|
21
|
+
name.demodulize.underscore.to_sym
|
22
|
+
end
|
23
|
+
|
24
|
+
def config
|
25
|
+
@config.merge(BASE_CONFIG)
|
26
|
+
end
|
27
|
+
|
28
|
+
def locals_and_globals
|
29
|
+
locals = []
|
30
|
+
globals = []
|
31
|
+
|
32
|
+
config.each do |key, metadata|
|
33
|
+
locals << key
|
34
|
+
globals << key if metadata[:global]
|
35
|
+
end
|
36
|
+
|
37
|
+
[locals, globals]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize(config, io)
|
42
|
+
@problems = []
|
43
|
+
@config = config
|
44
|
+
@io = io
|
45
|
+
end
|
46
|
+
|
47
|
+
def run
|
48
|
+
@problems = []
|
49
|
+
|
50
|
+
detect if config(:enabled)
|
51
|
+
@problems.each do |problem|
|
52
|
+
@io.puts(message(**problem))
|
12
53
|
end
|
54
|
+
|
55
|
+
success = @problems.empty?
|
56
|
+
@problems = nil
|
57
|
+
success
|
13
58
|
end
|
14
59
|
|
15
60
|
private
|
16
61
|
|
17
|
-
def
|
18
|
-
|
62
|
+
def config(key)
|
63
|
+
local = @config.detectors.fetch(underscored_name).fetch(key)
|
64
|
+
return local if !self.class.config.fetch(key)[:global]
|
65
|
+
|
66
|
+
global = @config.globals[key]
|
67
|
+
return local if global.nil?
|
68
|
+
|
69
|
+
# Right now, all globals are arrays so we can merge them here. Once
|
70
|
+
# we add non-array globals we'll need to support per-global merging.
|
71
|
+
Array.new(local).concat(global)
|
72
|
+
end
|
73
|
+
|
74
|
+
def detect
|
75
|
+
raise("#detect should be implemented by a subclass")
|
76
|
+
end
|
77
|
+
|
78
|
+
def message(**_attrs)
|
79
|
+
raise("#message should be implemented by a subclass")
|
80
|
+
end
|
81
|
+
|
82
|
+
def problem!(**attrs)
|
83
|
+
@problems << attrs
|
84
|
+
end
|
85
|
+
|
86
|
+
def warning(message)
|
87
|
+
puts(message)
|
19
88
|
end
|
20
89
|
|
21
90
|
def connection
|
22
91
|
@connection ||= ActiveRecord::Base.connection
|
23
92
|
end
|
24
93
|
|
25
|
-
def indexes(table_name)
|
26
|
-
connection.indexes(table_name)
|
94
|
+
def indexes(table_name, except: [])
|
95
|
+
connection.indexes(table_name).reject do |index|
|
96
|
+
except.include?(index.name)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def tables(except: [])
|
101
|
+
tables =
|
102
|
+
if ActiveRecord::VERSION::STRING >= "5.1"
|
103
|
+
connection.tables
|
104
|
+
else
|
105
|
+
connection.data_sources
|
106
|
+
end
|
107
|
+
|
108
|
+
tables.reject do |table|
|
109
|
+
except.include?(table)
|
110
|
+
end
|
27
111
|
end
|
28
112
|
|
29
|
-
def
|
30
|
-
connection.
|
113
|
+
def primary_key(table_name)
|
114
|
+
primary_key_name = connection.primary_key(table_name)
|
115
|
+
return nil if primary_key_name.nil?
|
116
|
+
|
117
|
+
column(table_name, primary_key_name)
|
31
118
|
end
|
32
119
|
|
33
|
-
def
|
34
|
-
connection.
|
120
|
+
def column(table_name, column_name)
|
121
|
+
connection.columns(table_name).find { |column| column.name == column_name }
|
35
122
|
end
|
36
123
|
|
37
124
|
def views
|
38
125
|
@views ||=
|
39
126
|
if connection.respond_to?(:views)
|
40
127
|
connection.views
|
41
|
-
elsif
|
42
|
-
ActiveRecord::Base.connection.
|
43
|
-
SELECT
|
128
|
+
elsif postgresql?
|
129
|
+
ActiveRecord::Base.connection.select_values(<<-SQL)
|
130
|
+
SELECT relname FROM pg_class WHERE relkind IN ('m', 'v')
|
131
|
+
SQL
|
132
|
+
elsif connection.adapter_name == "Mysql2"
|
133
|
+
ActiveRecord::Base.connection.select_values(<<-SQL)
|
134
|
+
SHOW FULL TABLES WHERE table_type = 'VIEW'
|
44
135
|
SQL
|
45
|
-
else
|
136
|
+
else
|
46
137
|
# We don't support this Rails/database combination yet.
|
47
|
-
|
138
|
+
[]
|
48
139
|
end
|
49
140
|
end
|
50
141
|
|
51
|
-
def
|
52
|
-
|
142
|
+
def not_null_check_constraint_exists?(table, column)
|
143
|
+
check_constraints(table).any? do |definition|
|
144
|
+
definition =~ /\A#{column.name} IS NOT NULL\z/i ||
|
145
|
+
definition =~ /\A#{connection.quote_column_name(column.name)} IS NOT NULL\z/i
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def check_constraints(table_name)
|
150
|
+
# ActiveRecord 6.1+
|
151
|
+
if connection.respond_to?(:supports_check_constraints?) && connection.supports_check_constraints?
|
152
|
+
connection.check_constraints(table_name).select(&:validated?).map(&:expression)
|
153
|
+
elsif postgresql?
|
154
|
+
definitions =
|
155
|
+
connection.select_values(<<-SQL)
|
156
|
+
SELECT pg_get_constraintdef(oid, true)
|
157
|
+
FROM pg_constraint
|
158
|
+
WHERE contype = 'c'
|
159
|
+
AND convalidated
|
160
|
+
AND conrelid = #{connection.quote(table_name)}::regclass
|
161
|
+
SQL
|
162
|
+
|
163
|
+
definitions.map { |definition| definition[/CHECK \((.+)\)/m, 1] }
|
164
|
+
else
|
165
|
+
# We don't support this Rails/database combination yet.
|
166
|
+
[]
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def models(except: [])
|
171
|
+
ActiveRecord::Base.descendants.reject do |model|
|
172
|
+
model.name.start_with?("HABTM_") || except.include?(model.name)
|
173
|
+
end
|
53
174
|
end
|
54
175
|
|
55
|
-
def
|
56
|
-
|
176
|
+
def underscored_name
|
177
|
+
self.class.underscored_name
|
57
178
|
end
|
58
179
|
|
59
|
-
def
|
60
|
-
|
180
|
+
def postgresql?
|
181
|
+
["PostgreSQL", "PostGIS"].include?(connection.adapter_name)
|
61
182
|
end
|
62
183
|
end
|
63
184
|
end
|
@@ -4,77 +4,88 @@ require "active_record_doctor/detectors/base"
|
|
4
4
|
|
5
5
|
module ActiveRecordDoctor
|
6
6
|
module Detectors
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
class ExtraneousIndexes < Base # :nodoc:
|
8
|
+
@description = "identify indexes that can be dropped without degrading performance"
|
9
|
+
@config = {
|
10
|
+
ignore_tables: {
|
11
|
+
description: "tables whose indexes should never be reported as extraneous",
|
12
|
+
global: true
|
13
|
+
},
|
14
|
+
ignore_indexes: {
|
15
|
+
description: "indexes that should never be reported as extraneous",
|
16
|
+
global: true
|
17
|
+
}
|
18
|
+
}
|
11
19
|
|
12
|
-
|
13
|
-
|
20
|
+
private
|
21
|
+
|
22
|
+
def message(extraneous_index:, replacement_indexes:)
|
23
|
+
if replacement_indexes.nil?
|
24
|
+
"remove #{extraneous_index} - coincides with the primary key on the table"
|
25
|
+
else
|
26
|
+
"remove #{extraneous_index} - can be replaced by #{replacement_indexes.join(' or ')}"
|
27
|
+
end
|
14
28
|
end
|
15
29
|
|
16
|
-
|
30
|
+
def detect
|
31
|
+
subindexes_of_multi_column_indexes
|
32
|
+
indexed_primary_keys
|
33
|
+
end
|
17
34
|
|
18
35
|
def subindexes_of_multi_column_indexes
|
19
|
-
tables.
|
20
|
-
table == "schema_migrations"
|
21
|
-
end.flat_map do |table|
|
36
|
+
tables(except: config(:ignore_tables)).each do |table|
|
22
37
|
indexes = indexes(table)
|
23
|
-
maximum_indexes = indexes.select do |index|
|
24
|
-
maximal?(indexes, index)
|
25
|
-
end
|
26
38
|
|
27
|
-
indexes.
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
+
indexes.each do |index|
|
40
|
+
next if config(:ignore_indexes).include?(index.name)
|
41
|
+
|
42
|
+
replacement_indexes = indexes.select do |other_index|
|
43
|
+
index != other_index && replaceable_with?(index, other_index)
|
44
|
+
end
|
45
|
+
|
46
|
+
next if replacement_indexes.empty?
|
47
|
+
|
48
|
+
problem!(
|
49
|
+
extraneous_index: index.name,
|
50
|
+
replacement_indexes: replacement_indexes.map(&:name).sort
|
51
|
+
)
|
39
52
|
end
|
40
53
|
end
|
41
54
|
end
|
42
55
|
|
43
56
|
def indexed_primary_keys
|
44
|
-
|
45
|
-
table
|
46
|
-
|
47
|
-
|
48
|
-
table
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
]
|
53
|
-
end.flat_map do |table, indexes|
|
54
|
-
indexes.map do |index|
|
55
|
-
[index.name, [:primary_key, table]]
|
57
|
+
tables(except: config(:ignore_tables)).each do |table|
|
58
|
+
indexes(table).each do |index|
|
59
|
+
next if config(:ignore_indexes).include?(index.name)
|
60
|
+
|
61
|
+
primary_key = connection.primary_key(table)
|
62
|
+
next if index.columns != [primary_key] || index.where
|
63
|
+
|
64
|
+
problem!(extraneous_index: index.name, replacement_indexes: nil)
|
56
65
|
end
|
57
66
|
end
|
58
67
|
end
|
59
68
|
|
60
|
-
def
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
69
|
+
def replaceable_with?(index1, index2)
|
70
|
+
return false if index1.type != index2.type
|
71
|
+
return false if index1.using != index2.using
|
72
|
+
return false if index1.where != index2.where
|
73
|
+
return false if opclasses(index1) != opclasses(index2)
|
65
74
|
|
66
|
-
|
67
|
-
def cover?(lhs, rhs)
|
68
|
-
case [lhs.unique, rhs.unique]
|
75
|
+
case [index1.unique, index2.unique]
|
69
76
|
when [true, true]
|
70
|
-
|
71
|
-
when [
|
77
|
+
(index2.columns - index1.columns).empty?
|
78
|
+
when [true, false]
|
72
79
|
false
|
73
80
|
else
|
74
|
-
prefix?(
|
81
|
+
prefix?(index1, index2)
|
75
82
|
end
|
76
83
|
end
|
77
84
|
|
85
|
+
def opclasses(index)
|
86
|
+
index.respond_to?(:opclasses) ? index.opclasses : nil
|
87
|
+
end
|
88
|
+
|
78
89
|
def prefix?(lhs, rhs)
|
79
90
|
lhs.columns.count <= rhs.columns.count &&
|
80
91
|
rhs.columns[0...lhs.columns.count] == lhs.columns
|
@@ -4,32 +4,40 @@ require "active_record_doctor/detectors/base"
|
|
4
4
|
|
5
5
|
module ActiveRecordDoctor
|
6
6
|
module Detectors
|
7
|
-
|
8
|
-
|
9
|
-
@
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
end.map do |model|
|
19
|
-
[
|
20
|
-
model.name,
|
21
|
-
connection.columns(model.table_name).select do |column|
|
22
|
-
column.type == :boolean &&
|
23
|
-
has_presence_validator?(model, column)
|
24
|
-
end.map(&:name)
|
25
|
-
]
|
26
|
-
end.reject do |_model_name, columns|
|
27
|
-
columns.empty?
|
28
|
-
end))
|
29
|
-
end
|
7
|
+
class IncorrectBooleanPresenceValidation < Base # :nodoc:
|
8
|
+
@description = "detect persence (instead of inclusion) validators on boolean columns"
|
9
|
+
@config = {
|
10
|
+
ignore_models: {
|
11
|
+
description: "models whose validators should not be checked",
|
12
|
+
global: true
|
13
|
+
},
|
14
|
+
ignore_attributes: {
|
15
|
+
description: "attributes, written as Model.attribute, whose validators should not be checked"
|
16
|
+
}
|
17
|
+
}
|
30
18
|
|
31
19
|
private
|
32
20
|
|
21
|
+
def message(model:, attribute:)
|
22
|
+
# rubocop:disable Layout/LineLength
|
23
|
+
"replace the `presence` validator on #{model}.#{attribute} with `inclusion` - `presence` can't be used on booleans"
|
24
|
+
# rubocop:enable Layout/LineLength
|
25
|
+
end
|
26
|
+
|
27
|
+
def detect
|
28
|
+
models(except: config(:ignore_models)).each do |model|
|
29
|
+
next unless model.table_exists?
|
30
|
+
|
31
|
+
connection.columns(model.table_name).each do |column|
|
32
|
+
next if config(:ignore_attributes).include?("#{model.name}.#{column.name}")
|
33
|
+
next unless column.type == :boolean
|
34
|
+
next unless has_presence_validator?(model, column)
|
35
|
+
|
36
|
+
problem!(model: model.name, attribute: column.name)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
33
41
|
def has_presence_validator?(model, column)
|
34
42
|
model.validators.any? do |validator|
|
35
43
|
validator.kind == :presence && validator.attributes.include?(column.name.to_sym)
|