database_consistency 2.0.4 → 3.0.5

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/bin/database_consistency +19 -2
  3. data/lib/database_consistency/checkers/association_checkers/association_checker.rb +11 -0
  4. data/lib/database_consistency/checkers/association_checkers/foreign_key_cascade_checker.rb +5 -4
  5. data/lib/database_consistency/checkers/association_checkers/foreign_key_checker.rb +1 -1
  6. data/lib/database_consistency/checkers/association_checkers/foreign_key_type_checker.rb +46 -35
  7. data/lib/database_consistency/checkers/association_checkers/missing_dependent_destroy_checker.rb +53 -0
  8. data/lib/database_consistency/checkers/base_checker.rb +1 -2
  9. data/lib/database_consistency/checkers/column_checkers/column_checker.rb +4 -0
  10. data/lib/database_consistency/checkers/column_checkers/missing_index_find_by_checker.rb +192 -0
  11. data/lib/database_consistency/checkers/column_checkers/null_constraint_checker.rb +1 -1
  12. data/lib/database_consistency/checkers/enum_checkers/enum_checker.rb +4 -0
  13. data/lib/database_consistency/checkers/index_checkers/index_checker.rb +4 -0
  14. data/lib/database_consistency/checkers/index_checkers/redundant_index_checker.rb +2 -2
  15. data/lib/database_consistency/checkers/index_checkers/unique_index_checker.rb +7 -4
  16. data/lib/database_consistency/checkers/model_checkers/model_checker.rb +4 -0
  17. data/lib/database_consistency/checkers/model_checkers/view_primary_key_checker.rb +36 -0
  18. data/lib/database_consistency/checkers/validator_checkers/missing_unique_index_checker.rb +16 -4
  19. data/lib/database_consistency/checkers/validator_checkers/validator_checker.rb +4 -0
  20. data/lib/database_consistency/checkers/validators_fraction_checkers/validators_fraction_checker.rb +4 -0
  21. data/lib/database_consistency/configuration.rb +8 -7
  22. data/lib/database_consistency/debug_context.rb +0 -1
  23. data/lib/database_consistency/files_helper.rb +53 -0
  24. data/lib/database_consistency/helper.rb +215 -2
  25. data/lib/database_consistency/prism_helper.rb +38 -0
  26. data/lib/database_consistency/rescue_error.rb +3 -3
  27. data/lib/database_consistency/version.rb +1 -1
  28. data/lib/database_consistency/writers/autofix/migration_base.rb +1 -1
  29. data/lib/database_consistency/writers/autofix_writer.rb +32 -1
  30. data/lib/database_consistency/writers/base_writer.rb +5 -4
  31. data/lib/database_consistency/writers/simple/association_foreign_type_missing_null_constraint.rb +1 -1
  32. data/lib/database_consistency/writers/simple/association_missing_index.rb +1 -1
  33. data/lib/database_consistency/writers/simple/association_missing_null_constraint.rb +1 -1
  34. data/lib/database_consistency/writers/simple/enum_values_inconsistent_with_ar_enum.rb +1 -1
  35. data/lib/database_consistency/writers/simple/enum_values_inconsistent_with_inclusion.rb +1 -1
  36. data/lib/database_consistency/writers/simple/has_one_missing_unique_index.rb +1 -1
  37. data/lib/database_consistency/writers/simple/implicit_order_column_missing.rb +1 -1
  38. data/lib/database_consistency/writers/simple/inconsistent_enum_type.rb +1 -1
  39. data/lib/database_consistency/writers/simple/inconsistent_types.rb +2 -2
  40. data/lib/database_consistency/writers/simple/length_validator_greater_limit.rb +1 -1
  41. data/lib/database_consistency/writers/simple/length_validator_lower_limit.rb +1 -1
  42. data/lib/database_consistency/writers/simple/length_validator_missing.rb +1 -1
  43. data/lib/database_consistency/writers/simple/missing_association_class.rb +1 -1
  44. data/lib/database_consistency/writers/simple/missing_dependent_destroy.rb +22 -0
  45. data/lib/database_consistency/writers/simple/missing_foreign_key.rb +1 -1
  46. data/lib/database_consistency/writers/simple/missing_foreign_key_cascade.rb +1 -1
  47. data/lib/database_consistency/writers/simple/missing_index_find_by.rb +32 -0
  48. data/lib/database_consistency/writers/simple/missing_table.rb +1 -1
  49. data/lib/database_consistency/writers/simple/missing_unique_index.rb +1 -1
  50. data/lib/database_consistency/writers/simple/missing_uniqueness_validation.rb +1 -1
  51. data/lib/database_consistency/writers/simple/null_constraint_association_misses_validator.rb +1 -1
  52. data/lib/database_consistency/writers/simple/null_constraint_misses_validator.rb +1 -1
  53. data/lib/database_consistency/writers/simple/null_constraint_missing.rb +1 -1
  54. data/lib/database_consistency/writers/simple/possible_null.rb +1 -1
  55. data/lib/database_consistency/writers/simple/redundant_case_insensitive_option.rb +1 -1
  56. data/lib/database_consistency/writers/simple/redundant_index.rb +1 -1
  57. data/lib/database_consistency/writers/simple/redundant_unique_index.rb +1 -1
  58. data/lib/database_consistency/writers/simple/small_primary_key.rb +1 -1
  59. data/lib/database_consistency/writers/simple/three_state_boolean.rb +1 -1
  60. data/lib/database_consistency/writers/simple/view_missing_primary_key.rb +21 -0
  61. data/lib/database_consistency/writers/simple/view_primary_key_column_missing.rb +21 -0
  62. data/lib/database_consistency.rb +14 -7
  63. metadata +13 -4
