database_consistency 2.1.3 → 3.0.1

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/lib/database_consistency/checkers/association_checkers/foreign_key_cascade_checker.rb +5 -4
  3. data/lib/database_consistency/checkers/association_checkers/missing_dependent_destroy_checker.rb +1 -1
  4. data/lib/database_consistency/checkers/column_checkers/missing_index_find_by_checker.rb +187 -0
  5. data/lib/database_consistency/checkers/index_checkers/unique_index_checker.rb +7 -4
  6. data/lib/database_consistency/checkers/model_checkers/view_primary_key_checker.rb +36 -0
  7. data/lib/database_consistency/checkers/validator_checkers/missing_unique_index_checker.rb +7 -3
  8. data/lib/database_consistency/configuration.rb +1 -1
  9. data/lib/database_consistency/files_helper.rb +52 -0
  10. data/lib/database_consistency/helper.rb +33 -1
  11. data/lib/database_consistency/prism_helper.rb +38 -0
  12. data/lib/database_consistency/rescue_error.rb +3 -3
  13. data/lib/database_consistency/version.rb +1 -1
  14. data/lib/database_consistency/writers/autofix/migration_base.rb +1 -1
  15. data/lib/database_consistency/writers/simple/association_foreign_type_missing_null_constraint.rb +1 -1
  16. data/lib/database_consistency/writers/simple/association_missing_index.rb +1 -1
  17. data/lib/database_consistency/writers/simple/association_missing_null_constraint.rb +1 -1
  18. data/lib/database_consistency/writers/simple/enum_values_inconsistent_with_ar_enum.rb +1 -1
  19. data/lib/database_consistency/writers/simple/enum_values_inconsistent_with_inclusion.rb +1 -1
  20. data/lib/database_consistency/writers/simple/has_one_missing_unique_index.rb +1 -1
  21. data/lib/database_consistency/writers/simple/implicit_order_column_missing.rb +1 -1
  22. data/lib/database_consistency/writers/simple/inconsistent_enum_type.rb +1 -1
  23. data/lib/database_consistency/writers/simple/inconsistent_types.rb +2 -2
  24. data/lib/database_consistency/writers/simple/length_validator_greater_limit.rb +1 -1
  25. data/lib/database_consistency/writers/simple/length_validator_lower_limit.rb +1 -1
  26. data/lib/database_consistency/writers/simple/length_validator_missing.rb +1 -1
  27. data/lib/database_consistency/writers/simple/missing_association_class.rb +1 -1
  28. data/lib/database_consistency/writers/simple/missing_foreign_key.rb +1 -1
  29. data/lib/database_consistency/writers/simple/missing_foreign_key_cascade.rb +1 -1
  30. data/lib/database_consistency/writers/simple/missing_index_find_by.rb +32 -0
  31. data/lib/database_consistency/writers/simple/missing_table.rb +1 -1
  32. data/lib/database_consistency/writers/simple/missing_unique_index.rb +1 -1
  33. data/lib/database_consistency/writers/simple/missing_uniqueness_validation.rb +1 -1
  34. data/lib/database_consistency/writers/simple/null_constraint_association_misses_validator.rb +1 -1
  35. data/lib/database_consistency/writers/simple/null_constraint_misses_validator.rb +1 -1
  36. data/lib/database_consistency/writers/simple/null_constraint_missing.rb +1 -1
  37. data/lib/database_consistency/writers/simple/possible_null.rb +1 -1
  38. data/lib/database_consistency/writers/simple/redundant_case_insensitive_option.rb +1 -1
  39. data/lib/database_consistency/writers/simple/redundant_index.rb +1 -1
  40. data/lib/database_consistency/writers/simple/redundant_unique_index.rb +1 -1
  41. data/lib/database_consistency/writers/simple/small_primary_key.rb +1 -1
  42. data/lib/database_consistency/writers/simple/three_state_boolean.rb +1 -1
  43. data/lib/database_consistency/writers/simple/view_missing_primary_key.rb +21 -0
  44. data/lib/database_consistency/writers/simple/view_primary_key_column_missing.rb +21 -0
  45. data/lib/database_consistency.rb +7 -0
  46. metadata +13 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67c90295e24b6b49a4e010a612729f86951d2dda7daba5ac06b091e6b435e040