@@ -6,6 +6,10 @@ module DatabaseConsistency
6
6
  class ValidatorChecker < BaseChecker
7
7
  attr_reader :model, :attribute, :validator
8
8
 
9
+ def self.processor
10
+ Processors::ValidatorsProcessor
11
+ end
12
+
9
13
  def initialize(model, attribute, validator)
10
14
  super()
11
15
  @model = model
@@ -6,6 +6,10 @@ module DatabaseConsistency
6
6
  class ValidatorsFractionChecker < BaseChecker
7
7
  attr_reader :model, :attribute, :validators
8
8
 
9
+ def self.processor
10
+ Processors::ValidatorsFractionsProcessor
11
+ end
12
+
9
13
  def initialize(model, attribute, validators)
10
14
  super()
11
15
  @model = model
@@ -8,15 +8,16 @@ module DatabaseConsistency
8
8
  DEFAULT_PATH = '.database_consistency.yml'
9
9
 
10
10
  def initialize(file_paths = DEFAULT_PATH)
11
- @configuration = existing_configurations(file_paths).then do |existing_paths|
12
- if existing_paths.any?
13
- puts "Loaded configurations: #{existing_paths.join(', ')}"
14
- else
15
- puts 'No configurations were provided'
16
- end
17
- extract_configurations(existing_paths)
11
+ existing_paths = existing_configurations(file_paths)
12
+
13
+ if existing_paths.any?
14
+ puts "Loaded configurations: #{existing_paths.join(', ')}"
15
+ else
16
+ puts 'No configuration files were provided'
18
17
  end
19
18
 
19
+ @configuration = extract_configurations(existing_paths)
20
+
20
21
  load_custom_checkers
21
22
  end
22
23
 
@@ -30,7 +30,6 @@ module DatabaseConsistency
30
30
  store.each do |key, value|
31
31
  destination.puts("#{key}: #{value}")
32
32
  end
33
- clear!
34
33
  end
35
34
 