4
- data.tar.gz: f1958934302e46a7b21d600a9418bbf58bef2921a5b4d5c2dd5fcccc5a4acda8
3
+ metadata.gz: 530670a1dff61ea7ba5cbdd22628c2c2151448af0df367afb4f4f204577f3134
4
+ data.tar.gz: b42ffbd20ccc21a292362a30f905fe2829c91635dfc347d8475d9772d2159fec
5
5
  SHA512:
6
- metadata.gz: 3bb2a7b1dc68923711cad75c25c2e27a0208ac625a911f61b6e9a6459b5c71640800329f920f14373c4202f100f36b2bf05f2a683772bace47ebc5dd3b3b4ea4
7
- data.tar.gz: e6a7b06b4bd9d7b3ff002e24c8a320b952c208e864f07ec4f0c255de23b032f14610bf893cc737d3ca2713f1357ee0e0307d91d4a611c48b693e330b1145891e
6
+ metadata.gz: 6b2d3fef5f27beb7e78c10863cccaef2a53d1d5018284fea8bd0d359e6c51b9cc4d0121060ae49a0ce190d2be1616b94271836a851b5ff16f1d48bf48ced1848
7
+ data.tar.gz: 15c12a901db6176972dbac03ea900035c07f76ea748f4fe8e2cd45aae2c5046c2d047997b455f155c48c2aa3ca8c315ff956f6cabeae1edd108e09cee662886e
@@ -28,6 +28,7 @@ module DatabaseConsistency
28
28
  def preconditions
29
29
  !association.polymorphic? &&
30
30
  !association.belongs_to? &&
31
+ association.association_primary_key.present? &&
31
32
  foreign_key &&
32
33
  DEPENDENT_OPTIONS.include?(dependent_option)
33
34
  rescue StandardError
@@ -68,18 +69,18 @@ module DatabaseConsistency
68
69
  association.klass
69
70
  .connection
70
71
  .foreign_keys(association.klass.table_name)
71
- .find { |fk| fk.column == association.foreign_key.to_s }
72
+ .find { |fk| (Helper.extract_columns(association.foreign_key) - Array.wrap(fk.column)).empty? }
72
73
  end
73
74
 
74
- def report_template(status, error_slug: nil)
75
+ def report_template(status, error_slug: nil) # rubocop:disable Metrics/AbcSize
75
76
  Report.new(
76
77
  status: status,
77
78
  error_message: nil,
78
79
  error_slug: error_slug,
79
80
  primary_table: association.table_name.to_s,
80
- primary_key: association.association_primary_key.to_s,
81
+ primary_key: Helper.extract_columns(association.association_primary_key).join('+'),
81
82
  foreign_table: association.active_record.table_name.to_s,
82
- foreign_key: association.foreign_key.to_s,
83
+ foreign_key: Helper.extract_columns(association.foreign_key).join('+'),
83
84
  cascade_option: required_foreign_key_cascade,
84
85
  **report_attributes
85
86
  )
@@ -15,7 +15,7 @@ module DatabaseConsistency
15
15
  private
16
16
 
17
17
  def preconditions
18
- association.belongs_to? && foreign_key
18
+ association.belongs_to? && !association.polymorphic? && foreign_key
19
19
  end
20
20
 
21
21
  def check
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'prism'
5
+ rescue LoadError
6
+ # Prism is not available; this checker will be disabled on Ruby < 3.3
7
+ end
8
+
9
+ module DatabaseConsistency
10
+ module Checkers
11
+ # This class checks for columns used in find_by queries that are missing a database index.
12
+ # It uses the Prism parser (Ruby stdlib since 3.3) to traverse the AST of all project
13
+ # source files (found by iterating loaded constants and excluding gem paths) and detect
14
+ # calls such as find_by_<column>, find_by(column: ...) and find_by("column" => ...).
15
+ # The checker is automatically skipped on Ruby versions where Prism is not available.
16
+ class MissingIndexFindByChecker < ColumnChecker
17
+ Report = ReportBuilder.define(
18
+ DatabaseConsistency::Report,
19
+ :source_location,
20
+ :total_findings_count
21
+ )
22
+
23
+ private
24
+
25
+ # We skip check when:
26
+ # - Prism is not available (Ruby < 3.3)
27
+ # - column is the primary key (always indexed)
28
+ # - column name does not appear in any find_by call across project source files
29
+ def preconditions
30
+ defined?(Prism) && !primary_key_column? && find_by_used?
31
+ end
32
+
33
+ # Table of possible statuses
34
+ # | index | status |
35
+ # | -------- | ------ |
36
+ # | present | ok |
37
+ # | missing | fail |
38
+ def check
39
+ if indexed?
40
+ report_template(:ok)
41
+ else
42
+ report_template(:fail, error_slug: :missing_index_find_by)
43
+ end
44
+ end
45
+
46
+ def report_template(status, error_slug: nil)
47
+ Report.new(
48
+ status: status,
49
+ error_slug: error_slug,
50
+ error_message: nil,
51
+ source_location: (status == :fail ? @find_by_location : nil),
52
+ total_findings_count: (status == :fail ? @find_by_count : nil),
53
+ **report_attributes
54
+ )
55
+ end
56
+
57
+ def find_by_used?
58
+ entry = PrismHelper.find_by_calls_index.dig(model.name.to_s, column.name.to_s)
59
+ return false unless entry
60
+
61
+ @find_by_location = entry[:first_location]
62
+ @find_by_count = entry[:total_findings_count]
63
+ true
64
+ end
65
+
66
+ def indexed?
67
+ model.connection.indexes(model.table_name).any? do |index|
68
+ Helper.extract_index_columns(index.columns).first == column.name.to_s
69
+ end
70
+ end
71
+
72
+ def primary_key_column?
73
+ column.name.to_s == model.primary_key.to_s
74
+ end
75
+
76
+ if defined?(Prism)
77
+ # Prism AST visitor that collects ALL find_by calls from a source file into a results hash.
78
+ # Key: [model_name, column_name] -- model_name is derived from the explicit receiver or the
79
+ # lexical class/module scope for bare calls. Bare calls outside any class are ignored.
80
+ # Value: "file:line" location of the first matching call.
81
+ #
82
+ # Handles:
83
+ # - find_by_<col>(<value>) / Model.find_by_<col>! (dynamic finder)
84
+ # - find_by(col: <value>) / Model.find_by col: (symbol-key hash)
85
+ # - find_by("col" => <value>) (string-key hash)
86
+ #
87
+ # Defined only when Prism is available (Ruby 3.3+).
88
+ class FindByCollector < Prism::Visitor
89
+ # Matches the full column name from a dynamic finder method name.
90
+ # e.g. find_by_email -> "email", find_by_first_name -> "first_name"
91
+ # Multi-column patterns like find_by_name_and_email extract "name_and_email"
92
+ # which won't match any single-column name, so there are no false positives.
93
+ DYNAMIC_FINDER_RE = /\Afind_by_(.+?)!?\z/.freeze
94
+
95
+ attr_reader :results
96
+
97
+ def initialize(file)
98
+ super()
99
+ @file = file
100
+ @results = {}
101
+ @scope_stack = []
102
+ end
103
+
104
+ def visit_class_node(node)
105
+ @scope_stack.push(constant_path_name(node.constant_path))
106
+ super
107
+ ensure
108
+ @scope_stack.pop
109
+ end
110
+
111
+ def visit_module_node(node)
112
+ @scope_stack.push(constant_path_name(node.constant_path))
113
+ super
114
+ ensure
115
+ @scope_stack.pop
116
+ end
117
+
118
+ def visit_call_node(node)
119
+ name = node.name.to_s
120
+ if (match = DYNAMIC_FINDER_RE.match(name))
121
+ model_key = receiver_to_model_key(node.receiver)
122
+ store(model_key, match[1], node) unless model_key == :skip
123
+ elsif name == 'find_by' && node.arguments
124
+ col = single_hash_column(node.arguments)
125
+ model_key = receiver_to_model_key(node.receiver)
126
+ store(model_key, col, node) if col && model_key != :skip
127
+ end
128
+ super
129
+ end
130
+
131
+ private
132
+
133
+ def current_scope
134
+ @scope_stack.empty? ? nil : @scope_stack.join('::')
135
+ end
136
+
137
+ def store(model_key, col, node)
138
+ key = [model_key, col]
139
+ @results[key] ||= []
140
+ @results[key] << "#{@file}:#{node.location.start_line}"
141
+ end
142
+
143
+ def receiver_to_model_key(receiver)
144
+ case receiver
145
+ when nil then current_scope || :skip
146
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
147
+ constant_path_name(receiver)
148
+ when Prism::CallNode
149
+ scoped_receiver_model(receiver)
150
+ else
151
+ :skip
152
+ end
153
+ end
154
+
155
+ def scoped_receiver_model(call_node)
156
+ return :skip unless %w[unscoped includes].include?(call_node.name.to_s)
157
+
158
+ rec = call_node.receiver
159
+ return :skip unless rec.is_a?(Prism::ConstantReadNode) || rec.is_a?(Prism::ConstantPathNode)
160
+
161
+ constant_path_name(rec)
162
+ end
163
+
164
+ def constant_path_name(node)
165
+ case node
166
+ when Prism::ConstantReadNode then node.name.to_s
167
+ when Prism::ConstantPathNode then "#{constant_path_name(node.parent)}::#{node.name}"
168
+ end
169
+ end
170
+
171
+ def single_hash_column(arguments_node)
172
+ arguments_node.arguments.each do |arg|
173
+ next unless arg.is_a?(Prism::KeywordHashNode) && arg.elements.size == 1
174
+
175
+ assoc = arg.elements.first
176
+ next unless assoc.is_a?(Prism::AssocNode)
177
+
178
+ key = assoc.key
179
+ return key.unescaped if key.is_a?(Prism::SymbolNode) || key.is_a?(Prism::StringNode)
180
+ end
181
+ nil
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
@@ -29,10 +29,13 @@ module DatabaseConsistency
29
29
  def valid?