36
35
  private
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabaseConsistency
4
+ # The module contains file system helper methods for locating project source files.
5
+ module FilesHelper
6
+ module_function
7
+
8
+ # Returns all unique project source file paths (non-gem Ruby files from loaded constants).
9
+ # Memoized so the file system walk happens once per database_consistency run.
10
+ def project_source_files
11
+ @project_source_files ||=
12
+ if Module.respond_to?(:const_source_location)
13
+ collect_source_files
14
+ else
15
+ []
16
+ end
17
+ end
18
+
19
+ def collect_source_files
20
+ files = []
21
+ ObjectSpace.each_object(Module) { |mod| files << source_file_path(mod) }
22
+ files.compact.uniq
23
+ end
24
+
25
+ def source_file_path(mod)
26
+ name = mod.name
27
+ return unless name
28
+
29
+ file, = Module.const_source_location(name)
30
+ return unless file && File.exist?(file)
31
+ return if excluded_source_file?(file)
32
+
33
+ file
34
+ rescue StandardError, ScriptError
35
+ nil
36
+ end
37
+
38
+ def excluded_source_file?(file)
39
+ return true if defined?(Bundler) && file.include?(Bundler.bundle_path.to_s)
40
+ return true if defined?(Gem) && file.include?(Gem::RUBYGEMS_DIR)
41
+
42
+ excluded_by_ruby_stdlib?(file)
43
+ end
44
+
45
+ def excluded_by_ruby_stdlib?(file)
46
+ return false unless defined?(RbConfig)
47
+
48
+ file.include?(RbConfig::CONFIG['rubylibdir']) ||
49
+ file.include?(RbConfig::CONFIG['bindir']) ||
50
+ file.include?(RbConfig::CONFIG['sbindir'])
51
+ end
52
+ end
53
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module DatabaseConsistency
4
4
  # The module contains helper methods
5
- module Helper
5
+ module Helper # rubocop:disable Metrics/ModuleLength
6
6
  module_function
7
7
 
8
8
  def adapter
@@ -49,7 +49,7 @@ module DatabaseConsistency
49
49
  def connected?(klass)
50
50
  klass.connection
51
51
  rescue ActiveRecord::ConnectionNotEstablished
52
- puts "#{klass} doesn't have active connection: ignoring"
52
+ puts "#{klass} does not have an active connection, skipping"
53
53
  false
54
54
  end
55
55
 
@@ -112,10 +112,223 @@ module DatabaseConsistency
112
112
  end
113
113
  end
114
114
 
115
+ def inclusion_validator_values(validator)
116
+ value = validator.options[:in]
117
+
118
+ if value.is_a?(Proc) && value.arity.zero?
119
+ value.call
120
+ else
121
+ Array.wrap(value)
122
+ end
123
+ end
124
+
125
+ def btree_index?(index)
126
+ (index.type.nil? || index.type.to_s == 'btree') &&
127
+ (index.using.nil? || index.using.to_s == 'btree')
128
+ end
129
+
130
+ def extract_columns(str)
131
+ case str
132
+ when Array
133
+ str.map(&:to_s)
134
+ when String
135
+ str.scan(/(\w+)/).flatten
136
+ when Symbol
137
+ [str.to_s]
138
+ else
139
+ raise "Unexpected type for columns: #{str.class} with value: #{str}"
140
+ end
141
+ end
142
+
115
143
  def foreign_key_or_attribute(model, attribute)
116
144
  model._reflect_on_association(attribute)&.foreign_key || attribute
117
145
  end
118
146
 
147
+ # Returns the normalized WHERE SQL produced by a conditions proc, or nil if
148
+ # it cannot be determined (complex proc, unsupported AR version, etc.).
149
+ def conditions_where_sql(model, conditions)
150
+ sql = model.unscoped.instance_exec(&conditions).to_sql
151
+ where_part = sql.split(/\bWHERE\b/i, 2).last
152
+ return nil unless where_part
153
+
154
+ normalize_condition_sql(where_part.gsub("#{model.quoted_table_name}.", '').gsub('"', ''))
155
+ rescue StandardError
156
+ nil
157
+ end
158
+
159
+ # Builds the effective uniqueness constraint enforced by a validator by
160
+ # combining its explicit `conditions` proc with implicit guards such as
161
+ # `allow_nil` / `allow_blank`.
162
+ def uniqueness_validator_where_sql(model, attribute, validator)
163
+ conditions_sql = conditions_where_sql(model, validator.options[:conditions])
164
+ guard_sql = uniqueness_validator_guard_sql(model, attribute, validator)
165
+
166
+ sql_parts = [conditions_sql, guard_sql].reject { |part| part.nil? || part == '' }
167
+ return nil if sql_parts.empty?
168
+
169
+ normalize_condition_sql(sql_parts.join(' AND '))
170
+ end
171
+
172
+ # Returns true when validator conditions and index WHERE clause are a valid
173
+ # pairing: both absent means a match; exactly one present means no match;
174
+ # when both present the normalized SQL is compared.
175
+ def conditions_match_index?(model, attribute, validator, index_where)
176
+ validator_where = uniqueness_validator_where_sql(model, attribute, validator)
177
+ return true if validator_where.nil? && index_where.blank?
178
+ return true if index_where.blank? && validator_guard_only?(model, attribute, validator)
179
+ return false if validator_where.nil? || index_where.blank?
180
+
181
+ normalized_where = normalize_condition_sql(index_where)
182
+ validator_where.casecmp?(normalized_where)
183
+ end
184
+
185
+ # Normalizes SQL predicates into a canonical form so semantically equivalent
186
+ # Rails validators and database partial indexes can be compared safely.
187
+ def normalize_condition_sql(sql)
188
+ sql
189
+ .to_s
190
+ .then { |value| strip_outer_parentheses(value) }
191
+ .then { |value| normalize_sql(value) }
192
+ .then { |value| normalize_boolean_predicates(value) }
193
+ .then { |value| normalize_array_any_predicates(value) }
194
+ .then { |value| normalize_negated_blank_or_nil_predicates(value) }
195
+ .then { |value| sort_and_clauses(value) }
196
+ end
197
+
198
+ # Applies lightweight SQL normalization without changing the logical meaning.
199
+ def normalize_sql(sql)
200
+ # `/::\w+/` removes PostgreSQL casts like `column::text`.
201
+ normalized_sql = sql.gsub(/::\w+/, '')
202
+ # `/\(([a-z_][\w.]*)\)/i` unwraps a bare identifier surrounded by
203
+ # parentheses, e.g. `(internal_name)` -> `internal_name`.
204
+ normalized_sql = normalized_sql.gsub(/\(([a-z_][\w.]*)\)/i, '\1')
205
+ # `/\bTRUE\b/i` and `/\bFALSE\b/i` normalize boolean literals to `1` / `0`
206
+ # so they match SQL generated by Active Record on some adapters.
207
+ normalized_sql = normalized_sql.gsub(/\bTRUE\b/i, '1').gsub(/\bFALSE\b/i, '0')
208
+ # `/\s*<>\s*/` rewrites the SQL inequality operator `<>` to `!=`.
209
+ normalized_sql = normalized_sql.gsub(/\s*<>\s*/, ' != ')
210
+ # `/\bIS\s+NOT\s+NULL\b/i` normalizes `IS NOT NULL` spacing and casing.
211
+ normalized_sql = normalized_sql.gsub(/\bIS\s+NOT\s+NULL\b/i, ' IS NOT NULL')
212
+ # `/\bIS\s+NULL\b/i` normalizes `IS NULL` spacing and casing.
213
+ normalized_sql = normalized_sql.gsub(/\bIS\s+NULL\b/i, ' IS NULL')
214
+ # `/ = 't'/` and `/ = 'f'/` normalize PostgreSQL boolean literals stored
215
+ # as `'t'` / `'f'` inside comparisons.
216
+ normalized_sql = normalized_sql.gsub(/ = 't'/, ' = 1').gsub(/ = 'f'/, ' = 0')
217
+ # `/\s+/` collapses any run of whitespace to a single space.
218
+ normalized_sql = normalized_sql.gsub(/\s+/, ' ')
219
+ normalized_sql.strip
220
+ end
221
+
222
+ # Repeatedly removes one wrapping layer of parentheses when the whole SQL
223
+ # fragment is enclosed, e.g. `((foo))` -> `foo`.
224
+ def strip_outer_parentheses(sql)
225
+ stripped_sql = sql.strip
226
+
227
+ stripped_sql = stripped_sql[1..-2].strip while wrapped_with_parentheses?(stripped_sql)
228
+
229
+ stripped_sql
230
+ end
231
+
232
+ # Returns true only when the string is entirely wrapped by one outer pair of
233
+ # parentheses, not when parentheses close earlier inside the expression.
234
+ def wrapped_with_parentheses?(sql)
235
+ return false unless sql.start_with?('(') && sql.end_with?(')')
236
+
237
+ depth = 0
238
+
239
+ sql[1..-2].each_char do |char|
240
+ depth = parenthesis_depth(depth, char)
241
+ return false if depth.negative?
242
+ end
243
+
244
+ depth.zero?
245
+ end
246
+
247
+ # Tracks parenthesis nesting depth character by character.
248
+ def parenthesis_depth(depth, char)
249
+ case char
250
+ when '('
251
+ depth + 1
252
+ when ')'
253
+ depth - 1
254
+ else
255
+ depth
256
+ end
257
+ end
258
+
259
+ # Rewrites shorthand boolean predicates into explicit comparisons so
260
+ # `flag` and `NOT flag` line up with `flag = true/false`.
261
+ def normalize_boolean_predicates(sql)
262
+ normalized_sql = sql.dup
263
+
264
+ # Matches a bare negated boolean predicate such as `NOT archived`
265
+ # appearing at the start of an expression, after `AND` / `OR`, or after
266
+ # an opening parenthesis, and rewrites it to `archived = 0`.
267
+ normalized_sql.gsub!(
268
+ /(^|(?:\bAND\b|\bOR\b|\())\s*NOT\s+([a-z_][\w.]*)\s*(?=$|(?:\bAND\b|\bOR\b|\)))/i
269
+ ) { "#{Regexp.last_match(1)} #{Regexp.last_match(2)} = 0" }
270
+
271
+ # Matches a bare boolean predicate such as `most_recent` appearing in the
272
+ # same structural positions, and rewrites it to `most_recent = 1`.
273
+ normalized_sql.gsub!(
274
+ /(^|(?:\bAND\b|\bOR\b|\())\s*([a-z_][\w.]*)\s*(?=$|(?:\bAND\b|\bOR\b|\)))/i
275
+ ) { "#{Regexp.last_match(1)} #{Regexp.last_match(2)} = 1" }
276
+
277
+ normalized_sql.gsub(/\s+/, ' ').strip
278
+ end
279
+
280
+ # Rewrites PostgreSQL's `= ANY (ARRAY[...])` form into an `IN (...)` form
281
+ # so it matches the SQL Active Record typically generates for arrays.
282
+ def normalize_array_any_predicates(sql)
283
+ sql.gsub(
284
+ # Matches `column = ANY (ARRAY[...])`, capturing the column name and the
285
+ # full array payload so it can be converted to `column IN (...)`.
286
+ /([a-z_][\w.]*)\s*=\s*ANY\s*\(ARRAY\[(.*?)\]\)/i
287
+ ) { "#{Regexp.last_match(1)} IN (#{Regexp.last_match(2).gsub(/\s+/, ' ').strip})" }
288
+ end
289
+
290
+ # Rewrites negated "blank or nil" predicates into the same shape used by
291
+ # `allow_blank`-derived guards: `IS NOT NULL AND != ''`.
292
+ def normalize_negated_blank_or_nil_predicates(sql)
293
+ sql.gsub(
294
+ # Matches SQL like `NOT (column = '' OR column IS NULL)` while enforcing
295
+ # the same column name on both sides via backreference `\1`.
296
+ /NOT\s+\(\s*\(?([a-z_][\w.]*)\s*=\s*''\s+OR\s+\1\s+IS\s+NULL\)?\s*\)/i
297
+ ) { "#{Regexp.last_match(1)} IS NOT NULL AND #{Regexp.last_match(1)} != ''" }
298
+ end
299
+
300
+ # Sorts simple `AND` clauses so `a AND b` and `b AND a` normalize to the
301
+ # same string before comparison.
302
+ def sort_and_clauses(sql)
303
+ # Matches `AND` with surrounding whitespace and splits the expression into
304
+ # comparable clause fragments.
305
+ clauses = sql.split(/\s+AND\s+/i)
306
+ return sql if clauses.length == 1
307
+
308
+ clauses.map! { |clause| strip_outer_parentheses(clause) }
309
+ clauses.sort.join(' AND ')
310
+ end
311
+
312
+ # Builds the implicit SQL guard introduced by validator options that skip
313
+ # nil or blank values instead of validating them.
314
+ def uniqueness_validator_guard_sql(model, attribute, validator)
315
+ attribute_name = foreign_key_or_attribute(model, attribute).to_s
316
+
317
+ if validator.options[:allow_blank]
318
+ "#{attribute_name} IS NOT NULL AND #{attribute_name} != ''"
319
+ elsif validator.options[:allow_nil]
320
+ "#{attribute_name} IS NOT NULL"
321
+ end
322
+ end
323
+
324
+ # A validator with only `allow_nil` / `allow_blank` and no explicit
325
+ # conditions is still satisfied by a full unique index, because the database
326
+ # constraint is stricter than the validator.
327
+ def validator_guard_only?(model, attribute, validator)
328
+ uniqueness_validator_guard_sql(model, attribute, validator).present? &&
329
+ validator.options[:conditions].nil?
330
+ end
331
+
119
332
  # @return [String]