30
30
  uniqueness_validators = model.validators.select { |validator| validator.kind == :uniqueness }
31
31
 
32
- uniqueness_validators.any? do |validator|
33
- validator.attributes.any? do |attribute|
34
- sorted_index_columns == Helper.sorted_uniqueness_validator_columns(attribute, validator, model)
35
- end
32
+ uniqueness_validators.any? { |validator| validator_matches?(validator) }
33
+ end
34
+
35
+ def validator_matches?(validator)
36
+ validator.attributes.any? do |attribute|
37
+ sorted_index_columns == Helper.sorted_uniqueness_validator_columns(attribute, validator, model) &&
38
+ Helper.conditions_match_index?(model, validator.options[:conditions], index.where)
36
39
  end
37
40
  end
38
41
 
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabaseConsistency
4
+ module Checkers
5
+ # This class checks that a model pointing to a view has a primary_key set and that column exists
6
+ class ViewPrimaryKeyChecker < ModelChecker
7
+ private
8
+
9
+ def preconditions
10
+ ActiveRecord::VERSION::MAJOR >= 5 &&
11
+ !model.abstract_class? &&
12
+ model.connection.view_exists?(model.table_name)
13
+ end
14
+
15
+ # Table of possible statuses
16
+ # | primary_key set | column exists | status |
17
+ # | --------------- | ------------- | ------ |
18
+ # | no | - | fail |
19
+ # | yes | no | fail |
20
+ # | yes | yes | ok |
21
+ def check
22
+ if model.primary_key.blank?
23
+ report_template(:fail, error_slug: :view_missing_primary_key)
24
+ elsif !primary_key_column_exists?
25
+ report_template(:fail, error_slug: :view_primary_key_column_missing)
26
+ else
27
+ report_template(:ok)
28
+ end
29
+ end
30
+
31
+ def primary_key_column_exists?
32
+ Array(model.primary_key).all? { |key| model.column_names.include?(key.to_s) }
33
+ end
34
+ end
35
+ end
36
+ end
@@ -47,9 +47,13 @@ module DatabaseConsistency
47
47
  end
48
48
 
49
49
  def unique_index
50
- @unique_index ||= model.connection.indexes(model.table_name).find do |index|
51
- index.unique && Helper.extract_index_columns(index.columns).sort == sorted_uniqueness_validator_columns
52
- end
50
+ @unique_index ||= model.connection.indexes(model.table_name).find { |index| index_matches?(index) }
51
+ end
52
+
53
+ def index_matches?(index)
54
+ index.unique &&
55
+ Helper.extract_index_columns(index.columns).sort == sorted_uniqueness_validator_columns &&
56
+ Helper.conditions_match_index?(model, validator.options[:conditions], index.where)
53
57
  end
54
58
 
55
59
  def primary_key_covers_validation?
@@ -13,7 +13,7 @@ module DatabaseConsistency
13
13
  if existing_paths.any?