120
333
  def wrapped_attribute_name(attribute, validator, model)
121
334
  attribute = foreign_key_or_attribute(model, attribute)
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabaseConsistency
4
+ # The module contains Prism AST helper methods for scanning project source files.
5
+ module PrismHelper
6
+ module_function
7
+
8
+ # Returns a memoized index: {model_name => {column_name => "file:line"}}.
9
+ # Built once per run by scanning all project source files with Prism (Ruby 3.3+).
10
+ # Bare find_by calls are resolved to their lexical class/module scope.
11
+ def find_by_calls_index
12
+ return {} unless defined?(Prism)
13
+
14
+ @find_by_calls_index ||= build_find_by_calls_index
15
+ end
16
+
17
+ def build_find_by_calls_index
18
+ FilesHelper.project_source_files.each_with_object({}) do |file, index|
19
+ collector = Checkers::MissingIndexFindByChecker::FindByCollector.new(file)
20
+ collector.visit(Prism.parse_file(file).value)
21
+ merge_collector_results(collector.results, index)
22
+ rescue StandardError
23
+ nil
24
+ end
25
+ end
26
+
27
+ def merge_collector_results(results, index)
28
+ results.each do |(model_key, col), locations|
29
+ index[model_key] ||= {}
30
+ if (entry = index[model_key][col])
31
+ entry[:total_findings_count] += locations.size
32
+ else
33
+ index[model_key][col] = { first_location: locations.first, total_findings_count: locations.size }
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -16,9 +16,9 @@ module DatabaseConsistency
16
16
  end