14
14
  puts "Loaded configurations: #{existing_paths.join(', ')}"
15
15
  else
16
- puts 'No configurations were provided'
16
+ puts 'No configuration files were provided'
17
17
  end
18
18
 
19
19
  @configuration = extract_configurations(existing_paths)
@@ -0,0 +1,52 @@
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
+ return unless (name = mod.name)
27
+
28
+ file, = Module.const_source_location(name)
29
+ return unless file && File.exist?(file)
30
+ return if excluded_source_file?(file)
31
+
32
+ file
33
+ rescue NameError, ArgumentError
34
+ nil
35
+ end
36
+
37
+ def excluded_source_file?(file)
38
+ return true if defined?(Bundler) && file.include?(Bundler.bundle_path.to_s)
39
+ return true if defined?(Gem) && file.include?(Gem::RUBYGEMS_DIR)
40
+
41
+ excluded_by_ruby_stdlib?(file)
42
+ end
43
+
44
+ def excluded_by_ruby_stdlib?(file)
45
+ return false unless defined?(RbConfig)
46
+
47
+ file.include?(RbConfig::CONFIG['rubylibdir']) ||
48
+ file.include?(RbConfig::CONFIG['bindir']) ||
49
+ file.include?(RbConfig::CONFIG['sbindir'])
50
+ end
51
+ end
52
+ end
@@ -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
 
@@ -144,6 +144,38 @@ module DatabaseConsistency
144
144
  model._reflect_on_association(attribute)&.foreign_key || attribute
145
145
  end
146
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_sql(where_part.gsub("#{model.quoted_table_name}.", '').gsub('"', ''))
155
+ rescue StandardError
156
+ nil
157
+ end
158
+
159
+ # Returns true when validator conditions and index WHERE clause are a valid
160
+ # pairing: both absent means a match; exactly one present means no match;
161
+ # when both present the normalized SQL is compared.
162
+ def conditions_match_index?(model, conditions, index_where)
163
+ return true if conditions.nil? && index_where.blank?
164
+ return false if conditions.nil? || index_where.blank?
165
+
166
+ conditions_sql = conditions_where_sql(model, conditions)
167
+ # Strip one level of outer parentheses that some databases (e.g. PostgreSQL)
168
+ # add when storing/returning the index WHERE clause.
169
+ normalized_where = normalize_sql(index_where.sub(/\A\s*\((.+)\)\s*\z/m, '\1'))
170
+ conditions_sql&.casecmp?(normalized_where)
171
+ end
172
+
173
+ def normalize_sql(sql)
174
+ sql.gsub(/\bTRUE\b/i, '1').gsub(/\bFALSE\b/i, '0')
175
+ .gsub(/ = 't'/, ' = 1').gsub(/ = 'f'/, ' = 0')
176
+ .strip
177
+ end
178
+
147
179
  # @return [String]
148
180
  def wrapped_attribute_name(attribute, validator, model)
149
181
  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.1.3'
4
+ VERSION = '3.0.1'
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
@@ -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
@@ -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
@@ -7,7 +7,7 @@ module DatabaseConsistency
7
7
  private
8
8
 
9
9
  def template
10
- 'should have foreign key with on_delete: :%<cascade_option>s in the database'
10
+ 'should have a foreign key with on_delete: :%<cascade_option>s in the database'
11
11
  end
12
12
 
13
13
  def attributes
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabaseConsistency
4
+ module Writers
5
+ module Simple
6
+ class MissingIndexFindBy < Base # :nodoc:
7
+ private
8
+
9
+ def template
10
+ 'column is used in find_by but is missing an index%<source_location>s'
11
+ end
12
+
13
+ def attributes
14
+ if report.source_location
15
+ count = report.total_findings_count || 1
16
+ count_str = count > 1 ? ", and #{count - 1} more" : ''
17
+ { source_location: " (found at #{report.source_location}#{count_str})" }
18
+ else
19
+ { source_location: '' }
20
+ end
21
+ end
22
+
23
+ def unique_attributes
24
+ {
25
+ table_or_model_name: report.table_or_model_name,
26
+ column_or_attribute_name: report.column_or_attribute_name
27
+ }
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -7,7 +7,7 @@ module DatabaseConsistency
7
7
  private
8
8
 
9
9
  def template