17
17
 
18
18
  def initialize
19
- puts 'Hey, some checks fail with an error, please open an issue on github at https://github.com/djezzzl/database_consistency.'
20
- puts "Attach the created file: #{filename}"
21
- puts 'Thank you, for your contribution!'
19
+ puts 'Some checks failed with an error. Please open an issue on GitHub at https://github.com/djezzzl/database_consistency.'
20
+ puts "Attach the generated file: #{filename}"
21
+ puts 'Thank you for your contribution!'
22
22
  puts '(c) Evgeniy Demin <lawliet.djez@gmail.com>'
23
23
  end
24
24
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DatabaseConsistency
4
- VERSION = '2.0.4'
4
+ VERSION = '3.0.5'
5
5
  end
@@ -10,7 +10,7 @@ module DatabaseConsistency
10
10
  file_path = migration_path(migration_name)
11
11
 
12
12
  if Dir[migration_path_pattern(migration_name)].any?
13
- p "Skipping migration #{migration_name} because it already exists"
13
+ puts "Skipping migration #{migration_name} because it already exists"
14
14
  else
15
15
  File.write(file_path, migration)
16
16
  end
@@ -5,6 +5,8 @@ module DatabaseConsistency
5
5
  module Writers
6
6
  # The simplest formatter
7
7
  class AutofixWriter < BaseWriter
8
+ UnknownCheckerError = Class.new(StandardError)
9
+
8
10
  SLUG_TO_GENERATOR = {
9
11
  association_missing_index: Autofix::AssociationMissingIndex,
10
12
  association_missing_null_constraint: Autofix::NullConstraintMissing,
@@ -20,6 +22,29 @@ module DatabaseConsistency
20
22
  three_state_boolean: Autofix::NullConstraintMissing
21
23
  }.freeze
22
24
 
25
+ class << self
26
+ def validate_scope!(scope)
27
+ return if scope.nil?
28
+
29
+ known = concrete_checker_names
30
+ unknown = scope - known
31
+ return if unknown.empty?
32
+
33
+ raise UnknownCheckerError,
34
+ "unknown checker(s): #{unknown.join(', ')}. Known: #{known.join(', ')}"
35
+ end
36
+
37
+ private
38
+
39
+ def concrete_checker_names
40
+ Checkers::BaseChecker.descendants
41
+ .reject { |checker| checker.superclass == Checkers::BaseChecker }
42
+ .map(&:checker_name)
43
+ .uniq
44
+ .sort
45
+ end
46
+ end
47
+
23
48
  def write
24
49
  unique_generators.each(&:fix!)
25
50
  end
@@ -35,7 +60,13 @@ module DatabaseConsistency
35
60
  end
36
61
 
37
62
  def fix?(report)
38
- report.status == :fail
63
+ report.status == :fail && scoped?(report)
64
+ end
65
+
66
+ def scoped?(report)
67
+ return true if opts.nil?
68
+
69
+ opts.include?(report.checker_name)
39
70
  end
40
71
 
41
72
  def generator(report)
@@ -4,15 +4,16 @@ module DatabaseConsistency
4
4
  module Writers
5
5
  # The base class for writers
6
6
  class BaseWriter
7
- attr_reader :results, :config
7
+ attr_reader :results, :config, :opts
8
8
 
9
- def initialize(results, config: Configuration.new)
9
+ def initialize(results, config: Configuration.new, opts: nil)
10
10
  @results = results
11
11
  @config = config
12
+ @opts = opts
12
13
  end
13
14
 
14
- def self.write(results, config: Configuration.new)
15
- new(results, config: config).write
15
+ def self.write(results, config: Configuration.new, opts: nil)
16
+ new(results, config: config, opts: opts).write
16
17
  end
17
18
  end
18
19
  end
@@ -7,7 +7,7 @@ module DatabaseConsistency
7
7
  private
8
8
 
9
9
  def template
10
- 'association foreign type column should be required in the database'
10
+ 'association foreign type column should be NOT NULL'
11
11
  end
12
12
 
13
13
  def unique_attributes
@@ -7,7 +7,7 @@ module DatabaseConsistency
7
7
  private
8
8
 
9
9
  def template
10
- 'associated model should have proper index in the database'
10
+ 'associated model should have an index in the database'
11
11
  end
12
12
 
13
13
  def unique_attributes
@@ -7,7 +7,7 @@ module DatabaseConsistency
7
7
  private
8
8
 
9
9
  def template
10
- 'association foreign key column should be required in the database'
10
+ 'association foreign key column should be NOT NULL'
11
11
  end
12
12
 
13
13
  def unique_attributes
@@ -7,7 +7,7 @@ module DatabaseConsistency
7
7
  private
8
8
 
9
9
  def template
10
- 'enum has [%<enum_values>s] values but ActiveRecord enum has [%<declared_values>s] values'
10
+ 'database enum has values [%<enum_values>s] but ActiveRecord enum has values [%<declared_values>s]'
11
11
  end