10
- 'should have a table in the database'
10
+ 'should have a corresponding table 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
- 'model should have proper unique index in the database'
10
+ '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
- 'index is unique in the database but do not have uniqueness validator'
10
+ 'index is unique in the database but does not have a uniqueness 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
- 'column is required in the database but do not have presence validator for association %<association_name>s'
10
+ 'column is NOT NULL but does not have a presence validator for association %<association_name>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
- 'column is required in the database but does not have a validator disallowing nil values'
10
+ 'column is NOT NULL but does not have a validator disallowing nil values'
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 should be required in the database'
10
+ '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
- 'column is required but there is possible null value insert'
10
+ 'column is NOT NULL but may receive a NULL value'
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
- "has case insensitive type and doesn't require case_sensitive: false option"
10
+ 'column has a case-insensitive type and does not need the case_sensitive: false option'
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
- 'index is redundant as %<covered_index_name>s covers it'
10
+ 'index is redundant because %<covered_index_name>s covers it'
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
- 'index uniqueness is redundant as %<covered_index_name>s covers it'
10
+ 'index uniqueness is redundant because %<covered_index_name>s covers it'
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
- 'column has int/serial type but recommended to have bigint/bigserial/uuid'
10
+ 'column has int/serial type but it is recommended to use bigint/bigserial/uuid'
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
- 'boolean column should have NOT NULL constraint'
10
+ 'boolean column should be NOT NULL'
11
11
  end
12
12
 
13
13
  def unique_attributes
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabaseConsistency
4
+ module Writers
5
+ module Simple
6
+ class ViewMissingPrimaryKey < Base # :nodoc:
7
+ private
8
+
9
+ def template
10
+ 'model backed by a database view should have primary_key set'
11
+ end
12
+
13
+ def unique_attributes
14
+ {
15
+ table_or_model_name: report.table_or_model_name
16
+ }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatabaseConsistency
4
+ module Writers
5
+ module Simple
6
+ class ViewPrimaryKeyColumnMissing < Base # :nodoc:
7
+ private
8
+
9
+ def template
10
+ 'model backed by a database view has primary_key set to a non-existent column'
11
+ end
12
+
13
+ def unique_attributes
14
+ {
15
+ table_or_model_name: report.table_or_model_name
16
+ }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -4,6 +4,8 @@ require 'active_record'
4
4
 
5
5
  require 'database_consistency/version'
6
6
  require 'database_consistency/helper'
7
+ require 'database_consistency/files_helper'
8
+ require 'database_consistency/prism_helper'
7
9
  require 'database_consistency/configuration'
8
10
  require 'database_consistency/rescue_error'
9
11
  require 'database_consistency/errors'
@@ -44,6 +46,9 @@ require 'database_consistency/writers/simple/missing_association_class'
44
46
  require 'database_consistency/writers/simple/missing_table'
45
47
  require 'database_consistency/writers/simple/implicit_order_column_missing'
46
48
  require 'database_consistency/writers/simple/missing_dependent_destroy'
49
+ require 'database_consistency/writers/simple/missing_index_find_by'
50
+ require 'database_consistency/writers/simple/view_missing_primary_key'
51
+ require 'database_consistency/writers/simple/view_primary_key_column_missing'
47
52
  require 'database_consistency/writers/simple_writer'
48
53
 
49
54
  require 'database_consistency/writers/autofix/helpers/migration'
@@ -77,6 +82,7 @@ require 'database_consistency/checkers/enum_checkers/enum_type_checker'
77
82
 
78
83
  require 'database_consistency/checkers/model_checkers/model_checker'
79
84
  require 'database_consistency/checkers/model_checkers/missing_table_checker'
85
+ require 'database_consistency/checkers/model_checkers/view_primary_key_checker'
80
86
 
81
87
  require 'database_consistency/checkers/association_checkers/association_checker'
82
88
  require 'database_consistency/checkers/association_checkers/missing_index_checker'
@@ -93,6 +99,7 @@ require 'database_consistency/checkers/column_checkers/primary_key_type_checker'
93
99
  require 'database_consistency/checkers/column_checkers/enum_value_checker'
94
100
  require 'database_consistency/checkers/column_checkers/three_state_boolean_checker'
95
101
  require 'database_consistency/checkers/column_checkers/implicit_ordering_checker'
102
+ require 'database_consistency/checkers/column_checkers/missing_index_find_by_checker'
96
103
 
97
104
  require 'database_consistency/checkers/validator_checkers/validator_checker'
98
105
  require 'database_consistency/checkers/validator_checkers/missing_unique_index_checker'
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: database_consistency
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.3
4
+ version: 3.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evgeniy Demin
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-03-07 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: activerecord
@@ -135,6 +136,7 @@ dependencies:
135
136
  - - ">"
136
137
  - !ruby/object:Gem::Version
137
138
  version: '1.3'
139
+ description:
138
140
  email:
139
141
  - lawliet.djez@gmail.com
140
142
  executables:
@@ -156,6 +158,7 @@ files:
156
158
  - lib/database_consistency/checkers/column_checkers/enum_value_checker.rb
157
159
  - lib/database_consistency/checkers/column_checkers/implicit_ordering_checker.rb
158
160
  - lib/database_consistency/checkers/column_checkers/length_constraint_checker.rb
161
+ - lib/database_consistency/checkers/column_checkers/missing_index_find_by_checker.rb
159
162
  - lib/database_consistency/checkers/column_checkers/null_constraint_checker.rb
160
163
  - lib/database_consistency/checkers/column_checkers/primary_key_type_checker.rb
161
164
  - lib/database_consistency/checkers/column_checkers/three_state_boolean_checker.rb
@@ -167,6 +170,7 @@ files:
167
170
  - lib/database_consistency/checkers/index_checkers/unique_index_checker.rb
168
171
  - lib/database_consistency/checkers/model_checkers/missing_table_checker.rb
169
172
  - lib/database_consistency/checkers/model_checkers/model_checker.rb
173
+ - lib/database_consistency/checkers/model_checkers/view_primary_key_checker.rb
170
174
  - lib/database_consistency/checkers/validator_checkers/case_sensitive_unique_validation_checker.rb
171
175
  - lib/database_consistency/checkers/validator_checkers/missing_unique_index_checker.rb
172
176
  - lib/database_consistency/checkers/validator_checkers/validator_checker.rb
@@ -178,7 +182,9 @@ files:
178
182
  - lib/database_consistency/databases/types/sqlite.rb
179
183
  - lib/database_consistency/debug_context.rb
180
184
  - lib/database_consistency/errors.rb
185
+ - lib/database_consistency/files_helper.rb
181
186
  - lib/database_consistency/helper.rb
187
+ - lib/database_consistency/prism_helper.rb
182
188
  - lib/database_consistency/processors/associations_processor.rb
183
189
  - lib/database_consistency/processors/base_processor.rb
184
190
  - lib/database_consistency/processors/columns_processor.rb
@@ -227,6 +233,7 @@ files:
227
233
  - lib/database_consistency/writers/simple/missing_dependent_destroy.rb
228
234
  - lib/database_consistency/writers/simple/missing_foreign_key.rb
229
235
  - lib/database_consistency/writers/simple/missing_foreign_key_cascade.rb
236
+ - lib/database_consistency/writers/simple/missing_index_find_by.rb
230
237
  - lib/database_consistency/writers/simple/missing_table.rb
231
238
  - lib/database_consistency/writers/simple/missing_unique_index.rb
232
239
  - lib/database_consistency/writers/simple/missing_uniqueness_validation.rb
@@ -239,6 +246,8 @@ files:
239
246
  - lib/database_consistency/writers/simple/redundant_unique_index.rb
240
247
  - lib/database_consistency/writers/simple/small_primary_key.rb
241
248
  - lib/database_consistency/writers/simple/three_state_boolean.rb
249
+ - lib/database_consistency/writers/simple/view_missing_primary_key.rb
250
+ - lib/database_consistency/writers/simple/view_primary_key_column_missing.rb
242
251
  - lib/database_consistency/writers/simple_writer.rb
243
252
  - lib/database_consistency/writers/todo_writer.rb
244
253
  homepage: https://github.com/djezzzl/database_consistency
@@ -273,7 +282,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
273
282
  - !ruby/object:Gem::Version
274
283
  version: '0'
275
284
  requirements: []
276
- rubygems_version: 3.7.2
285
+ rubygems_version: 3.4.19
286
+ signing_key:
277
287
  specification_version: 4
278
288
  summary: Provide an easy way to check the consistency of the database constraints
279
289
  with the application validations.