12
12
 
13
13
  def attributes
@@ -7,7 +7,7 @@ module DatabaseConsistency
7
7
  private
8
8
 
9
9
  def template
10
- 'enum has [%<enum_values>s] values but ActiveRecord inclusion validation has [%<declared_values>s] values'
10
+ 'database enum has values [%<enum_values>s] but inclusion validation has values [%<declared_values>s]'
11
11
  end
12
12
 
13
13
  def attributes
@@ -7,7 +7,7 @@ module DatabaseConsistency
7
7
  private
8
8
 
9
9
  def template
10
- 'associated model should have proper unique index in the database'
10
+ 'associated model should have a unique index in the database'
11
11
  end
12
12
 
13
13
  def unique_attributes
@@ -7,7 +7,7 @@ module DatabaseConsistency
7
7
  private
8
8
 
9
9
  def template
10
- 'implicit_order_column is recommended when using uuid column type for primary key'
10
+ 'setting implicit_order_column is recommended when using UUID as the primary key type'
11
11
  end
12
12
 
13
13
  def unique_attributes
@@ -7,7 +7,7 @@ module DatabaseConsistency
7
7
  private
8
8
 
9
9
  def template
10
- 'enum has %<values_types>s types but column has %<column_type>s type'
10
+ 'enum values have %<values_types>s types but the column has %<column_type>s type'
11
11
  end
12
12
 
13
13
  def attributes
@@ -7,8 +7,8 @@ module DatabaseConsistency
7
7
  private
8
8
 
9
9
  def template
10
- "foreign key (%<fk_name>s) with type (%<fk_type>s) doesn't "\
11
- 'cover primary key (%<pk_name>s) with type (%<pk_type>s)'
10
+ 'foreign key (%<fk_name>s) with type (%<fk_type>s) does not match '\
11
+ 'primary key (%<pk_name>s) with type (%<pk_type>s)'
12
12
  end
13
13
 
14
14
  def attributes
@@ -7,7 +7,7 @@ module DatabaseConsistency
7
7
  private
8
8
 
9
9
  def template
10
- 'column has greater limit in the database than in length validator'
10
+ 'column character limit is greater than the length validator allows'
11
11
  end
12
12
 
13
13
  def unique_attributes
@@ -7,7 +7,7 @@ module DatabaseConsistency
7
7
  private
8
8
 
9
9
  def template
10
- 'column has lower limit in the database than in length validator'
10
+ 'column character limit is less than the length validator allows'
11
11
  end
12
12
 
13
13
  def unique_attributes
@@ -7,7 +7,7 @@ module DatabaseConsistency
7
7
  private
8
8
 
9
9
  def template
10
- 'column has limit in the database but do not have length validator'
10
+ 'column has a character length limit but does not have a length validator'
11
11
  end
12
12
 
13
13
  def unique_attributes
@@ -7,7 +7,7 @@ module DatabaseConsistency
7
7
  private
8
8
 
9
9
  def template
10
- 'refers to undefined model %<class_name>s'
10
+ 'refers to a non-existent model %<class_name>s'
11
11
  end
12
12
 
13
13
  def unique_attributes
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabaseConsistency
4
+ module Writers
5
+ module Simple
6
+ class MissingDependentDestroy < Base # :nodoc:
7
+ private
8
+
9
+ def template
10
+ 'should have a corresponding has_one/has_many association with dependent option (destroy, delete, delete_all, nullify) or a foreign key with on_delete (cascade, nullify)' # rubocop:disable Layout/LineLength
11
+ end
12
+
13
+ def unique_attributes
14
+ {
15
+ model_name: report.model_name,
16
+ attribute_name: report.attribute_name
17
+ }
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -7,7 +7,7 @@ module DatabaseConsistency
7
7
  private
8
8
 
9
9
  def template
10
- 'should have foreign key in the database'
10
+ 'should have a foreign key in the database'
11
11
  end
12
12
 
13
13
  def unique_attributes